Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions apps/desktop/src/main/lib/debug-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type {
DebugChannelOptions,
DebugChannelTransport,
} from "shared/debug-channel";
import { createDebugChannel } from "shared/debug-channel";

let sentryModulePromise: Promise<
typeof import("@sentry/electron/main")
> | null = null;

function getSentry() {
if (!sentryModulePromise) {
sentryModulePromise = import("@sentry/electron/main");
}
return sentryModulePromise;
}

function createMainTransport(): DebugChannelTransport {
return {
addBreadcrumb(entry) {
void getSentry()
.then((Sentry) => {
Sentry.addBreadcrumb({
category: entry.namespace,
level: entry.level,
message: entry.message,
data: entry.data,
});
})
.catch(() => {});
},
captureMessage(entry) {
void getSentry()
.then((Sentry) => {
Sentry.withScope((scope) => {
scope.setLevel(entry.level);
scope.setTag("debug_namespace", entry.namespace);
if (entry.fingerprint) {
scope.setFingerprint(entry.fingerprint);
}
if (entry.data) {
scope.setContext("debug", entry.data);
}
Sentry.captureMessage(`[${entry.namespace}] ${entry.message}`);
});
})
.catch(() => {});
},
captureException(error, entry) {
void getSentry()
.then((Sentry) => {
Sentry.withScope((scope) => {
scope.setLevel(entry.level);
scope.setTag("debug_namespace", entry.namespace);
if (entry.fingerprint) {
scope.setFingerprint(entry.fingerprint);
}
if (entry.data) {
scope.setContext("debug", entry.data);
}
Sentry.captureException(error);
});
})
.catch(() => {});
},
};
}

export function createMainDebugChannel(
options: Omit<DebugChannelOptions, "transport">,
) {
return createDebugChannel({
...options,
transport: createMainTransport(),
});
}
14 changes: 14 additions & 0 deletions apps/desktop/src/main/terminal-host/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createMainDebugChannel } from "../lib/debug-channel";

const DEBUG_TERMINAL = process.env.SUPERSET_TERMINAL_DEBUG === "1";

// terminal host のログは、再起動前提の再現を避けるため
// Sentry には常時送る。
// これにより renderer 側の停止、hidden terminal の滞留、
// PTY/emulator の backpressure を事後に追いやすくする。
// env フラグは同じ内容を console にも出すかだけを制御する。
export const terminalHostDebug = createMainDebugChannel({
namespace: "terminal.host",
enabled: true,
mirrorToConsole: DEBUG_TERMINAL,
});
25 changes: 25 additions & 0 deletions apps/desktop/src/main/terminal-host/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
TerminalSnapshot,
} from "../lib/terminal-host/types";
import { treeKillAsync } from "../lib/tree-kill";
import { terminalHostDebug } from "./debug";
import {
createFrameHeader,
PtySubprocessFrameDecoder,
Expand Down Expand Up @@ -1245,6 +1246,18 @@ export class Session {
}

this.emulatorWriteBackpressured = true;
terminalHostDebug.warn(
"emulator-backpressure-paused",
{
sessionId: this.sessionId,
queuedBytes: this.emulatorWriteQueuedBytes,
clientCount: this.attachedClients.size,
},
{
captureMessage: true,
fingerprint: ["terminal.host", "emulator-backpressure-paused"],
},
);
console.warn(
`[Session ${this.sessionId}] Emulator backlog reached ${this.emulatorWriteQueuedBytes} bytes, pausing PTY reads`,
);
Expand All @@ -1260,6 +1273,18 @@ export class Session {
}

this.emulatorWriteBackpressured = false;
terminalHostDebug.info(
"emulator-backpressure-resumed",
{
sessionId: this.sessionId,
queuedBytes: this.emulatorWriteQueuedBytes,
clientCount: this.attachedClients.size,
},
{
captureMessage: true,
fingerprint: ["terminal.host", "emulator-backpressure-resumed"],
},
);
this.updateSubprocessStdoutFlow();
}

Expand Down
76 changes: 76 additions & 0 deletions apps/desktop/src/renderer/lib/debug-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type {
DebugChannelOptions,
DebugChannelTransport,
} from "shared/debug-channel";
import { createDebugChannel } from "shared/debug-channel";

let sentryModulePromise: Promise<
typeof import("@sentry/electron/renderer")
> | null = null;

function getSentry() {
if (!sentryModulePromise) {
sentryModulePromise = import("@sentry/electron/renderer");
}
return sentryModulePromise;
}

function createRendererTransport(): DebugChannelTransport {
return {
addBreadcrumb(entry) {
void getSentry()
.then((Sentry) => {
Sentry.addBreadcrumb({
category: entry.namespace,
level: entry.level,
message: entry.message,
data: entry.data,
});
})
.catch(() => {});
},
captureMessage(entry) {
void getSentry()
.then((Sentry) => {
Sentry.withScope((scope) => {
scope.setLevel(entry.level);
scope.setTag("debug_namespace", entry.namespace);
if (entry.fingerprint) {
scope.setFingerprint(entry.fingerprint);
}
if (entry.data) {
scope.setContext("debug", entry.data);
}
Sentry.captureMessage(`[${entry.namespace}] ${entry.message}`);
});
})
.catch(() => {});
},
captureException(error, entry) {
void getSentry()
.then((Sentry) => {
Sentry.withScope((scope) => {
scope.setLevel(entry.level);
scope.setTag("debug_namespace", entry.namespace);
if (entry.fingerprint) {
scope.setFingerprint(entry.fingerprint);
}
if (entry.data) {
scope.setContext("debug", entry.data);
}
Sentry.captureException(error);
});
})
.catch(() => {});
},
};
}

export function createRendererDebugChannel(
options: Omit<DebugChannelOptions, "transport">,
) {
return createDebugChannel({
...options,
transport: createRendererTransport(),
});
}
52 changes: 52 additions & 0 deletions apps/desktop/src/renderer/lib/terminal/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createRendererDebugChannel } from "renderer/lib/debug-channel";
import type { DebugData } from "shared/debug-channel";

function isTerminalDebugEnabled(): boolean {
try {
return globalThis.localStorage?.getItem("SUPERSET_TERMINAL_DEBUG") === "1";
} catch {
return false;
}
}

// terminal renderer のログは v1/v2 の両経路で再利用する前提で置く。
// 主調査対象:
// - タブ切り替えや reattach 後に Codex 系 TUI の再描画が崩れる問題
// - 入力は通っていそうなのに画面へ描画されない問題
// 副次仮説:
// - hidden terminal が data を受け続けて xterm を回し続ける問題
// 生の terminal 本文は送らず、状態遷移と byte/count 集計だけを残す。
// とくに visible terminal 問題では「入力」「受信」「xterm.write 実行」の
// 3 点を突き合わせたいので、下の helper で共通集計する。
// こうしておくと Sentry 上で検索しやすく、payload も肥大化しにくい。
export const terminalRendererDebug = createRendererDebugChannel({
namespace: "terminal.renderer",
enabled: true,
mirrorToConsole: isTerminalDebugEnabled(),
});

export function logTerminalWrite(
source: string,
bytes: number,
data?: DebugData,
): void {
terminalRendererDebug.increment("xterm-write-events", 1, {
data: { source, ...(data ?? {}) },
});
terminalRendererDebug.observe("xterm-write-bytes", bytes, {
data: { source, ...(data ?? {}) },
});
}

export function logTerminalInput(
source: string,
bytes: number,
data?: DebugData,
): void {
terminalRendererDebug.increment("terminal-input-events", 1, {
data: { source, ...(data ?? {}) },
});
terminalRendererDebug.observe("terminal-input-bytes", bytes, {
data: { source, ...(data ?? {}) },
});
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ProgressAddon } from "@xterm/addon-progress";
import type { SearchAddon } from "@xterm/addon-search";
import type { TerminalAppearance } from "./appearance";
import { terminalRendererDebug } from "./debug";
import {
type LinkHoverInfo,
type TerminalLinkHandlers,
Expand Down Expand Up @@ -43,7 +44,7 @@ class TerminalRuntimeRegistryImpl {

entry = {
runtime: null,
transport: createTransport(),
transport: createTransport(terminalId),
linkManager: null,
pendingLinkHandlers: null,
};
Expand All @@ -59,6 +60,18 @@ class TerminalRuntimeRegistryImpl {
appearance: TerminalAppearance,
) {
const entry = this.getOrCreateEntry(terminalId);
terminalRendererDebug.info(
"runtime-attach",
{
terminalId,
hasExistingRuntime: entry.runtime !== null,
connectionState: entry.transport.connectionState,
},
{
captureMessage: true,
fingerprint: ["terminal.renderer", "runtime-attach"],
},
);

if (!entry.runtime) {
entry.runtime = createRuntime(terminalId, appearance);
Expand Down Expand Up @@ -104,6 +117,17 @@ class TerminalRuntimeRegistryImpl {
detach(terminalId: string) {
const entry = this.entries.get(terminalId);
if (!entry?.runtime) return;
terminalRendererDebug.info(
"runtime-detach",
{
terminalId,
connectionState: entry.transport.connectionState,
},
{
captureMessage: true,
fingerprint: ["terminal.renderer", "runtime-detach"],
},
);

detachFromContainer(entry.runtime);
}
Expand All @@ -126,6 +150,18 @@ class TerminalRuntimeRegistryImpl {
dispose(terminalId: string) {
const entry = this.entries.get(terminalId);
if (!entry) return;
terminalRendererDebug.info(
"runtime-dispose",
{
terminalId,
hasRuntime: entry.runtime !== null,
connectionState: entry.transport.connectionState,
},
{
captureMessage: true,
fingerprint: ["terminal.renderer", "runtime-dispose"],
},
);

entry.linkManager?.dispose();

Expand Down
Loading
Loading