# Commands and files

> Run any command in a machine and get what it wrote, with or without a shell, as its user or as root; copy files in and out of it.

Run any command in a machine and get back what it wrote and how it ended. Copy files in and out of it, up to 5 GiB at a time.

wm:

```sh
$ wm exec box -- python3 -c "print(2+2)"
4
$ wm sh box 'ls -la | wc -l'
7
```

Python:

```python
result = client.machines.exec(machine.id, ["python3", "-c", "print(2+2)"])
print(result.exit_code, result.stdout)        # 0 4

result = client.machines.exec(machine.id, shell="ls -la | wc -l")
```

TypeScript:

```ts
const result = await client.machines.exec(machine.id, ["python3", "-c", "print(2+2)"]);
console.log(result.exit_code, result.stdout); // 0 4

await client.machines.exec(machine.id, { shell: "ls -la | wc -l" });
```

curl:

```sh
curl -X POST https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/exec \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"command": ["python3", "-c", "print(2+2)"]}'
```

## Run a command

Give a command one of two ways, never both:

- `command`: the program and its words, run as they are, with no shell. Nothing in them is read as a pipe, a wildcard or a variable. `wm exec box -- <program> [words...]`.
- `shell`: one line for `sh -lc`, with its pipes, redirections and variables. `wm sh box '<line>'`.

When the command ends, the answer holds what it wrote and how it ended:

Answer:

```json
{
  "exit_code": 0,
  "stdout": "4\n",
  "stderr": "",
  "duration_ms": 38.2,
  "truncated": false
}
```

A command that fails still gets a `200` answer: its `exit_code` is the command's own word, not a refusal of the API. `wm exec` and `wm sh` print stdout and stderr apart, and exit with the command's exit code.

## Where, and as whom

A command starts in the machine's work folder, the one file paths call `work/`. `cwd` (`--cwd`) starts it elsewhere: an absolute path in the machine, beginning `/`. It runs as the machine's own user; `"user": "root"` (`--root`) runs it as root, inside the machine only, never on our servers. A paused machine is woken for it.

Terminal:

```sh
$ wm exec box --root -- id -un
root
$ wm exec box --cwd /tmp -- pwd
/tmp
```

## Environment

`env` (`--env KEY=VALUE`, once for each) adds up to 256 variables to the command's environment. A name is letters, digits and `_`, and does not begin with a digit.

wm:

```sh
$ wm sh box --env STAGE=test --env WORKERS=4 -- 'echo $STAGE $WORKERS'
test 4
```

API:

```json
{"shell": "echo $STAGE $WORKERS", "env": {"STAGE": "test", "WORKERS": "4"}}
```

## Output

stdout and stderr each come back whole up to 8 MiB. Past that they are cut, and `truncated` is true. For a program that writes more, send its output to a file and copy the file out.

stdout comes as text. For bytes that are not text, ask for `"binary_output": true`: then stdout comes in `stdout_base64`, as base64. `wm` asks for it, so what it prints is the command's bytes exactly as they came.

## Long work

A command may run 10 minutes. Past that the call answers [`504 timeout`](https://docs.wheelmachine.ai/errors/timeout.html), and the command may still be running in the machine. For longer work, start it in the background and read what it writes:

Terminal:

```sh
$ wm sh box 'nohup python3 train.py > train.log 2>&1 &'
$ wm exec box -- tail -n 5 train.log
epoch 3/20  loss 0.412
$ wm cp box:work/train.log .
```

While the job works, the machine is not idle, so it does not pause by itself under it.

## Run it once

A command may change things, so it is never sent twice by itself: the libraries and `wm` try a call again after a dropped connection, but not a command once it went out. When the API answers [`503 unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) to a command, its result did not come back: it may or may not have run. Check before you run it again.

## Files

wm:

```sh
$ wm cp ./data.csv box:work/              # in: work/data.csv
$ wm cp ./data.csv box:work/in/today.csv  # in, under another name; folders are made
$ wm cp box:work/results.csv .            # out, into this folder
$ wm cp box:work/results.csv - | head -3   # out, to standard output
```

Python:

```python
from pathlib import Path

client.machines.write_file(machine.id, "work/data.csv", Path("data.csv"))
client.machines.write_file(machine.id, "work/hello.txt", b"hello\n")

data = client.machines.read_file(machine.id, "work/results.csv")           # bytes
client.machines.read_file(machine.id, "work/results.csv", to="results.csv")  # into a file
```

TypeScript:

```ts
await client.machines.writeFile(machine.id, "work/data.csv", { path: "data.csv" });
await client.machines.writeFile(machine.id, "work/hello.txt", new TextEncoder().encode("hello\n"));

const data = await client.machines.readFile(machine.id, "work/results.csv");      // Uint8Array
await client.machines.readFile(machine.id, "work/results.csv", { to: "results.csv" }); // into a file
```

curl:

```sh
# in: PUT the bytes, with their length (curl -T sends it)
curl -T data.csv "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/data.csv" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY"

# out: GET them
curl "https://api.wheelmachine.ai/v1/machines/m_01jb2xk3n4p5q6r7s8t9v0w1x2/files?path=work/results.csv" \
  -H "Authorization: Bearer $WHEELMACHINE_API_KEY" -o results.csv
```

A path in a machine begins `work/` (the work folder, where commands start) or `home/` (the machine's home). It names a file, not a folder, is at most 1024 characters, and never climbs out with `..`.

- **In** (`PUT`): the file's bytes with their length in `Content-Length`, up to 5 GiB a call. The folders on the way are made, and a file already there is replaced.
- **Out** (`GET`): the file's bytes, streamed.
- **In `wm cp`**: one side is a machine (`box:work/...`), the other a file here. A machine path ending in `/` is a folder, and the file keeps its name in it. `-` is standard input or output. At a terminal it shows the bytes copied and the speed as it goes.

`wm cp` copies one file. For a folder, pack it first:

Terminal:

```sh
$ tar czf - ./project | wm cp - box:work/project.tgz
$ wm exec box -- tar xzf project.tgz
$ wm exec box -- tar czf out.tgz results && wm cp box:work/out.tgz .
```

To copy from one machine to another, copy out to this computer and in again. A machine's files can be reached while it is on a server, running or paused; one that is not (stopped or archived, for example) answers [`409 conflict`](https://docs.wheelmachine.ai/errors/conflict.html): start it first.

## What can go wrong

| When | Answer |
|---|---|
| A path that does not begin `home/` or `work/`, or climbs out with `..` | [`400 invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) |
| Both `command` and `shell`, or neither | [`400 invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) |
| A file written without its length | [`411 invalid_request`](https://docs.wheelmachine.ai/errors/invalid_request.html) |
| No such file in the machine | [`404 not_found`](https://docs.wheelmachine.ai/errors/not_found.html) |
| The machine's files cannot be reached in its state | [`409 conflict`](https://docs.wheelmachine.ai/errors/conflict.html) |
| A file over 5 GiB, or more than the machine's disk takes | [`413 too_large`](https://docs.wheelmachine.ai/errors/too_large.html) |
| The key lacks `cloud:machines:exec` | [`403 forbidden`](https://docs.wheelmachine.ai/errors/forbidden.html) |
| A command that ran longer than 10 minutes | [`504 timeout`](https://docs.wheelmachine.ai/errors/timeout.html) |
| A command whose result did not come back | [`503 unavailable`](https://docs.wheelmachine.ai/errors/unavailable.html) |
