A user could not get the VirtualHere plugin to use their VirtualHere client and asked, reasonably, where the logs were. There was no good answer, and the reason they were stuck turned out to be ours. **The runner could not see /tmp.** `punktfunk-scripting.service` set PrivateTmp=yes, which hands the unit a private tmpfs. But integrating with things already running on the box is the entire job of a plugin, and on Linux those talk over /tmp: VirtualHere's client IPC is the FIFO pair /tmp/vhclient + /tmp/vhclient_response, X11 is /tmp/.X11-unix. So the plugin launched the vendor binary happily and could then never reach the daemon behind it — while the same command worked perfectly in the operator's own shell, because that shell has the real /tmp. No config change could fix it, which is exactly the loop the report described. PrivateTmp is now off, with /tmp added to ReadWritePaths (which ProtectSystem=strict would otherwise make read-only). **Plugin logs now land in the console.** Plugins are not host child processes — the runner is a separate bun process that import()s each plugin in-process — so nothing they print passed through the host's tracing, and the console's Logs page could not show a single plugin line. The fallback was journalctl on Linux; on Windows the runner's scheduled task writes no log file at all, so a failing plugin was diagnosable only by stopping the task and re-running the runner by hand. Both mean shell access on the host box, which is what the console exists to avoid — and it left the one question a stuck user asks with no answer. So the runner now tees its output to POST /api/v1/plugins/logs, and those lines join the host's own ring under one cursor, targeted plugin:<name>. The console grows a Host/Plugins switch beside the level filter; an empty Plugins view says the thing that is actually usually wrong (the runner isn't running) rather than "adjust the filter". The shipper keeps stdout authoritative — journald and foreground output are unchanged whatever the host is doing — and is built so that logging can never hurt the thing being logged: it never throws into a caller, holds a bounded queue that drops oldest and then says how many, backs off when the host is away (a restart is normal), and re-sends a batch the host failed to take. Lines logged while a POST is in flight are kept, which cost one round to get right: the first version held its recursion guard across the await and silently dropped exactly the lines a busy plugin produces. Runner lines that report a failure (a refused unit file, a crashed plugin, a give-up) now go out at warn/error instead of all arriving as INFO, so the console's level filter means something for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
130 lines
3.1 KiB
TypeScript
130 lines
3.1 KiB
TypeScript
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import type { LogEntry } from "@/api/gen/model/logEntry";
|
|
import { LogsCard } from "@/sections/Logs/LogsCard";
|
|
import { LogsView } from "@/sections/Logs/view";
|
|
|
|
const noop = () => {};
|
|
|
|
// A deterministic slice of host logs covering every level, incl. the gamepad-driver health lines
|
|
// the page exists to surface — no live host needed.
|
|
const BASE = 1_750_000_000_000;
|
|
const entry = (
|
|
seq: number,
|
|
level: string,
|
|
target: string,
|
|
msg: string,
|
|
): LogEntry => ({ seq, ts_ms: BASE + seq * 750, level, target, msg });
|
|
|
|
const fixtureEntries: LogEntry[] = [
|
|
entry(
|
|
1,
|
|
"INFO",
|
|
"punktfunk_host",
|
|
"punktfunk-host 0.4.2 (punktfunk_core ABI v2)",
|
|
),
|
|
entry(
|
|
2,
|
|
"INFO",
|
|
"punktfunk_host::mgmt",
|
|
"management API listening over HTTPS addr=0.0.0.0:47990",
|
|
),
|
|
entry(
|
|
3,
|
|
"DEBUG",
|
|
"punktfunk_host::discovery",
|
|
"mDNS advertise _punktfunk._udp pair=required",
|
|
),
|
|
entry(
|
|
4,
|
|
"INFO",
|
|
"punktfunk_host::punktfunk1",
|
|
"session start mode=1920x1080@60 codec=hevc",
|
|
),
|
|
entry(
|
|
5,
|
|
"INFO",
|
|
"punktfunk_host::inject",
|
|
"virtual Xbox 360 created (Windows XUSB companion)",
|
|
),
|
|
entry(
|
|
6,
|
|
"WARN",
|
|
"punktfunk_host::inject",
|
|
"gamepad driver not attached to Global\\pfxusb-shm-0 after 3s — is the pf_xusb driver installed? (punktfunk-host.exe driver install --gamepad)",
|
|
),
|
|
entry(
|
|
7,
|
|
"ERROR",
|
|
"punktfunk_host::inject",
|
|
"virtual Xbox 360 creation failed — controller input disabled (is the pf_xusb driver installed?)",
|
|
),
|
|
entry(
|
|
8,
|
|
"INFO",
|
|
"punktfunk_host::encode",
|
|
"NVENC opened 1920x1080 nv12 gop=inf rfi=on",
|
|
),
|
|
// Lines the plugin runner shipped up (`POST /api/v1/plugins/logs`), targeted `plugin:<name>`.
|
|
// They share the ring and the cursor with the host's own, which is what the Host/Plugins
|
|
// filter exists to separate — so the fixture has to carry both to be worth screenshotting.
|
|
entry(9, "INFO", "plugin:runner", "starting virtualhere"),
|
|
entry(
|
|
10,
|
|
"INFO",
|
|
"plugin:virtualhere",
|
|
"bound couch-deck.11 (Thrustmaster T300RS) for stream",
|
|
),
|
|
entry(
|
|
11,
|
|
"ERROR",
|
|
"plugin:virtualhere",
|
|
"vhclientx86_64 failed (ETIMEDOUT) — the VirtualHere client is not answering on /tmp/vhclient",
|
|
),
|
|
];
|
|
|
|
const meta = {
|
|
title: "Pages/Logs",
|
|
component: LogsView,
|
|
parameters: { layout: "padded" },
|
|
} satisfies Meta<typeof LogsView>;
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof meta>;
|
|
|
|
// The real page layout (LogsView) with the pure viewer card + fixture entries in its slot.
|
|
// `shareMode` is probed from the browser on the live page; the stories pin one of each so both the
|
|
// desktop (clipboard) and mobile (share sheet) affordance stay covered by the screenshot run.
|
|
export const Following: Story = {
|
|
args: {
|
|
viewer: (
|
|
<LogsCard
|
|
entries={fixtureEntries}
|
|
follow
|
|
onFollow={noop}
|
|
onClear={noop}
|
|
onDownload={noop}
|
|
onShare={noop}
|
|
shareMode="copy"
|
|
dropped={false}
|
|
/>
|
|
),
|
|
},
|
|
};
|
|
|
|
export const PausedWithGap: Story = {
|
|
args: {
|
|
viewer: (
|
|
<LogsCard
|
|
entries={fixtureEntries}
|
|
follow={false}
|
|
onFollow={noop}
|
|
onClear={noop}
|
|
onDownload={noop}
|
|
onShare={noop}
|
|
shareMode="share"
|
|
dropped
|
|
/>
|
|
),
|
|
},
|
|
};
|