@8bitbish/screenshot-service 1.3.15 → 1.5.0
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/bin/setup.js +17 -21
- package/dist/dvtHelper.d.ts +33 -0
- package/dist/dvtHelper.d.ts.map +1 -0
- package/dist/dvtHelper.js +204 -0
- package/dist/dvtHelper.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +129 -64
- package/dist/index.js.map +1 -1
- package/dist/python/pmd3_helper.py +211 -0
- package/dist/setup/detect.d.ts.map +1 -1
- package/dist/setup/detect.js +17 -7
- package/dist/setup/detect.js.map +1 -1
- package/dist/setup/index.d.ts +2 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +6 -1
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/install.d.ts +1 -1
- package/dist/setup/install.d.ts.map +1 -1
- package/dist/setup/install.js +14 -12
- package/dist/setup/install.js.map +1 -1
- package/dist/setup/python.d.ts +13 -0
- package/dist/setup/python.d.ts.map +1 -1
- package/dist/setup/python.js +78 -5
- package/dist/setup/python.js.map +1 -1
- package/dist/setup/tunneld.d.ts +57 -0
- package/dist/setup/tunneld.d.ts.map +1 -0
- package/dist/setup/tunneld.js +198 -0
- package/dist/setup/tunneld.js.map +1 -0
- package/dist/setup/types.d.ts +1 -0
- package/dist/setup/types.d.ts.map +1 -1
- package/dist/wizard/renderer/index.html +2 -2
- package/package.json +5 -2
package/bin/setup.js
CHANGED
|
@@ -104,33 +104,29 @@ async function runSetupIos() {
|
|
|
104
104
|
ok('Installed: ' + python);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
`printf '%s\\n' '${rule}' > '${sudoersFile}'`,
|
|
122
|
-
`chmod 440 '${sudoersFile}'`,
|
|
123
|
-
].join('\n'), { mode: 0o755 });
|
|
107
|
+
// Background tunnel service. Not optional any more: checkIosSetup() keys
|
|
108
|
+
// `ok` off the daemon, and the old on-demand path needed a password that a
|
|
109
|
+
// non-interactive host app can never supply.
|
|
110
|
+
step(3, 'Background tunnel service');
|
|
111
|
+
const { tunnelDaemonStatus, tunnelDaemonInstallScript } = require('../dist/setup');
|
|
112
|
+
if (tunnelDaemonStatus().ok) {
|
|
113
|
+
ok('Background tunnel service already installed');
|
|
114
|
+
} else {
|
|
115
|
+
console.log(' The device tunnel needs root. Installing it as a launchd service means it');
|
|
116
|
+
console.log(' starts at boot, restarts itself if it wedges, and captures never wait on');
|
|
117
|
+
console.log(' a password. macOS will ask for your password once.');
|
|
118
|
+
console.log('');
|
|
119
|
+
const scriptPath = path.join(tmpdir(), 'screenshot-service-tunneld-setup.sh');
|
|
120
|
+
writeFileSync(scriptPath, '#!/bin/bash\nset -e\n' + tunnelDaemonInstallScript() + '\n', { mode: 0o755 });
|
|
124
121
|
try {
|
|
125
122
|
execSync(`osascript -e 'do shell script "${scriptPath}" with administrator privileges'`, { stdio: 'pipe' });
|
|
126
|
-
ok('
|
|
123
|
+
ok('Background tunnel service installed');
|
|
127
124
|
} catch {
|
|
128
|
-
warn('
|
|
125
|
+
warn('Tunnel service setup was cancelled — iPhone capture will not work until it is installed.');
|
|
126
|
+
warn('Re-run this command, or use Device management → iOS in the app.');
|
|
129
127
|
} finally {
|
|
130
128
|
try { unlinkSync(scriptPath); } catch {}
|
|
131
129
|
}
|
|
132
|
-
} else {
|
|
133
|
-
log(C.dim + ' Skipped. You will be prompted for your Mac password the first time per session.' + C.reset);
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
// Device instructions
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare class DvtHelper {
|
|
2
|
+
private proc;
|
|
3
|
+
private ready;
|
|
4
|
+
private pending;
|
|
5
|
+
private nextId;
|
|
6
|
+
private buf;
|
|
7
|
+
/** Sticky: once the worker has proven unusable, stop paying to retry it. */
|
|
8
|
+
private disabled;
|
|
9
|
+
private reset;
|
|
10
|
+
private start;
|
|
11
|
+
private onData;
|
|
12
|
+
private send;
|
|
13
|
+
/** True when the worker is usable. Never throws — callers fall back. */
|
|
14
|
+
available(): Promise<boolean>;
|
|
15
|
+
/** Connect up front so the first capture doesn't pay the handshake. */
|
|
16
|
+
warm(udid?: string | null): Promise<void>;
|
|
17
|
+
screenshot(path: string, udid?: string | null): Promise<Record<string, unknown>>;
|
|
18
|
+
foregroundApp(udid?: string | null): Promise<Record<string, unknown>>;
|
|
19
|
+
/**
|
|
20
|
+
* Screenshot and foreground-app together. Issued CONCURRENTLY on purpose:
|
|
21
|
+
* one DVT connection multiplexes channels, so proclist overlaps the image
|
|
22
|
+
* transfer and is close to free. Doing this with two CLI processes instead
|
|
23
|
+
* silently yields no image — the sessions collide.
|
|
24
|
+
*/
|
|
25
|
+
capture(path: string, udid?: string | null, wantForegroundApp?: boolean): Promise<{
|
|
26
|
+
bundleId: string;
|
|
27
|
+
name: string;
|
|
28
|
+
} | null>;
|
|
29
|
+
stop(): void;
|
|
30
|
+
}
|
|
31
|
+
export declare const dvtHelper: DvtHelper;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=dvtHelper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dvtHelper.d.ts","sourceRoot":"","sources":["../dvtHelper.ts"],"names":[],"mappings":"AAqCA,cAAM,SAAS;IACb,OAAO,CAAC,IAAI,CAA6B;IACzC,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,OAAO,CAA8B;IAC7C,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,GAAG,CAAM;IACjB,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAS;IAEzB,OAAO,CAAC,KAAK;YAcC,KAAK;IAyCnB,OAAO,CAAC,MAAM;IAyBd,OAAO,CAAC,IAAI;IAiBZ,wEAAwE;IAClE,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;IAenC,uEAAuE;IACjE,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAK7C,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAKxC;;;;;OAKG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,EAAE,iBAAiB,UAAO;kBAQ9C,MAAM;cAAQ,MAAM;;IAGhD,IAAI;CAKL;AAED,eAAO,MAAM,SAAS,WAAkB,CAAC"}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── Persistent pymobiledevice3 worker ────────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// Client for python/pmd3_helper.py. See that file for why it exists; briefly:
|
|
5
|
+
// every pmd3 CLI call pays ~1.8s of import + handshake before doing ~1s of
|
|
6
|
+
// work, and a capture needs two. Holding one session takes a screenshot plus
|
|
7
|
+
// foreground-app lookup from ~6.8s to ~2.5s.
|
|
8
|
+
//
|
|
9
|
+
// This is an OPTIMISATION, never a requirement. Every failure path falls back
|
|
10
|
+
// to the CLI, so a machine where the worker can't start is slow, not broken.
|
|
11
|
+
// Set SCREENSHOT_SERVICE_NO_HELPER=1 to force the CLI path.
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.dvtHelper = void 0;
|
|
14
|
+
const child_process_1 = require("child_process");
|
|
15
|
+
const fs_1 = require("fs");
|
|
16
|
+
const path_1 = require("path");
|
|
17
|
+
const logger_1 = require("./logger");
|
|
18
|
+
const python_1 = require("./setup/python");
|
|
19
|
+
/** Worker startup: interpreter boot + pymobiledevice3 import. */
|
|
20
|
+
const READY_TIMEOUT_MS = 15000;
|
|
21
|
+
/** Per-request ceiling. Screenshots measured ~2.5s; this is generous. */
|
|
22
|
+
const REQUEST_TIMEOUT_MS = 30000;
|
|
23
|
+
function helperScriptPath() {
|
|
24
|
+
// dist/dvtHelper.js -> dist/python/pmd3_helper.py (copied by copy-assets.js).
|
|
25
|
+
const candidates = [
|
|
26
|
+
(0, path_1.join)(__dirname, "python", "pmd3_helper.py"),
|
|
27
|
+
(0, path_1.join)(__dirname, "..", "python", "pmd3_helper.py"),
|
|
28
|
+
];
|
|
29
|
+
return candidates.find(fs_1.existsSync) ?? null;
|
|
30
|
+
}
|
|
31
|
+
class DvtHelper {
|
|
32
|
+
constructor() {
|
|
33
|
+
this.proc = null;
|
|
34
|
+
this.ready = null;
|
|
35
|
+
this.pending = new Map();
|
|
36
|
+
this.nextId = 1;
|
|
37
|
+
this.buf = "";
|
|
38
|
+
/** Sticky: once the worker has proven unusable, stop paying to retry it. */
|
|
39
|
+
this.disabled = false;
|
|
40
|
+
}
|
|
41
|
+
reset(reason) {
|
|
42
|
+
const proc = this.proc;
|
|
43
|
+
this.proc = null;
|
|
44
|
+
this.ready = null;
|
|
45
|
+
for (const [id, p] of this.pending) {
|
|
46
|
+
clearTimeout(p.timer);
|
|
47
|
+
p.reject(new Error(`dvt helper ${reason}`));
|
|
48
|
+
this.pending.delete(id);
|
|
49
|
+
}
|
|
50
|
+
if (proc) {
|
|
51
|
+
try {
|
|
52
|
+
proc.kill();
|
|
53
|
+
}
|
|
54
|
+
catch { /* already gone */ }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async start() {
|
|
58
|
+
if (this.ready)
|
|
59
|
+
return this.ready;
|
|
60
|
+
this.ready = (async () => {
|
|
61
|
+
const script = helperScriptPath();
|
|
62
|
+
if (!script)
|
|
63
|
+
throw new Error("pmd3_helper.py not found in package");
|
|
64
|
+
const python = await (0, python_1.findPythonAsync)();
|
|
65
|
+
if (!python)
|
|
66
|
+
throw new Error("no python with pymobiledevice3");
|
|
67
|
+
const proc = (0, child_process_1.spawn)(python, [script], { stdio: ["pipe", "pipe", "pipe"] });
|
|
68
|
+
this.proc = proc;
|
|
69
|
+
// pymobiledevice3 logs to stderr; drain it so the pipe can't fill and
|
|
70
|
+
// block the child, but don't treat it as failure.
|
|
71
|
+
proc.stderr?.on("data", () => { });
|
|
72
|
+
proc.on("exit", (code) => {
|
|
73
|
+
if (this.proc === proc)
|
|
74
|
+
this.reset(`exited (code ${code})`);
|
|
75
|
+
});
|
|
76
|
+
proc.on("error", (err) => {
|
|
77
|
+
if (this.proc === proc)
|
|
78
|
+
this.reset(`spawn error: ${err.message}`);
|
|
79
|
+
});
|
|
80
|
+
await new Promise((resolve, reject) => {
|
|
81
|
+
const timer = setTimeout(() => reject(new Error("helper did not report ready in time")), READY_TIMEOUT_MS);
|
|
82
|
+
const onReady = () => { clearTimeout(timer); resolve(); };
|
|
83
|
+
proc.stdout?.on("data", (chunk) => this.onData(chunk, onReady));
|
|
84
|
+
});
|
|
85
|
+
})();
|
|
86
|
+
try {
|
|
87
|
+
await this.ready;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
this.reset("failed to start");
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
return this.ready;
|
|
94
|
+
}
|
|
95
|
+
onData(chunk, onReady) {
|
|
96
|
+
this.buf += chunk.toString();
|
|
97
|
+
let nl;
|
|
98
|
+
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
99
|
+
const line = this.buf.slice(0, nl);
|
|
100
|
+
this.buf = this.buf.slice(nl + 1);
|
|
101
|
+
if (!line.trim())
|
|
102
|
+
continue;
|
|
103
|
+
let msg;
|
|
104
|
+
try {
|
|
105
|
+
msg = JSON.parse(line);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
continue; // stray output — not ours
|
|
109
|
+
}
|
|
110
|
+
if (msg.ready === true) {
|
|
111
|
+
onReady?.();
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const id = msg.id;
|
|
115
|
+
if (id === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
const p = this.pending.get(id);
|
|
118
|
+
if (!p)
|
|
119
|
+
continue;
|
|
120
|
+
this.pending.delete(id);
|
|
121
|
+
clearTimeout(p.timer);
|
|
122
|
+
if (msg.ok === true)
|
|
123
|
+
p.resolve(msg);
|
|
124
|
+
else
|
|
125
|
+
p.reject(new Error(String(msg.error ?? "helper error")));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
send(action, extra = {}) {
|
|
129
|
+
const proc = this.proc;
|
|
130
|
+
if (!proc?.stdin)
|
|
131
|
+
return Promise.reject(new Error("dvt helper not running"));
|
|
132
|
+
const id = this.nextId++;
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
this.pending.delete(id);
|
|
136
|
+
// A timed-out worker is presumed wedged; drop it so the next call
|
|
137
|
+
// starts clean rather than queueing behind a dead session.
|
|
138
|
+
this.reset("request timed out");
|
|
139
|
+
reject(new Error(`dvt helper timed out after ${REQUEST_TIMEOUT_MS}ms`));
|
|
140
|
+
}, REQUEST_TIMEOUT_MS);
|
|
141
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
142
|
+
proc.stdin.write(JSON.stringify({ id, action, ...extra }) + "\n");
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** True when the worker is usable. Never throws — callers fall back. */
|
|
146
|
+
async available() {
|
|
147
|
+
if (this.disabled)
|
|
148
|
+
return false;
|
|
149
|
+
if (process.env.SCREENSHOT_SERVICE_NO_HELPER)
|
|
150
|
+
return false;
|
|
151
|
+
try {
|
|
152
|
+
await this.start();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
this.disabled = true;
|
|
157
|
+
logger_1.log.warn("dvt helper unavailable; using the CLI path", {
|
|
158
|
+
reason: e instanceof Error ? e.message : String(e),
|
|
159
|
+
});
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Connect up front so the first capture doesn't pay the handshake. */
|
|
164
|
+
async warm(udid) {
|
|
165
|
+
if (!(await this.available()))
|
|
166
|
+
return;
|
|
167
|
+
await this.send("ping", udid ? { udid } : {}).catch(() => { });
|
|
168
|
+
}
|
|
169
|
+
async screenshot(path, udid) {
|
|
170
|
+
await this.start();
|
|
171
|
+
return this.send("screenshot", { path, ...(udid ? { udid } : {}) });
|
|
172
|
+
}
|
|
173
|
+
async foregroundApp(udid) {
|
|
174
|
+
await this.start();
|
|
175
|
+
return this.send("foregroundApp", udid ? { udid } : {});
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Screenshot and foreground-app together. Issued CONCURRENTLY on purpose:
|
|
179
|
+
* one DVT connection multiplexes channels, so proclist overlaps the image
|
|
180
|
+
* transfer and is close to free. Doing this with two CLI processes instead
|
|
181
|
+
* silently yields no image — the sessions collide.
|
|
182
|
+
*/
|
|
183
|
+
async capture(path, udid, wantForegroundApp = true) {
|
|
184
|
+
await this.start();
|
|
185
|
+
const shot = this.send("screenshot", { path, ...(udid ? { udid } : {}) });
|
|
186
|
+
const fg = wantForegroundApp
|
|
187
|
+
? this.send("foregroundApp", udid ? { udid } : {}).catch(() => null)
|
|
188
|
+
: Promise.resolve(null);
|
|
189
|
+
const [, fgRes] = await Promise.all([shot, fg]);
|
|
190
|
+
const app = fgRes?.app ?? null;
|
|
191
|
+
return app;
|
|
192
|
+
}
|
|
193
|
+
stop() {
|
|
194
|
+
if (!this.proc)
|
|
195
|
+
return;
|
|
196
|
+
try {
|
|
197
|
+
this.proc.stdin?.write(JSON.stringify({ action: "quit" }) + "\n");
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
this.reset("stopped");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
exports.dvtHelper = new DvtHelper();
|
|
204
|
+
//# sourceMappingURL=dvtHelper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dvtHelper.js","sourceRoot":"","sources":["../dvtHelper.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAOA,OAAO,EAAwH,UAAU,EAAE,aAAa,EAA0C,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,12 @@ const path_1 = require("path");
|
|
|
26
26
|
const os_1 = require("os");
|
|
27
27
|
const device_1 = require("./device");
|
|
28
28
|
const logger_1 = require("./logger");
|
|
29
|
+
// Shared resolver — index.ts used to keep its own copy with a SHORTER candidate
|
|
30
|
+
// list, so a Mac with an unlinked (keg-only) brew python passed detection and
|
|
31
|
+
// then failed every capture. One list, one cache, one behaviour.
|
|
32
|
+
const python_1 = require("./setup/python");
|
|
33
|
+
const dvtHelper_1 = require("./dvtHelper");
|
|
34
|
+
const tunneld_1 = require("./setup/tunneld");
|
|
29
35
|
var logger_2 = require("./logger");
|
|
30
36
|
Object.defineProperty(exports, "setLogger", { enumerable: true, get: function () { return logger_2.setLogger; } });
|
|
31
37
|
const android_device_1 = require("./android-device");
|
|
@@ -44,30 +50,6 @@ var wizard_1 = require("./wizard");
|
|
|
44
50
|
Object.defineProperty(exports, "createSetupWindow", { enumerable: true, get: function () { return wizard_1.createSetupWindow; } });
|
|
45
51
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
46
52
|
const PMD3 = ["-m", "pymobiledevice3"];
|
|
47
|
-
// ── Python detection ──────────────────────────────────────────────────────────
|
|
48
|
-
function findPython() {
|
|
49
|
-
// pymobiledevice3 requires Python 3.11+. Prefer newer versions, then fall back to PATH `python3`.
|
|
50
|
-
const candidates = [
|
|
51
|
-
"/opt/homebrew/bin/python3.14",
|
|
52
|
-
"/opt/homebrew/bin/python3.13",
|
|
53
|
-
"/opt/homebrew/bin/python3.12",
|
|
54
|
-
"/opt/homebrew/bin/python3.11",
|
|
55
|
-
"/usr/local/bin/python3.14",
|
|
56
|
-
"/usr/local/bin/python3.13",
|
|
57
|
-
"/usr/local/bin/python3.12",
|
|
58
|
-
"/usr/local/bin/python3.11",
|
|
59
|
-
"python3",
|
|
60
|
-
];
|
|
61
|
-
const probe = "import sys, pymobiledevice3; sys.exit(0 if sys.version_info >= (3, 11) else 1)";
|
|
62
|
-
for (const p of candidates) {
|
|
63
|
-
try {
|
|
64
|
-
(0, child_process_1.execFileSync)(p, ["-c", probe], { stdio: "ignore" });
|
|
65
|
-
return p;
|
|
66
|
-
}
|
|
67
|
-
catch { }
|
|
68
|
-
}
|
|
69
|
-
throw new Error("Python 3.11+ with pymobiledevice3 not found. Run: npx @8bitbish/screenshot-service setup-ios");
|
|
70
|
-
}
|
|
71
53
|
// ── Tunnel management ─────────────────────────────────────────────────────────
|
|
72
54
|
/**
|
|
73
55
|
* Process-liveness check only — does NOT confirm tunneld's HTTP server is
|
|
@@ -114,35 +96,79 @@ function killTunneldProcess() {
|
|
|
114
96
|
// -n means "non-interactive" — sudo fails immediately if a password is needed.
|
|
115
97
|
return !isTunnelRunning();
|
|
116
98
|
}
|
|
99
|
+
/** Poll the health endpoint until it answers, or the budget runs out. */
|
|
100
|
+
async function waitForHealthy(budgetMs) {
|
|
101
|
+
const deadline = Date.now() + budgetMs;
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
if (await isTunnelHealthy())
|
|
104
|
+
return true;
|
|
105
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Ask launchd to kill and respawn the tunnel daemon. `kickstart -k` handles
|
|
111
|
+
* the zombie case (alive process, dead HTTP server) in one step, and the
|
|
112
|
+
* setup wizard installs a sudoers rule permitting exactly this one command
|
|
113
|
+
* without a password — so unlike the old `sudo pkill`, it works from a
|
|
114
|
+
* non-interactive host app.
|
|
115
|
+
*
|
|
116
|
+
* A failed kickstart is not fatal: KeepAlive may already be respawning the
|
|
117
|
+
* job, so we poll regardless and let the health check decide.
|
|
118
|
+
*/
|
|
119
|
+
async function kickstartTunnelDaemon() {
|
|
120
|
+
try {
|
|
121
|
+
(0, child_process_1.execFileSync)("sudo", ["-n", "/bin/launchctl", "kickstart", "-k", `system/${tunneld_1.TUNNELD_LABEL}`], {
|
|
122
|
+
stdio: "ignore",
|
|
123
|
+
timeout: 5000,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
logger_1.log.warn("launchctl kickstart failed (no sudoers grant, or launchd refused); relying on KeepAlive", {
|
|
128
|
+
label: tunneld_1.TUNNELD_LABEL,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// Must exceed ThrottleInterval: launchd may hold a respawn for the full
|
|
132
|
+
// throttle window, and a shorter budget would report failure a moment
|
|
133
|
+
// before the daemon returns. Throttle + 5s covers the tunnel's own startup.
|
|
134
|
+
return waitForHealthy((tunneld_1.TUNNELD_THROTTLE_SECONDS + 5) * 1000);
|
|
135
|
+
}
|
|
117
136
|
async function startTunnel() {
|
|
118
|
-
// Fast path:
|
|
137
|
+
// Fast path: a healthy tunnel — no work to do. On a machine with the
|
|
138
|
+
// LaunchDaemon this is the ONLY branch that ever runs, because launchd
|
|
139
|
+
// brought the tunnel up at boot.
|
|
119
140
|
if (isTunnelRunning() && await isTunnelHealthy())
|
|
120
141
|
return;
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
142
|
+
// ── Managed daemon ────────────────────────────────────────────────────────
|
|
143
|
+
// launchd owns the tunnel's lifecycle. We never spawn it ourselves and never
|
|
144
|
+
// invoke sudo on the capture path; the worst case is asking launchd to
|
|
145
|
+
// recycle a wedged job.
|
|
146
|
+
const daemon = (0, tunneld_1.tunnelDaemonStatus)();
|
|
147
|
+
if (daemon.present) {
|
|
148
|
+
logger_1.log.warn("tunnel unhealthy; asking launchd to recycle the daemon", { label: tunneld_1.TUNNELD_LABEL });
|
|
149
|
+
if (await kickstartTunnelDaemon())
|
|
150
|
+
return;
|
|
151
|
+
throw new Error("The device tunnel isn't responding and launchd couldn't restart it. " +
|
|
152
|
+
`See ${tunneld_1.TUNNELD_LOG_PATH}, or re-run iOS setup from the app to reinstall the tunnel service.`);
|
|
153
|
+
}
|
|
154
|
+
// ── Legacy on-demand path ─────────────────────────────────────────────────
|
|
155
|
+
// Machines set up before the daemon existed. Kept working, but every branch
|
|
156
|
+
// here needs root and cannot prompt for a password, which is precisely why
|
|
157
|
+
// the daemon replaced it — so the errors point at setup, not at a log file.
|
|
131
158
|
if (isTunnelRunning()) {
|
|
132
159
|
logger_1.log.warn("tunneld process is alive but HTTP endpoint unreachable; attempting restart", { port: 49151 });
|
|
133
160
|
const killed = killTunneldProcess();
|
|
134
161
|
if (!killed) {
|
|
135
|
-
const err = new Error("The
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
"
|
|
162
|
+
const err = new Error("The device tunnel is unresponsive and running as root, so this app can't restart it. " +
|
|
163
|
+
"Re-run iOS setup from the app (Device management → iOS) to install the background tunnel " +
|
|
164
|
+
"service, which recovers from this automatically. To unblock right now, run:\n" +
|
|
165
|
+
" sudo pkill -f 'pymobiledevice3 remote tunneld'");
|
|
139
166
|
logger_1.log.error("tunneld zombie detected; passwordless kill failed", err);
|
|
140
167
|
throw err;
|
|
141
168
|
}
|
|
142
|
-
// Give the OS a moment to reap the process before spawning a replacement.
|
|
143
169
|
await new Promise((r) => setTimeout(r, 500));
|
|
144
170
|
}
|
|
145
|
-
const python =
|
|
171
|
+
const python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
146
172
|
// Open the log file synchronously so spawn gets a real fd. Using
|
|
147
173
|
// createWriteStream here is racy — the stream's fd is null until the async
|
|
148
174
|
// 'open' fires, and spawn rejects WriteStreams with fd: null as invalid stdio.
|
|
@@ -151,20 +177,15 @@ async function startTunnel() {
|
|
|
151
177
|
detached: true,
|
|
152
178
|
stdio: ["ignore", logFd, logFd],
|
|
153
179
|
});
|
|
154
|
-
// Spawn dup'd the fd for the child; close our parent-side copy to avoid leaking.
|
|
155
180
|
(0, fs_1.closeSync)(logFd);
|
|
156
181
|
child.unref();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
while (Date.now() < deadline) {
|
|
161
|
-
if (await isTunnelHealthy()) {
|
|
162
|
-
registerExitHandler();
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
182
|
+
if (await waitForHealthy(10000)) {
|
|
183
|
+
registerExitHandler();
|
|
184
|
+
return;
|
|
166
185
|
}
|
|
167
|
-
throw new Error("
|
|
186
|
+
throw new Error("The device tunnel didn't start. This Mac still uses the old on-demand tunnel, which needs a " +
|
|
187
|
+
"password this app can't ask for. Re-run iOS setup from the app (Device management → iOS) to " +
|
|
188
|
+
`install the background tunnel service. Details: ${(0, path_1.join)((0, os_1.homedir)(), ".tunneld.log")}`);
|
|
168
189
|
}
|
|
169
190
|
/** Manually stop the tunnel. */
|
|
170
191
|
function stopTunnel() {
|
|
@@ -353,12 +374,30 @@ async function getDeviceInfoCached(python, udid) {
|
|
|
353
374
|
deviceInfoCache.set(key, { info, ts: Date.now() });
|
|
354
375
|
return info;
|
|
355
376
|
}
|
|
377
|
+
/** Fill in CFBundleShortVersionString for an app the worker already identified. */
|
|
378
|
+
async function appVersionFor(python, udid, app) {
|
|
379
|
+
try {
|
|
380
|
+
const { stdout } = await execFileAsync(python, await (0, device_1.pmd3Args)(udid, "apps", "query", app.bundleId), { timeout: 8000 });
|
|
381
|
+
const meta = JSON.parse(stdout)?.[app.bundleId] ?? {};
|
|
382
|
+
const short = meta["CFBundleShortVersionString"] ?? null;
|
|
383
|
+
const build = meta["CFBundleVersion"] ?? null;
|
|
384
|
+
return {
|
|
385
|
+
bundleId: app.bundleId,
|
|
386
|
+
name: app.name,
|
|
387
|
+
version: short && build ? `${short} (${build})` : short ?? build ?? null,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// Version is a nice-to-have; identity is the useful part.
|
|
392
|
+
return { bundleId: app.bundleId, name: app.name, version: null };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
356
395
|
async function triggerPhoneScreenshot(options) {
|
|
357
396
|
const timeout = options?.timeout ?? 30000;
|
|
358
397
|
const includeLocation = options?.includeLocation ?? false;
|
|
359
398
|
const includeForegroundApp = options?.includeForegroundApp ?? true;
|
|
360
399
|
let udid = options?.deviceId ?? null;
|
|
361
|
-
const python =
|
|
400
|
+
const python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
362
401
|
// Start tunnel on first call — stays alive for the session
|
|
363
402
|
await startTunnel();
|
|
364
403
|
// If the caller didn't specify a device and the only iPhone we can reach is
|
|
@@ -381,8 +420,28 @@ async function triggerPhoneScreenshot(options) {
|
|
|
381
420
|
const outPath = (0, path_1.join)((0, os_1.tmpdir)(), `iphone-screenshot-${Date.now()}.png`);
|
|
382
421
|
const maxAttempts = 12;
|
|
383
422
|
try {
|
|
423
|
+
// ── Fast path: the persistent worker ────────────────────────────────────
|
|
424
|
+
// One held DVT session instead of two fresh CLI processes. Screenshot and
|
|
425
|
+
// proclist are issued together and multiplex over the single connection,
|
|
426
|
+
// so the metadata is nearly free. ~2.5s here vs ~6.8s below.
|
|
427
|
+
//
|
|
428
|
+
// Purely an optimisation: anything unexpected drops through to the CLI
|
|
429
|
+
// path, so a machine where the worker won't run is slower, not broken.
|
|
430
|
+
let helperApp = null;
|
|
431
|
+
let helperCaptured = false;
|
|
432
|
+
if (await dvtHelper_1.dvtHelper.available()) {
|
|
433
|
+
try {
|
|
434
|
+
helperApp = await dvtHelper_1.dvtHelper.capture(outPath, udid, includeForegroundApp);
|
|
435
|
+
helperCaptured = (0, fs_1.existsSync)(outPath);
|
|
436
|
+
}
|
|
437
|
+
catch (e) {
|
|
438
|
+
logger_1.log.warn("dvt helper capture failed; falling back to the CLI path", {
|
|
439
|
+
error: e instanceof Error ? e.message : String(e),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
384
443
|
let lastErr = null;
|
|
385
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
444
|
+
for (let i = 0; !helperCaptured && i < maxAttempts; i++) {
|
|
386
445
|
try {
|
|
387
446
|
await execFileAsync(python, await (0, device_1.pmd3Args)(udid, "developer", "dvt", "screenshot", outPath), { timeout });
|
|
388
447
|
if ((0, fs_1.existsSync)(outPath)) {
|
|
@@ -406,11 +465,17 @@ async function triggerPhoneScreenshot(options) {
|
|
|
406
465
|
}
|
|
407
466
|
await new Promise((r) => setTimeout(r, 800));
|
|
408
467
|
}
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
468
|
+
// The worker returns the foreground app's identity but not its version:
|
|
469
|
+
// that comes from `apps query`, which is installation-proxy rather than
|
|
470
|
+
// DVT. Fetch it via the CLI only when an app is actually foregrounded —
|
|
471
|
+
// on the Home Screen (SpringBoard, isApplication false) there is nothing
|
|
472
|
+
// to query and the call is skipped entirely.
|
|
473
|
+
// TODO: move `apps query` into the worker too and drop this last spawn.
|
|
474
|
+
const foregroundPromise = helperCaptured
|
|
475
|
+
? (helperApp ? appVersionFor(python, udid, helperApp) : Promise.resolve(null))
|
|
476
|
+
: includeForegroundApp
|
|
477
|
+
? (0, device_1.getForegroundApp)(python, udid)
|
|
478
|
+
: Promise.resolve(null);
|
|
414
479
|
const [image, device, foregroundApp, location] = await Promise.all([
|
|
415
480
|
(0, promises_1.readFile)(outPath),
|
|
416
481
|
deviceInfoPromise,
|
|
@@ -524,7 +589,7 @@ async function fetchLatestPhoneRecording(options) {
|
|
|
524
589
|
const timeout = options?.timeout ?? 60000;
|
|
525
590
|
const includeLocation = options?.includeLocation ?? false;
|
|
526
591
|
let udid = options?.deviceId ?? null;
|
|
527
|
-
const python =
|
|
592
|
+
const python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
528
593
|
await startTunnel();
|
|
529
594
|
// Same auto-resolve as triggerPhoneScreenshot — host apps that don't know
|
|
530
595
|
// about deviceId still need to reach iOS 17+ Wi-Fi-only iPhones.
|
|
@@ -693,7 +758,7 @@ const DEV_MODE_STICKY_TTL_MS = 30000;
|
|
|
693
758
|
async function listIosDevices() {
|
|
694
759
|
let python;
|
|
695
760
|
try {
|
|
696
|
-
python =
|
|
761
|
+
python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
697
762
|
}
|
|
698
763
|
catch {
|
|
699
764
|
return [];
|
|
@@ -890,7 +955,7 @@ async function unlinkIosDevice(options) {
|
|
|
890
955
|
const udid = options.deviceId;
|
|
891
956
|
if (!/^[0-9A-Fa-f-]{8,}$/.test(udid))
|
|
892
957
|
throw new Error(`Invalid UDID: ${udid}`);
|
|
893
|
-
const python =
|
|
958
|
+
const python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
894
959
|
try {
|
|
895
960
|
await execFileAsync(python, await (0, device_1.pmd3Args)(udid, "lockdown", "unpair"), { timeout: 8000 });
|
|
896
961
|
return;
|
|
@@ -1033,7 +1098,7 @@ async function getIosWirelessEnabled(options) {
|
|
|
1033
1098
|
const udid = options?.deviceId ?? null;
|
|
1034
1099
|
let python;
|
|
1035
1100
|
try {
|
|
1036
|
-
python =
|
|
1101
|
+
python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
1037
1102
|
}
|
|
1038
1103
|
catch {
|
|
1039
1104
|
return false;
|
|
@@ -1054,7 +1119,7 @@ async function getIosWirelessEnabled(options) {
|
|
|
1054
1119
|
*/
|
|
1055
1120
|
async function enableIosDeveloperMode(options) {
|
|
1056
1121
|
const udid = options?.deviceId ?? null;
|
|
1057
|
-
const python =
|
|
1122
|
+
const python = await (0, python_1.findPythonAsyncOrThrow)();
|
|
1058
1123
|
await startTunnel().catch(() => { }); // best-effort; amfi works over USB without tunnel
|
|
1059
1124
|
await (0, device_1.enableDeveloperMode)(python, udid);
|
|
1060
1125
|
}
|