@engine-room/after-effects-mcp 0.4.0 → 0.5.1
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/README.md +5 -5
- package/bin/server.js +1730 -671
- package/package.json +1 -1
- package/panel/CSXS/manifest.xml +2 -2
- package/panel/client/main.js +323 -20
- package/panel/jsx/bundle.jsx +797 -67
- package/panel/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@engine-room/after-effects-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Control Adobe After Effects with AI — describe the animation you want and it gets built: layers, keyframes, effects, expressions and text, all editable afterwards.",
|
|
6
6
|
"license": "MIT",
|
package/panel/CSXS/manifest.xml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.
|
|
2
|
+
<ExtensionManifest Version="11.0" ExtensionBundleId="games.engine-room.ae-mcp" ExtensionBundleVersion="0.5.1"
|
|
3
3
|
ExtensionBundleName="AE MCP Bridge" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
|
4
4
|
<ExtensionList>
|
|
5
|
-
<Extension Id="games.engine-room.ae-mcp.panel" Version="0.
|
|
5
|
+
<Extension Id="games.engine-room.ae-mcp.panel" Version="0.5.1" />
|
|
6
6
|
</ExtensionList>
|
|
7
7
|
<ExecutionEnvironment>
|
|
8
8
|
<HostList>
|
package/panel/client/main.js
CHANGED
|
@@ -212,10 +212,137 @@
|
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// ---------- Port file (so MCP server can discover) ----------
|
|
215
|
+
// Written only once a port is actually bound, and only with that port. The
|
|
216
|
+
// file is a hint for the server, never the authority — the socket is — and
|
|
217
|
+
// the one way it lies is by describing a bind that did not happen or a panel
|
|
218
|
+
// that has since gone (issue #92). So it is written after `listen` succeeds
|
|
219
|
+
// and removed again on unload, and never with a port this panel merely tried.
|
|
220
|
+
var portDir = path.join(os.homedir(), ".engineroom-ae-mcp");
|
|
221
|
+
var portFile = path.join(portDir, "port");
|
|
222
|
+
var configFile = path.join(portDir, "config.json");
|
|
223
|
+
|
|
215
224
|
function writePortFile(port) {
|
|
216
|
-
|
|
217
|
-
try { fs.
|
|
218
|
-
|
|
225
|
+
try { fs.mkdirSync(portDir, { recursive: true }); } catch (e) {}
|
|
226
|
+
try { fs.writeFileSync(portFile, String(port), "utf8"); } catch (e) { log("warn", "could not write port file: " + e.message); }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Only this panel's own entry. Another panel may have written the file since
|
|
230
|
+
// — a second After Effects instance, say — and its port is not ours to erase.
|
|
231
|
+
function removePortFile(port) {
|
|
232
|
+
try {
|
|
233
|
+
if (fs.readFileSync(portFile, "utf8").trim() === String(port)) fs.unlinkSync(portFile);
|
|
234
|
+
} catch (e) {}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------- Bridge token (issue #106) ----------
|
|
238
|
+
// Why a token and not a CORS rule, and why per port: docs/fragile-areas-bridge.md.
|
|
239
|
+
// One per panel session. 384 is 0600 for engines that predate 0o literals.
|
|
240
|
+
var sessionToken = crypto.randomBytes(32).toString("hex");
|
|
241
|
+
var TOKEN_MODE = 384;
|
|
242
|
+
|
|
243
|
+
function tokenFile(port) { return path.join(portDir, "token-" + port); }
|
|
244
|
+
|
|
245
|
+
function writeTokenFile(port) {
|
|
246
|
+
try { fs.mkdirSync(portDir, { recursive: true }); } catch (e) {}
|
|
247
|
+
try {
|
|
248
|
+
fs.writeFileSync(tokenFile(port), sessionToken, { encoding: "utf8", mode: TOKEN_MODE });
|
|
249
|
+
// writeFileSync applies `mode` only when it creates the file.
|
|
250
|
+
try { fs.chmodSync(tokenFile(port), TOKEN_MODE); } catch (e) {}
|
|
251
|
+
} catch (e) {
|
|
252
|
+
log("error", "could not write the bridge token file " + tokenFile(port) + ": " + e.message +
|
|
253
|
+
" — the MCP server will not be able to authenticate, and every tool call will be refused.");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Same rule as the port file: only this panel's own entry.
|
|
258
|
+
function removeTokenFile(port) {
|
|
259
|
+
try {
|
|
260
|
+
if (fs.readFileSync(tokenFile(port), "utf8").trim() === sessionToken) fs.unlinkSync(tokenFile(port));
|
|
261
|
+
} catch (e) {}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function sameToken(given) {
|
|
265
|
+
if (typeof given !== "string" || given.length !== sessionToken.length) return false;
|
|
266
|
+
try {
|
|
267
|
+
return crypto.timingSafeEqual(Buffer.from(given, "utf8"), Buffer.from(sessionToken, "utf8"));
|
|
268
|
+
} catch (e) {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// The token is what holds. The Origin check is depth: browsers always send one,
|
|
274
|
+
// Node's fetch and `ws` never do.
|
|
275
|
+
function authFailure(req) {
|
|
276
|
+
var h = (req && req.headers) || {};
|
|
277
|
+
if (h.origin) return "it came from a web page (Origin: " + h.origin + ")";
|
|
278
|
+
if (!sameToken(h["x-ae-mcp-token"])) return "the bridge token was missing or did not match";
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Written for an MCP server older than this panel: it relays `error` verbatim,
|
|
283
|
+
// so the remedy has to be in here.
|
|
284
|
+
function denialMessage() {
|
|
285
|
+
return "This After Effects bridge requires a token and this request did not carry a valid one. " +
|
|
286
|
+
"Nothing was run in After Effects. The panel writes the token beside its port file, in " + portDir +
|
|
287
|
+
", each time it binds; a local process can read it and a web page cannot, which is the entire point " +
|
|
288
|
+
"(issue #106). If you are the MCP server: you are older than the panel After Effects is running — " +
|
|
289
|
+
"update after-effects-mcp to 0.5.1 or newer and reconnect it.";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Rate-limited: a page firing in a loop must not fill the panel's log.
|
|
293
|
+
var denied = 0;
|
|
294
|
+
var deniedLoggedAt = 0;
|
|
295
|
+
function noteDenial(what, why) {
|
|
296
|
+
denied += 1;
|
|
297
|
+
var now = Date.now();
|
|
298
|
+
if (denied === 1 || now - deniedLoggedAt > 10000) {
|
|
299
|
+
deniedLoggedAt = now;
|
|
300
|
+
log("warn", "refused a request to " + what + ": " + why +
|
|
301
|
+
(denied > 1 ? " (" + denied + " refused so far)" : "") +
|
|
302
|
+
". If this is the MCP server rather than a web page, it is older than this panel: update it and reconnect.");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------- Config ----------
|
|
307
|
+
// A CEP panel cannot read an environment variable the user set: it runs in
|
|
308
|
+
// After Effects' process, with Adobe's environment. So its two knobs live in
|
|
309
|
+
// a JSON file beside the port file, ~/.engineroom-ae-mcp/config.json:
|
|
310
|
+
//
|
|
311
|
+
// { "port": 7777, "allowPortWalk": false }
|
|
312
|
+
//
|
|
313
|
+
// port the port to bind. Default 7777. 0 binds an ephemeral port,
|
|
314
|
+
// which is what the offline tests use.
|
|
315
|
+
// allowPortWalk when the port is held by another AE MCP panel, move to the
|
|
316
|
+
// next free one instead of waiting for it. For people who run
|
|
317
|
+
// two After Effects instances at once. Off by default: on one
|
|
318
|
+
// machine the holder is a zombie, and walking around it is
|
|
319
|
+
// what issue #92 was.
|
|
320
|
+
//
|
|
321
|
+
// Anything unreadable is logged and ignored; the defaults are what shipped.
|
|
322
|
+
var DEFAULT_PORT = 7777;
|
|
323
|
+
function readConfig() {
|
|
324
|
+
var cfg = { port: DEFAULT_PORT, allowPortWalk: false, bindRetryMs: 2000, source: null };
|
|
325
|
+
var raw;
|
|
326
|
+
try { raw = fs.readFileSync(configFile, "utf8"); } catch (e) { return cfg; }
|
|
327
|
+
var parsed;
|
|
328
|
+
try { parsed = JSON.parse(raw); }
|
|
329
|
+
catch (e) { log("warn", "ignoring " + configFile + " — not valid JSON (" + e.message + ")"); return cfg; }
|
|
330
|
+
if (!parsed || typeof parsed !== "object") {
|
|
331
|
+
log("warn", "ignoring " + configFile + " — expected a JSON object");
|
|
332
|
+
return cfg;
|
|
333
|
+
}
|
|
334
|
+
cfg.source = configFile;
|
|
335
|
+
if (parsed.port !== undefined) {
|
|
336
|
+
if (typeof parsed.port === "number" && parsed.port >= 0 && parsed.port <= 65535 && parsed.port === Math.floor(parsed.port)) cfg.port = parsed.port;
|
|
337
|
+
else log("warn", "ignoring \"port\" in " + configFile + " — expected a whole number 0-65535, got " + JSON.stringify(parsed.port));
|
|
338
|
+
}
|
|
339
|
+
if (parsed.allowPortWalk !== undefined) {
|
|
340
|
+
if (typeof parsed.allowPortWalk === "boolean") cfg.allowPortWalk = parsed.allowPortWalk;
|
|
341
|
+
else log("warn", "ignoring \"allowPortWalk\" in " + configFile + " — expected true or false");
|
|
342
|
+
}
|
|
343
|
+
// Undocumented on purpose: how often to retry a held port. Tests shorten it.
|
|
344
|
+
if (typeof parsed.bindRetryMs === "number" && parsed.bindRetryMs >= 50) cfg.bindRetryMs = parsed.bindRetryMs;
|
|
345
|
+
return cfg;
|
|
219
346
|
}
|
|
220
347
|
|
|
221
348
|
// ---------- WS broadcast ----------
|
|
@@ -806,19 +933,30 @@
|
|
|
806
933
|
return new Promise(function (resolve, reject) {
|
|
807
934
|
var server = http.createServer(function (req, res) {
|
|
808
935
|
var url = req.url || "/";
|
|
809
|
-
|
|
810
|
-
|
|
936
|
+
|
|
937
|
+
// No CORS headers, deliberately: nothing that legitimately calls this
|
|
938
|
+
// bridge is a web page (issue #106).
|
|
811
939
|
|
|
812
940
|
if (url === "/health") {
|
|
813
941
|
res.statusCode = 200;
|
|
814
942
|
res.setHeader("content-type", "application/json");
|
|
815
943
|
// bundleHash identifies the code this panel is *running*, which is what
|
|
816
944
|
// the server needs to know before sending an op the panel may predate.
|
|
945
|
+
// `ok` + `bundleHash` is also how another copy of this panel, and the
|
|
946
|
+
// server's discovery, recognise this as an AE MCP panel — see
|
|
947
|
+
// isOurHealth below and isPanelHealth in the server's discovery.ts.
|
|
948
|
+
// The port is read off the socket, not the argument: port 0 binds an
|
|
949
|
+
// ephemeral one and the argument would say 0.
|
|
950
|
+
var addr = server.address();
|
|
817
951
|
res.end(JSON.stringify({
|
|
818
952
|
ok: true,
|
|
819
|
-
port: port,
|
|
953
|
+
port: addr && addr.port ? addr.port : port,
|
|
820
954
|
bundleLoaded: true,
|
|
821
955
|
bundleHash: loadedBundleHash,
|
|
956
|
+
// The gate exists, never anything about the token. Absent on a
|
|
957
|
+
// pre-#106 panel, which is how the server tells "needs a token"
|
|
958
|
+
// from "too old to want one".
|
|
959
|
+
auth: true,
|
|
822
960
|
ts: Date.now()
|
|
823
961
|
}));
|
|
824
962
|
return;
|
|
@@ -829,6 +967,17 @@
|
|
|
829
967
|
return;
|
|
830
968
|
}
|
|
831
969
|
|
|
970
|
+
// Every route below reaches After Effects. Checked before the body is
|
|
971
|
+
// read, so an unidentified caller cannot make us buffer anything.
|
|
972
|
+
var denial = authFailure(req);
|
|
973
|
+
if (denial) {
|
|
974
|
+
noteDenial(url, denial);
|
|
975
|
+
res.statusCode = 403;
|
|
976
|
+
res.setHeader("content-type", "application/json");
|
|
977
|
+
res.end(JSON.stringify({ ok: false, error: denialMessage(), code: "bridge_unauthorized" }));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
|
|
832
981
|
var bufs = [];
|
|
833
982
|
req.on("data", function (c) { bufs.push(c); });
|
|
834
983
|
req.on("end", function () {
|
|
@@ -887,26 +1036,171 @@
|
|
|
887
1036
|
});
|
|
888
1037
|
}
|
|
889
1038
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1039
|
+
// ---------- Who holds the port? ----------
|
|
1040
|
+
// The same test the server's discovery makes (isPanelHealth in discovery.ts):
|
|
1041
|
+
// `ok` plus `bundleHash` — or `bundleLoaded`, on a panel from before the hash
|
|
1042
|
+
// existed — is what nothing else on a loopback port would answer with.
|
|
1043
|
+
function isOurHealth(body) {
|
|
1044
|
+
return !!body && typeof body === "object" && body.ok === true &&
|
|
1045
|
+
("bundleHash" in body || "bundleLoaded" in body);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// Never rejects: every outcome is a classification the caller acts on.
|
|
1049
|
+
// panel — another AE MCP panel; its health body is attached
|
|
1050
|
+
// other — something answered, but not as the panel
|
|
1051
|
+
// silent — accepted the connection and said nothing, or errored
|
|
1052
|
+
function identifyHolder(port) {
|
|
1053
|
+
return new Promise(function (resolve) {
|
|
1054
|
+
var done = false;
|
|
1055
|
+
function finish(r) { if (!done) { done = true; resolve(r); } }
|
|
1056
|
+
var req = http.get({ host: "127.0.0.1", port: port, path: "/health", timeout: 1500 }, function (res) {
|
|
1057
|
+
var bufs = [];
|
|
1058
|
+
res.on("data", function (c) { bufs.push(c); });
|
|
1059
|
+
res.on("end", function () {
|
|
1060
|
+
var body = null;
|
|
1061
|
+
try { body = JSON.parse(Buffer.concat(bufs).toString("utf8")); } catch (e) {}
|
|
1062
|
+
if (res.statusCode === 200 && isOurHealth(body)) finish({ kind: "panel", health: body });
|
|
1063
|
+
else finish({ kind: "other", detail: "answered HTTP " + res.statusCode + (body ? " with an unfamiliar body" : " with no JSON") });
|
|
1064
|
+
});
|
|
1065
|
+
res.on("error", function (e) { finish({ kind: "silent", detail: e.message }); });
|
|
1066
|
+
});
|
|
1067
|
+
req.on("timeout", function () { req.destroy(); finish({ kind: "silent", detail: "accepted the connection but did not answer within 1.5s" }); });
|
|
1068
|
+
req.on("error", function (e) { finish({ kind: "silent", detail: e.message }); });
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// 7777 through 7799 was the old ceiling; kept as a distance so a configured
|
|
1073
|
+
// start port walks the same number of steps.
|
|
1074
|
+
var WALK_LIMIT = 22;
|
|
1075
|
+
|
|
1076
|
+
/**
|
|
1077
|
+
* Bind the configured port, and decide what to do when it is taken.
|
|
1078
|
+
*
|
|
1079
|
+
* It used to walk: EADDRINUSE, try the next port, up to 7799, then write
|
|
1080
|
+
* whatever it got to the port file. On one machine nothing else ever holds
|
|
1081
|
+
* 7777, so the thing being walked around was always a previous instance of
|
|
1082
|
+
* this same panel — a CEPHtmlEngine that lingered across an After Effects
|
|
1083
|
+
* relaunch, or a second panel window — and the walk routed round it in
|
|
1084
|
+
* silence, leaving the port file as a second source of truth that could
|
|
1085
|
+
* disagree with the socket. Four incidents in a week (issue #92).
|
|
1086
|
+
*
|
|
1087
|
+
* So the holder is identified first. If it is one of us, this panel does not
|
|
1088
|
+
* move: it says so in its status and log, and retries the bind on an interval
|
|
1089
|
+
* so it takes the port over the moment the other one exits, with no restart.
|
|
1090
|
+
* If it is something else, or answers nothing, the walk happens as before —
|
|
1091
|
+
* loudly, naming the holder and the port chosen, because that is the one
|
|
1092
|
+
* case where the port file legitimately says something other than 7777.
|
|
1093
|
+
* `allowPortWalk` in config.json opts back into walking past our own panel,
|
|
1094
|
+
* for people who really run two After Effects instances.
|
|
1095
|
+
*/
|
|
1096
|
+
function startServers(cfg) {
|
|
1097
|
+
var startPort = cfg.port;
|
|
1098
|
+
var retryMs = cfg.bindRetryMs;
|
|
1099
|
+
var waits = 0;
|
|
1100
|
+
|
|
1101
|
+
function bindWith(p) {
|
|
894
1102
|
return startHttp(p).then(function (server) {
|
|
1103
|
+
// Read back rather than assumed: port 0 binds an ephemeral one.
|
|
1104
|
+
var bound = server.address().port;
|
|
1105
|
+
// The socket is already accepting, so the token must be on disk before
|
|
1106
|
+
// this promise resolves — not later in the boot.
|
|
1107
|
+
writeTokenFile(bound);
|
|
895
1108
|
// Mount WS on the same HTTP server.
|
|
896
|
-
var wss = new WebSocket.Server({
|
|
1109
|
+
var wss = new WebSocket.Server({
|
|
1110
|
+
server: server,
|
|
1111
|
+
path: "/events",
|
|
1112
|
+
// WebSockets ignore the same-origin policy entirely, so without this
|
|
1113
|
+
// any page could watch the user's event stream (issue #106).
|
|
1114
|
+
verifyClient: function (info, cb) {
|
|
1115
|
+
var why = authFailure(info.req);
|
|
1116
|
+
if (!why) return cb(true);
|
|
1117
|
+
noteDenial("/events", why);
|
|
1118
|
+
cb(false, 403, "Forbidden");
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
897
1121
|
wss.on("connection", function (ws) {
|
|
898
1122
|
wsClients.add(ws);
|
|
899
1123
|
ws.on("close", function () { wsClients.delete(ws); });
|
|
900
1124
|
});
|
|
901
|
-
return { port:
|
|
902
|
-
})
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1125
|
+
return { port: bound, server: server, wss: wss };
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function walk(p, tries, why) {
|
|
1130
|
+
var next = p + 1;
|
|
1131
|
+
if (tries >= WALK_LIMIT) {
|
|
1132
|
+
throw new Error("ports " + startPort + " through " + p + " are all in use — nothing free to bind. Last holder: " + why);
|
|
1133
|
+
}
|
|
1134
|
+
log("warn", "port " + p + " is held by " + why + " — moving to port " + next + ". " +
|
|
1135
|
+
"The MCP server finds this port through the port file; if tool calls fail on " + p +
|
|
1136
|
+
" while check_setup passes, this is why, and the server re-checks the port on the next refused call.");
|
|
1137
|
+
return attempt(next, tries + 1);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function waitForHolder(p, holder) {
|
|
1141
|
+
var same = holder.health.bundleHash && holder.health.bundleHash === loadedBundleHash;
|
|
1142
|
+
var hash = holder.health.bundleHash
|
|
1143
|
+
? String(holder.health.bundleHash).slice(0, 12)
|
|
1144
|
+
: "no bundleHash — a panel older than 0.3";
|
|
1145
|
+
setStatus("waiting — port " + p + " is held by another AE MCP panel", "warn");
|
|
1146
|
+
if (waits === 0) {
|
|
1147
|
+
log("warn", "port " + p + " is already held by another AE MCP panel (bundle " + hash + ", " +
|
|
1148
|
+
(same ? "the same version as this one" : "a DIFFERENT version from this one") + "). " +
|
|
1149
|
+
"Not moving to another port: that would leave the port file pointing at whichever panel bound last, " +
|
|
1150
|
+
"and the MCP server talking to the wrong one (issue #92).");
|
|
1151
|
+
log("warn", "Retrying the bind every " + (retryMs / 1000) + "s; this panel takes the port over the moment " +
|
|
1152
|
+
"the other one exits, with no restart. If After Effects was relaunched and this persists, the previous " +
|
|
1153
|
+
"panel process is still running: quit After Effects fully, check Activity Monitor / Task Manager for a " +
|
|
1154
|
+
"leftover CEPHtmlEngine, and reopen it. To run two After Effects at once instead, set " +
|
|
1155
|
+
"\"allowPortWalk\": true in " + configFile + ".");
|
|
1156
|
+
} else if (waits % 30 === 0) {
|
|
1157
|
+
log("info", "still waiting for port " + p + " (" + waits + " attempts)");
|
|
1158
|
+
}
|
|
1159
|
+
waits += 1;
|
|
1160
|
+
return new Promise(function (r) { setTimeout(r, retryMs); }).then(function () { return attempt(p, 0); });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function attempt(p, tries) {
|
|
1164
|
+
return bindWith(p).catch(function (e) {
|
|
1165
|
+
if (e.code !== "EADDRINUSE") throw e;
|
|
1166
|
+
return identifyHolder(p).then(function (holder) {
|
|
1167
|
+
if (holder.kind === "panel") {
|
|
1168
|
+
if (cfg.allowPortWalk) {
|
|
1169
|
+
return walk(p, tries, "another AE MCP panel, and allowPortWalk is on in " + configFile);
|
|
1170
|
+
}
|
|
1171
|
+
return waitForHolder(p, holder);
|
|
1172
|
+
}
|
|
1173
|
+
var why = holder.kind === "other"
|
|
1174
|
+
? "something that is not an AE MCP panel (" + holder.detail + ")"
|
|
1175
|
+
: "something that did not identify itself (" + holder.detail + ")";
|
|
1176
|
+
return walk(p, tries, why);
|
|
1177
|
+
});
|
|
907
1178
|
});
|
|
908
1179
|
}
|
|
909
|
-
return attempt(startPort);
|
|
1180
|
+
return attempt(startPort, 0);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// ---------- Shutdown ----------
|
|
1184
|
+
// When the panel window closes or After Effects quits, give the port back and
|
|
1185
|
+
// take this panel's port-file entry with it. A panel that exits cleanly must
|
|
1186
|
+
// not leave a file naming a port nobody listens on — that is the other half
|
|
1187
|
+
// of how the port file came to disagree with the socket (issue #92). A
|
|
1188
|
+
// process that dies without unloading still leaves the file; the server's
|
|
1189
|
+
// discovery treats the file as a hint for exactly that reason.
|
|
1190
|
+
var running = null;
|
|
1191
|
+
function shutdown() {
|
|
1192
|
+
if (!running) return;
|
|
1193
|
+
var s = running;
|
|
1194
|
+
running = null;
|
|
1195
|
+
try { s.wss.close(); } catch (e) {}
|
|
1196
|
+
try { if (typeof s.server.closeAllConnections === "function") s.server.closeAllConnections(); } catch (e) {}
|
|
1197
|
+
try { s.server.close(); } catch (e) {}
|
|
1198
|
+
removePortFile(s.port);
|
|
1199
|
+
removeTokenFile(s.port);
|
|
1200
|
+
}
|
|
1201
|
+
if (typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
1202
|
+
window.addEventListener("beforeunload", shutdown);
|
|
1203
|
+
window.addEventListener("unload", shutdown);
|
|
910
1204
|
}
|
|
911
1205
|
|
|
912
1206
|
// ---------- Boot sequence ----------
|
|
@@ -916,14 +1210,23 @@
|
|
|
916
1210
|
$ae.textContent = (host && host.appName ? host.appName : "AE") + " " + (host && host.appVersion ? host.appVersion : "");
|
|
917
1211
|
} catch (e) {}
|
|
918
1212
|
|
|
1213
|
+
var config = readConfig();
|
|
1214
|
+
if (config.source) {
|
|
1215
|
+
log("info", "read " + config.source + ": port " + config.port + (config.allowPortWalk ? ", allowPortWalk on" : ""));
|
|
1216
|
+
}
|
|
1217
|
+
|
|
919
1218
|
loadJsxBundle()
|
|
920
|
-
.then(function () { return startServers(
|
|
1219
|
+
.then(function () { return startServers(config); })
|
|
921
1220
|
.then(function (s) {
|
|
1221
|
+
running = s;
|
|
1222
|
+
// The only write of the port file, and only with the port actually bound.
|
|
922
1223
|
writePortFile(s.port);
|
|
923
1224
|
$port.textContent = String(s.port);
|
|
924
1225
|
$port.className = "ok";
|
|
925
1226
|
setStatus("ready", "ok");
|
|
926
|
-
log("info", "Bridge listening on http://127.0.0.1:" + s.port
|
|
1227
|
+
log("info", "Bridge listening on http://127.0.0.1:" + s.port +
|
|
1228
|
+
(s.port !== config.port && config.port !== 0 ? " (configured port " + config.port + " was taken — see above)" : ""));
|
|
1229
|
+
log("info", "Calls are authenticated: the MCP server reads this session's token from " + tokenFile(s.port) + ".");
|
|
927
1230
|
})
|
|
928
1231
|
.catch(function (e) {
|
|
929
1232
|
setStatus("failed", "err");
|