@l22-io/orchard-mcp 0.6.0 → 0.6.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.
- package/build/bridge.d.ts +13 -2
- package/build/bridge.js +139 -23
- package/build/bridge.js.map +1 -1
- package/build/tools/mail.js +15 -6
- package/build/tools/mail.js.map +1 -1
- package/package.json +2 -2
- package/swift/.build/AppleBridge.app/Contents/MacOS/apple-bridge +0 -0
- package/swift/.build/AppleBridge.app.sha256 +1 -1
- package/swift/Sources/AppleBridge/AppleBridge.swift +6 -1
- package/swift/Sources/AppleBridge/Doctor.swift +43 -77
- package/swift/Sources/AppleBridge/Keynote.swift +1 -35
- package/swift/Sources/AppleBridge/Mail.swift +30 -35
- package/swift/Sources/AppleBridge/Notes.swift +1 -35
- package/swift/Sources/AppleBridge/Numbers.swift +2 -63
- package/swift/Sources/AppleBridge/OsascriptRunner.swift +171 -0
- package/swift/Sources/AppleBridge/Pages.swift +1 -35
package/build/bridge.d.ts
CHANGED
|
@@ -3,13 +3,24 @@ export interface BridgeResponse {
|
|
|
3
3
|
data?: unknown;
|
|
4
4
|
error?: string;
|
|
5
5
|
}
|
|
6
|
+
export interface BridgeOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Per-call timeout in milliseconds. Defaults to 30_000.
|
|
9
|
+
* Long-running operations (e.g. mail content searches, large file reads)
|
|
10
|
+
* should pass a larger value. When the timeout fires the entire child
|
|
11
|
+
* process group is killed via SIGTERM (then SIGKILL) — this is required
|
|
12
|
+
* for tools that spawn osascript grandchildren, which would otherwise
|
|
13
|
+
* be orphaned and keep Mail.app / Notes.app wedged on Apple Events.
|
|
14
|
+
*/
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
}
|
|
6
17
|
/**
|
|
7
18
|
* Execute an apple-bridge subcommand and return parsed JSON.
|
|
8
19
|
* Tries direct execution first; falls back to .app bundle mode
|
|
9
20
|
* (via `open`) when direct execution returns a permission error.
|
|
10
21
|
*/
|
|
11
|
-
export declare function callBridge(args: string[]): Promise<BridgeResponse>;
|
|
22
|
+
export declare function callBridge(args: string[], opts?: BridgeOptions): Promise<BridgeResponse>;
|
|
12
23
|
/**
|
|
13
24
|
* Convenience: call bridge, check status, return data or throw.
|
|
14
25
|
*/
|
|
15
|
-
export declare function bridgeData(args: string[]): Promise<unknown>;
|
|
26
|
+
export declare function bridgeData(args: string[], opts?: BridgeOptions): Promise<unknown>;
|
package/build/bridge.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
3
2
|
import { resolve, dirname } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { readFile, unlink, access } from "node:fs/promises";
|
|
6
5
|
import { randomUUID } from "node:crypto";
|
|
7
6
|
import { tmpdir } from "node:os";
|
|
8
|
-
const execFileAsync = promisify(execFile);
|
|
9
7
|
// Reason: Resolve the Swift binary path relative to this file's location.
|
|
10
8
|
// In development: swift/.build/release/apple-bridge
|
|
11
9
|
// In npm package: swift/.build/release/apple-bridge (shipped alongside)
|
|
@@ -42,42 +40,160 @@ function getAppBundlePath() {
|
|
|
42
40
|
}
|
|
43
41
|
return resolve(__dirname, "..", "swift", ".build", "AppleBridge.app");
|
|
44
42
|
}
|
|
43
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
44
|
+
const SIGKILL_GRACE_MS = 2_000;
|
|
45
45
|
/**
|
|
46
46
|
* Execute an apple-bridge subcommand and return parsed JSON.
|
|
47
47
|
* Tries direct execution first; falls back to .app bundle mode
|
|
48
48
|
* (via `open`) when direct execution returns a permission error.
|
|
49
49
|
*/
|
|
50
|
-
export async function callBridge(args) {
|
|
50
|
+
export async function callBridge(args, opts = {}) {
|
|
51
51
|
const bin = getBridgePath();
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (stderr) {
|
|
58
|
-
// Reason: stderr is used for Swift warnings/diagnostics, log but don't fail.
|
|
59
|
-
console.error(`[apple-bridge stderr] ${stderr.trim()}`);
|
|
52
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
53
|
+
const direct = await runBridgeProcess(bin, args, timeoutMs);
|
|
54
|
+
if (direct.status === "error" || direct.parsed == null) {
|
|
55
|
+
if (direct.spawnError) {
|
|
56
|
+
return { status: "error", error: direct.spawnError };
|
|
60
57
|
}
|
|
61
|
-
|
|
58
|
+
if (direct.timedOut) {
|
|
59
|
+
return {
|
|
60
|
+
status: "error",
|
|
61
|
+
error: `apple-bridge timed out after ${timeoutMs}ms`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (direct.parsed) {
|
|
66
|
+
const parsed = direct.parsed;
|
|
62
67
|
// If the command returned a permission error, retry via .app bundle
|
|
63
68
|
if (parsed.status === "error" &&
|
|
64
69
|
typeof parsed.error === "string" &&
|
|
65
70
|
parsed.error.includes("access denied")) {
|
|
66
|
-
return callBridgeViaApp(args);
|
|
71
|
+
return callBridgeViaApp(args, timeoutMs);
|
|
67
72
|
}
|
|
68
73
|
return parsed;
|
|
69
74
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
return { status: "error", error: direct.spawnError ?? "apple-bridge returned no output" };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Spawn apple-bridge in its own process group so we can SIGTERM the entire
|
|
79
|
+
* group on timeout. Required because Swift's Foundation.Process does not
|
|
80
|
+
* cascade signals to its child osascript processes — without this, a
|
|
81
|
+
* cancelled/timed-out mail search leaves osascript orphaned and Mail.app
|
|
82
|
+
* locked on Apple Events for as long as the script keeps iterating.
|
|
83
|
+
*/
|
|
84
|
+
function runBridgeProcess(bin, args, timeoutMs) {
|
|
85
|
+
return new Promise((resolvePromise) => {
|
|
86
|
+
let child;
|
|
87
|
+
try {
|
|
88
|
+
child = spawn(bin, args, {
|
|
89
|
+
detached: true, // own process group; -pid kills the whole tree
|
|
90
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
const msg = err instanceof Error ? err.message : "Failed to spawn apple-bridge";
|
|
95
|
+
resolvePromise({ status: "error", spawnError: msg });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const pid = child.pid;
|
|
99
|
+
if (!pid) {
|
|
100
|
+
resolvePromise({ status: "error", spawnError: "apple-bridge spawn returned no pid" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const chunks = [];
|
|
104
|
+
const errChunks = [];
|
|
105
|
+
let totalBytes = 0;
|
|
106
|
+
const MAX_BYTES = 10 * 1024 * 1024;
|
|
107
|
+
let settled = false;
|
|
108
|
+
let timedOut = false;
|
|
109
|
+
let killEscalated = false;
|
|
110
|
+
let sigkillTimer = null;
|
|
111
|
+
const killGroup = (signal) => {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(-pid, signal);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Group may already be gone; nothing to do.
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
// Reason: Swift's apple-bridge has no SIGTERM handler and dies on the first
|
|
120
|
+
// signal, which causes Node's "close" event to fire almost immediately.
|
|
121
|
+
// If we cleared sigkillTimer at that point, any osascript grandchild
|
|
122
|
+
// wedged in an Apple Event RPC to Mail.app / Notes.app would be orphaned
|
|
123
|
+
// (PPID=1) and continue to hold Mail.app's event queue hostage. So once
|
|
124
|
+
// we have committed to escalating, the SIGKILL must fire regardless of
|
|
125
|
+
// when the bridge process itself closes.
|
|
126
|
+
const escalateKill = () => {
|
|
127
|
+
if (killEscalated)
|
|
128
|
+
return;
|
|
129
|
+
killEscalated = true;
|
|
130
|
+
killGroup("SIGTERM");
|
|
131
|
+
sigkillTimer = setTimeout(() => killGroup("SIGKILL"), SIGKILL_GRACE_MS);
|
|
132
|
+
sigkillTimer.unref();
|
|
133
|
+
};
|
|
134
|
+
const settle = (result) => {
|
|
135
|
+
if (settled)
|
|
136
|
+
return;
|
|
137
|
+
settled = true;
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
if (!killEscalated && sigkillTimer)
|
|
140
|
+
clearTimeout(sigkillTimer);
|
|
141
|
+
// When escalation is in flight, leave the SIGKILL timer alone so it
|
|
142
|
+
// can reap any grandchildren the bridge spawned (see escalateKill).
|
|
143
|
+
resolvePromise(result);
|
|
144
|
+
};
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
timedOut = true;
|
|
147
|
+
escalateKill();
|
|
148
|
+
}, timeoutMs);
|
|
149
|
+
child.stdout?.on("data", (d) => {
|
|
150
|
+
totalBytes += d.length;
|
|
151
|
+
if (totalBytes > MAX_BYTES) {
|
|
152
|
+
escalateKill();
|
|
153
|
+
settle({
|
|
154
|
+
status: "error",
|
|
155
|
+
spawnError: `apple-bridge output exceeded ${MAX_BYTES} bytes`,
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
chunks.push(d);
|
|
160
|
+
});
|
|
161
|
+
child.stderr?.on("data", (d) => {
|
|
162
|
+
errChunks.push(d);
|
|
163
|
+
});
|
|
164
|
+
child.on("error", (err) => {
|
|
165
|
+
settle({ status: "error", spawnError: err.message });
|
|
166
|
+
});
|
|
167
|
+
child.on("close", () => {
|
|
168
|
+
const stderr = Buffer.concat(errChunks).toString("utf8").trim();
|
|
169
|
+
if (stderr) {
|
|
170
|
+
console.error(`[apple-bridge stderr] ${stderr}`);
|
|
171
|
+
}
|
|
172
|
+
if (timedOut) {
|
|
173
|
+
settle({ status: "error", timedOut: true });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(stdout);
|
|
179
|
+
settle({ status: "ok", parsed });
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
const msg = err instanceof Error ? err.message : "JSON parse failed";
|
|
183
|
+
settle({
|
|
184
|
+
status: "error",
|
|
185
|
+
spawnError: `apple-bridge returned invalid JSON: ${msg}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|
|
74
190
|
}
|
|
75
191
|
/**
|
|
76
192
|
* Launch apple-bridge via .app bundle using `open`, with output written to
|
|
77
193
|
* a temp file. Required on macOS Sequoia where CLI tools cannot obtain
|
|
78
194
|
* TCC permissions (e.g. Reminders) without an .app bundle context.
|
|
79
195
|
*/
|
|
80
|
-
async function callBridgeViaApp(args) {
|
|
196
|
+
async function callBridgeViaApp(args, timeoutMs) {
|
|
81
197
|
const appPath = getAppBundlePath();
|
|
82
198
|
const outputFile = resolve(tmpdir(), `apple-bridge-${randomUUID()}.json`);
|
|
83
199
|
try {
|
|
@@ -99,9 +215,9 @@ async function callBridgeViaApp(args) {
|
|
|
99
215
|
child.kill();
|
|
100
216
|
resolvePromise({
|
|
101
217
|
status: "error",
|
|
102
|
-
error:
|
|
218
|
+
error: `apple-bridge .app bundle timed out after ${timeoutMs}ms`,
|
|
103
219
|
});
|
|
104
|
-
},
|
|
220
|
+
}, timeoutMs);
|
|
105
221
|
child.on("close", async () => {
|
|
106
222
|
clearTimeout(timeout);
|
|
107
223
|
try {
|
|
@@ -130,8 +246,8 @@ async function callBridgeViaApp(args) {
|
|
|
130
246
|
/**
|
|
131
247
|
* Convenience: call bridge, check status, return data or throw.
|
|
132
248
|
*/
|
|
133
|
-
export async function bridgeData(args) {
|
|
134
|
-
const result = await callBridge(args);
|
|
249
|
+
export async function bridgeData(args, opts) {
|
|
250
|
+
const result = await callBridge(args, opts);
|
|
135
251
|
if (result.status === "error") {
|
|
136
252
|
throw new Error(result.error ?? "apple-bridge returned an error");
|
|
137
253
|
}
|
package/build/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
|
package/build/tools/mail.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { bridgeData } from "../bridge.js";
|
|
3
|
+
// Reason: Mail tools that iterate mailboxes via AppleScript can legitimately
|
|
4
|
+
// run for a minute or more on large accounts. The Swift bridge has a 90s
|
|
5
|
+
// osascript watchdog (Mail.swift `defaultAppleScriptTimeout`); the TS timeout
|
|
6
|
+
// must outlive that so the Swift watchdog reports a clean error instead of
|
|
7
|
+
// being cut off mid-execution by the TS-side process-group kill.
|
|
8
|
+
const MAIL_SCAN_TIMEOUT_MS = 120_000;
|
|
3
9
|
export function registerMailTools(server) {
|
|
4
10
|
server.tool("mail.list_accounts", "List all Apple Mail accounts with their mailboxes and unread counts. Requires Mail.app to be running.", {}, async () => {
|
|
5
11
|
const data = await bridgeData(["mail-accounts"]);
|
|
@@ -20,12 +26,15 @@ export function registerMailTools(server) {
|
|
|
20
26
|
if (limit) {
|
|
21
27
|
args.push("--limit", String(limit));
|
|
22
28
|
}
|
|
23
|
-
const data = await bridgeData(args);
|
|
29
|
+
const data = await bridgeData(args, { timeoutMs: MAIL_SCAN_TIMEOUT_MS });
|
|
24
30
|
return {
|
|
25
31
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
26
32
|
};
|
|
27
33
|
});
|
|
28
|
-
server.tool("mail.search", "Search email messages by subject, sender, body, or all fields (default: all). Returns headers only (no body).
|
|
34
|
+
server.tool("mail.search", "Search email messages by subject, sender, body, or all fields (default: all). Returns headers only (no body). " +
|
|
35
|
+
"SCOPE RULE: body or all-fields search across mailbox='all' AND no specific account is REFUSED — it can lock " +
|
|
36
|
+
"Mail.app for minutes. Narrow the scope: pick a specific account, a specific mailbox, or set searchIn to " +
|
|
37
|
+
"'subject' or 'sender' before using mailbox='all'.", {
|
|
29
38
|
query: z
|
|
30
39
|
.string()
|
|
31
40
|
.describe("Search term to match against message fields (controlled by searchIn)"),
|
|
@@ -36,11 +45,11 @@ export function registerMailTools(server) {
|
|
|
36
45
|
mailbox: z
|
|
37
46
|
.string()
|
|
38
47
|
.optional()
|
|
39
|
-
.describe("Mailbox to search in (default: inbox). Use 'all' to search all mailboxes."),
|
|
48
|
+
.describe("Mailbox to search in (default: inbox). Use 'all' to search all mailboxes (requires a specific account when searchIn includes body)."),
|
|
40
49
|
searchIn: z
|
|
41
50
|
.enum(["subject", "sender", "body", "all"])
|
|
42
51
|
.optional()
|
|
43
|
-
.describe("Fields to search: subject, sender, body, or all (default: all)"),
|
|
52
|
+
.describe("Fields to search: subject, sender, body, or all (default: all). Use 'subject' or 'sender' when searching across mailbox='all' without an account filter."),
|
|
44
53
|
limit: z
|
|
45
54
|
.number()
|
|
46
55
|
.int()
|
|
@@ -71,7 +80,7 @@ export function registerMailTools(server) {
|
|
|
71
80
|
if (offset !== undefined) {
|
|
72
81
|
args.push("--offset", String(offset));
|
|
73
82
|
}
|
|
74
|
-
const data = await bridgeData(args);
|
|
83
|
+
const data = await bridgeData(args, { timeoutMs: MAIL_SCAN_TIMEOUT_MS });
|
|
75
84
|
return {
|
|
76
85
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
77
86
|
};
|
|
@@ -161,7 +170,7 @@ export function registerMailTools(server) {
|
|
|
161
170
|
if (offset !== undefined) {
|
|
162
171
|
args.push("--offset", String(offset));
|
|
163
172
|
}
|
|
164
|
-
const data = await bridgeData(args);
|
|
173
|
+
const data = await bridgeData(args, { timeoutMs: MAIL_SCAN_TIMEOUT_MS });
|
|
165
174
|
return {
|
|
166
175
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
167
176
|
};
|
package/build/tools/mail.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../../src/tools/mail.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE1C,MAAM,UAAU,iBAAiB,CAAC,MAAiB;IACjD,MAAM,CAAC,IAAI,CACT,oBAAoB,EACpB,uGAAuG,EACvG,EAAE,EACF,KAAK,IAAI,EAAE;QACT,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC;QACjD,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;SACjE,CAAC;IACJ,CAAC,CACF,CAAC;IAEF,MAAM,CAAC,IAAI,CACT,qBAAqB,EACrB,qJAAqJ,EACrJ;QACE,KAAK,EAAE,CAAC;aACL,MAAM,EAAE;aACR,GAAG,EAAE;aACL,GAAG,CAAC,CAAC,CAAC;aACN,GAAG,CAAC,GAAG,CAAC;aACR,QAAQ,EAAE;aACV,QAAQ,CACP,mEAAmE,CACpE;KACJ,EACD,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAClB,MAAM,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;QAC7B,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtC,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"mail.js","sourceRoot":"","sources":["../../src/tools/mail.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@l22-io/orchard-mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "MCP server for Apple Calendar, Mail, Reminders, and Files on macOS using native EventKit",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"scripts": {
|
|
21
21
|
"build": "npm run build:swift && npm run build:ts",
|
|
22
22
|
"build:ts": "tsc && chmod 755 build/index.js",
|
|
23
|
-
"build:swift": "cd swift && swift build -c release -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Sources/AppleBridge/Info.plist",
|
|
23
|
+
"build:swift": "cd swift && swift build -c release -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Sources/AppleBridge/Info.plist && mkdir -p .build/AppleBridge.app/Contents/MacOS && cp .build/release/apple-bridge .build/AppleBridge.app/Contents/MacOS/apple-bridge && cp Sources/AppleBridge/Info.plist .build/AppleBridge.app/Contents/Info.plist && codesign --force --sign - .build/AppleBridge.app",
|
|
24
24
|
"postinstall": "bash scripts/postinstall.sh",
|
|
25
25
|
"prepublishOnly": "npm run build:ts && bash scripts/prepublish.sh",
|
|
26
26
|
"dev": "tsc --watch",
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8362b4b7635f775e1847b883917e1839bb8e0f3522b2b497ce471150d0ddaba4 apple-bridge
|
|
@@ -7,6 +7,11 @@ struct AppleBridge: AsyncParsableCommand {
|
|
|
7
7
|
// This allows any subcommand to write to a file instead of stdout,
|
|
8
8
|
// needed for .app bundle mode on macOS Sequoia where stdout is not capturable.
|
|
9
9
|
static func main() async {
|
|
10
|
+
// Install before parsing so a SIGTERM during arg parsing (rare, but
|
|
11
|
+
// possible if node tears us down before we finish startup) still
|
|
12
|
+
// reaps any osascript spawned by a partial subcommand.
|
|
13
|
+
OsascriptRunner.installSignalHandlers()
|
|
14
|
+
|
|
10
15
|
var args = Array(CommandLine.arguments.dropFirst())
|
|
11
16
|
if let idx = args.firstIndex(of: "--output"), idx + 1 < args.count {
|
|
12
17
|
JSONOutput.outputPath = args[idx + 1]
|
|
@@ -28,7 +33,7 @@ struct AppleBridge: AsyncParsableCommand {
|
|
|
28
33
|
static let configuration = CommandConfiguration(
|
|
29
34
|
commandName: "apple-bridge",
|
|
30
35
|
abstract: "Native macOS bridge for Apple Calendar, Mail, Reminders, Numbers, Pages, and Keynote.",
|
|
31
|
-
version: "0.
|
|
36
|
+
version: "0.6.3",
|
|
32
37
|
subcommands: [
|
|
33
38
|
Calendars.self,
|
|
34
39
|
Events.self,
|
|
@@ -9,7 +9,7 @@ import Foundation
|
|
|
9
9
|
enum DoctorBridge {
|
|
10
10
|
static func run() async {
|
|
11
11
|
var report: [String: Any] = [
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.6.3",
|
|
13
13
|
"platform": "macOS",
|
|
14
14
|
"systemVersion": ProcessInfo.processInfo.operatingSystemVersionString
|
|
15
15
|
]
|
|
@@ -147,30 +147,20 @@ enum DoctorBridge {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
private static func checkIWorkApp(_ appName: String) -> [String: Any] {
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
let outPipe = Pipe()
|
|
154
|
-
let errPipe = Pipe()
|
|
155
|
-
task.standardOutput = outPipe
|
|
156
|
-
task.standardError = errPipe
|
|
157
|
-
|
|
158
|
-
do {
|
|
159
|
-
try task.run()
|
|
160
|
-
task.waitUntilExit()
|
|
161
|
-
if task.terminationStatus == 0 {
|
|
162
|
-
return ["installed": true, "accessible": true]
|
|
163
|
-
} else {
|
|
164
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
165
|
-
let errStr = String(data: errData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
166
|
-
if errStr.contains("-600") || errStr.contains("not running") {
|
|
167
|
-
return ["installed": true, "accessible": false, "note": "\(appName) is not running."]
|
|
168
|
-
}
|
|
169
|
-
return ["installed": false, "accessible": false, "note": "\(appName) may not be installed."]
|
|
170
|
-
}
|
|
171
|
-
} catch {
|
|
172
|
-
return ["installed": false, "accessible": false, "note": "Could not check \(appName): \(error.localizedDescription)"]
|
|
150
|
+
let script = "tell application \"\(appName)\" to return name"
|
|
151
|
+
guard let result = OsascriptRunner.runRaw(script: script, timeout: doctorAppleScriptTimeout) else {
|
|
152
|
+
return ["installed": false, "accessible": false, "note": "Could not spawn osascript to check \(appName)."]
|
|
173
153
|
}
|
|
154
|
+
if result.timedOut {
|
|
155
|
+
return ["installed": true, "accessible": false, "note": "\(appName) did not respond within \(Int(doctorAppleScriptTimeout))s."]
|
|
156
|
+
}
|
|
157
|
+
if result.status == 0 {
|
|
158
|
+
return ["installed": true, "accessible": true]
|
|
159
|
+
}
|
|
160
|
+
if result.stderr.contains("-600") || result.stderr.contains("not running") {
|
|
161
|
+
return ["installed": true, "accessible": false, "note": "\(appName) is not running."]
|
|
162
|
+
}
|
|
163
|
+
return ["installed": false, "accessible": false, "note": "\(appName) may not be installed."]
|
|
174
164
|
}
|
|
175
165
|
|
|
176
166
|
private static func contactsAuthName(_ status: CNAuthorizationStatus) -> String {
|
|
@@ -185,68 +175,44 @@ enum DoctorBridge {
|
|
|
185
175
|
}
|
|
186
176
|
|
|
187
177
|
private static func checkNotesAccess() -> [String: Any] {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
let output = String(data: data, encoding: .utf8)?
|
|
201
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
202
|
-
return [
|
|
203
|
-
"accessible": true,
|
|
204
|
-
"accountCount": Int(output ?? "0") ?? 0
|
|
205
|
-
]
|
|
206
|
-
}
|
|
178
|
+
return checkAppAccess(appName: "Notes")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static func checkMailAccess() -> [String: Any] {
|
|
182
|
+
// Reason: Try a minimal AppleScript to see if Mail.app is accessible.
|
|
183
|
+
// This doesn't send the permission prompt -- it just checks if we can talk to Mail.
|
|
184
|
+
return checkAppAccess(appName: "Mail")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static func checkAppAccess(appName: String) -> [String: Any] {
|
|
188
|
+
let script = "tell application \"\(appName)\" to count of accounts"
|
|
189
|
+
guard let result = OsascriptRunner.runRaw(script: script, timeout: doctorAppleScriptTimeout) else {
|
|
207
190
|
return [
|
|
208
191
|
"accessible": false,
|
|
209
|
-
"note": "
|
|
192
|
+
"note": "Failed to spawn osascript to probe \(appName)."
|
|
210
193
|
]
|
|
211
|
-
}
|
|
194
|
+
}
|
|
195
|
+
if result.timedOut {
|
|
212
196
|
return [
|
|
213
197
|
"accessible": false,
|
|
214
|
-
"note": "
|
|
198
|
+
"note": "\(appName).app did not respond within \(Int(doctorAppleScriptTimeout))s. It may be busy or unresponsive; system_doctor refuses to wait longer to avoid orphaning osascript."
|
|
215
199
|
]
|
|
216
200
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
private static func checkMailAccess() -> [String: Any] {
|
|
220
|
-
// Reason: Try a minimal AppleScript to see if Mail.app is accessible.
|
|
221
|
-
// This doesn't send the permission prompt -- it just checks if we can talk to Mail.
|
|
222
|
-
let task = Process()
|
|
223
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
224
|
-
task.arguments = ["-e", "tell application \"Mail\" to count of accounts"]
|
|
225
|
-
let pipe = Pipe()
|
|
226
|
-
task.standardOutput = pipe
|
|
227
|
-
task.standardError = Pipe()
|
|
228
|
-
|
|
229
|
-
do {
|
|
230
|
-
try task.run()
|
|
231
|
-
task.waitUntilExit()
|
|
232
|
-
if task.terminationStatus == 0 {
|
|
233
|
-
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
234
|
-
let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
235
|
-
return [
|
|
236
|
-
"accessible": true,
|
|
237
|
-
"accountCount": Int(output ?? "0") ?? 0
|
|
238
|
-
]
|
|
239
|
-
} else {
|
|
240
|
-
return [
|
|
241
|
-
"accessible": false,
|
|
242
|
-
"note": "Mail automation permission not yet granted or Mail.app not running."
|
|
243
|
-
]
|
|
244
|
-
}
|
|
245
|
-
} catch {
|
|
201
|
+
if result.status == 0 {
|
|
246
202
|
return [
|
|
247
|
-
"accessible":
|
|
248
|
-
"
|
|
203
|
+
"accessible": true,
|
|
204
|
+
"accountCount": Int(result.stdout) ?? 0
|
|
249
205
|
]
|
|
250
206
|
}
|
|
207
|
+
return [
|
|
208
|
+
"accessible": false,
|
|
209
|
+
"note": "\(appName) automation permission not yet granted or \(appName).app not running."
|
|
210
|
+
]
|
|
251
211
|
}
|
|
212
|
+
|
|
213
|
+
/// Hard timeout for any AppleScript invocation issued by the doctor. The
|
|
214
|
+
/// doctor's job is to report state quickly; if Mail.app or Notes.app
|
|
215
|
+
/// cannot answer "count of accounts" in this window they are by definition
|
|
216
|
+
/// not accessible.
|
|
217
|
+
private static let doctorAppleScriptTimeout: TimeInterval = 5
|
|
252
218
|
}
|
|
@@ -530,41 +530,7 @@ enum KeynoteBridge {
|
|
|
530
530
|
// MARK: - AppleScript Execution
|
|
531
531
|
|
|
532
532
|
private static func runAppleScript(_ script: String) -> String? {
|
|
533
|
-
|
|
534
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
535
|
-
task.arguments = ["-e", script]
|
|
536
|
-
|
|
537
|
-
let outPipe = Pipe()
|
|
538
|
-
let errPipe = Pipe()
|
|
539
|
-
task.standardOutput = outPipe
|
|
540
|
-
task.standardError = errPipe
|
|
541
|
-
|
|
542
|
-
do {
|
|
543
|
-
try task.run()
|
|
544
|
-
task.waitUntilExit()
|
|
545
|
-
|
|
546
|
-
if task.terminationStatus != 0 {
|
|
547
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
548
|
-
let errStr = String(data: errData, encoding: .utf8)?
|
|
549
|
-
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
550
|
-
|
|
551
|
-
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
552
|
-
JSONOutput.error("Keynote automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
|
|
553
|
-
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
554
|
-
JSONOutput.error("Keynote is not running. It will be launched automatically on next attempt.")
|
|
555
|
-
} else {
|
|
556
|
-
JSONOutput.error("AppleScript error: \(errStr)")
|
|
557
|
-
}
|
|
558
|
-
return nil
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
562
|
-
return String(data: data, encoding: .utf8)?
|
|
563
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
564
|
-
} catch {
|
|
565
|
-
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
566
|
-
return nil
|
|
567
|
-
}
|
|
533
|
+
return OsascriptRunner.run(script: script, appName: "Keynote")
|
|
568
534
|
}
|
|
569
535
|
|
|
570
536
|
private static func escapeForAppleScript(_ str: String) -> String {
|
|
@@ -116,6 +116,23 @@ enum MailBridge {
|
|
|
116
116
|
|
|
117
117
|
/// Search messages by subject, sender, body, or all fields across accounts.
|
|
118
118
|
static func search(query: String, account: String?, mailbox: String?, limit: Int, searchIn: String, offset: Int?) {
|
|
119
|
+
// Refuse the catastrophic combination: body-content search across every
|
|
120
|
+
// mailbox of every account. AppleScript's `content contains` predicate
|
|
121
|
+
// forces Mail to load message bodies; iterating that across all
|
|
122
|
+
// mailboxes can lock Mail.app on Apple Event processing for many
|
|
123
|
+
// minutes (it cannot service quit / UI events while the script runs).
|
|
124
|
+
// See: orphaned osascript incident (PID 85905, ~11min before kill).
|
|
125
|
+
let searchesContent = (searchIn == "body" || searchIn == "all")
|
|
126
|
+
let allAccounts = (account == nil || account == "all")
|
|
127
|
+
let allMailboxes = (mailbox == "all")
|
|
128
|
+
if searchesContent && allAccounts && allMailboxes {
|
|
129
|
+
JSONOutput.error(
|
|
130
|
+
"Refusing body/all-fields search across every mailbox of every account — this can lock Mail.app for minutes. " +
|
|
131
|
+
"Narrow the scope: pass --account <name>, or pass --mailbox <name>, or pass --search-in subject (or --search-in sender)."
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
let searchQuery = escapeForAppleScript(query)
|
|
120
137
|
let effectiveOffset = offset ?? 0
|
|
121
138
|
let whereClause: String
|
|
@@ -620,42 +637,20 @@ enum MailBridge {
|
|
|
620
637
|
|
|
621
638
|
// MARK: - AppleScript Execution
|
|
622
639
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
let errPipe = Pipe()
|
|
630
|
-
task.standardOutput = outPipe
|
|
631
|
-
task.standardError = errPipe
|
|
632
|
-
|
|
633
|
-
do {
|
|
634
|
-
try task.run()
|
|
635
|
-
task.waitUntilExit()
|
|
636
|
-
|
|
637
|
-
if task.terminationStatus != 0 {
|
|
638
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
639
|
-
let errStr = String(data: errData, encoding: .utf8)?
|
|
640
|
-
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
641
|
-
|
|
642
|
-
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
643
|
-
JSONOutput.error("Mail automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > Mail.")
|
|
644
|
-
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
645
|
-
JSONOutput.error("Mail.app is not running. Open Mail.app and try again.")
|
|
646
|
-
} else {
|
|
647
|
-
JSONOutput.error("AppleScript error: \(errStr)")
|
|
648
|
-
}
|
|
649
|
-
return nil
|
|
650
|
-
}
|
|
640
|
+
/// Mail.app's per-account/per-mailbox fallback can iterate large folders;
|
|
641
|
+
/// 90s is long enough for legitimate searches on big accounts but short
|
|
642
|
+
/// enough that node's default 30s timeout (which fires first) plus our
|
|
643
|
+
/// signal handler still reap osascript before it wedges Mail.app for
|
|
644
|
+
/// other concurrent apple-bridge instances.
|
|
645
|
+
private static let mailAppleScriptTimeout: TimeInterval = 90
|
|
651
646
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
647
|
+
private static func runAppleScript(_ script: String) -> String? {
|
|
648
|
+
return OsascriptRunner.run(
|
|
649
|
+
script: script,
|
|
650
|
+
timeout: mailAppleScriptTimeout,
|
|
651
|
+
appName: "Mail",
|
|
652
|
+
timeoutHint: "Narrow the search scope (specific --account or --mailbox) or use --search-in subject."
|
|
653
|
+
)
|
|
659
654
|
}
|
|
660
655
|
|
|
661
656
|
private static func escapeForAppleScript(_ str: String) -> String {
|
|
@@ -184,41 +184,7 @@ enum NotesBridge {
|
|
|
184
184
|
// MARK: - AppleScript plumbing
|
|
185
185
|
|
|
186
186
|
private static func runAppleScript(_ script: String) -> String? {
|
|
187
|
-
|
|
188
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
189
|
-
task.arguments = ["-e", script]
|
|
190
|
-
|
|
191
|
-
let outPipe = Pipe()
|
|
192
|
-
let errPipe = Pipe()
|
|
193
|
-
task.standardOutput = outPipe
|
|
194
|
-
task.standardError = errPipe
|
|
195
|
-
|
|
196
|
-
do {
|
|
197
|
-
try task.run()
|
|
198
|
-
task.waitUntilExit()
|
|
199
|
-
|
|
200
|
-
if task.terminationStatus != 0 {
|
|
201
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
202
|
-
let errStr = String(data: errData, encoding: .utf8)?
|
|
203
|
-
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
204
|
-
|
|
205
|
-
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
206
|
-
JSONOutput.error("Notes automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > Notes.")
|
|
207
|
-
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
208
|
-
JSONOutput.error("Notes.app is not running. Open Notes.app and try again.")
|
|
209
|
-
} else {
|
|
210
|
-
JSONOutput.error("AppleScript error: \(errStr)")
|
|
211
|
-
}
|
|
212
|
-
return nil
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
216
|
-
return String(data: data, encoding: .utf8)?
|
|
217
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
218
|
-
} catch {
|
|
219
|
-
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
220
|
-
return nil
|
|
221
|
-
}
|
|
187
|
+
return OsascriptRunner.run(script: script, appName: "Notes")
|
|
222
188
|
}
|
|
223
189
|
|
|
224
190
|
private static func escapeForAppleScript(_ str: String) -> String {
|
|
@@ -444,74 +444,13 @@ enum NumbersBridge {
|
|
|
444
444
|
// MARK: - AppleScript Execution
|
|
445
445
|
|
|
446
446
|
private static func runAppleScript(_ script: String) -> String? {
|
|
447
|
-
|
|
448
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
449
|
-
task.arguments = ["-e", script]
|
|
450
|
-
|
|
451
|
-
let outPipe = Pipe()
|
|
452
|
-
let errPipe = Pipe()
|
|
453
|
-
task.standardOutput = outPipe
|
|
454
|
-
task.standardError = errPipe
|
|
455
|
-
|
|
456
|
-
do {
|
|
457
|
-
try task.run()
|
|
458
|
-
task.waitUntilExit()
|
|
459
|
-
|
|
460
|
-
if task.terminationStatus != 0 {
|
|
461
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
462
|
-
let errStr = String(data: errData, encoding: .utf8)?
|
|
463
|
-
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
464
|
-
|
|
465
|
-
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
466
|
-
JSONOutput.error("Numbers automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
|
|
467
|
-
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
468
|
-
JSONOutput.error("Numbers is not running. It will be launched automatically on next attempt.")
|
|
469
|
-
} else {
|
|
470
|
-
JSONOutput.error("AppleScript error: \(errStr)")
|
|
471
|
-
}
|
|
472
|
-
return nil
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
476
|
-
return String(data: data, encoding: .utf8)?
|
|
477
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
478
|
-
} catch {
|
|
479
|
-
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
480
|
-
return nil
|
|
481
|
-
}
|
|
447
|
+
return OsascriptRunner.run(script: script, appName: "Numbers")
|
|
482
448
|
}
|
|
483
449
|
|
|
484
450
|
// MARK: - JXA Execution
|
|
485
451
|
|
|
486
452
|
private static func runJXA(_ script: String) -> String? {
|
|
487
|
-
|
|
488
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
489
|
-
task.arguments = ["-l", "JavaScript", "-e", script]
|
|
490
|
-
|
|
491
|
-
let outPipe = Pipe()
|
|
492
|
-
let errPipe = Pipe()
|
|
493
|
-
task.standardOutput = outPipe
|
|
494
|
-
task.standardError = errPipe
|
|
495
|
-
|
|
496
|
-
do {
|
|
497
|
-
try task.run()
|
|
498
|
-
task.waitUntilExit()
|
|
499
|
-
|
|
500
|
-
if task.terminationStatus != 0 {
|
|
501
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
502
|
-
let errStr = String(data: errData, encoding: .utf8)?
|
|
503
|
-
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
504
|
-
JSONOutput.error("JXA error: \(errStr)")
|
|
505
|
-
return nil
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
509
|
-
return String(data: data, encoding: .utf8)?
|
|
510
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
511
|
-
} catch {
|
|
512
|
-
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
513
|
-
return nil
|
|
514
|
-
}
|
|
453
|
+
return OsascriptRunner.run(script: script, language: .javaScript, appName: "Numbers")
|
|
515
454
|
}
|
|
516
455
|
|
|
517
456
|
private static func escapeForAppleScript(_ str: String) -> String {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Darwin
|
|
3
|
+
|
|
4
|
+
// Reason: Single PID slot for the currently-running osascript child. apple-bridge
|
|
5
|
+
// runs one subcommand per invocation and each runOsascript call is synchronous,
|
|
6
|
+
// so at most one osascript is alive at any moment. Stored as sig_atomic_t so
|
|
7
|
+
// the C-convention signal handler can read it without locks (async-signal-safe).
|
|
8
|
+
// File-scope is required: @convention(c) closures cannot capture Swift state, so
|
|
9
|
+
// the handler reaches it as a C global.
|
|
10
|
+
private var currentChildPid: sig_atomic_t = 0
|
|
11
|
+
|
|
12
|
+
/// Language flavour passed to `osascript`. AppleScript is the default; JXA is
|
|
13
|
+
/// used by Numbers for native JSON output via JavaScript for Automation.
|
|
14
|
+
enum OsascriptLanguage {
|
|
15
|
+
case appleScript
|
|
16
|
+
case javaScript
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Raw result of an osascript invocation. Used by callers that need to inspect
|
|
20
|
+
/// status without emitting a JSON error envelope (e.g. Doctor's probes).
|
|
21
|
+
struct OsascriptResult {
|
|
22
|
+
let status: Int32
|
|
23
|
+
let stdout: String
|
|
24
|
+
let stderr: String
|
|
25
|
+
let timedOut: Bool
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
enum OsascriptRunner {
|
|
29
|
+
|
|
30
|
+
/// Default watchdog window. Long enough for typical iWork/Notes/Mail
|
|
31
|
+
/// operations under load; short enough that the Swift watchdog fires
|
|
32
|
+
/// before node's per-call timeout under default conditions.
|
|
33
|
+
static let defaultTimeout: TimeInterval = 120
|
|
34
|
+
|
|
35
|
+
/// SIGKILL grace period after the initial SIGTERM. Apple Events held by
|
|
36
|
+
/// host apps (Mail, Notes) can keep osascript unresponsive to SIGTERM for
|
|
37
|
+
/// a moment; SIGKILL is uncatchable so the second signal always wins.
|
|
38
|
+
private static let sigkillGrace: TimeInterval = 2
|
|
39
|
+
|
|
40
|
+
/// Install signal handlers that kill the currently-running osascript child
|
|
41
|
+
/// before apple-bridge dies from SIGTERM/SIGINT/SIGHUP. Required because
|
|
42
|
+
/// Foundation.Process on macOS spawns its child into a new process group,
|
|
43
|
+
/// so the node-side group-kill in src/bridge.ts only hits apple-bridge --
|
|
44
|
+
/// the osascript grandchild gets orphaned (PPID=1) and keeps holding
|
|
45
|
+
/// Mail.app's Apple Event queue hostage. The handler is async-signal-safe:
|
|
46
|
+
/// it only reads currentChildPid (sig_atomic_t) and calls kill/signal/raise,
|
|
47
|
+
/// all of which are listed as signal-safe by POSIX.
|
|
48
|
+
static func installSignalHandlers() {
|
|
49
|
+
let handler: @convention(c) (Int32) -> Void = { signo in
|
|
50
|
+
let pid = pid_t(currentChildPid)
|
|
51
|
+
if pid > 0 {
|
|
52
|
+
_ = kill(pid, SIGKILL)
|
|
53
|
+
}
|
|
54
|
+
// Restore default disposition and re-raise so we exit with the
|
|
55
|
+
// standard signal status (and any system-level cleanup runs).
|
|
56
|
+
signal(signo, SIG_DFL)
|
|
57
|
+
raise(signo)
|
|
58
|
+
}
|
|
59
|
+
signal(SIGTERM, handler)
|
|
60
|
+
signal(SIGINT, handler)
|
|
61
|
+
signal(SIGHUP, handler)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Convenience entry point that emits a `JSONOutput.error` and returns nil
|
|
65
|
+
/// on any failure (timeout, non-zero exit, spawn failure). On success
|
|
66
|
+
/// returns the trimmed stdout. `appName` is used to build the standard
|
|
67
|
+
/// permission-denied / not-running messages that every iWork module emits;
|
|
68
|
+
/// `timeoutHint` is an optional sentence appended to the timeout message
|
|
69
|
+
/// (used by Mail to suggest narrowing the search scope).
|
|
70
|
+
static func run(
|
|
71
|
+
script: String,
|
|
72
|
+
language: OsascriptLanguage = .appleScript,
|
|
73
|
+
timeout: TimeInterval = defaultTimeout,
|
|
74
|
+
appName: String,
|
|
75
|
+
timeoutHint: String? = nil
|
|
76
|
+
) -> String? {
|
|
77
|
+
guard let result = runRaw(script: script, language: language, timeout: timeout) else {
|
|
78
|
+
JSONOutput.error("Failed to spawn osascript")
|
|
79
|
+
return nil
|
|
80
|
+
}
|
|
81
|
+
if result.timedOut {
|
|
82
|
+
var msg = "\(appName) AppleScript exceeded \(Int(timeout))s timeout - killed to free \(appName)."
|
|
83
|
+
if let hint = timeoutHint {
|
|
84
|
+
msg += " \(hint)"
|
|
85
|
+
}
|
|
86
|
+
JSONOutput.error(msg)
|
|
87
|
+
return nil
|
|
88
|
+
}
|
|
89
|
+
if result.status != 0 {
|
|
90
|
+
let errStr = result.stderr.isEmpty ? "Unknown error" : result.stderr
|
|
91
|
+
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
92
|
+
JSONOutput.error("\(appName) automation permission denied. Grant access in System Settings > Privacy & Security > Automation > apple-bridge > \(appName).")
|
|
93
|
+
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
94
|
+
JSONOutput.error("\(appName) is not running. Open \(appName) and try again.")
|
|
95
|
+
} else {
|
|
96
|
+
JSONOutput.error("AppleScript error: \(errStr)")
|
|
97
|
+
}
|
|
98
|
+
return nil
|
|
99
|
+
}
|
|
100
|
+
return result.stdout
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Raw runner that returns status + stdout + stderr without side effects on
|
|
104
|
+
/// the JSON envelope. Returns nil only on spawn failure. Used by callers
|
|
105
|
+
/// that need to inspect status (e.g. Doctor probing "is Notes accessible").
|
|
106
|
+
static func runRaw(
|
|
107
|
+
script: String,
|
|
108
|
+
language: OsascriptLanguage = .appleScript,
|
|
109
|
+
timeout: TimeInterval = defaultTimeout
|
|
110
|
+
) -> OsascriptResult? {
|
|
111
|
+
let task = Process()
|
|
112
|
+
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
113
|
+
switch language {
|
|
114
|
+
case .appleScript:
|
|
115
|
+
task.arguments = ["-e", script]
|
|
116
|
+
case .javaScript:
|
|
117
|
+
task.arguments = ["-l", "JavaScript", "-e", script]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let outPipe = Pipe()
|
|
121
|
+
let errPipe = Pipe()
|
|
122
|
+
task.standardOutput = outPipe
|
|
123
|
+
task.standardError = errPipe
|
|
124
|
+
|
|
125
|
+
do {
|
|
126
|
+
try task.run()
|
|
127
|
+
} catch {
|
|
128
|
+
return nil
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let pid = task.processIdentifier
|
|
132
|
+
currentChildPid = sig_atomic_t(pid)
|
|
133
|
+
defer { currentChildPid = 0 }
|
|
134
|
+
|
|
135
|
+
let timeoutLock = NSLock()
|
|
136
|
+
var didTimeOut = false
|
|
137
|
+
|
|
138
|
+
let watchdog = DispatchWorkItem {
|
|
139
|
+
guard task.isRunning else { return }
|
|
140
|
+
timeoutLock.lock()
|
|
141
|
+
didTimeOut = true
|
|
142
|
+
timeoutLock.unlock()
|
|
143
|
+
task.terminate()
|
|
144
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + sigkillGrace) {
|
|
145
|
+
if task.isRunning { kill(pid, SIGKILL) }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: watchdog)
|
|
149
|
+
|
|
150
|
+
task.waitUntilExit()
|
|
151
|
+
watchdog.cancel()
|
|
152
|
+
|
|
153
|
+
let outData = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
154
|
+
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
155
|
+
let stdout = String(data: outData, encoding: .utf8)?
|
|
156
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
157
|
+
let stderr = String(data: errData, encoding: .utf8)?
|
|
158
|
+
.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
159
|
+
|
|
160
|
+
timeoutLock.lock()
|
|
161
|
+
let timedOut = didTimeOut
|
|
162
|
+
timeoutLock.unlock()
|
|
163
|
+
|
|
164
|
+
return OsascriptResult(
|
|
165
|
+
status: task.terminationStatus,
|
|
166
|
+
stdout: stdout,
|
|
167
|
+
stderr: stderr,
|
|
168
|
+
timedOut: timedOut
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -403,41 +403,7 @@ enum PagesBridge {
|
|
|
403
403
|
// MARK: - AppleScript Execution
|
|
404
404
|
|
|
405
405
|
private static func runAppleScript(_ script: String) -> String? {
|
|
406
|
-
|
|
407
|
-
task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
|
408
|
-
task.arguments = ["-e", script]
|
|
409
|
-
|
|
410
|
-
let outPipe = Pipe()
|
|
411
|
-
let errPipe = Pipe()
|
|
412
|
-
task.standardOutput = outPipe
|
|
413
|
-
task.standardError = errPipe
|
|
414
|
-
|
|
415
|
-
do {
|
|
416
|
-
try task.run()
|
|
417
|
-
task.waitUntilExit()
|
|
418
|
-
|
|
419
|
-
if task.terminationStatus != 0 {
|
|
420
|
-
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
|
421
|
-
let errStr = String(data: errData, encoding: .utf8)?
|
|
422
|
-
.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown error"
|
|
423
|
-
|
|
424
|
-
if errStr.contains("-1743") || errStr.contains("not allowed") {
|
|
425
|
-
JSONOutput.error("Pages automation permission denied. Grant access in System Settings > Privacy & Security > Automation.")
|
|
426
|
-
} else if errStr.contains("-600") || errStr.contains("not running") {
|
|
427
|
-
JSONOutput.error("Pages is not running. It will be launched automatically on next attempt.")
|
|
428
|
-
} else {
|
|
429
|
-
JSONOutput.error("AppleScript error: \(errStr)")
|
|
430
|
-
}
|
|
431
|
-
return nil
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
let data = outPipe.fileHandleForReading.readDataToEndOfFile()
|
|
435
|
-
return String(data: data, encoding: .utf8)?
|
|
436
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
437
|
-
} catch {
|
|
438
|
-
JSONOutput.error("Failed to run osascript: \(error.localizedDescription)")
|
|
439
|
-
return nil
|
|
440
|
-
}
|
|
406
|
+
return OsascriptRunner.run(script: script, appName: "Pages")
|
|
441
407
|
}
|
|
442
408
|
|
|
443
409
|
private static func escapeForAppleScript(_ str: String) -> String {
|