Commands and files
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
$ wm exec box -- python3 -c "print(2+2)"
4
$ wm sh box 'ls -la | wc -l'
7Python
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
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
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 forsh -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:
{
"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.
$ wm exec box --root -- id -un
root
$ wm exec box --cwd /tmp -- pwd
/tmpEnvironment#
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
$ wm sh box --env STAGE=test --env WORKERS=4 -- 'echo $STAGE $WORKERS'
test 4API
{"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,
and the command may still be running in the machine. For longer work, start it in the background and read what it
writes:
$ 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 to a command, its result did not come back: it
may or may not have run. Check before you run it again.
Files#
wm
$ 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 outputPython
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 fileTypeScript
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 filecurl
# 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.csvA 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 inContent-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:
$ 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: 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 |
Both command and shell, or neither | 400 invalid_request |
| A file written without its length | 411 invalid_request |
| No such file in the machine | 404 not_found |
| The machine's files cannot be reached in its state | 409 conflict |
| A file over 5 GiB, or more than the machine's disk takes | 413 too_large |
The key lacks cloud:machines:exec | 403 forbidden |
| A command that ran longer than 10 minutes | 504 timeout |
| A command whose result did not come back | 503 unavailable |