livedesk 0.1.605 → 0.1.607
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/bin/livedesk-client.js +95 -49
- package/client/package.json +5 -5
- package/client/src/runtime/client-runtime-server.js +16 -8
- package/diagnostics/livedesk-mode3-capture-smoke.mjs +177 -110
- package/electron/main.mjs +83 -54
- package/package.json +6 -6
- package/web/dist/app.webmanifest +1 -1
- package/web/dist/assets/{LiveDeskApp-Bv0fQKd_.js → LiveDeskApp-Dw86X0VP.js} +3 -3
- package/web/dist/assets/{index-DRenaErr.js → index-C4QRbhif.js} +2 -2
- package/web/dist/index.html +1 -1
- package/web/dist/livedesk-build-evidence.json +19 -19
- package/web/dist/sw.js +1 -1
|
@@ -80,11 +80,12 @@ const RETIRED_CLIENT_MODEL_SINGLE_OPTIONS = new Set(['--ai', '--no-ai', '--fake-
|
|
|
80
80
|
const RETIRED_CLIENT_MODEL_VALUE_OPTION = '--ai-model';
|
|
81
81
|
let activeAgentProcess = null;
|
|
82
82
|
let roleRestartRequest = null;
|
|
83
|
-
let agentRestartRequest = null;
|
|
84
|
-
let linuxVideoAccelerationStatus = null;
|
|
85
|
-
let discoveryWakeController = new AbortController();
|
|
86
|
-
let networkChangeMonitor = null;
|
|
87
|
-
|
|
83
|
+
let agentRestartRequest = null;
|
|
84
|
+
let linuxVideoAccelerationStatus = null;
|
|
85
|
+
let discoveryWakeController = new AbortController();
|
|
86
|
+
let networkChangeMonitor = null;
|
|
87
|
+
let desktopMainAuthSession = null;
|
|
88
|
+
const sessionRefreshesInFlight = new WeakMap();
|
|
88
89
|
const disposeAgentTerminationHandlers = installAgentTerminationHandlers({
|
|
89
90
|
getAgentProcess: () => activeAgentProcess
|
|
90
91
|
});
|
|
@@ -1097,8 +1098,11 @@ export function clearCachedHubTarget() {
|
|
|
1097
1098
|
}
|
|
1098
1099
|
}
|
|
1099
1100
|
|
|
1100
|
-
function readSavedSessionFromFile() {
|
|
1101
|
-
|
|
1101
|
+
function readSavedSessionFromFile() {
|
|
1102
|
+
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
|
|
1103
|
+
return desktopMainAuthSession;
|
|
1104
|
+
}
|
|
1105
|
+
for (const path of [preferredClientAuthPath(), CLIENT_AUTH_PATH, UNIFIED_CLIENT_AUTH_PATH]) {
|
|
1102
1106
|
try {
|
|
1103
1107
|
const state = JSON.parse(readFileSync(path, 'utf8'));
|
|
1104
1108
|
const raw = state?.[CLIENT_AUTH_STORAGE_KEY];
|
|
@@ -1113,20 +1117,43 @@ function readSavedSessionFromFile() {
|
|
|
1113
1117
|
return null;
|
|
1114
1118
|
}
|
|
1115
1119
|
|
|
1116
|
-
function writeSavedSessionToFile(session) {
|
|
1117
|
-
const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
|
|
1118
|
-
if (!normalized.ok) {
|
|
1119
|
-
return false;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1120
|
+
function writeSavedSessionToFile(session) {
|
|
1121
|
+
const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
|
|
1122
|
+
if (!normalized.ok) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
|
|
1126
|
+
return Boolean(desktopMainAuthSession?.access_token);
|
|
1127
|
+
}
|
|
1128
|
+
const storage = createFileStorage(preferredClientAuthPath());
|
|
1122
1129
|
storage.setItem(CLIENT_AUTH_STORAGE_KEY, JSON.stringify({ ...session, ...normalized.session }));
|
|
1123
1130
|
return true;
|
|
1124
1131
|
}
|
|
1125
1132
|
|
|
1126
|
-
function clearSavedSession() {
|
|
1127
|
-
|
|
1128
|
-
rmSync(
|
|
1129
|
-
}
|
|
1133
|
+
function clearSavedSession() {
|
|
1134
|
+
desktopMainAuthSession = null;
|
|
1135
|
+
rmSync(CLIENT_AUTH_PATH, { force: true });
|
|
1136
|
+
rmSync(UNIFIED_CLIENT_AUTH_PATH, { force: true });
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
export function acceptDesktopMainAuthSession(session) {
|
|
1140
|
+
if (!isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) return null;
|
|
1141
|
+
const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
|
|
1142
|
+
if (!normalized.ok) return null;
|
|
1143
|
+
desktopMainAuthSession = {
|
|
1144
|
+
...session,
|
|
1145
|
+
...normalized.session,
|
|
1146
|
+
user: {
|
|
1147
|
+
...(session?.user && typeof session.user === 'object' ? session.user : {}),
|
|
1148
|
+
...normalized.session.user
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
return desktopMainAuthSession;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
export function clearDesktopMainAuthSession() {
|
|
1155
|
+
desktopMainAuthSession = null;
|
|
1156
|
+
}
|
|
1130
1157
|
|
|
1131
1158
|
export async function fetchSupabaseWithDeadline(
|
|
1132
1159
|
input,
|
|
@@ -1167,22 +1194,28 @@ export async function fetchSupabaseWithDeadline(
|
|
|
1167
1194
|
}
|
|
1168
1195
|
}
|
|
1169
1196
|
|
|
1170
|
-
async function createSupabaseClient() {
|
|
1171
|
-
const { createClient } = await import('@supabase/supabase-js');
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1197
|
+
export async function createSupabaseClient() {
|
|
1198
|
+
const { createClient } = await import('@supabase/supabase-js');
|
|
1199
|
+
const desktopMainOwnsAuth = isTruthy(process.env.LIVEDESK_DESKTOP_HOST);
|
|
1200
|
+
return createClient(SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY, {
|
|
1201
|
+
global: {
|
|
1202
|
+
fetch: fetchSupabaseWithDeadline
|
|
1203
|
+
},
|
|
1204
|
+
...(desktopMainOwnsAuth ? {
|
|
1205
|
+
// Electron Main rotates and encrypts the refresh token. The local
|
|
1206
|
+
// runtime receives only the current in-memory session and asks this
|
|
1207
|
+
// client to attach its access token to database requests.
|
|
1208
|
+
accessToken: async () => desktopMainAuthSession?.access_token || null
|
|
1209
|
+
} : { auth: {
|
|
1210
|
+
autoRefreshToken: false,
|
|
1211
|
+
persistSession: true,
|
|
1212
|
+
detectSessionInUrl: false,
|
|
1213
|
+
flowType: 'pkce',
|
|
1214
|
+
storageKey: CLIENT_AUTH_STORAGE_KEY,
|
|
1215
|
+
storage: createFileStorage(preferredClientAuthPath())
|
|
1216
|
+
} })
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1186
1219
|
|
|
1187
1220
|
function getNestedErrorCode(error) {
|
|
1188
1221
|
let cursor = error;
|
|
@@ -1313,8 +1346,12 @@ async function recoverRotatedSession(supabase, previousSession) {
|
|
|
1313
1346
|
return null;
|
|
1314
1347
|
}
|
|
1315
1348
|
|
|
1316
|
-
async function refreshSessionIfNeededCore(supabase) {
|
|
1317
|
-
|
|
1349
|
+
async function refreshSessionIfNeededCore(supabase) {
|
|
1350
|
+
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
|
|
1351
|
+
const normalized = normalizeRuntimeAuthSession(desktopMainAuthSession, { requireRefreshToken: true });
|
|
1352
|
+
return normalized.ok ? desktopMainAuthSession : null;
|
|
1353
|
+
}
|
|
1354
|
+
let existingSession = null;
|
|
1318
1355
|
try {
|
|
1319
1356
|
const { data: existing } = await supabase.auth.getSession();
|
|
1320
1357
|
existingSession = existing?.session || null;
|
|
@@ -1382,12 +1419,17 @@ function openBrowser(url) {
|
|
|
1382
1419
|
spawn(command, [launchUrl.toString()], { detached: true, stdio: 'ignore' }).unref();
|
|
1383
1420
|
}
|
|
1384
1421
|
|
|
1385
|
-
async function activateSupabaseSession(supabase, session) {
|
|
1386
|
-
const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
|
|
1387
|
-
if (!normalized.ok) {
|
|
1388
|
-
throw new Error(normalized.error);
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1422
|
+
async function activateSupabaseSession(supabase, session) {
|
|
1423
|
+
const normalized = normalizeRuntimeAuthSession(session, { requireRefreshToken: true });
|
|
1424
|
+
if (!normalized.ok) {
|
|
1425
|
+
throw new Error(normalized.error);
|
|
1426
|
+
}
|
|
1427
|
+
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) {
|
|
1428
|
+
const activeSession = acceptDesktopMainAuthSession(session);
|
|
1429
|
+
if (!activeSession) throw new Error('desktop-main-session-required');
|
|
1430
|
+
return activeSession;
|
|
1431
|
+
}
|
|
1432
|
+
const { data: current } = await supabase.auth.getSession();
|
|
1391
1433
|
if (current?.session?.access_token === normalized.session.access_token) {
|
|
1392
1434
|
const activeSession = current.session;
|
|
1393
1435
|
if (!writeSavedSessionToFile(activeSession)) {
|
|
@@ -3173,7 +3215,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
3173
3215
|
|
|
3174
3216
|
if (requestUrl.pathname === '/logout') {
|
|
3175
3217
|
try {
|
|
3176
|
-
await supabase.auth.signOut();
|
|
3218
|
+
await supabase.auth.signOut({ scope: 'local' });
|
|
3177
3219
|
} catch {
|
|
3178
3220
|
}
|
|
3179
3221
|
clearSavedSession();
|
|
@@ -3410,10 +3452,13 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3410
3452
|
roleVersion: process.env.LIVEDESK_ROLE_VERSION,
|
|
3411
3453
|
savedSession: options.savedSession,
|
|
3412
3454
|
loadSavedSession: options.loadSavedSession,
|
|
3413
|
-
initialChoice: options.initialChoice,
|
|
3414
|
-
initialChoiceMessage: options.initialChoiceMessage,
|
|
3415
|
-
|
|
3416
|
-
|
|
3455
|
+
initialChoice: options.initialChoice,
|
|
3456
|
+
initialChoiceMessage: options.initialChoiceMessage,
|
|
3457
|
+
onAuthSessionAccepted: acceptDesktopMainAuthSession,
|
|
3458
|
+
onAuthSessionCleared: clearDesktopMainAuthSession,
|
|
3459
|
+
beginGoogleSignIn: async redirectTo => {
|
|
3460
|
+
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) throw new Error('desktop-main-auth-required');
|
|
3461
|
+
const activeSupabase = await getSupabase();
|
|
3417
3462
|
if (!activeSupabase?.auth) throw new Error('supabase-session-required');
|
|
3418
3463
|
const { data, error } = await activeSupabase.auth.signInWithOAuth({
|
|
3419
3464
|
provider: 'google',
|
|
@@ -3426,9 +3471,10 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
3426
3471
|
if (error) throw error;
|
|
3427
3472
|
if (!data?.url) throw new Error('google-authorization-url-missing');
|
|
3428
3473
|
return { url: data.url };
|
|
3429
|
-
},
|
|
3430
|
-
exchangeGoogleCode: async code => {
|
|
3431
|
-
|
|
3474
|
+
},
|
|
3475
|
+
exchangeGoogleCode: async code => {
|
|
3476
|
+
if (isTruthy(process.env.LIVEDESK_DESKTOP_HOST)) throw new Error('desktop-main-auth-required');
|
|
3477
|
+
const activeSupabase = await getSupabase();
|
|
3432
3478
|
if (!activeSupabase?.auth) throw new Error('supabase-session-required');
|
|
3433
3479
|
const { data, error } = await activeSupabase.auth.exchangeCodeForSession(code);
|
|
3434
3480
|
if (error) throw error;
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livedesk/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.262",
|
|
4
4
|
"description": "VuvoDesk local remote client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
"ws": "^8.18.3"
|
|
43
43
|
},
|
|
44
44
|
"optionalDependencies": {
|
|
45
|
-
"@livedesk/fast-linux-x64": "0.1.
|
|
46
|
-
"@livedesk/fast-osx-arm64": "0.1.
|
|
47
|
-
"@livedesk/fast-osx-x64": "0.1.
|
|
48
|
-
"@livedesk/fast-win-x64": "0.1.
|
|
45
|
+
"@livedesk/fast-linux-x64": "0.1.458",
|
|
46
|
+
"@livedesk/fast-osx-arm64": "0.1.458",
|
|
47
|
+
"@livedesk/fast-osx-x64": "0.1.458",
|
|
48
|
+
"@livedesk/fast-win-x64": "0.1.458"
|
|
49
49
|
},
|
|
50
50
|
"publishConfig": {
|
|
51
51
|
"access": "public"
|
|
@@ -979,9 +979,13 @@ function writeSavedSession(session) {
|
|
|
979
979
|
return true;
|
|
980
980
|
}
|
|
981
981
|
|
|
982
|
-
function clearSavedSession() {
|
|
983
|
-
|
|
984
|
-
}
|
|
982
|
+
function clearSavedSession() {
|
|
983
|
+
if (process.env.LIVEDESK_DESKTOP_HOST === '1') {
|
|
984
|
+
rmSync(CLIENT_AUTH_PATH, { force: true });
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
writePrivateJsonAtomic(CLIENT_AUTH_PATH, {});
|
|
988
|
+
}
|
|
985
989
|
|
|
986
990
|
function readBody(req) {
|
|
987
991
|
return new Promise((resolveBody, reject) => {
|
|
@@ -1316,8 +1320,11 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1316
1320
|
runtime.emit('client.auth.session', { result: state.auth.lastResult, error: message });
|
|
1317
1321
|
};
|
|
1318
1322
|
|
|
1319
|
-
const complete = (choice, message = 'Client credentials accepted. Finding the Hub.') => {
|
|
1320
|
-
|
|
1323
|
+
const complete = (choice, message = 'Client credentials accepted. Finding the Hub.') => {
|
|
1324
|
+
if (choice?.session?.access_token) {
|
|
1325
|
+
options.onAuthSessionAccepted?.(choice.session);
|
|
1326
|
+
}
|
|
1327
|
+
lastChoice = choice || lastChoice;
|
|
1321
1328
|
if (completed) {
|
|
1322
1329
|
if (choice?.session?.access_token) {
|
|
1323
1330
|
const accountProfile = readClientAccountProfile(choice.session);
|
|
@@ -1634,9 +1641,10 @@ export function createClientRuntimeServer(options = {}) {
|
|
|
1634
1641
|
respondJson(200, { ok: true, authenticated: true, persisted: true, role: 'client' });
|
|
1635
1642
|
return;
|
|
1636
1643
|
}
|
|
1637
|
-
if (pathname === '/api/auth/session' && req.method === 'DELETE') {
|
|
1638
|
-
clearSavedSession();
|
|
1639
|
-
|
|
1644
|
+
if (pathname === '/api/auth/session' && req.method === 'DELETE') {
|
|
1645
|
+
clearSavedSession();
|
|
1646
|
+
options.onAuthSessionCleared?.();
|
|
1647
|
+
loggedOut = true;
|
|
1640
1648
|
completed = false;
|
|
1641
1649
|
lastChoice = null;
|
|
1642
1650
|
state.manager = '';
|
|
@@ -130,7 +130,7 @@ if (!ffmpegStaticPath || !existsSync(ffmpegStaticPath)) {
|
|
|
130
130
|
throw new Error('The packaged ffmpeg-static decoder is unavailable.');
|
|
131
131
|
}
|
|
132
132
|
let ffmpegPath = ffmpegStaticPath;
|
|
133
|
-
try {
|
|
133
|
+
try {
|
|
134
134
|
ffmpegPath = String(require('@ffmpeg-installer/ffmpeg')?.path || ffmpegStaticPath);
|
|
135
135
|
} catch {
|
|
136
136
|
// ffmpeg-static remains the portable decoder and compatibility fallback.
|
|
@@ -554,7 +554,7 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
|
|
|
554
554
|
const socket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/input/ws`, {
|
|
555
555
|
perMessageDeflate: false
|
|
556
556
|
});
|
|
557
|
-
const sendInput = async (input, timeoutMs = 5_000) => {
|
|
557
|
+
const sendInput = async (input, timeoutMs = 5_000) => {
|
|
558
558
|
const outcome = waitForWebSocketJson(
|
|
559
559
|
socket,
|
|
560
560
|
message => (
|
|
@@ -577,17 +577,101 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
|
|
|
577
577
|
captureGeneration: binding.captureGeneration
|
|
578
578
|
}
|
|
579
579
|
}));
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
580
|
+
try {
|
|
581
|
+
return await outcome;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`Windows input ${input.type} seq=${input.inputSeq} did not return an outcome: `
|
|
585
|
+
+ `${error instanceof Error ? error.message : String(error)}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const dispatchReset = (reason) => {
|
|
591
|
+
if (socket.readyState !== WebSocket.OPEN) {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
socket.send(JSON.stringify({
|
|
595
|
+
type: 'input',
|
|
596
|
+
deviceId: binding.deviceId,
|
|
597
|
+
fireAndForget: true,
|
|
598
|
+
input: {
|
|
599
|
+
type: 'keyboard.reset',
|
|
600
|
+
inputSeq: 0,
|
|
601
|
+
requestAck: false,
|
|
602
|
+
pressedCodes: [],
|
|
603
|
+
shiftKey: false,
|
|
604
|
+
ctrlKey: false,
|
|
605
|
+
altKey: false,
|
|
606
|
+
metaKey: false,
|
|
607
|
+
repeat: false,
|
|
608
|
+
reason,
|
|
609
|
+
monitorIndex: binding.monitorIndex,
|
|
610
|
+
controlSessionId: binding.sessionId,
|
|
611
|
+
controlCommandId: binding.commandId,
|
|
612
|
+
captureGeneration: binding.captureGeneration
|
|
613
|
+
}
|
|
614
|
+
}));
|
|
615
|
+
return true;
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
let resetDispatched = false;
|
|
619
|
+
try {
|
|
620
|
+
const ready = waitForWebSocketJson(socket, message => message.type === 'RemoteInputSocketReady');
|
|
621
|
+
await Promise.all([waitForWebSocketOpen(socket), ready]);
|
|
622
|
+
const pointer = await sendInput({
|
|
623
|
+
type: 'pointermove',
|
|
624
|
+
inputSeq: 1,
|
|
625
|
+
inputEventId: `windows-physical-pointer-${Date.now()}`,
|
|
626
|
+
normalizedX: 0.5,
|
|
627
|
+
normalizedY: 0.5
|
|
628
|
+
});
|
|
629
|
+
assert.equal(pointer.type, 'RemoteInputApplied', String(pointer.error || 'Windows pointer input failed.'));
|
|
630
|
+
assert.equal(pointer.duplicate, false, 'Windows pointer input was acknowledged as a duplicate.');
|
|
631
|
+
assert.equal(pointer.nativeInputBackend, 'win32-setcursorpos');
|
|
632
|
+
assert.equal(Number(pointer.nativeEventsRequested), 1);
|
|
633
|
+
assert.equal(Number(pointer.nativeEventsAccepted), 1);
|
|
634
|
+
|
|
635
|
+
const shiftDown = await sendInput({
|
|
636
|
+
type: 'keydown',
|
|
637
|
+
inputSeq: 2,
|
|
638
|
+
inputEventId: `windows-physical-shift-down-${Date.now()}`,
|
|
639
|
+
key: 'Shift',
|
|
640
|
+
code: 'ShiftLeft',
|
|
641
|
+
keyCode: 16,
|
|
642
|
+
location: 1,
|
|
643
|
+
pressedCodes: ['ShiftLeft'],
|
|
644
|
+
shiftKey: true,
|
|
645
|
+
ctrlKey: false,
|
|
646
|
+
altKey: false,
|
|
647
|
+
metaKey: false,
|
|
648
|
+
repeat: false
|
|
649
|
+
});
|
|
650
|
+
assert.equal(shiftDown.type, 'RemoteInputApplied', String(shiftDown.error || 'Windows Shift keydown failed.'));
|
|
651
|
+
assert.equal(shiftDown.duplicate, false, 'Windows Shift keydown was acknowledged as a duplicate.');
|
|
652
|
+
|
|
653
|
+
const shiftUp = await sendInput({
|
|
654
|
+
type: 'keyup',
|
|
655
|
+
inputSeq: 3,
|
|
656
|
+
inputEventId: `windows-physical-shift-up-${Date.now()}`,
|
|
657
|
+
key: 'Shift',
|
|
658
|
+
code: 'ShiftLeft',
|
|
659
|
+
keyCode: 16,
|
|
660
|
+
location: 1,
|
|
661
|
+
pressedCodes: [],
|
|
662
|
+
shiftKey: false,
|
|
663
|
+
ctrlKey: false,
|
|
664
|
+
altKey: false,
|
|
665
|
+
metaKey: false,
|
|
666
|
+
repeat: false
|
|
667
|
+
});
|
|
668
|
+
assert.equal(shiftUp.type, 'RemoteInputApplied', String(shiftUp.error || 'Windows Shift keyup failed.'));
|
|
669
|
+
assert.equal(shiftUp.duplicate, false, 'Windows Shift keyup was acknowledged as a duplicate.');
|
|
670
|
+
|
|
671
|
+
const probe = await sendInput({
|
|
672
|
+
type: 'keyboard.probe',
|
|
673
|
+
inputSeq: 4,
|
|
674
|
+
inputEventId: `windows-physical-probe-${Date.now()}`,
|
|
591
675
|
pressedCodes: [],
|
|
592
676
|
shiftKey: false,
|
|
593
677
|
ctrlKey: false,
|
|
@@ -606,26 +690,31 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
|
|
|
606
690
|
/diagnostic keyboard isolation hook did not become ready/i,
|
|
607
691
|
'The forced late hook start did not fail at the bounded ready deadline.'
|
|
608
692
|
);
|
|
609
|
-
const
|
|
610
|
-
type: '
|
|
611
|
-
inputSeq:
|
|
612
|
-
inputEventId: `windows-late-start-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
assert.equal(
|
|
622
|
-
assert.equal(
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
693
|
+
const recovery = await sendInput({
|
|
694
|
+
type: 'pointermove',
|
|
695
|
+
inputSeq: 5,
|
|
696
|
+
inputEventId: `windows-late-start-recovery-${Date.now()}`,
|
|
697
|
+
normalizedX: 0.5,
|
|
698
|
+
normalizedY: 0.5
|
|
699
|
+
});
|
|
700
|
+
assert.equal(
|
|
701
|
+
recovery.type,
|
|
702
|
+
'RemoteInputApplied',
|
|
703
|
+
String(recovery.error || 'Windows input owner did not recover after the bounded hook failure.')
|
|
704
|
+
);
|
|
705
|
+
assert.equal(recovery.nativeInputBackend, 'win32-setcursorpos');
|
|
706
|
+
assert.equal(Number(recovery.nativeEventsRequested), 1);
|
|
707
|
+
assert.equal(Number(recovery.nativeEventsAccepted), 1);
|
|
708
|
+
resetDispatched = dispatchReset('windows-hook-late-start-cleanup');
|
|
709
|
+
assert.equal(resetDispatched, true, 'Windows late-start cleanup reset was not dispatched.');
|
|
710
|
+
return {
|
|
711
|
+
lateStartFailure: true,
|
|
712
|
+
pointerBackend: String(pointer.nativeInputBackend || ''),
|
|
713
|
+
normalKeyAcks: 2,
|
|
714
|
+
error: String(probe.error || ''),
|
|
715
|
+
recoveryApplied: true,
|
|
716
|
+
resetDispatched
|
|
717
|
+
};
|
|
629
718
|
}
|
|
630
719
|
assert.equal(probe.type, 'RemoteInputApplied', String(probe.error || 'Windows native input probe failed.'));
|
|
631
720
|
assert.equal(probe.duplicate, false, 'Windows native input probe was acknowledged as a duplicate.');
|
|
@@ -637,57 +726,28 @@ async function runWindowsInputAckProbe(binding, { expectHookLateStartFailure = f
|
|
|
637
726
|
assert.equal(probe.focusChanged, false, 'Windows native input probe changed the foreground window.');
|
|
638
727
|
assert.equal(probe.inputDiagnostic, true);
|
|
639
728
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
metaKey: false,
|
|
649
|
-
repeat: false,
|
|
650
|
-
reason: 'windows-native-input-diagnostic-cleanup'
|
|
651
|
-
});
|
|
652
|
-
assert.equal(reset.type, 'RemoteInputApplied', String(reset.error || 'Windows native input reset failed.'));
|
|
653
|
-
assert.equal(reset.duplicate, false, 'Windows native input reset was acknowledged as a duplicate.');
|
|
654
|
-
resetApplied = true;
|
|
655
|
-
return {
|
|
656
|
-
backend: String(probe.nativeInputBackend || ''),
|
|
729
|
+
resetDispatched = dispatchReset('windows-native-input-diagnostic-cleanup');
|
|
730
|
+
assert.equal(resetDispatched, true, 'Windows native input cleanup reset was not dispatched.');
|
|
731
|
+
return {
|
|
732
|
+
pointerBackend: String(pointer.nativeInputBackend || ''),
|
|
733
|
+
pointerRequested: Number(pointer.nativeEventsRequested || 0),
|
|
734
|
+
pointerAccepted: Number(pointer.nativeEventsAccepted || 0),
|
|
735
|
+
normalKeyAcks: 2,
|
|
736
|
+
backend: String(probe.nativeInputBackend || ''),
|
|
657
737
|
requested: Number(probe.nativeEventsRequested || 0),
|
|
658
738
|
accepted: Number(probe.nativeEventsAccepted || 0),
|
|
659
739
|
hookObserved: Number(probe.hookEventsObserved || 0),
|
|
660
740
|
hookBlocked: Number(probe.hookEventsBlocked || 0),
|
|
661
741
|
duplicate: probe.duplicate === true,
|
|
662
742
|
focusChanged: probe.focusChanged === true,
|
|
663
|
-
|
|
664
|
-
};
|
|
665
|
-
} finally {
|
|
666
|
-
if (!
|
|
667
|
-
try {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
fireAndForget: true,
|
|
672
|
-
input: {
|
|
673
|
-
type: 'keyboard.reset',
|
|
674
|
-
inputSeq: 0,
|
|
675
|
-
requestAck: false,
|
|
676
|
-
pressedCodes: [],
|
|
677
|
-
shiftKey: false,
|
|
678
|
-
ctrlKey: false,
|
|
679
|
-
altKey: false,
|
|
680
|
-
metaKey: false,
|
|
681
|
-
repeat: false,
|
|
682
|
-
reason: 'windows-native-input-diagnostic-finally',
|
|
683
|
-
monitorIndex: binding.monitorIndex,
|
|
684
|
-
controlSessionId: binding.sessionId,
|
|
685
|
-
controlCommandId: binding.commandId,
|
|
686
|
-
captureGeneration: binding.captureGeneration
|
|
687
|
-
}
|
|
688
|
-
}));
|
|
689
|
-
} catch {
|
|
690
|
-
// Closing the exact input owner also triggers RemoteFast ReleaseAll.
|
|
743
|
+
resetDispatched
|
|
744
|
+
};
|
|
745
|
+
} finally {
|
|
746
|
+
if (!resetDispatched) {
|
|
747
|
+
try {
|
|
748
|
+
dispatchReset('windows-native-input-diagnostic-finally');
|
|
749
|
+
} catch {
|
|
750
|
+
// Closing the exact input owner also triggers RemoteFast ReleaseAll.
|
|
691
751
|
}
|
|
692
752
|
}
|
|
693
753
|
try { socket.close(); } catch {}
|
|
@@ -1704,9 +1764,9 @@ function removeMacDiagnosticSignalHandlers() {
|
|
|
1704
1764
|
macSignalHandlers.clear();
|
|
1705
1765
|
}
|
|
1706
1766
|
|
|
1707
|
-
installMacDiagnosticSignalHandlers();
|
|
1708
|
-
|
|
1709
|
-
try {
|
|
1767
|
+
installMacDiagnosticSignalHandlers();
|
|
1768
|
+
|
|
1769
|
+
inputDiagnostic: try {
|
|
1710
1770
|
macIsolation = await prepareMacDiagnosticIsolation();
|
|
1711
1771
|
if (isMacPhysicalGate) {
|
|
1712
1772
|
macDiagnosticAbort.throwIfRequested();
|
|
@@ -1924,7 +1984,8 @@ try {
|
|
|
1924
1984
|
if (metadata.deviceId !== connectedDeviceId || metadata.frameMode !== 'mode3-h264-hw') return;
|
|
1925
1985
|
const payload = Buffer.from(data.subarray(4 + metadataLength));
|
|
1926
1986
|
frames.push({ metadata, receivedAt: performance.now(), bytes: payload.length, payload });
|
|
1927
|
-
|
|
1987
|
+
const requiredFrameCount = windowsInputAck ? 1 : (isMacPhysicalGate ? 75 : 90);
|
|
1988
|
+
if (frames.length >= requiredFrameCount) {
|
|
1928
1989
|
finish(resolve, frames);
|
|
1929
1990
|
}
|
|
1930
1991
|
};
|
|
@@ -1953,9 +2014,40 @@ try {
|
|
|
1953
2014
|
monitorSelections: { [connectedDeviceId]: requestedMonitorIndex }
|
|
1954
2015
|
}));
|
|
1955
2016
|
|
|
1956
|
-
const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
|
|
1957
|
-
let activeInputBindingFrame = frames.at(-1);
|
|
1958
|
-
|
|
2017
|
+
const frames = await withMacDiagnosticAbort(framePromise, cancelFrameWait);
|
|
2018
|
+
let activeInputBindingFrame = frames.at(-1);
|
|
2019
|
+
if (windowsInputAck) {
|
|
2020
|
+
const inputProbe = await runWindowsInputAckProbe({
|
|
2021
|
+
deviceId: connectedDeviceId,
|
|
2022
|
+
sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
|
|
2023
|
+
commandId: String(activeInputBindingFrame.metadata.commandId || ''),
|
|
2024
|
+
captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
|
|
2025
|
+
monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
|
|
2026
|
+
}, {
|
|
2027
|
+
expectHookLateStartFailure: windowsInputHookLateStart
|
|
2028
|
+
});
|
|
2029
|
+
if (windowsInputHookLateStart) {
|
|
2030
|
+
console.log(
|
|
2031
|
+
`Windows diagnostic hook late-start cleanup OK: expected timeout, `
|
|
2032
|
+
+ `Agent responsive=${inputProbe.recoveryApplied}, reset dispatched=${inputProbe.resetDispatched}.`
|
|
2033
|
+
);
|
|
2034
|
+
} else {
|
|
2035
|
+
console.log(
|
|
2036
|
+
`Windows pointer input ACK: ${inputProbe.pointerAccepted}/${inputProbe.pointerRequested} `
|
|
2037
|
+
+ `via ${inputProbe.pointerBackend}; normal key ACK: ${inputProbe.normalKeyAcks}/2; `
|
|
2038
|
+
+ `isolated modifier ACK: `
|
|
2039
|
+
+ `${inputProbe.accepted}/${inputProbe.requested} via ${inputProbe.backend}, `
|
|
2040
|
+
+ `hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
|
|
2041
|
+
+ `duplicate=${inputProbe.duplicate}, `
|
|
2042
|
+
+ `focusChanged=${inputProbe.focusChanged}, reset dispatched=${inputProbe.resetDispatched}.`
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
// The Windows input gate shares only the minimum current Control binding
|
|
2046
|
+
// setup with the video smoke. Encoder profile and soak assertions belong
|
|
2047
|
+
// to their own commands and must not obscure native input evidence.
|
|
2048
|
+
break inputDiagnostic;
|
|
2049
|
+
}
|
|
2050
|
+
const initialCaptureIdentity = switchMonitor && isWindows
|
|
1959
2051
|
? await waitFor(
|
|
1960
2052
|
() => findWindowsCaptureIdentity(agentLogs, requestedMonitorIndex),
|
|
1961
2053
|
3_000,
|
|
@@ -2426,31 +2518,6 @@ try {
|
|
|
2426
2518
|
console.log(`Mode 3 monitor transition OK: ${requestedMonitorIndex} -> ${nextMonitorIndex}, first frame ${switchedFirstFrameMs.toFixed(0)}ms, generation ${previousCaptureGeneration} -> ${firstSwitchedFrame.metadata.captureGeneration}.`);
|
|
2427
2519
|
}
|
|
2428
2520
|
|
|
2429
|
-
if (windowsInputAck) {
|
|
2430
|
-
const inputProbe = await runWindowsInputAckProbe({
|
|
2431
|
-
deviceId: connectedDeviceId,
|
|
2432
|
-
sessionId: String(activeInputBindingFrame.metadata.sessionId || ''),
|
|
2433
|
-
commandId: String(activeInputBindingFrame.metadata.commandId || ''),
|
|
2434
|
-
captureGeneration: Number(activeInputBindingFrame.metadata.captureGeneration || 0),
|
|
2435
|
-
monitorIndex: Number(activeInputBindingFrame.metadata.monitorIndex || 0)
|
|
2436
|
-
}, {
|
|
2437
|
-
expectHookLateStartFailure: windowsInputHookLateStart
|
|
2438
|
-
});
|
|
2439
|
-
if (windowsInputHookLateStart) {
|
|
2440
|
-
console.log(
|
|
2441
|
-
`Windows diagnostic hook late-start cleanup OK: expected timeout, `
|
|
2442
|
-
+ `Agent responsive, reset=${inputProbe.resetApplied}.`
|
|
2443
|
-
);
|
|
2444
|
-
} else {
|
|
2445
|
-
console.log(
|
|
2446
|
-
`Windows native modifier input ACK: ${inputProbe.accepted}/${inputProbe.requested} `
|
|
2447
|
-
+ `via ${inputProbe.backend}, hook=${inputProbe.hookObserved}/${inputProbe.hookBlocked}, `
|
|
2448
|
-
+ `duplicate=${inputProbe.duplicate}, `
|
|
2449
|
-
+ `focusChanged=${inputProbe.focusChanged}, reset=${inputProbe.resetApplied}.`
|
|
2450
|
-
);
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
|
|
2454
2521
|
if (transitionToMode5) {
|
|
2455
2522
|
const previousFrameSocket = frameSocket;
|
|
2456
2523
|
frameSocket = new WebSocket(`ws://127.0.0.1:${httpPort}/api/remote/frames/ws?devices=${encodeURIComponent(connectedDeviceId)}`);
|