fix: improve device pod trace diagnostics
This commit is contained in:
+127
-8
@@ -32,6 +32,9 @@ function classifyBlocker(message) {
|
||||
if (/profile not found/u.test(message)) return 'profile-missing';
|
||||
if (/profile invalid|requires/u.test(message)) return 'profile-invalid';
|
||||
if (/approval/u.test(message)) return 'approval-required';
|
||||
if (/resource is not registered under the requested gateway session|resourceGatewaySessionId/u.test(message)) return 'gateway-session-mismatch';
|
||||
if (/gateway session .*not found|gateway session .*not registered|gateway.*not.*registered/u.test(message)) return 'gateway-session-unavailable';
|
||||
if (/spawn C:\\Windows\\system32\\cmd\.exe ENOENT|cmd\.exe ENOENT/u.test(message)) return 'host-cmd-unavailable';
|
||||
if (/unsupported|invalid target selector|invalid io-probe (?:path|selector)/u.test(message)) return 'invalid-request';
|
||||
return 'operation-failed';
|
||||
}
|
||||
@@ -46,6 +49,8 @@ function nextForBlocker(blocker, details = {}) {
|
||||
];
|
||||
}
|
||||
if (blocker === 'profile-invalid') {
|
||||
const names = availableProfileNames();
|
||||
if (names.length > 1) return profileChoiceNext(names);
|
||||
return [
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`,
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id ${podId}`
|
||||
@@ -60,9 +65,39 @@ function nextForBlocker(blocker, details = {}) {
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`
|
||||
];
|
||||
}
|
||||
if (blocker === 'gateway-session-mismatch') {
|
||||
return [
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`,
|
||||
'update the profile gatewaySessionId to the resourceGatewaySessionId reported by cloud-api, then rerun doctor --probe',
|
||||
'do not bypass device-pod-cli with generic gateway shell'
|
||||
];
|
||||
}
|
||||
if (blocker === 'gateway-session-unavailable') {
|
||||
return [
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`,
|
||||
'restart or reconnect the target hwlab-gateway session, then rerun the same device-pod-cli command',
|
||||
'do not guess another gateway session without updating the profile'
|
||||
];
|
||||
}
|
||||
if (blocker === 'host-cmd-unavailable') {
|
||||
return [
|
||||
`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId} --probe --full`,
|
||||
'the gateway route was reached but cannot spawn Windows cmd.exe; repair the Windows gateway host or use a profile bound to a Windows cmd-capable gateway',
|
||||
'stop retrying lower-level gateway shell commands until doctor --probe is green'
|
||||
];
|
||||
}
|
||||
return [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${podId}`];
|
||||
}
|
||||
|
||||
function profileChoiceNext(names) {
|
||||
const script = 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs';
|
||||
return [
|
||||
`${script} profile list`,
|
||||
...names.map((name) => `${script} doctor --pod-id ${name}`),
|
||||
...names.map((name) => `${script} health --pod-id ${name}`)
|
||||
];
|
||||
}
|
||||
|
||||
function podIdFromMessage(message) {
|
||||
const match = String(message || '').match(/([A-Za-z0-9_.-]+)\.json\b/u);
|
||||
return match?.[1] || null;
|
||||
@@ -143,11 +178,15 @@ function parseOptions(argv) {
|
||||
function resolvePodId(requested) {
|
||||
if (requested) return requested;
|
||||
if (process.env.DEVICE_POD_ID) return process.env.DEVICE_POD_ID;
|
||||
const names = fs.existsSync(PROFILE_DIR)
|
||||
? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json'))
|
||||
: [];
|
||||
const names = availableProfileNames();
|
||||
if (names.length === 1) return names[0];
|
||||
throw new Error('profile invalid: pass <devicePodId>:<surface> or set DEVICE_POD_ID when multiple/no profiles exist');
|
||||
throw new Error(`profile invalid: pass <devicePodId>:<surface> or set DEVICE_POD_ID when multiple/no profiles exist; available profiles: ${names.join(', ') || '(none)'}`);
|
||||
}
|
||||
|
||||
function availableProfileNames() {
|
||||
return fs.existsSync(PROFILE_DIR)
|
||||
? fs.readdirSync(PROFILE_DIR).filter((name) => name.endsWith('.json')).map((name) => path.basename(name, '.json')).sort()
|
||||
: [];
|
||||
}
|
||||
|
||||
function readProfile(podId) {
|
||||
@@ -228,7 +267,7 @@ function conStart710007511Profile(overrides = {}) {
|
||||
]
|
||||
},
|
||||
cloudApiUrl: 'http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667',
|
||||
gatewaySessionId: 'gws_DESKTOP-1MHOD9I',
|
||||
gatewaySessionId: 'gws_YUAN',
|
||||
resourceId: 'res_windows_host',
|
||||
capabilityId: 'cap_windows_cmd_exec',
|
||||
hostWorkspaceRoot: 'F:\\Work\\ConStart\\projects\\71-00075-11',
|
||||
@@ -302,18 +341,22 @@ function profileMissingDoctor(podId, file) {
|
||||
};
|
||||
}
|
||||
|
||||
function doctor(parsed) {
|
||||
async function doctor(parsed) {
|
||||
const podId = parsed.podId || parsed._[0] || process.env.DEVICE_POD_ID || null;
|
||||
try {
|
||||
const profile = readProfile(podId);
|
||||
const script = 'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs';
|
||||
const selector = `${profile.podId}:workspace:/`;
|
||||
const hostProfile = hostProfilePath(profile);
|
||||
const hostHealthCommand = buildHostHealthCommand(profile);
|
||||
const probe = parsed.probe === true
|
||||
? await probeHostHealth(profile, hostHealthCommand, parsed)
|
||||
: undefined;
|
||||
const uploadRoot = `${profile.gatewaySessionId}:${String(profile.hostWorkspaceRoot || '').replace(/^[A-Za-z]:/u, (drive) => `/${drive[0].toLowerCase()}`).replace(/\\/gu, '/')}`;
|
||||
print({
|
||||
ok: true,
|
||||
action: 'doctor',
|
||||
status: 'succeeded',
|
||||
status: probe?.ok === false ? 'degraded' : 'succeeded',
|
||||
...profileSummary(profile),
|
||||
profileDir: PROFILE_DIR,
|
||||
route: routeSummary(profile),
|
||||
@@ -327,10 +370,12 @@ function doctor(parsed) {
|
||||
show: `${script} profile show --pod-id ${profile.podId}`,
|
||||
health: `${script} health --pod-id ${profile.podId}`,
|
||||
plan: `${script} ${selector} ls --dry-run`,
|
||||
probe: `${script} doctor --pod-id ${profile.podId} --probe`,
|
||||
uploadHostCli: `node /app/tools/tran.mjs ${uploadRoot} upload /app/skills/device-pod-cli/assets/device-host-cli.mjs tools/device-host-cli.mjs`,
|
||||
uploadProfile: `node /app/tools/tran.mjs ${uploadRoot} upload ${profile.__profilePath} .device-pod/${profile.podId}.json`,
|
||||
hostHealth: `node /app/tools/tran.mjs ${uploadRoot} cmd -- node tools\\device-host-cli.mjs --profile ${hostProfile} --pod-id ${profile.podId} health`
|
||||
},
|
||||
probe,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
});
|
||||
@@ -345,6 +390,35 @@ function doctor(parsed) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildHostHealthCommand(profile) {
|
||||
const hostArgs = ['--profile', hostProfilePath(profile), '--pod-id', profile.podId, 'health'];
|
||||
return `${profile.hostCli} ${hostArgs.map(cmdQuote).join(' ')}`;
|
||||
}
|
||||
|
||||
async function probeHostHealth(profile, command, parsed) {
|
||||
const result = await invoke(profile, command, {
|
||||
...parsed,
|
||||
timeoutMs: Number(parsed.timeoutMs || 10000),
|
||||
traceId: parsed.traceId || `trc_device_pod_doctor_${Date.now()}`
|
||||
});
|
||||
const diagnosis = diagnoseInvokeFailure(profile, result, { operation: 'doctor.host-health' });
|
||||
return {
|
||||
ok: result.status === 'succeeded',
|
||||
status: result.status,
|
||||
command,
|
||||
traceId: result.traceId,
|
||||
operationId: result.operationId,
|
||||
requestId: result.requestId,
|
||||
httpStatus: result.httpStatus,
|
||||
blocker: diagnosis.blocker,
|
||||
next: diagnosis.next,
|
||||
dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch),
|
||||
hostJson: result.hostJson,
|
||||
evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId },
|
||||
gatewayBody: parsed.full ? result.gatewayBody : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function handleProfileCommand(parsed) {
|
||||
const subcommand = parsed._[0] || 'list';
|
||||
if (subcommand === 'list') return listProfiles();
|
||||
@@ -477,6 +551,35 @@ async function invoke(profile, command, parsed) {
|
||||
return { requestId, operationId, traceId, httpStatus: response.status, status, dispatch, hostJson, gatewayBody: response.body };
|
||||
}
|
||||
|
||||
function diagnoseInvokeFailure(profile, result, context = {}) {
|
||||
if (result.status === 'succeeded') {
|
||||
return { ok: true, blocker: null, next: [] };
|
||||
}
|
||||
const text = JSON.stringify({
|
||||
error: result.gatewayBody?.error,
|
||||
dispatch: result.dispatch,
|
||||
hostJson: result.hostJson
|
||||
});
|
||||
const blocker = classifyBlocker(text);
|
||||
const next = nextForBlocker(blocker, { podId: profile.podId, message: text });
|
||||
return {
|
||||
ok: false,
|
||||
blocker,
|
||||
next,
|
||||
operation: context.operation || null,
|
||||
summary: summarizeFailureText(text),
|
||||
route: routeSummary(profile),
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeFailureText(text) {
|
||||
return String(text || '')
|
||||
.replace(/\s+/gu, ' ')
|
||||
.slice(0, 500);
|
||||
}
|
||||
|
||||
function requestJson(url, options = {}) {
|
||||
const body = JSON.stringify(options.body || {});
|
||||
const target = new URL(url);
|
||||
@@ -533,6 +636,7 @@ function hostJsonSummary(hostJson) {
|
||||
function help() {
|
||||
print({ ok: true, action: 'help', scope: 'HWLAB internal code agents only; not for external UniDesk workspaces.', usage: [
|
||||
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-freq',
|
||||
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id device-pod-71-freq --probe',
|
||||
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile list',
|
||||
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile show --pod-id device-pod-71-freq',
|
||||
'node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs profile create --pod-id device-pod-71-00075-11 --force',
|
||||
@@ -553,6 +657,20 @@ async function main() {
|
||||
const parsed = parseOptions(argv);
|
||||
if (selector.kind === 'doctor') return doctor(parsed);
|
||||
if (selector.kind === 'profile') return handleProfileCommand(parsed);
|
||||
if (selector.kind === 'health' && !parsed.podId && !process.env.DEVICE_POD_ID) {
|
||||
const names = availableProfileNames();
|
||||
if (names.length !== 1) return print({
|
||||
ok: false,
|
||||
action: 'health',
|
||||
status: 'failed',
|
||||
blocker: 'profile-invalid',
|
||||
profileDir: PROFILE_DIR,
|
||||
availableProfiles: names,
|
||||
next: profileChoiceNext(names),
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
}, 1);
|
||||
}
|
||||
const profile = readProfile(selector.podId || parsed.podId);
|
||||
if (selector.kind === 'health') return print({ ok: true, action: 'health', status: 'succeeded', ...profileSummary(profile), route: routeSummary(profile), next: [`node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs doctor --pod-id ${profile.podId}`] });
|
||||
const built = await buildHostCommand(selector, parsed);
|
||||
@@ -561,7 +679,8 @@ async function main() {
|
||||
if (parsed.dryRun) return print({ ok: true, action: 'device-pod.plan', status: 'planned', selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile) });
|
||||
const result = await invoke(profile, command, parsed);
|
||||
const ok = result.status === 'succeeded';
|
||||
print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1);
|
||||
const diagnosis = ok ? null : diagnoseInvokeFailure(profile, result, { operation: built.operation });
|
||||
print({ ok, action: 'device-pod.invoke', status: result.status, selector, operation: built.operation, mutating: built.mutating, command, ...profileSummary(profile), route: routeSummary(profile), traceId: result.traceId, operationId: result.operationId, requestId: result.requestId, httpStatus: result.httpStatus, blocker: diagnosis?.blocker, next: diagnosis?.next, diagnosis, ...hostJsonSummary(result.hostJson), dispatch: parsed.full ? result.dispatch : compactDispatch(result.dispatch), hostJson: result.hostJson, evidence: { auditId: result.dispatch.auditId, evidenceId: result.dispatch.evidenceId }, gatewayBody: parsed.full ? result.gatewayBody : undefined }, ok ? 0 : 1);
|
||||
}
|
||||
|
||||
main().catch((error) => fail('device-pod-cli', error));
|
||||
|
||||
Reference in New Issue
Block a user