从启动进程的请求之外,读取后台进程运行时的输出。页面可以在进程打印每一行时显示内容,Worker 也可以等待进程打印某一行,例如开发服务器输出的 ready。
- 一个运行后台进程的 Worker,包含脚本、
run()、status(),以及运行后台进程中的路由。
-
添加一个方法,在进程运行时通过服务器发送事件发送其输出:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async follow(id: string, stream: "stdout" | "stderr"): Promise<Response | undefined> { const status = await this.status(id); if (!status) { return undefined; } const path = `${ROOT}/${id}/${stream}.log`; const argv = status.state === "running" ? ["tail", "-n", "+1", "-F", "--pid", `${status.pid}`, path] : ["cat", path]; const process = await this.ctx.container!.exec(argv, { stderr: "ignore" }); const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>(); const writer = writable.getWriter(); const encoder = new TextEncoder(); const send = (event: string, data: unknown) => writer.write( encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`), ); let exited = false; const markExited = () => { exited = true; }; process.exitCode.then(markExited, markExited); // Signaling a process that has exited records an internal error. const stop = () => { if (!exited) process.kill(); }; // Stop following when the client disconnects. writer.closed.catch(stop); // Detect a disconnected client while the log is quiet. const heartbeat = setInterval(() => { writer.write(encoder.encode(": keep-alive\n\n")).catch(() => {}); }, 5_000); const pump = async () => { try { for await (const text of process.stdout!.pipeThrough( new TextDecoderStream(), )) { await send(stream, text); } const ended = await this.status(id); const exitCode = ended?.state === "exited" ? ended.exitCode : null; await send("exit", { exitCode }); await writer.close(); } catch (error) { stop(); // End the response, so a connected client does not wait forever. await writer.abort(error).catch(() => {}); } finally { clearInterval(heartbeat); } }; void pump(); return new Response(readable, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", }, }); } }follow()会发送目前的输出,然后持续发送新输出,直到进程退出且tail --pid停止。每个事件以流名称命名,并包含一段 JSON 编码的输出块,可能是一行的一部分或若干行。输出结束时,follow()会发送带退出码的exit事件;若进程未记录退出码,则为null。页面可用EventSource读取该流,如流式传输命令输出所示。follow()只有在写入失败时才会发现客户端已断开。每五秒一次的 keep-alive 注释会在日志安静时触发该写入,从而让follow()停止tail。没有该注释时,tail会一直运行到进程退出。 -
在 Worker 路由的
logs分支中,当查询参数包含follow时跟踪输出。将此代码添加到sandbox.logs()调用之前:src/index.tsts if (url.searchParams.has("follow")) { const response = await sandbox.follow(id, stream); return response ?? new Response("Not found", { status: 404 }); } -
部署 Worker:
npx wrangler deployyarn wrangler deploypnpm wrangler deploy -
启动名为
ticker的进程,然后跟踪其输出。将示例主机名替换为 Wrangler 显示的workers.devURL:curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/processes/ticker --request POST curl "https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/processes/ticker/logs?follow" --no-buffer第二个请求会先将当前已输出的行作为一个事件发送,之后每秒发送一个新事件:
event: stdout data: "ready\ntick 1\ntick 2\ntick 3\ntick 4\n" event: stdout data: "tick 5\n" event: stdout data: "tick 6\n"在查询参数中添加
stream=stderr,即可跟踪标准错误。 -
在另一个终端中停止进程:
curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/processes/ticker --request DELETE跟踪到的输出以
exit事件结束:event: exit data: {"exitCode":143}
开发服务器在接受请求时会打印一行,例如 ready。要在使用服务器之前等待该行,请检查日志文件,直到某行匹配或进程退出:
-
添加检查进程日志的脚本:
src/index.jsjs // Prints the first matching line and exits with 0. Exits with 3 when the // process ends without printing one, 4 when there is no such process, and // 5 when the process was lost, for example in an earlier instance. const WAIT = `${CURRENT} dir=$1; pattern=$2 [ -d "$dir" ] || exit 4 while :; do exited=false; [ -e "$dir/exit-code" ] && exited=true if ! $exited && [ -e "$dir/pid" ] && ! { current "$dir" && kill -0 "$pid" 2>/dev/null; }; then sleep 0.1; [ -e "$dir/exit-code" ] || exit 5; exited=true fi line=$(grep -h -m 1 -E -e "$pattern" "$dir/stdout.log" "$dir/stderr.log" 2>/dev/null | head -n 1) [ -n "$line" ] && { printf '%s\\n' "$line"; exit 0; } $exited && exit 3 sleep 0.2 done`;src/index.tsts // Prints the first matching line and exits with 0. Exits with 3 when the // process ends without printing one, 4 when there is no such process, and // 5 when the process was lost, for example in an earlier instance. const WAIT = `${CURRENT} dir=$1; pattern=$2 [ -d "$dir" ] || exit 4 while :; do exited=false; [ -e "$dir/exit-code" ] && exited=true if ! $exited && [ -e "$dir/pid" ] && ! { current "$dir" && kill -0 "$pid" 2>/dev/null; }; then sleep 0.1; [ -e "$dir/exit-code" ] || exit 5; exited=true fi line=$(grep -h -m 1 -E -e "$pattern" "$dir/stdout.log" "$dir/stderr.log" 2>/dev/null | head -n 1) [ -n "$line" ] && { printf '%s\\n' "$line"; exit 0; } $exited && exit 3 sleep 0.2 done`;不要改为将
tail -F管道到grep -m 1:grep退出后,tail会一直运行到下一行到达。 -
添加在时限内运行该脚本的方法:
src/index.tsts export class MyContainer extends DurableObject<Env> { // ... async waitForLog(id: string, pattern: string, timeoutMs: number) { const container = this.ctx.container; if (!container?.running) { return undefined; } const result = await run(container, [ "timeout", "--kill-after=5", `${timeoutMs / 1000}`, "sh", "-c", WAIT, "sh", `${ROOT}/${id}`, pattern, ]); if (result.exitCode === 0) { return { state: "matched", line: result.stdout.trimEnd() }; } if (result.exitCode === 3) { return { state: "exited" }; } if (result.exitCode === 4) { return undefined; } if (result.exitCode === 5) { return { state: "lost" }; } // 124 when the time limit passes, or 137 if timeout also sends SIGKILL. if (result.exitCode === 124 || result.exitCode === 137) { return { state: "timed-out" }; } throw new Error(`The log check failed with exit code ${result.exitCode}`); } }时限到达时,
timeout会停止脚本。若五秒后脚本仍在运行,--kill-after=5会用SIGKILL停止它。 -
在
startProcess()之后调用它,例如使用模式^ready:src/index.tsts const ready = await sandbox.waitForLog(id, "^ready", 30_000);{ "state": "matched", "line": "ready" }该模式是扩展正则表达式。进程结束但未打印匹配行时,状态为
exited;进程结束但未记录退出代码时,状态为lost;时间限制先到时,状态为timed-out。如果没有进程使用该 ID,此方法会返回undefined。如果进程在先前实例停止时仍在运行,即使其日志匹配,状态也会是lost,因为current()会检查启动 ID。