ispyx 1.1.1 → 1.1.3

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.
@@ -68,7 +68,8 @@ try {
68
68
  # Already loaded by LogonUI from a previous install: keep it if it is byte-identical.
69
69
  if (-not (Test-Path $installedDll) -or (Get-FileHash $installedDll).Hash -ne (Get-FileHash $DllPath).Hash) { throw }
70
70
  }
71
- regsvr32.exe /s $installedDll 2>$null
71
+ # regsvr32 is a GUI-subsystem exe: a bare call returns before it finishes, so the read-back below raced it.
72
+ Start-Process -FilePath regsvr32.exe -ArgumentList '/s', "`"$installedDll`"" -Wait -WindowStyle Hidden
72
73
  $actual = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Classes\CLSID\{1aa7e1e6-21de-4ad5-ae6e-454401b4b427}\InprocServer32' -Name '(default)' -ErrorAction SilentlyContinue).'(default)'
73
74
  if ($actual -ne $installedDll) {
74
75
  throw "Registration did not take effect (registry has '$actual', expected '$installedDll')."
@@ -8,7 +8,7 @@ Set-StrictMode -Version 2
8
8
  $ErrorActionPreference = 'Continue' # best effort: still clean up what we can if one step fails
9
9
 
10
10
  $root = 'C:\ProgramData\ispyx'
11
- regsvr32.exe /u /s (Join-Path $root 'ispyx_unlock.dll') 2>$null
11
+ Start-Process -FilePath regsvr32.exe -ArgumentList '/u', '/s', "`"$(Join-Path $root 'ispyx_unlock.dll')`"" -Wait -WindowStyle Hidden
12
12
  Remove-Item -Path (Join-Path $root 'unlock.secret') -Force -ErrorAction SilentlyContinue
13
13
  Remove-Item -Path (Join-Path $root 'unlock.signal') -Force -ErrorAction SilentlyContinue
14
14
  # May be held open by a running LogonUI until the next lock cycle; harmless once unregistered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispyx",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "Local desktop-control MCP server: lets an AI use your keyboard, mouse and screen (windows, screenshots, clicks, typing, clipboard). stdio only, Windows and macOS.",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -107,13 +107,19 @@ export async function runUpdate() {
107
107
  // quietly: the `ispyx mcp` forwarders stay connected and reach the new resident on their next call
108
108
  try { await callOwner(pipe, '__restart', {}); } catch { /* gone before answering */ }
109
109
  for (let i = 0; i < 40 && await residentRunning(pipe); i++) await sleep(150);
110
+ // the pipe frees before the process has finished exiting; it still holds koffi.node (EBUSY) until then
111
+ await sleep(2000);
110
112
  }
111
113
  step('Installing the latest ispyx (npm install -g ispyx@latest)');
112
- const code = await new Promise((resolve) => {
113
- const c = spawn('npm', ['install', '-g', 'ispyx@latest'], { stdio: 'inherit', shell: true });
114
- c.on('error', () => resolve(1));
115
- c.on('exit', (n) => resolve(n ?? 1));
116
- });
114
+ let code = 1;
115
+ for (let attempt = 0; attempt < 3 && code !== 0; attempt++) {
116
+ if (attempt) { step('Retrying (a file was still in use)'); await sleep(2500); }
117
+ code = await new Promise((resolve) => {
118
+ const c = spawn('npm', ['install', '-g', 'ispyx@latest'], { stdio: 'inherit', shell: true });
119
+ c.on('error', () => resolve(1));
120
+ c.on('exit', (n) => resolve(n ?? 1));
121
+ });
122
+ }
117
123
  if (code !== 0) process.stderr.write('ispyx: the update failed (see the npm output above).\n');
118
124
  // Start the new copy: npm replaced the files in place, so a fresh process loads them.
119
125
  if (wasRunning) { step('Starting ispyx'); spawnResident(); }
@@ -8,6 +8,9 @@ const advapi32 = koffi.load('advapi32.dll');
8
8
  const dwmapi = koffi.load('dwmapi.dll');
9
9
  const ole32 = koffi.load('ole32.dll');
10
10
  const winmm = koffi.load('winmm.dll');
11
+ const wtsapi32 = koffi.load('wtsapi32.dll');
12
+ const wtsQuery = wtsapi32.func('bool WTSQuerySessionInformationW(intptr_t server, uint32 session, int cls, _Out_ void **buf, _Out_ uint32_t *len)');
13
+ const wtsFree = wtsapi32.func('void WTSFreeMemory(void *p)');
11
14
  const timeBeginPeriod = winmm.func('uint32 timeBeginPeriod(uint32 ms)');
12
15
 
13
16
  export const RECT = koffi.struct('RECT', { left: 'int32', top: 'int32', right: 'int32', bottom: 'int32' });
@@ -236,12 +239,26 @@ export const rootWindowAt = (x, y) => {
236
239
  };
237
240
  export const foregroundWindow = () => f.GetForegroundWindow();
238
241
 
242
+ // WTSINFOEX_LEVEL1 for the current session: int32 at index 4 is SessionFlags (0 = locked, 1 = unlocked).
243
+ function sessionFlagsLocked() {
244
+ const out = [null];
245
+ const len = [0];
246
+ if (!wtsQuery(0, 0xFFFFFFFF, 25, out, len) || !out[0]) return false;
247
+ try {
248
+ return koffi.decode(out[0], koffi.array('int32_t', 5))[4] === 0;
249
+ } finally {
250
+ wtsFree(out[0]);
251
+ }
252
+ }
253
+
239
254
  export function isScreenLocked() {
240
255
  // The input desktop cannot be opened while the secure (lock / UAC) desktop is active.
241
256
  const d = f.OpenInputDesktop(0, false, 0x100);
242
257
  if (!d) return true;
243
258
  f.CloseDesktop(d);
244
- return false;
259
+ // The lock screen's cover (clock and widgets, before the sign-in tiles) does not switch desktops, so the
260
+ // check above passes while it is showing; the session's own lock flag does not have that blind spot.
261
+ return sessionFlagsLocked();
245
262
  }
246
263
 
247
264
  export function windowDpi(hwnd) {