跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

跟踪后台进程

最后更新 查看 MarkdownAgent 设置

从启动进程的请求之外,读取后台进程运行时的输出。页面可以在进程打印每一行时显示内容,Worker 也可以等待进程打印某一行,例如开发服务器输出的 ready。

先决条件

  • 一个运行后台进程的 Worker,包含脚本、run()、status(),以及运行后台进程中的路由。

跟踪输出

  1. 添加一个方法,在进程运行时通过服务器发送事件发送其输出:

    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 会一直运行到进程退出。

  2. 在 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 });
    }
  3. 部署 Worker:

    npx wrangler deploy
  4. 启动名为 ticker 的进程,然后跟踪其输出。将示例主机名替换为 Wrangler 显示的 workers.dev URL:

    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,即可跟踪标准错误。

  5. 在另一个终端中停止进程:

    curl https://<YOUR_WORKER>.<YOUR_SUBDOMAIN>.workers.dev/processes/ticker --request DELETE

    跟踪到的输出以 exit 事件结束:

    event: exit
    data: {"exitCode":143}

等待日志行

开发服务器在接受请求时会打印一行,例如 ready。要在使用服务器之前等待该行,请检查日志文件,直到某行匹配或进程退出:

  1. 添加检查进程日志的脚本:

    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 会一直运行到下一行到达。

  2. 添加在时限内运行该脚本的方法:

    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 停止它。

  3. 在 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。

相关资源

这篇文档对您有帮助吗?