Archived
fix: stream docker helper logs inline
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@jmfederico/pi-web": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Stream Docker update/restart helper logs inline after scheduling detached maintenance work.
|
||||||
+2
-2
@@ -59,7 +59,7 @@ Defaults:
|
|||||||
|
|
||||||
Updating recreates the Docker `sessiond` container. Active Pi agent runtimes in this Docker install may stop, so update while sessions are idle. Persisted PI WEB state, Pi config, and session history under the data directory are kept.
|
Updating recreates the Docker `sessiond` container. Active Pi agent runtimes in this Docker install may stop, so update while sessions are idle. Persisted PI WEB state, Pi config, and session history under the data directory are kept.
|
||||||
|
|
||||||
Inside the Docker runtime, the Updates panel uses `pi-web-docker` for status, update, and restart commands. Update and restart commands first start a detached helper container with the same Docker/host mounts and generated Compose environment, including the project name, ports/data paths, helper image, and generated UID/GID/Docker group. The helper then runs Docker Compose, so work continues even when `web`, `sessiond`, or the PI WEB terminal that launched the command exits.
|
Inside the Docker runtime, the Updates panel uses `pi-web-docker` for status, update, and restart commands. Update and restart commands first start a detached helper container with the same Docker/host mounts and generated Compose environment, including the project name, ports/data paths, helper image, and generated UID/GID/Docker group. After scheduling the helper, the command streams that helper's logs inline and prints the `docker logs -f` command needed to reconnect. The helper still runs independently, so work continues even when `web`, `sessiond`, or the PI WEB terminal that launched the command exits.
|
||||||
|
|
||||||
### Command matrix
|
### Command matrix
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ Useful development commands:
|
|||||||
./docker/pi-web-docker --dev stop
|
./docker/pi-web-docker --dev stop
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart `sessiond` manually after changes that affect `src/server/sessiond.ts`, daemon ownership, or session-daemon-only code paths. Restarting only `web` is enough for ordinary API/client/plugin development reloads. Commands launched from the Updates panel use the same detached `pi-web-docker` helper as runtime mode, so update/restart work continues after the current PI WEB terminal or container exits. In both modes detached helpers load the generated Docker env and run as the generated `PI_WEB_UID:PI_WEB_GID` with the generated Docker group; development helpers still refuse UID 0 unless `--allow-root` is explicit.
|
Restart `sessiond` manually after changes that affect `src/server/sessiond.ts`, daemon ownership, or session-daemon-only code paths. Restarting only `web` is enough for ordinary API/client/plugin development reloads. Commands launched from the Updates panel use the same detached `pi-web-docker` helper as runtime mode, stream the helper's logs inline after it starts, and keep update/restart work running after the current PI WEB terminal or container exits. In both modes detached helpers load the generated Docker env and run as the generated `PI_WEB_UID:PI_WEB_GID` with the generated Docker group; development helpers still refuse UID 0 unless `--allow-root` is explicit.
|
||||||
|
|
||||||
The dev setup intentionally has the same Docker socket and profile-specific host mounts as the runtime setup. The same trust warnings apply. The command refuses to run development mode as UID 0, or to generate a dev env with `PI_WEB_UID=0`, unless you pass `--allow-root`; use that override only when root-owned checkout writes are intentional.
|
The dev setup intentionally has the same Docker socket and profile-specific host mounts as the runtime setup. The same trust warnings apply. The command refuses to run development mode as UID 0, or to generate a dev env with `PI_WEB_UID=0`, unless you pass `--allow-root`; use that override only when root-owned checkout writes are intentional.
|
||||||
|
|
||||||
|
|||||||
+32
-2
@@ -33,7 +33,8 @@ Commands:
|
|||||||
cli <pi-web args...> Run the pi-web CLI in the web container
|
cli <pi-web args...> Run the pi-web CLI in the web container
|
||||||
|
|
||||||
Update and restart commands launched inside a PI WEB Docker container start an
|
Update and restart commands launched inside a PI WEB Docker container start an
|
||||||
independent helper container first so work can continue after web/sessiond exits.
|
independent helper container first, then stream the helper logs inline. The
|
||||||
|
helper continues running if the terminal or web/sessiond exits.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -598,6 +599,35 @@ cleanup_old_helpers() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream_detached_helper_logs() {
|
||||||
|
helper_name=$1
|
||||||
|
printf '\n'
|
||||||
|
printf 'Streaming detached PI WEB Docker helper logs inline.\n'
|
||||||
|
printf 'If this terminal disconnects, the helper keeps running.\n'
|
||||||
|
printf 'Reconnect with: docker logs -f %s\n' "$helper_name"
|
||||||
|
printf '\n'
|
||||||
|
|
||||||
|
if docker logs -f "$helper_name"; then
|
||||||
|
logs_status=0
|
||||||
|
else
|
||||||
|
logs_status=$?
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$logs_status" -ne 0 ]; then
|
||||||
|
log "pi-web-docker: detached helper log streaming stopped with status $logs_status"
|
||||||
|
log "pi-web-docker: reconnect with: docker logs -f $helper_name"
|
||||||
|
return "$logs_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
helper_status=$(docker inspect --format '{{.State.ExitCode}}' "$helper_name" 2>/dev/null || true)
|
||||||
|
if is_unsigned_int "$helper_status" && [ "$helper_status" -ne 0 ]; then
|
||||||
|
log "pi-web-docker: detached helper exited with status $helper_status"
|
||||||
|
return "$helper_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
start_detached_helper() {
|
start_detached_helper() {
|
||||||
action=$1
|
action=$1
|
||||||
is_truthy "${PI_WEB_DOCKER_RUNTIME:-}" || die "detached helpers are only available inside the PI WEB Docker runtime"
|
is_truthy "${PI_WEB_DOCKER_RUNTIME:-}" || die "detached helpers are only available inside the PI WEB Docker runtime"
|
||||||
@@ -675,7 +705,7 @@ start_detached_helper() {
|
|||||||
container_id=$(docker "$@") || die "could not start detached Docker helper"
|
container_id=$(docker "$@") || die "could not start detached Docker helper"
|
||||||
printf 'Started detached PI WEB Docker helper: %s\n' "$helper_name"
|
printf 'Started detached PI WEB Docker helper: %s\n' "$helper_name"
|
||||||
printf 'Container ID: %s\n' "$container_id"
|
printf 'Container ID: %s\n' "$container_id"
|
||||||
printf 'Follow progress with: docker logs -f %s\n' "$helper_name"
|
stream_detached_helper_logs "$helper_name"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_detached_action() {
|
run_detached_action() {
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { execFile as execFileCallback } from "node:child_process";
|
||||||
|
import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { dirname, join, resolve } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const execFile = promisify(execFileCallback);
|
||||||
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||||
|
|
||||||
|
describe("pi-web-docker entrypoint", () => {
|
||||||
|
it("streams detached helper logs inline after scheduling runtime updates", async () => {
|
||||||
|
const tempDir = await mkdtemp(join(tmpdir(), "pi-web-docker-entrypoint-"));
|
||||||
|
try {
|
||||||
|
const runtimeRoot = join(tempDir, "runtime");
|
||||||
|
const binDir = join(tempDir, "bin");
|
||||||
|
const dockerCallsPath = join(tempDir, "docker-calls.log");
|
||||||
|
await mkdir(runtimeRoot);
|
||||||
|
await mkdir(binDir);
|
||||||
|
await writeFile(join(runtimeRoot, ".env"), [
|
||||||
|
`PI_WEB_DOCKER_INSTALL_DIR=${runtimeRoot}`,
|
||||||
|
"COMPOSE_PROJECT_NAME=pi-web-test",
|
||||||
|
"PI_WEB_UID=1000",
|
||||||
|
"PI_WEB_GID=1000",
|
||||||
|
"DOCKER_GID=998",
|
||||||
|
"PI_WEB_IMAGE=pi-web:test",
|
||||||
|
"",
|
||||||
|
].join("\n"));
|
||||||
|
|
||||||
|
const fakeDockerPath = join(binDir, "docker");
|
||||||
|
await writeFile(fakeDockerPath, fakeDockerScript(dockerCallsPath));
|
||||||
|
await chmod(fakeDockerPath, 0o755);
|
||||||
|
|
||||||
|
const { stdout, stderr } = await execFile(join(repoRoot, "docker/pi-web-docker"), ["update"], {
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PATH: `${binDir}:${process.env["PATH"] ?? ""}`,
|
||||||
|
PI_WEB_DOCKER_RUNTIME: "1",
|
||||||
|
PI_WEB_DOCKER_MODE: "runtime",
|
||||||
|
PI_WEB_DOCKER_INSTALL_DIR: runtimeRoot,
|
||||||
|
PI_WEB_DOCKER_CONTAINER_ID: "current-web-container",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const output = `${stdout}${stderr}`;
|
||||||
|
expect(output).toContain("Started detached PI WEB Docker helper: pi-web-docker-update-");
|
||||||
|
expect(output).toContain("Streaming detached PI WEB Docker helper logs inline.");
|
||||||
|
expect(output).toContain("Reconnect with: docker logs -f pi-web-docker-update-");
|
||||||
|
expect(output).toContain("helper log: update in progress");
|
||||||
|
expect(output).not.toContain("Follow progress with:");
|
||||||
|
|
||||||
|
const dockerCalls = await readFile(dockerCallsPath, "utf8");
|
||||||
|
expect(dockerCalls).toContain("__run-detached update");
|
||||||
|
expect(dockerCalls).toMatch(/(?:^|\n)logs -f pi-web-docker-update-\d{14}-\d+(?:\n|$)/);
|
||||||
|
} finally {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function fakeDockerScript(dockerCallsPath: string): string {
|
||||||
|
return `#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
printf '%s\\n' "$*" >> ${shellQuote(dockerCallsPath)}
|
||||||
|
case "$1" in
|
||||||
|
ps)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
run)
|
||||||
|
printf '%s\\n' fake-helper-container-id
|
||||||
|
;;
|
||||||
|
logs)
|
||||||
|
printf '%s\\n' 'helper log: update in progress'
|
||||||
|
;;
|
||||||
|
inspect)
|
||||||
|
printf '%s\\n' 0
|
||||||
|
;;
|
||||||
|
rm)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'unexpected docker command: %s\\n' "$*" >&2
|
||||||
|
exit 42
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shellQuote(value: string): string {
|
||||||
|
return `'${value.replaceAll("'", `'"'"'`)}'`;
|
||||||
|
}
|
||||||
@@ -309,7 +309,9 @@ describe("Docker command assets", () => {
|
|||||||
const result = await runDockerCommand(["restart-sessiond"], runtimeEnv(fakeDocker, installDir));
|
const result = await runDockerCommand(["restart-sessiond"], runtimeEnv(fakeDocker, installDir));
|
||||||
|
|
||||||
expect(result.stdout).toContain("Started detached PI WEB Docker helper");
|
expect(result.stdout).toContain("Started detached PI WEB Docker helper");
|
||||||
expect(result.stdout).toContain("Follow progress with: docker logs -f pi-web-docker-restart-sessiond-");
|
expect(result.stdout).toContain("Streaming detached PI WEB Docker helper logs inline.");
|
||||||
|
expect(result.stdout).toContain("Reconnect with: docker logs -f pi-web-docker-restart-sessiond-");
|
||||||
|
expect(result.stdout).toContain("fake helper log");
|
||||||
const log = await readFile(fakeDocker.logPath, "utf8");
|
const log = await readFile(fakeDocker.logPath, "utf8");
|
||||||
expect(log).toContain("container inspect");
|
expect(log).toContain("container inspect");
|
||||||
expect(log).toContain("run -d");
|
expect(log).toContain("run -d");
|
||||||
@@ -518,6 +520,22 @@ case "\${1:-}" in
|
|||||||
printf 'fake-helper-container-id\n'
|
printf 'fake-helper-container-id\n'
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
|
logs)
|
||||||
|
printf 'fake helper log\n'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
inspect)
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
*State.ExitCode*)
|
||||||
|
printf '0\n'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
printf '{}\n'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
printf 'unexpected fake docker args: %s\n' "$*" >&2
|
printf 'unexpected fake docker args: %s\n' "$*" >&2
|
||||||
exit 9
|
exit 9
|
||||||
|
|||||||
Reference in New Issue
Block a user