Skip to content

Commit 2c811b9

Browse files
author
Hyeoncheol Kim
committed
fix(vm-probe): restore warm probe metrics via V2 runtime sync
1 parent f4881ef commit 2c811b9

9 files changed

Lines changed: 50 additions & 59 deletions

‎e2e/lesson-probe-regression.e2e.spec.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,8 @@ async function ensureActiveWindowIndexAtLeast(page: Page, minIndex: number) {
210210
}
211211

212212
async function ensureWindowNameEquals(page: Page, expectedName: string) {
213+
await ensureWindowCountAtLeast(page, 1);
214+
213215
const currentName = ((await getVmBridgeStatus(page)).metrics.windowName ?? '').trim();
214216
if (currentName === expectedName) {
215217
return;
@@ -225,6 +227,8 @@ async function ensureWindowNameEquals(page: Page, expectedName: string) {
225227
}
226228

227229
async function ensureSessionNameEquals(page: Page, expectedName: string) {
230+
await ensureSessionCountAtLeast(page, 1);
231+
228232
const currentName = ((await getVmBridgeStatus(page)).metrics.sessionName ?? '').trim();
229233
if (currentName === expectedName) {
230234
return;
602 Bytes
Binary file not shown.

‎public/vm/alpine-tmux-fs.json‎

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.
-584 KB
Binary file not shown.

‎src/pages/Practice/hooks/usePracticeVmBootstrap.ts‎

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,19 @@ export function usePracticeVmBootstrap({
188188
emulatorRef.current.serial0_send(data);
189189
});
190190

191+
const markBridgeReadyIfNeeded = () => {
192+
if (vmInternalBridgeReadyRef.current || !emulatorRef.current) {
193+
return;
194+
}
195+
vmInternalBridgeReadyRef.current = true;
196+
if (vmWarmBannerPendingRef.current) {
197+
emulatorRef.current.serial0_send(`${BANNER_TRIGGER_COMMAND}\n`);
198+
vmWarmBannerPendingRef.current = false;
199+
}
200+
sendInternalCommand(terminalGeometrySyncCommand);
201+
requestBootstrapProbe();
202+
};
203+
191204
const writeByte = (value: number) => {
192205
terminal.write(Uint8Array.of(value & 0xff));
193206
const outputCaptureResult = consumeTerminalOutputByte(value, {
@@ -197,6 +210,16 @@ export function usePracticeVmBootstrap({
197210
lineBufferRef.current = outputCaptureResult.nextState.lineBuffer;
198211
outputEscapeSequenceRef.current = outputCaptureResult.nextState.inEscapeSequence;
199212

213+
// Shell prompts are usually rendered without a trailing newline.
214+
// Detect prompt state from the in-progress line buffer so internal probes can start.
215+
const promptLine = stripAnsi(outputCaptureResult.nextState.lineBuffer).replace(/\r/g, '');
216+
const hasPromptInBuffer = /[#$]\s*$/.test(promptLine.trimEnd());
217+
if (hasPromptInBuffer) {
218+
setVmStatusText(t('부팅 완료, 명령 입력 가능'));
219+
setVmStatus('running');
220+
markBridgeReadyIfNeeded();
221+
}
222+
200223
if (outputCaptureResult.completedLine === null) {
201224
return;
202225
}
@@ -218,14 +241,8 @@ export function usePracticeVmBootstrap({
218241
setVmStatus('running');
219242
}
220243

221-
if (hasShellPrompt && !vmInternalBridgeReadyRef.current && emulatorRef.current) {
222-
vmInternalBridgeReadyRef.current = true;
223-
if (vmWarmBannerPendingRef.current) {
224-
emulatorRef.current.serial0_send(`${BANNER_TRIGGER_COMMAND}\n`);
225-
vmWarmBannerPendingRef.current = false;
226-
}
227-
sendInternalCommand(terminalGeometrySyncCommand);
228-
requestBootstrapProbe();
244+
if (hasShellPrompt) {
245+
markBridgeReadyIfNeeded();
229246
}
230247
};
231248

‎src/pages/Practice/probeCommands.test.ts‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ describe('probeCommands', () => {
1919
it('keeps periodic probe command short and stable', () => {
2020
expect(PROBE_TRIGGER_COMMAND).toBe(BASE_PROBE_TRIGGER_COMMAND);
2121
expect(PROBE_LOOP_START_COMMAND).toContain(BASE_PROBE_TRIGGER_COMMAND);
22-
expect(PROBE_TRIGGER_COMMAND).toContain(PROBE_STATE_MARKER);
23-
expect(PROBE_TRIGGER_COMMAND).toContain('tmux list-sessions -F');
24-
expect(PROBE_TRIGGER_COMMAND).toContain('tmux list-windows -a -F');
25-
expect(PROBE_TRIGGER_COMMAND).toContain('tmux list-panes -a -F');
26-
expect(PROBE_TRIGGER_COMMAND).toContain('TMUXWEB_SEARCH_COUNT');
22+
expect(PROBE_TRIGGER_COMMAND).toContain('/usr/bin/tmux-tuto-probe');
23+
expect(PROBE_TRIGGER_COMMAND).not.toContain(PROBE_STATE_MARKER);
2724
expectValidShellSyntax(PROBE_TRIGGER_COMMAND);
2825
});
2926

‎src/pages/Practice/probeCommands.ts‎

Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,8 @@
11
export const PROBE_STATE_MARKER = 'TMUXWEB_STATE_V2';
22

3-
const PROBE_STATE_TRIGGER_LINES = [
4-
'TMUXWEB_TMUX=0',
5-
'if tmux -V >/dev/null 2>&1; then TMUXWEB_TMUX=1; fi',
6-
'TMUXWEB_SESSION=-1',
7-
'TMUXWEB_WINDOW=-1',
8-
'TMUXWEB_PANE=-1',
9-
'TMUXWEB_MODE=0',
10-
'TMUXWEB_SESSION_NAME=""',
11-
'TMUXWEB_WINDOW_NAME=""',
12-
'TMUXWEB_ACTIVE_WINDOW=-1',
13-
'TMUXWEB_LAYOUT=""',
14-
'TMUXWEB_ZOOMED=0',
15-
'TMUXWEB_SYNC=0',
16-
'TMUXWEB_SEARCH=0',
17-
'TMUXWEB_SEARCH_MATCHED=0',
18-
'if [ "$TMUXWEB_TMUX" -eq 1 ]; then',
19-
'TMUXWEB_SESSION="$(tmux list-sessions -F \'#{session_id}\' 2>/dev/null | wc -l | tr -d \' \\n\\r\')"',
20-
'if [ -z "${TMUXWEB_SESSION:-}" ]; then TMUXWEB_SESSION=-1; fi',
21-
'TMUXWEB_WINDOW="$(tmux list-windows -a -F \'#{window_id}\' 2>/dev/null | wc -l | tr -d \' \\n\\r\')"',
22-
'if [ -z "${TMUXWEB_WINDOW:-}" ]; then TMUXWEB_WINDOW=-1; fi',
23-
'TMUXWEB_PANE="$(tmux list-panes -a -F \'#{pane_id}\' 2>/dev/null | wc -l | tr -d \' \\n\\r\')"',
24-
'if [ -z "${TMUXWEB_PANE:-}" ]; then TMUXWEB_PANE=-1; fi',
25-
'TMUXWEB_MODE="$(tmux display-message -p \'#{?pane_in_mode,1,0}\' 2>/dev/null | tr -d \' \\n\\r\')"',
26-
'if [ -z "${TMUXWEB_MODE:-}" ]; then TMUXWEB_MODE=0; fi',
27-
'TMUXWEB_SESSION_NAME="$(tmux display-message -p \'#S\' 2>/dev/null | tr -d \'\\n\\r\\t\')"',
28-
'if [ -z "${TMUXWEB_SESSION_NAME:-}" ]; then TMUXWEB_SESSION_NAME="$(tmux list-sessions -F \'#{session_name}\' 2>/dev/null | head -n 1 | tr -d \'\\n\\r\\t\')"; fi',
29-
'TMUXWEB_WINDOW_NAME="$(tmux display-message -p \'#W\' 2>/dev/null | tr -d \'\\n\\r\\t\')"',
30-
'if [ -z "${TMUXWEB_WINDOW_NAME:-}" ] && [ -n "${TMUXWEB_SESSION_NAME:-}" ]; then TMUXWEB_WINDOW_NAME="$(tmux display-message -p -t "${TMUXWEB_SESSION_NAME}" \'#{window_name}\' 2>/dev/null | tr -d \'\\n\\r\\t\')"; fi',
31-
'TMUXWEB_ACTIVE_WINDOW="$(tmux display-message -p \'#{window_index}\' 2>/dev/null | tr -d \' \\n\\r\')"',
32-
'if [ -z "${TMUXWEB_ACTIVE_WINDOW:-}" ]; then TMUXWEB_ACTIVE_WINDOW=-1; fi',
33-
'TMUXWEB_LAYOUT="$(tmux display-message -p \'#{window_layout}\' 2>/dev/null | tr -d \'\\n\\r\\t\')"',
34-
'TMUXWEB_ZOOMED="$(tmux display-message -p \'#{window_zoomed_flag}\' 2>/dev/null | tr -d \' \\n\\r\')"',
35-
'if [ -z "${TMUXWEB_ZOOMED:-}" ]; then TMUXWEB_ZOOMED=0; fi',
36-
'TMUXWEB_SYNC_RAW="$(tmux show-window-options -v synchronize-panes 2>/dev/null | tr -d \'\\n\\r\')"',
37-
'if [ "${TMUXWEB_SYNC_RAW:-off}" = "on" ]; then TMUXWEB_SYNC=1; else TMUXWEB_SYNC=0; fi',
38-
'TMUXWEB_SEARCH_COUNT="$(tmux display-message -p \'#{search_count}\' 2>/dev/null | tr -d \' \\n\\r\')"',
39-
'TMUXWEB_SEARCH_PRESENT="$(tmux display-message -p \'#{search_present}\' 2>/dev/null | tr -d \' \\n\\r\')"',
40-
'if [ -n "${TMUXWEB_SEARCH_COUNT:-}" ] && [ "${TMUXWEB_SEARCH_COUNT}" -ge 1 ] 2>/dev/null; then TMUXWEB_SEARCH=1; if [ "${TMUXWEB_SEARCH_PRESENT:-0}" = "1" ]; then TMUXWEB_SEARCH_MATCHED=1; fi; fi',
41-
'fi',
42-
`printf "[[${PROBE_STATE_MARKER}:%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s]]\\n" "$TMUXWEB_TMUX" "$TMUXWEB_SESSION" "$TMUXWEB_WINDOW" "$TMUXWEB_PANE" "$TMUXWEB_MODE" "$TMUXWEB_SESSION_NAME" "$TMUXWEB_WINDOW_NAME" "$TMUXWEB_ACTIVE_WINDOW" "$TMUXWEB_LAYOUT" "$TMUXWEB_ZOOMED" "$TMUXWEB_SYNC" "$TMUXWEB_SEARCH" "$TMUXWEB_SEARCH_MATCHED" >/dev/ttyS1`,
43-
];
44-
45-
export const BASE_PROBE_TRIGGER_COMMAND = PROBE_STATE_TRIGGER_LINES.join('\n');
3+
// Keep probe dispatch short and deterministic over the ttyS2 relay.
4+
// Runtime probe shape is defined by /usr/bin/tmux-tuto-probe inside VM assets.
5+
export const BASE_PROBE_TRIGGER_COMMAND = '/usr/bin/tmux-tuto-probe >/dev/ttyS1 2>/dev/null';
466

477
export const SEARCH_PROBE_TRIGGER_COMMAND = BASE_PROBE_TRIGGER_COMMAND;
488

‎src/pages/Practice/vmTerminalStream.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,13 @@ describe('vmTerminalStream', () => {
6161
expect(d.completedLine).toBe('xy');
6262
expect(d.nextLineBuffer).toBe('');
6363
});
64+
65+
it('preserves tab delimiters from probe output snapshots', () => {
66+
const a = consumeProbeOutputByte('1'.charCodeAt(0), '');
67+
const b = consumeProbeOutputByte('\t'.charCodeAt(0), a.nextLineBuffer);
68+
const c = consumeProbeOutputByte('2'.charCodeAt(0), b.nextLineBuffer);
69+
const d = consumeProbeOutputByte('\n'.charCodeAt(0), c.nextLineBuffer);
70+
71+
expect(d.completedLine).toBe('1\t2');
72+
});
6473
});

‎src/pages/Practice/vmTerminalStream.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export type TerminalOutputCaptureState = {
88
inEscapeSequence: boolean;
99
};
1010

11+
function isPrintableOrTab(char: string) {
12+
return char >= ' ' || char === '\t';
13+
}
14+
1115
export function createInitialTerminalInputCaptureState(): TerminalInputCaptureState {
1216
return {
1317
lineBuffer: '',
@@ -132,7 +136,7 @@ export function consumeTerminalOutputByte(value: number, state: TerminalOutputCa
132136
};
133137
}
134138

135-
if (char >= ' ') {
139+
if (isPrintableOrTab(char)) {
136140
lineBuffer += char;
137141
}
138142

@@ -161,7 +165,7 @@ export function consumeProbeOutputByte(value: number, lineBuffer: string) {
161165
};
162166
}
163167

164-
if (char >= ' ') {
168+
if (isPrintableOrTab(char)) {
165169
return {
166170
nextLineBuffer: `${lineBuffer}${char}`,
167171
completedLine: null as string | null,

0 commit comments

Comments
 (0)