Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/lib/api/schema/exec.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2024 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { z } from "../framework";67import { PROJECT_EXEC_DEFAULT_TIMEOUT_S } from "@cocalc/util/consts/project";8import { FailedAPIOperationSchema } from "./common";9import { ComputeServerIdSchema } from "./compute/common";10import { ProjectIdSchema } from "./projects/common";1112const ExecInputCommon = z.object({13project_id: ProjectIdSchema,14});1516const ExecInputSchemaBlocking = ExecInputCommon.merge(17z.object({18compute_server_id: ComputeServerIdSchema.describe(19`If provided, the desired shell command will be run on the compute server whose id20is specified in this field (if available).`,21).optional(),22filesystem: z23.boolean()24.optional()25.describe(26`If \`true\`, this shell command runs in the fileserver container on the compute27server; otherwise, it runs on the main compute container.`,28),29path: z30.string()31.optional()32.describe(33"Path to working directory in which the shell command should be executed.",34),35command: z.string().describe("The shell command to execute."),36args: z37.array(z.string())38.optional()39.describe("An array of arguments to pass to the shell command."),40timeout: z41.number()42.min(0)43.default(PROJECT_EXEC_DEFAULT_TIMEOUT_S)44.optional()45.describe("Number of seconds before this shell command times out."),46max_output: z47.number()48.min(0)49.optional()50.describe("Maximum number of bytes to return from shell command output."),51bash: z52.boolean()53.optional()54.describe(55`If \`true\`, this command runs in a \`bash\` shell. To do so, the provided shell56command is written to a file and then executed via the \`bash\` command.`,57),58home: z59.string()60.optional()61.describe(62`Specify \`$HOME\`. If not set, it is inferred from the environment's \`$HOME\``,63),64uid: z65.number()66.min(0)67.optional()68.describe("Set the `UID` identity of the spawned process."),69gid: z70.number()71.min(0)72.optional()73.describe("Set the `GID` identity of the spawned process."),74aggregate: z75.union([76z.number(),77z.string(),78z.object({ value: z.union([z.string(), z.number()]) }),79])80.optional()81.describe(82`If provided, this shell command is aggregated as in83\`src/packages/backend/aggregate.js\`. This parameter allows one to specify84multiple callbacks to be executed against the output of the same command85(given identical arguments) within a 60-second window.`,86),87err_on_exit: z88.boolean()89.optional()90.describe(91`When \`true\` (the default),92this call will throw an error whenever the provided shell command93exits with a non-zero exit code.`,94),95env: z96.record(z.string(), z.string())97.optional()98.describe(99"Environment variables to be passed to the shell command upon execution.",100),101async_call: z.boolean().optional()102.describe(`If \`true\`, the execution happens asynchronously.103The API call does not block and returns an ID (\`job_id\`).104105Later, use that ID in a call to \`async_get\` to get status updates, partial output, and eventually the final result.106In such a call, you also have to set the \`project_id\`, because the results are cached in the project.107108Additionally and if not specified, \`max_output\` is set to 1MB and and \`timeout\` to 10 minutes.109110NOTE: This does not support executing code on compute servers – only inside the project itself.111112HINT: 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.113`),114}),115);116117const ExecInputSchemaAsyncCommon = ExecInputCommon.merge(118z.object({119project_id: ProjectIdSchema,120async_stats: z121.boolean()122.describe(123`If true, retrieve recorded statistics (CPU/memory) of the process and its child processes.`,124),125}),126);127128const ExecInputSchemaAsync = ExecInputSchemaAsyncCommon.merge(129z.object({130async_get: z.string()131.describe(`For a given \`job_id\` job, which has been returned when setting \`async_call=true\`,132retrieve the corresponding status or the result.133134The returned object contains the current \`stdout\` and \`stderr\` output, the \`pid\`,135as well as a status field indicating if the job is still running or has completed.136Start time and duration are returned as well.137138Note: Results are cached temporarily in the project.`),139async_await: z.boolean().optional()140.describe(`If \`true\`, the call opens a "hanging" HTTP polling connection,141until the given \`job_id\` job has completed.142If the job already finished, this is equivalent to an \`async_get\` call without this parameter.143144Note: If it times out, you have to reconnect on your end.`),145}),146);147148export const ExecInputSchema = z149.union([ExecInputSchemaBlocking, ExecInputSchemaAsync])150.refine((data) => {151if ("async_get" in data) {152return ExecInputSchemaAsync.safeParse(data).success;153} else {154return ExecInputSchemaBlocking.safeParse(data).success;155}156})157.describe("Perform arbitrary shell commands in a compute server or project.");158159const ExecOutputBlocking = z.object({160type: z.literal("blocking"),161stdout: z.string().describe("Output to stdout"),162stderr: z.string().describe("Output to stderr"),163exit_code: z164.number()165.describe(166"The numeric exit code. 0 usually means it ran without any issues.",167),168});169170const ExecOutputAsync = ExecOutputBlocking.extend({171type: z.literal("async"),172job_id: z.string().describe("The ID identifying the async operation"),173start: z.number().describe("UNIX timestamp, when the execution started"),174elapsed_s: z.string().optional().describe("How long the execution took"),175status: z // AsyncStatus176.union([z.literal("running"), z.literal("completed"), z.literal("error")])177.describe("Status of the async operation"),178pid: z179.number()180.min(0)181.optional()182.describe(183"Process ID. If not returned, then there has been a fundamenal problem spawning the process.",184),185stats: z186.array(187z.object({188timestamp: z.number().describe("UNIX epoch timestamp"),189mem_rss: z190.number()191.describe(192"Sum of residual memory usage of that process and its children.",193),194cpu_pct: z195.number()196.describe(197"Sum of percentage CPU usage of that process and its children.",198),199cpu_secs: z200.number()201.describe(202"Sum of CPU time usage (user+system) of that process and its children.",203),204}),205)206.optional()207.describe(208`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.209210You can visualize the data this way:211212\`\`\`python213import matplotlib.pyplot as plt214from datetime import datetime215216# Extract stats data217timestamps = [stat['timestamp'] for stat in data['stats']]218mem_rss = [stat['mem_rss'] for stat in data['stats']]219cpu_pct = [stat['cpu_pct'] for stat in data['stats']]220221# Convert timestamps to datetime objects222timestamps = [datetime.fromtimestamp(ts / 1000) for ts in timestamps]223224# Create plots225fig, ax1 = plt.subplots()226227# Memory usage228ax1.plot(timestamps, mem_rss, color='blue', label='Memory (RSS)')229ax1.set_xlabel('Time')230ax1.set_ylabel('Memory (MB)', color='blue')231ax1.tick_params(axis='y', labelcolor='blue')232ax1.set_ylim(bottom=0)233234# CPU utilization (secondary axis)235ax2 = ax1.twinx()236ax2.plot(timestamps, cpu_pct, color='red', label='CPU (%)')237ax2.set_ylabel('CPU (%)', color='red')238ax2.tick_params(axis='y', labelcolor='red')239ax2.set_ylim(bottom=0)240241# Add labels and legend242plt.title('Job Stats')243plt.legend(loc='upper left')244245# Display the plot246plt.show()247\`\`\`248`,249),250});251252export const ExecOutputSchema = z.union([253z254.discriminatedUnion("type", [ExecOutputBlocking, ExecOutputAsync])255.describe("Output of executed command."),256FailedAPIOperationSchema,257]);258259export type ExecInput = z.infer<typeof ExecInputSchema>;260export type ExecOutput = z.infer<typeof ExecOutputSchema>;261262263