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
|
|
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')
|
|
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.
|
|
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",
|
package/src/core/launcher.js
CHANGED
|
@@ -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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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) {
|