@dimina-kit/devtools 0.4.0-dev.20260702175315 → 0.4.0-dev.20260703051110
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/dist/main/app/app.js +5 -25
- package/dist/main/app/native-overview.d.ts +27 -0
- package/dist/main/app/native-overview.js +44 -0
- package/dist/main/index.bundle.js +826 -690
- package/dist/main/ipc/bridge-router.js +180 -141
- package/dist/main/ipc/project-fs.js +39 -21
- package/dist/main/services/automation/handlers/app.js +143 -109
- package/dist/main/services/mcp/tools/simulator-tools.js +118 -97
- package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
- package/dist/main/services/network-forward/dispatch-batch.js +21 -0
- package/dist/main/services/network-forward/index.js +5 -20
- package/dist/main/services/projects/create-project-service.js +54 -34
- package/dist/main/services/projects/types.d.ts +1 -32
- package/dist/main/services/service-console/console-api.js +36 -27
- package/dist/main/services/simulator-temp-files/disk.js +109 -86
- package/dist/main/services/workbench-coi-server.js +58 -45
- package/dist/main/utils/logger.d.ts +5 -12
- package/dist/main/utils/logger.js +5 -45
- package/dist/preload/instrumentation/wxml-extract.js +94 -62
- package/dist/preload/runtime/main-api-runner.d.ts +1 -1
- package/dist/preload/windows/main.cjs +284 -565
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +37 -39
- package/dist/preload/windows/simulator.cjs.map +2 -2
- package/dist/preload/windows/simulator.js +37 -39
- package/dist/render-host/render-inspect.js +63 -52
- package/dist/renderer/assets/index-D-ksyN1p.js +49 -0
- package/dist/renderer/assets/workbenchSettings-COhuFF-i.js +8 -0
- package/dist/renderer/entries/main/index.html +1 -1
- package/dist/renderer/entries/workbench-settings/index.html +1 -1
- package/dist/shared/appdata-accumulator.js +53 -48
- package/dist/shared/open-in-editor-resource-path.d.ts +56 -0
- package/dist/shared/open-in-editor-resource-path.js +133 -0
- package/dist/shared/open-in-editor.d.ts +2 -9
- package/dist/shared/open-in-editor.js +8 -93
- package/dist/shared/types.d.ts +13 -0
- package/dist/simulator/assets/{device-shell-CUl0ILfn.js → device-shell-CiLAPa0I.js} +2 -2
- package/dist/simulator/assets/simulator-Dvnxry3y.js +10 -0
- package/dist/simulator/simulator.html +1 -1
- package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
- package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
- package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
- package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
- package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +5 -5
- package/dist/renderer/assets/index-B-Dqb7G0.js +0 -49
- package/dist/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
- package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
- package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
|
@@ -94,6 +94,34 @@ function summarizeBridgeMsg(payload) {
|
|
|
94
94
|
}
|
|
95
95
|
/** Default timeout for a simulator-forwarded API call. */
|
|
96
96
|
const API_CALL_TIMEOUT_MS = 5_000;
|
|
97
|
+
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
98
|
+
// order): after a respawn/reopen the newest session is the live one — the
|
|
99
|
+
// first match could be a just-superseded session mid-teardown. The loop must
|
|
100
|
+
// scan every session (no early break) so the LAST inserted match wins.
|
|
101
|
+
function findAppSessionByAppId(state, appId) {
|
|
102
|
+
let match;
|
|
103
|
+
for (const ap of state.appSessions.values())
|
|
104
|
+
if (ap.appId === appId)
|
|
105
|
+
match = ap;
|
|
106
|
+
return match;
|
|
107
|
+
}
|
|
108
|
+
// No appId hint and no workspace session to disambiguate. Picking by appId is
|
|
109
|
+
// impossible, so fall back to the most-recent spawn — but ONLY when every live
|
|
110
|
+
// session belongs to the same app. Same-appId multiples are a respawn/reopen
|
|
111
|
+
// in progress where the newest is the live one (insertion order = spawn
|
|
112
|
+
// order). Multiple DISTINCT appIds mean a previous project is mid-teardown
|
|
113
|
+
// alongside the new one; any pick is a guess that can resolve the WRONG
|
|
114
|
+
// project's content (the screenshot/inspect-stale-app bug class), so prefer
|
|
115
|
+
// null over a wrong guess.
|
|
116
|
+
function resolveFallbackAppSession(state) {
|
|
117
|
+
let last;
|
|
118
|
+
const distinctAppIds = new Set();
|
|
119
|
+
for (const ap of state.appSessions.values()) {
|
|
120
|
+
last = ap;
|
|
121
|
+
distinctAppIds.add(ap.appId);
|
|
122
|
+
}
|
|
123
|
+
return distinctAppIds.size <= 1 ? last : undefined;
|
|
124
|
+
}
|
|
97
125
|
/**
|
|
98
126
|
* Resolve the "current" app session: prefer an explicit appId, else the
|
|
99
127
|
* workspace's active project, else the only / most-recently-created session.
|
|
@@ -101,24 +129,15 @@ const API_CALL_TIMEOUT_MS = 5_000;
|
|
|
101
129
|
* rather than assuming a single session.
|
|
102
130
|
*/
|
|
103
131
|
function resolveCurrentApp(state, ctx, appId) {
|
|
104
|
-
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
105
|
-
// order): after a respawn/reopen the newest session is the live one — the
|
|
106
|
-
// first match could be a just-superseded session mid-teardown.
|
|
107
132
|
if (appId) {
|
|
108
|
-
|
|
109
|
-
for (const ap of state.appSessions.values())
|
|
110
|
-
if (ap.appId === appId)
|
|
111
|
-
match = ap;
|
|
133
|
+
const match = findAppSessionByAppId(state, appId);
|
|
112
134
|
if (match)
|
|
113
135
|
return match;
|
|
114
136
|
}
|
|
115
137
|
const appInfo = ctx.workspace?.getSession?.()?.appInfo;
|
|
116
138
|
const activeAppId = appInfo?.appId;
|
|
117
139
|
if (activeAppId) {
|
|
118
|
-
|
|
119
|
-
for (const ap of state.appSessions.values())
|
|
120
|
-
if (ap.appId === activeAppId)
|
|
121
|
-
match = ap;
|
|
140
|
+
const match = findAppSessionByAppId(state, activeAppId);
|
|
122
141
|
if (match)
|
|
123
142
|
return match;
|
|
124
143
|
}
|
|
@@ -128,21 +147,7 @@ function resolveCurrentApp(state, ctx, appId) {
|
|
|
128
147
|
// while a close is in flight.
|
|
129
148
|
if (ctx.workspace?.isClosing?.())
|
|
130
149
|
return undefined;
|
|
131
|
-
|
|
132
|
-
// impossible, so fall back to the most-recent spawn — but ONLY when every live
|
|
133
|
-
// session belongs to the same app. Same-appId multiples are a respawn/reopen
|
|
134
|
-
// in progress where the newest is the live one (insertion order = spawn
|
|
135
|
-
// order). Multiple DISTINCT appIds mean a previous project is mid-teardown
|
|
136
|
-
// alongside the new one; any pick is a guess that can resolve the WRONG
|
|
137
|
-
// project's content (the screenshot/inspect-stale-app bug class), so prefer
|
|
138
|
-
// null over a wrong guess.
|
|
139
|
-
let last;
|
|
140
|
-
const distinctAppIds = new Set();
|
|
141
|
-
for (const ap of state.appSessions.values()) {
|
|
142
|
-
last = ap;
|
|
143
|
-
distinctAppIds.add(ap.appId);
|
|
144
|
-
}
|
|
145
|
-
return distinctAppIds.size <= 1 ? last : undefined;
|
|
150
|
+
return resolveFallbackAppSession(state);
|
|
146
151
|
}
|
|
147
152
|
export function installBridgeRouter(ctx) {
|
|
148
153
|
const state = {
|
|
@@ -1136,86 +1141,119 @@ const APP_LIFECYCLE_UNREGISTER = {
|
|
|
1136
1141
|
offAppHide: 'onAppHide',
|
|
1137
1142
|
offError: 'onError',
|
|
1138
1143
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
if (NAV_BAR_API_NAMES.has(name)) {
|
|
1143
|
-
if (!ap.simulatorWc.isDestroyed()) {
|
|
1144
|
-
ap.simulatorWc.send(E.NAV_BAR, {
|
|
1145
|
-
bridgeId: page.bridgeId,
|
|
1146
|
-
name,
|
|
1147
|
-
params,
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
const successResult = { errMsg: `${name}:ok` };
|
|
1151
|
-
sendCallback(ap, params.success, successResult);
|
|
1152
|
-
sendCallback(ap, params.complete, successResult);
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (NAV_ACTION_NAMES.has(name)) {
|
|
1156
|
-
const payload = {
|
|
1157
|
-
appSessionId: ap.appSessionId,
|
|
1144
|
+
function handleNavBarApi(ap, page, name, params) {
|
|
1145
|
+
if (!ap.simulatorWc.isDestroyed()) {
|
|
1146
|
+
ap.simulatorWc.send(E.NAV_BAR, {
|
|
1158
1147
|
bridgeId: page.bridgeId,
|
|
1159
|
-
name
|
|
1148
|
+
name,
|
|
1160
1149
|
params,
|
|
1161
|
-
|
|
1162
|
-
};
|
|
1163
|
-
if (!ap.simulatorWc.isDestroyed()) {
|
|
1164
|
-
ap.simulatorWc.send(E.NAV_ACTION, payload);
|
|
1165
|
-
}
|
|
1166
|
-
else {
|
|
1167
|
-
const fail = { errMsg: `${name}:fail simulator window destroyed` };
|
|
1168
|
-
sendCallback(ap, params.fail, fail);
|
|
1169
|
-
sendCallback(ap, params.complete, fail);
|
|
1170
|
-
}
|
|
1171
|
-
return;
|
|
1150
|
+
});
|
|
1172
1151
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
else {
|
|
1185
|
-
const fail = { errMsg: `${name}:fail simulator window destroyed` };
|
|
1186
|
-
sendCallback(ap, params.fail, fail);
|
|
1187
|
-
sendCallback(ap, params.complete, fail);
|
|
1188
|
-
}
|
|
1152
|
+
const successResult = { errMsg: `${name}:ok` };
|
|
1153
|
+
sendCallback(ap, params.success, successResult);
|
|
1154
|
+
sendCallback(ap, params.complete, successResult);
|
|
1155
|
+
}
|
|
1156
|
+
// Shared by handleNavActionApi/handleTabActionApi: both forward a same-shaped
|
|
1157
|
+
// payload to the simulator window, differing only in the event name and the
|
|
1158
|
+
// payload's static `name` union. Simulator-destroyed fails the callback
|
|
1159
|
+
// directly since there is no ack path to fail through otherwise.
|
|
1160
|
+
function sendActionOrFail(ap, eventName, payload, name, params) {
|
|
1161
|
+
if (!ap.simulatorWc.isDestroyed()) {
|
|
1162
|
+
ap.simulatorWc.send(eventName, payload);
|
|
1189
1163
|
return;
|
|
1190
1164
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1165
|
+
const fail = { errMsg: `${name}:fail simulator window destroyed` };
|
|
1166
|
+
sendCallback(ap, params.fail, fail);
|
|
1167
|
+
sendCallback(ap, params.complete, fail);
|
|
1168
|
+
}
|
|
1169
|
+
function handleNavActionApi(ap, page, name, params) {
|
|
1170
|
+
const payload = {
|
|
1171
|
+
appSessionId: ap.appSessionId,
|
|
1172
|
+
bridgeId: page.bridgeId,
|
|
1173
|
+
name: name,
|
|
1174
|
+
params,
|
|
1175
|
+
callbacks: extractCallbacks(params),
|
|
1176
|
+
};
|
|
1177
|
+
sendActionOrFail(ap, E.NAV_ACTION, payload, name, params);
|
|
1178
|
+
}
|
|
1179
|
+
function handleTabActionApi(ap, page, name, params) {
|
|
1180
|
+
const payload = {
|
|
1181
|
+
appSessionId: ap.appSessionId,
|
|
1182
|
+
bridgeId: page.bridgeId,
|
|
1183
|
+
name: name,
|
|
1184
|
+
params,
|
|
1185
|
+
callbacks: extractCallbacks(params),
|
|
1186
|
+
};
|
|
1187
|
+
sendActionOrFail(ap, E.TAB_ACTION, payload, name, params);
|
|
1188
|
+
}
|
|
1189
|
+
// App-level lifecycle listeners (wx.onAppShow / onAppHide / onError + off*).
|
|
1190
|
+
// The service encodes the listener as a keep callback id in `params.success`;
|
|
1191
|
+
// we store it and re-fire on main-window foreground/background (the driver in
|
|
1192
|
+
// installBridgeRouter) and on serviceHostError. `off*` clears the event's
|
|
1193
|
+
// listeners for the session. Returns true when `name` matched a lifecycle
|
|
1194
|
+
// register/unregister call (fully handled), false to fall through.
|
|
1195
|
+
function handleAppLifecycleToggle(state, ap, name, params) {
|
|
1196
1196
|
const lifecycleRegister = APP_LIFECYCLE_REGISTER[name];
|
|
1197
1197
|
if (lifecycleRegister) {
|
|
1198
1198
|
state.appLifecycle.register(ap.appSessionId, lifecycleRegister, params.success);
|
|
1199
|
-
return;
|
|
1199
|
+
return true;
|
|
1200
1200
|
}
|
|
1201
1201
|
const lifecycleUnregister = APP_LIFECYCLE_UNREGISTER[name];
|
|
1202
1202
|
if (lifecycleUnregister) {
|
|
1203
1203
|
// off(cb) carries the same evtId as the original on(cb) → removes just that
|
|
1204
1204
|
// listener; off() with no callback carries no id → clears the whole event.
|
|
1205
1205
|
state.appLifecycle.unregister(ap.appSessionId, lifecycleUnregister, params.success);
|
|
1206
|
+
return true;
|
|
1207
|
+
}
|
|
1208
|
+
return false;
|
|
1209
|
+
}
|
|
1210
|
+
// pageScrollTo acts on the page's render guest (scroll its document), which
|
|
1211
|
+
// only the main process can reach — run the scroll script in the invoking
|
|
1212
|
+
// page's render webContents rather than forwarding to the simulator.
|
|
1213
|
+
function handlePageScrollApi(ap, page, params) {
|
|
1214
|
+
const renderWc = page.renderWc;
|
|
1215
|
+
if (renderWc && !renderWc.isDestroyed()) {
|
|
1216
|
+
void renderWc.executeJavaScript(buildPageScrollScript(params)).catch(() => { });
|
|
1217
|
+
}
|
|
1218
|
+
const successResult = { errMsg: 'pageScrollTo:ok' };
|
|
1219
|
+
sendCallback(ap, params.success, successResult);
|
|
1220
|
+
sendCallback(ap, params.complete, successResult);
|
|
1221
|
+
}
|
|
1222
|
+
// Shared try/invoke/callback pattern used by the storageApi and
|
|
1223
|
+
// ctx.simulatorApis branches: relay the resolved value through
|
|
1224
|
+
// success+complete, or the thrown error through fail+complete with a
|
|
1225
|
+
// namespaced errMsg.
|
|
1226
|
+
async function invokeSimulatorApiAndCallback(ap, name, params, invoke) {
|
|
1227
|
+
try {
|
|
1228
|
+
const result = await invoke();
|
|
1229
|
+
sendCallback(ap, params.success, result);
|
|
1230
|
+
sendCallback(ap, params.complete, result);
|
|
1231
|
+
}
|
|
1232
|
+
catch (error) {
|
|
1233
|
+
const failResult = { errMsg: `${name}:fail ${error instanceof Error ? error.message : String(error)}` };
|
|
1234
|
+
sendCallback(ap, params.fail, failResult);
|
|
1235
|
+
sendCallback(ap, params.complete, failResult);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
1239
|
+
const name = String(body.name ?? '');
|
|
1240
|
+
const params = normalizeParams(body.params);
|
|
1241
|
+
if (NAV_BAR_API_NAMES.has(name)) {
|
|
1242
|
+
handleNavBarApi(ap, page, name, params);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (NAV_ACTION_NAMES.has(name)) {
|
|
1246
|
+
handleNavActionApi(ap, page, name, params);
|
|
1206
1247
|
return;
|
|
1207
1248
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1249
|
+
if (TAB_ACTION_NAMES.has(name)) {
|
|
1250
|
+
handleTabActionApi(ap, page, name, params);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
if (handleAppLifecycleToggle(state, ap, name, params))
|
|
1254
|
+
return;
|
|
1211
1255
|
if (name === 'pageScrollTo') {
|
|
1212
|
-
|
|
1213
|
-
if (renderWc && !renderWc.isDestroyed()) {
|
|
1214
|
-
void renderWc.executeJavaScript(buildPageScrollScript(params)).catch(() => { });
|
|
1215
|
-
}
|
|
1216
|
-
const successResult = { errMsg: 'pageScrollTo:ok' };
|
|
1217
|
-
sendCallback(ap, params.success, successResult);
|
|
1218
|
-
sendCallback(ap, params.complete, successResult);
|
|
1256
|
+
handlePageScrollApi(ap, page, params);
|
|
1219
1257
|
return;
|
|
1220
1258
|
}
|
|
1221
1259
|
// Native-host storage unification: route async wx.setStorage/getStorage/etc.
|
|
@@ -1224,16 +1262,8 @@ async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
|
1224
1262
|
// http:// origin. Without this, async writes and sync writes diverge across
|
|
1225
1263
|
// two origins even for the running mini-app.
|
|
1226
1264
|
if (ctx.storageApi && STORAGE_API_NAMES.has(name)) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
sendCallback(ap, params.success, result);
|
|
1230
|
-
sendCallback(ap, params.complete, result);
|
|
1231
|
-
}
|
|
1232
|
-
catch (error) {
|
|
1233
|
-
const failResult = { errMsg: `${name}:fail ${error instanceof Error ? error.message : String(error)}` };
|
|
1234
|
-
sendCallback(ap, params.fail, failResult);
|
|
1235
|
-
sendCallback(ap, params.complete, failResult);
|
|
1236
|
-
}
|
|
1265
|
+
const storageApi = ctx.storageApi;
|
|
1266
|
+
await invokeSimulatorApiAndCallback(ap, name, params, () => storageApi.invoke(ap.appId, name, params));
|
|
1237
1267
|
return;
|
|
1238
1268
|
}
|
|
1239
1269
|
// Main-process registry (downstream-registered host APIs via
|
|
@@ -1242,16 +1272,7 @@ async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
|
1242
1272
|
// MiniApp owns the DOM-touching defaults (wx.getSystemInfo, chooseImage,
|
|
1243
1273
|
// chooseMedia, fs.*, …) and the bridge-router can't run those itself.
|
|
1244
1274
|
if (ctx.simulatorApis.has(name)) {
|
|
1245
|
-
|
|
1246
|
-
const result = await ctx.simulatorApis.invoke(name, params);
|
|
1247
|
-
sendCallback(ap, params.success, result);
|
|
1248
|
-
sendCallback(ap, params.complete, result);
|
|
1249
|
-
}
|
|
1250
|
-
catch (error) {
|
|
1251
|
-
const failResult = { errMsg: `${name}:fail ${error instanceof Error ? error.message : String(error)}` };
|
|
1252
|
-
sendCallback(ap, params.fail, failResult);
|
|
1253
|
-
sendCallback(ap, params.complete, failResult);
|
|
1254
|
-
}
|
|
1275
|
+
await invokeSimulatorApiAndCallback(ap, name, params, () => ctx.simulatorApis.invoke(name, params));
|
|
1255
1276
|
return;
|
|
1256
1277
|
}
|
|
1257
1278
|
forwardApiCallToSimulator(state, ap, page, name, params);
|
|
@@ -1544,34 +1565,22 @@ function disposePageSession(state, ap, page) {
|
|
|
1544
1565
|
ap.pages.delete(page.bridgeId);
|
|
1545
1566
|
state.pageSessions.delete(page.bridgeId);
|
|
1546
1567
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1553
|
-
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1554
|
-
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
1555
|
-
// early-returns now that the session is gone — no recursion, no double-run.
|
|
1556
|
-
// (At whole-app shutdown the registry marks the entry released before calling
|
|
1557
|
-
// the fn, so this dispose() is a no-op then.)
|
|
1558
|
-
const registryHandle = ap.registryHandle;
|
|
1559
|
-
ap.registryHandle = null;
|
|
1560
|
-
void registryHandle?.dispose();
|
|
1561
|
-
state.appLifecycle.dispose(appSessionId);
|
|
1562
|
-
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1563
|
-
// page teardown below progressively empties (and finally clears).
|
|
1564
|
-
state.evictAppDataBridges(ap);
|
|
1565
|
-
// Drain any pending API calls owned by this app session. One-shot calls
|
|
1566
|
-
// normally self-clean on response/timeout, but persistent (`keep: true`)
|
|
1567
|
-
// subscriptions (e.g. audioListen) live with their timer cleared until
|
|
1568
|
-
// teardown — without this they would leak in pendingApiCalls forever.
|
|
1568
|
+
// Drain any pending API calls owned by this app session. One-shot calls
|
|
1569
|
+
// normally self-clean on response/timeout, but persistent (`keep: true`)
|
|
1570
|
+
// subscriptions (e.g. audioListen) live with their timer cleared until
|
|
1571
|
+
// teardown — without this they would leak in pendingApiCalls forever.
|
|
1572
|
+
function drainPendingApiCallsForSession(state, appSessionId) {
|
|
1569
1573
|
for (const [requestId, pending] of state.pendingApiCalls) {
|
|
1570
1574
|
if (pending.appSessionId !== appSessionId)
|
|
1571
1575
|
continue;
|
|
1572
1576
|
clearTimeout(pending.timer);
|
|
1573
1577
|
state.pendingApiCalls.delete(requestId);
|
|
1574
1578
|
}
|
|
1579
|
+
}
|
|
1580
|
+
// Closes every page's render guest and drops its wc/pageSessions bindings.
|
|
1581
|
+
// Caller clears `ap.pages` itself right after — this only tears each page
|
|
1582
|
+
// down, it doesn't touch the map.
|
|
1583
|
+
function closeSessionPages(state, ap) {
|
|
1575
1584
|
for (const page of ap.pages.values()) {
|
|
1576
1585
|
if (page.renderWc && !page.renderWc.isDestroyed()) {
|
|
1577
1586
|
state.wcIdToBridgeId.delete(page.renderWc.id);
|
|
@@ -1588,15 +1597,11 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1588
1597
|
}
|
|
1589
1598
|
state.pageSessions.delete(page.bridgeId);
|
|
1590
1599
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
// simulator wc must not accumulate one dead hook per soft reload until the
|
|
1597
|
-
// MaxListeners warning fires. Idempotent with teardown being triggered BY
|
|
1598
|
-
// one of these hooks (removing a fired once() is a no-op).
|
|
1599
|
-
ap.listenerBag.dispose();
|
|
1600
|
+
}
|
|
1601
|
+
// Three-way release of the session's service-host window: pool release (soft
|
|
1602
|
+
// reuse), pool reclaim of an externally-closed window, or a plain close for
|
|
1603
|
+
// the unpooled path.
|
|
1604
|
+
async function releaseServiceWindow(state, ap, opts) {
|
|
1600
1605
|
if (ap.poolEntryId !== null && state.pool && !opts.serviceAlreadyClosed) {
|
|
1601
1606
|
// Soft reuse (foundation.md §4.3): the pooled service-host webContents keeps
|
|
1602
1607
|
// its wc.id but is about to run a NEW app session. Reset its Connection
|
|
@@ -1623,12 +1628,14 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1623
1628
|
// pool only auto-reclaims on render-process-gone — so without this the in-use
|
|
1624
1629
|
// entry leaks in the pool forever, permanently shrinking capacity. Reclaim
|
|
1625
1630
|
// the slot explicitly (the window is already gone; releaseDestroyed won't
|
|
1626
|
-
// touch it).
|
|
1631
|
+
// touch it).
|
|
1627
1632
|
state.pool.releaseDestroyed(ap.poolEntryId);
|
|
1628
1633
|
}
|
|
1629
1634
|
else if (!opts.serviceAlreadyClosed && !ap.serviceWindow.isDestroyed()) {
|
|
1630
1635
|
ap.serviceWindow.close();
|
|
1631
1636
|
}
|
|
1637
|
+
}
|
|
1638
|
+
function unbindSessionFromSharedMaps(state, ap, appSessionId) {
|
|
1632
1639
|
// Value-checked unbind for the service wc: this delete runs after the
|
|
1633
1640
|
// pool-release await above, so on the pool path the next spawn may have
|
|
1634
1641
|
// already re-acquired the SAME window and rebound its wc id — only remove
|
|
@@ -1645,6 +1652,38 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1645
1652
|
if (hosted.size === 0)
|
|
1646
1653
|
state.simulatorWcIdToAppSessionIds.delete(ap.simulatorWc.id);
|
|
1647
1654
|
}
|
|
1655
|
+
}
|
|
1656
|
+
async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
1657
|
+
const ap = state.appSessions.get(appSessionId);
|
|
1658
|
+
if (!ap)
|
|
1659
|
+
return;
|
|
1660
|
+
state.appSessions.delete(appSessionId);
|
|
1661
|
+
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1662
|
+
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1663
|
+
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
1664
|
+
// early-returns now that the session is gone — no recursion, no double-run.
|
|
1665
|
+
// (At whole-app shutdown the registry marks the entry released before calling
|
|
1666
|
+
// the fn, so this dispose() is a no-op then.)
|
|
1667
|
+
const registryHandle = ap.registryHandle;
|
|
1668
|
+
ap.registryHandle = null;
|
|
1669
|
+
void registryHandle?.dispose();
|
|
1670
|
+
state.appLifecycle.dispose(appSessionId);
|
|
1671
|
+
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1672
|
+
// page teardown below progressively empties (and finally clears).
|
|
1673
|
+
state.evictAppDataBridges(ap);
|
|
1674
|
+
drainPendingApiCallsForSession(state, appSessionId);
|
|
1675
|
+
closeSessionPages(state, ap);
|
|
1676
|
+
ap.pages.clear();
|
|
1677
|
+
// Detach every session-scoped hook on emitters that outlive this session —
|
|
1678
|
+
// the shared simulator WCV's 'destroyed' and the service window's 'closed' —
|
|
1679
|
+
// BEFORE the window is released/closed below: a pool-recycled window must
|
|
1680
|
+
// not re-trigger this session's teardown from a stale 'closed', and the
|
|
1681
|
+
// simulator wc must not accumulate one dead hook per soft reload until the
|
|
1682
|
+
// MaxListeners warning fires. Idempotent with teardown being triggered BY
|
|
1683
|
+
// one of these hooks (removing a fired once() is a no-op).
|
|
1684
|
+
ap.listenerBag.dispose();
|
|
1685
|
+
await releaseServiceWindow(state, ap, opts);
|
|
1686
|
+
unbindSessionFromSharedMaps(state, ap, appSessionId);
|
|
1648
1687
|
// Only the local fallback server needs closing; the dev-server base is owned
|
|
1649
1688
|
// by the workspace session, not this app session.
|
|
1650
1689
|
if (ap.resourceServer) {
|
|
@@ -468,6 +468,40 @@ function writeFileSync(root, p, content) {
|
|
|
468
468
|
nodeFs.closeSync(fd);
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* Reads one directory's entries. Returns `null` (caller skips the directory)
|
|
473
|
+
* for `EACCES`/`ENOENT`/`ENOTDIR` — a directory can disappear or become
|
|
474
|
+
* unreadable between being queued and being walked. Any other error is
|
|
475
|
+
* rethrown.
|
|
476
|
+
*/
|
|
477
|
+
async function readDirEntriesSafe(dir) {
|
|
478
|
+
try {
|
|
479
|
+
return await fs.readdir(dir, { withFileTypes: true });
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
const code = err.code;
|
|
483
|
+
if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
|
|
484
|
+
return null;
|
|
485
|
+
throw err;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Classifies one readdir entry: a non-`SKIP_DIRS` directory is queued for
|
|
490
|
+
* walking; a plain file is recorded as a `safeRoot`-relative POSIX path.
|
|
491
|
+
* Skipped directories and non-file/non-directory entries (symlinks, sockets,
|
|
492
|
+
* etc.) are a no-op.
|
|
493
|
+
*/
|
|
494
|
+
function collectListEntry(entry, dir, safeRoot, out, queue) {
|
|
495
|
+
const full = path.join(dir, entry.name);
|
|
496
|
+
if (entry.isDirectory()) {
|
|
497
|
+
if (!SKIP_DIRS.has(entry.name))
|
|
498
|
+
queue.push(full);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (entry.isFile()) {
|
|
502
|
+
out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
471
505
|
async function listFiles(root, rootArg) {
|
|
472
506
|
const safeRoot = await resolveWithinProjectRoot(rootArg, root);
|
|
473
507
|
const out = [];
|
|
@@ -476,29 +510,13 @@ async function listFiles(root, rootArg) {
|
|
|
476
510
|
if (out.length >= LIST_FILE_LIMIT)
|
|
477
511
|
break;
|
|
478
512
|
const dir = queue.shift();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
catch (err) {
|
|
484
|
-
const code = err.code;
|
|
485
|
-
if (code === 'EACCES' || code === 'ENOENT' || code === 'ENOTDIR')
|
|
486
|
-
continue;
|
|
487
|
-
throw err;
|
|
488
|
-
}
|
|
489
|
-
for (const e of entries) {
|
|
513
|
+
const entries = await readDirEntriesSafe(dir);
|
|
514
|
+
if (entries === null)
|
|
515
|
+
continue;
|
|
516
|
+
for (const entry of entries) {
|
|
490
517
|
if (out.length >= LIST_FILE_LIMIT)
|
|
491
518
|
break;
|
|
492
|
-
|
|
493
|
-
if (e.isDirectory()) {
|
|
494
|
-
if (SKIP_DIRS.has(e.name))
|
|
495
|
-
continue;
|
|
496
|
-
queue.push(full);
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
if (!e.isFile())
|
|
500
|
-
continue;
|
|
501
|
-
out.push(path.relative(safeRoot, full).split(path.sep).join('/'));
|
|
519
|
+
collectListEntry(entry, dir, safeRoot, out, queue);
|
|
502
520
|
}
|
|
503
521
|
}
|
|
504
522
|
return out;
|