CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/lib/api/schema/exec.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2024 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { z } from "../framework";
7
8
import { PROJECT_EXEC_DEFAULT_TIMEOUT_S } from "@cocalc/util/consts/project";
9
import { FailedAPIOperationSchema } from "./common";
10
import { ComputeServerIdSchema } from "./compute/common";
11
import { ProjectIdSchema } from "./projects/common";
12
13
const ExecInputCommon = z.object({
14
project_id: ProjectIdSchema,
15
});
16
17
const ExecInputSchemaBlocking = ExecInputCommon.merge(
18
z.object({
19
compute_server_id: ComputeServerIdSchema.describe(
20
`If provided, the desired shell command will be run on the compute server whose id
21
is specified in this field (if available).`,
22
).optional(),
23
filesystem: z
24
.boolean()
25
.optional()
26
.describe(
27
`If \`true\`, this shell command runs in the fileserver container on the compute
28
server; otherwise, it runs on the main compute container.`,
29
),
30
path: z
31
.string()
32
.optional()
33
.describe(
34
"Path to working directory in which the shell command should be executed.",
35
),
36
command: z.string().describe("The shell command to execute."),
37
args: z
38
.array(z.string())
39
.optional()
40
.describe("An array of arguments to pass to the shell command."),
41
timeout: z
42
.number()
43
.min(0)
44
.default(PROJECT_EXEC_DEFAULT_TIMEOUT_S)
45
.optional()
46
.describe("Number of seconds before this shell command times out."),
47
max_output: z
48
.number()
49
.min(0)
50
.optional()
51
.describe("Maximum number of bytes to return from shell command output."),
52
bash: z
53
.boolean()
54
.optional()
55
.describe(
56
`If \`true\`, this command runs in a \`bash\` shell. To do so, the provided shell
57
command is written to a file and then executed via the \`bash\` command.`,
58
),
59
home: z
60
.string()
61
.optional()
62
.describe(
63
`Specify \`$HOME\`. If not set, it is inferred from the environment's \`$HOME\``,
64
),
65
uid: z
66
.number()
67
.min(0)
68
.optional()
69
.describe("Set the `UID` identity of the spawned process."),
70
gid: z
71
.number()
72
.min(0)
73
.optional()
74
.describe("Set the `GID` identity of the spawned process."),
75
aggregate: z
76
.union([
77
z.number(),
78
z.string(),
79
z.object({ value: z.union([z.string(), z.number()]) }),
80
])
81
.optional()
82
.describe(
83
`If provided, this shell command is aggregated as in
84
\`src/packages/backend/aggregate.js\`. This parameter allows one to specify
85
multiple callbacks to be executed against the output of the same command
86
(given identical arguments) within a 60-second window.`,
87
),
88
err_on_exit: z
89
.boolean()
90
.optional()
91
.describe(
92
`When \`true\` (the default),
93
this call will throw an error whenever the provided shell command
94
exits with a non-zero exit code.`,
95
),
96
env: z
97
.record(z.string(), z.string())
98
.optional()
99
.describe(
100
"Environment variables to be passed to the shell command upon execution.",
101
),
102
async_call: z.boolean().optional()
103
.describe(`If \`true\`, the execution happens asynchronously.
104
The API call does not block and returns an ID (\`job_id\`).
105
106
Later, use that ID in a call to \`async_get\` to get status updates, partial output, and eventually the final result.
107
In such a call, you also have to set the \`project_id\`, because the results are cached in the project.
108
109
Additionally and if not specified, \`max_output\` is set to 1MB and and \`timeout\` to 10 minutes.
110
111
NOTE: This does not support executing code on compute servers – only inside the project itself.
112
113
HINT: set \`err_on_exit=false\`, to recieve the real \`exit_code\` of the executed command and status ends with "completed", unless there is a fundamental problem running the command.
114
`),
115
}),
116
);
117
118
const ExecInputSchemaAsyncCommon = ExecInputCommon.merge(
119
z.object({
120
project_id: ProjectIdSchema,
121
async_stats: z
122
.boolean()
123
.describe(
124
`If true, retrieve recorded statistics (CPU/memory) of the process and its child processes.`,
125
),
126
}),
127
);
128
129
const ExecInputSchemaAsync = ExecInputSchemaAsyncCommon.merge(
130
z.object({
131
async_get: z.string()
132
.describe(`For a given \`job_id\` job, which has been returned when setting \`async_call=true\`,
133
retrieve the corresponding status or the result.
134
135
The returned object contains the current \`stdout\` and \`stderr\` output, the \`pid\`,
136
as well as a status field indicating if the job is still running or has completed.
137
Start time and duration are returned as well.
138
139
Note: Results are cached temporarily in the project.`),
140
async_await: z.boolean().optional()
141
.describe(`If \`true\`, the call opens a "hanging" HTTP polling connection,
142
until the given \`job_id\` job has completed.
143
If the job already finished, this is equivalent to an \`async_get\` call without this parameter.
144
145
Note: If it times out, you have to reconnect on your end.`),
146
}),
147
);
148
149
export const ExecInputSchema = z
150
.union([ExecInputSchemaBlocking, ExecInputSchemaAsync])
151
.refine((data) => {
152
if ("async_get" in data) {
153
return ExecInputSchemaAsync.safeParse(data).success;
154
} else {
155
return ExecInputSchemaBlocking.safeParse(data).success;
156
}
157
})
158
.describe("Perform arbitrary shell commands in a compute server or project.");
159
160
const ExecOutputBlocking = z.object({
161
type: z.literal("blocking"),
162
stdout: z.string().describe("Output to stdout"),
163
stderr: z.string().describe("Output to stderr"),
164
exit_code: z
165
.number()
166
.describe(
167
"The numeric exit code. 0 usually means it ran without any issues.",
168
),
169
});
170
171
const ExecOutputAsync = ExecOutputBlocking.extend({
172
type: z.literal("async"),
173
job_id: z.string().describe("The ID identifying the async operation"),
174
start: z.number().describe("UNIX timestamp, when the execution started"),
175
elapsed_s: z.string().optional().describe("How long the execution took"),
176
status: z // AsyncStatus
177
.union([z.literal("running"), z.literal("completed"), z.literal("error")])
178
.describe("Status of the async operation"),
179
pid: z
180
.number()
181
.min(0)
182
.optional()
183
.describe(
184
"Process ID. If not returned, then there has been a fundamenal problem spawning the process.",
185
),
186
stats: z
187
.array(
188
z.object({
189
timestamp: z.number().describe("UNIX epoch timestamp"),
190
mem_rss: z
191
.number()
192
.describe(
193
"Sum of residual memory usage of that process and its children.",
194
),
195
cpu_pct: z
196
.number()
197
.describe(
198
"Sum of percentage CPU usage of that process and its children.",
199
),
200
cpu_secs: z
201
.number()
202
.describe(
203
"Sum of CPU time usage (user+system) of that process and its children.",
204
),
205
}),
206
)
207
.optional()
208
.describe(
209
`Recorded metrics about the process. Each entry has a timestamp and corresponding cpu and memory usage, of that process and children. Initially, the sampling frequency is higher, but then it is spaced out. The total number of samples is truncated, discarding the oldest ones.
210
211
You can visualize the data this way:
212
213
\`\`\`python
214
import matplotlib.pyplot as plt
215
from datetime import datetime
216
217
# Extract stats data
218
timestamps = [stat['timestamp'] for stat in data['stats']]
219
mem_rss = [stat['mem_rss'] for stat in data['stats']]
220
cpu_pct = [stat['cpu_pct'] for stat in data['stats']]
221
222
# Convert timestamps to datetime objects
223
timestamps = [datetime.fromtimestamp(ts / 1000) for ts in timestamps]
224
225
# Create plots
226
fig, ax1 = plt.subplots()
227
228
# Memory usage
229
ax1.plot(timestamps, mem_rss, color='blue', label='Memory (RSS)')
230
ax1.set_xlabel('Time')
231
ax1.set_ylabel('Memory (MB)', color='blue')
232
ax1.tick_params(axis='y', labelcolor='blue')
233
ax1.set_ylim(bottom=0)
234
235
# CPU utilization (secondary axis)
236
ax2 = ax1.twinx()
237
ax2.plot(timestamps, cpu_pct, color='red', label='CPU (%)')
238
ax2.set_ylabel('CPU (%)', color='red')
239
ax2.tick_params(axis='y', labelcolor='red')
240
ax2.set_ylim(bottom=0)
241
242
# Add labels and legend
243
plt.title('Job Stats')
244
plt.legend(loc='upper left')
245
246
# Display the plot
247
plt.show()
248
\`\`\`
249
`,
250
),
251
});
252
253
export const ExecOutputSchema = z.union([
254
z
255
.discriminatedUnion("type", [ExecOutputBlocking, ExecOutputAsync])
256
.describe("Output of executed command."),
257
FailedAPIOperationSchema,
258
]);
259
260
export type ExecInput = z.infer<typeof ExecInputSchema>;
261
export type ExecOutput = z.infer<typeof ExecOutputSchema>;
262
263