dsh-builtin-browser 0.4.6 → 0.4.8
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/CHANGELOG.md +76 -0
- package/README.en.md +62 -491
- package/README.md +44 -470
- package/client.js +10 -10
- package/cordis.patch.yml +4 -4
- package/desktop-bridge/plugin-browser-bridge.js +1 -1
- package/docs/README.md +8 -4
- package/docs/architecture.md +82 -11
- package/docs/development.md +38 -0
- package/docs/limitations.md +49 -0
- package/docs/tool-reference.md +42 -42
- package/docs/user-guide.md +93 -37
- package/docs/why-browser.md +3 -3
- package/lib/browser/types.d.ts +2 -2
- package/lib/browser-electron/bridge-connection.d.ts +7 -1
- package/lib/browser-electron/bridge-connection.js +58 -3
- package/lib/browser-electron/desktop-bridge-host.d.ts +11 -0
- package/lib/browser-electron/desktop-bridge-host.js +21 -1
- package/lib/browser-electron/entry.d.ts +2 -2
- package/lib/browser-electron/entry.js +186 -103
- package/lib/browser-electron/history-store.d.ts +1 -1
- package/lib/browser-electron/history-store.js +1 -1
- package/lib/browser-electron/host-main.js +3 -3
- package/lib/browser-electron/provider.d.ts +32 -8
- package/lib/browser-electron/provider.js +97 -16
- package/lib/browser-electron/remote-host.js +1 -1
- package/lib/browser-electron/settings-store.d.ts +14 -6
- package/lib/browser-electron/settings-store.js +29 -9
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/tool-browser/element-fields.d.ts +4 -4
- package/lib/tool-browser/element-fields.js +6 -6
- package/lib/tool-browser/index.d.ts +6 -6
- package/lib/tool-browser/index.js +196 -139
- package/package.json +2 -2
- package/scripts/verify-host-compat.mjs +6 -6
- package/src/browser/types.ts +2 -2
- package/src/browser-electron/bridge-connection.ts +61 -3
- package/src/browser-electron/desktop-bridge-host.ts +22 -1
- package/src/browser-electron/entry.ts +101 -9
- package/src/browser-electron/history-store.ts +1 -1
- package/src/browser-electron/host-main.ts +3 -3
- package/src/browser-electron/provider.ts +135 -31
- package/src/browser-electron/remote-host.ts +1 -1
- package/src/browser-electron/settings-store.ts +32 -13
- package/src/index.ts +1 -1
- package/src/tool-browser/element-fields.ts +6 -6
- package/src/tool-browser/index.ts +197 -140
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `ctx.electronViewHost`, that host is used (embedded, human-machine shared
|
|
6
6
|
* view). Otherwise the plugin self-hosts: it spawns its own Electron child
|
|
7
7
|
* (`host-main.js`) and drives it over a local TCP JSON-RPC socket, so
|
|
8
|
-
* installing the plugin is enough for `
|
|
8
|
+
* installing the plugin is enough for `dsh_browser_*` tools to work on any
|
|
9
9
|
* surface.
|
|
10
10
|
* @module dsh-browser/browser-electron
|
|
11
11
|
*/
|
|
@@ -81,9 +81,10 @@ export function apply(ctx, config) {
|
|
|
81
81
|
// a sessionId). One conversation per process, so the list holds this one.
|
|
82
82
|
const sessionId = readSessionId(ctx);
|
|
83
83
|
// One settings document per plugin instance: the settings panel writes it, the
|
|
84
|
-
// provider reads it live, and both ends agree on the same file.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
// provider reads it live, and both ends agree on the same file. The logger is passed so a
|
|
85
|
+
// discarded value (a mistyped channel, most of all) is reported instead of being silently
|
|
86
|
+
// replaced by the default — "my setting did nothing" and "I misspelled it" must not look alike.
|
|
87
|
+
const settings = new SettingsStore(undefined, message => ctx.logger?.warn?.(message));
|
|
87
88
|
const providerConfig = {
|
|
88
89
|
httpOnly: config.httpOnly,
|
|
89
90
|
downloadDir: config.downloadDir,
|
|
@@ -96,6 +97,15 @@ export function apply(ctx, config) {
|
|
|
96
97
|
if (config.viewHost !== undefined) {
|
|
97
98
|
const external = ctx.browser.registerBrowserProvider(new ElectronBrowserProvider(config.viewHost, providerConfig));
|
|
98
99
|
ctx.effect(() => () => { external(); });
|
|
100
|
+
// The composition supplied the carrier, so `browser.channel` is not ours to honour on this
|
|
101
|
+
// surface. Saying so is the difference between a setting that cannot apply and a setting that
|
|
102
|
+
// appears not to have worked — the second is what sends people looking for a bug elsewhere.
|
|
103
|
+
const asked = settings.get().browser.channel;
|
|
104
|
+
if (asked !== 'sidebar' && asked !== 'bundled') {
|
|
105
|
+
ctx.logger?.warn?.(`dsh-builtin-browser: browser.channel is "${asked}", but this surface's carrier was `
|
|
106
|
+
+ 'supplied by its composition (the desktop shell), which outranks the setting. Set it to '
|
|
107
|
+
+ '"sidebar" to record the intent, or run on a surface that picks its own carrier.');
|
|
108
|
+
}
|
|
99
109
|
return;
|
|
100
110
|
}
|
|
101
111
|
// Otherwise start self-hosted — a working browser from the first call, on every
|
|
@@ -106,26 +116,69 @@ export function apply(ctx, config) {
|
|
|
106
116
|
const selfHosted = new RemoteElectronViewHost(defaultHostMainPath());
|
|
107
117
|
let unregister = ctx.browser.registerBrowserProvider(new ElectronBrowserProvider(selfHosted, providerConfig));
|
|
108
118
|
let upgraded = false;
|
|
119
|
+
/** The carrier an `adopt` installed, so a later choice can release it. */
|
|
120
|
+
let adoptedCarrier;
|
|
109
121
|
ctx.effect(() => () => {
|
|
110
122
|
unregister();
|
|
111
123
|
selfHosted.dispose();
|
|
112
124
|
});
|
|
125
|
+
/**
|
|
126
|
+
* Put the self-hosted browser back after an adopted carrier reports that it is gone.
|
|
127
|
+
*
|
|
128
|
+
* This is the half the startup retries never covered: a bridge that was alive at apply time and
|
|
129
|
+
* exited later left every subsequent call dialling a port nobody owns, because the swap happened
|
|
130
|
+
* once and only in one direction. Reaching here means the shell is gone, so the choice made at
|
|
131
|
+
* startup is no longer a choice — the alternative to falling back is a browser surface that does
|
|
132
|
+
* nothing until DSH is restarted.
|
|
133
|
+
* @param adopted - the carrier that reported the loss, disposed once its replacement is in place.
|
|
134
|
+
*/
|
|
135
|
+
const restoreSelfHosted = (adopted) => {
|
|
136
|
+
try {
|
|
137
|
+
const restored = new RemoteElectronViewHost(defaultHostMainPath());
|
|
138
|
+
unregister();
|
|
139
|
+
unregister = ctx.browser.registerBrowserProvider(new ElectronBrowserProvider(restored, providerConfig));
|
|
140
|
+
ctx.logger?.warn?.('dsh-builtin-browser: the desktop shell backed the sidebar browser and has exited, so the '
|
|
141
|
+
+ 'browser this plugin hosts itself takes over. Pages from the sidebar are gone with it; '
|
|
142
|
+
+ 'open the page again.');
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
// Nothing better is available: the adopted carrier is gone and a provider that throws is
|
|
146
|
+
// worse than one that explains.
|
|
147
|
+
ctx.logger?.warn?.(`dsh-builtin-browser: could not fall back to the self-hosted browser: ${String(error)}`);
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
adopted.dispose?.();
|
|
151
|
+
}
|
|
152
|
+
};
|
|
113
153
|
/**
|
|
114
154
|
* Swap the provider to another carrier, releasing the previous one.
|
|
115
155
|
* @param host - the carrier to adopt.
|
|
116
156
|
* @param note - a one-line description for the log.
|
|
117
157
|
*/
|
|
118
158
|
const adopt = (host, note) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
159
|
+
// This guard used to make the FIRST choice final, disposing anything that arrived later. That
|
|
160
|
+
// is right against two carriers racing in, and exactly wrong for a user changing the setting:
|
|
161
|
+
// `chooseCarrier` runs again on purpose, and a refusal here is indistinguishable from the
|
|
162
|
+
// write having done nothing. The previous carrier is now released by the replacement instead.
|
|
123
163
|
upgraded = true;
|
|
124
164
|
try {
|
|
125
165
|
unregister();
|
|
126
166
|
unregister = ctx.browser.registerBrowserProvider(new ElectronBrowserProvider(host, providerConfig));
|
|
127
167
|
selfHosted.dispose();
|
|
128
168
|
ctx.logger?.info?.(`dsh-builtin-browser: ${note}`);
|
|
169
|
+
// A borrowed carrier can lose the process behind it, and this is the only notification that
|
|
170
|
+
// will ever arrive. Registering AFTER the swap succeeded means a listener can never fire
|
|
171
|
+
// against a provider that was never installed.
|
|
172
|
+
host.onLost?.(() => { restoreSelfHosted(host); });
|
|
173
|
+
const previous = adoptedCarrier;
|
|
174
|
+
adoptedCarrier = host;
|
|
175
|
+
// Released AFTER the replacement is installed, so the surface is never without a provider.
|
|
176
|
+
if (previous !== undefined && previous !== host) {
|
|
177
|
+
try {
|
|
178
|
+
previous.dispose?.();
|
|
179
|
+
}
|
|
180
|
+
catch { /* the replacement is already serving */ }
|
|
181
|
+
}
|
|
129
182
|
}
|
|
130
183
|
catch (error) {
|
|
131
184
|
// Never leave the surface without a provider. The half-built host is disposed FIRST: an
|
|
@@ -153,103 +206,119 @@ export function apply(ctx, config) {
|
|
|
153
206
|
}
|
|
154
207
|
}
|
|
155
208
|
};
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Choose and install the carrier that `browser.channel` names RIGHT NOW.
|
|
211
|
+
*
|
|
212
|
+
* A function rather than straight-line code because the setting is read live everywhere else:
|
|
213
|
+
* the settings route writes the file and the provider picks the change up on its next call, so a
|
|
214
|
+
* channel that could only be honoured at apply time was the one setting whose write silently did
|
|
215
|
+
* nothing. Re-entrant by design — changing the channel means replacing the carrier, which is
|
|
216
|
+
* exactly what a second call does.
|
|
217
|
+
*/
|
|
218
|
+
const chooseCarrier = () => {
|
|
219
|
+
// A browser the user explicitly asked for outranks every automatic choice: the
|
|
220
|
+
// whole point of the setting is that they know which browser they want. It is
|
|
221
|
+
// launched with a plugin-owned profile, so their own windows are untouched.
|
|
222
|
+
//
|
|
223
|
+
// Registration is INERT: constructing the host starts nothing. The browser only
|
|
224
|
+
// launches when a command actually needs a page, so merely running DSH never
|
|
225
|
+
// spawns a browser window. (An earlier version launched it here, which meant a
|
|
226
|
+
// browser appeared the moment the plugin loaded — reported and fixed.)
|
|
227
|
+
const channel = settings.get().browser.channel;
|
|
228
|
+
// Naming a browser is a decision about which one to use, so it also decides whether the
|
|
229
|
+
// desktop sidebar may take over. Skipping discovery for an explicit choice is what makes
|
|
230
|
+
// that choice real — otherwise a user who picked Chrome would silently get the sidebar,
|
|
231
|
+
// and a user who picked a browser that is not installed would never see the explanation
|
|
232
|
+
// written for exactly that case, because the sidebar would quietly replace it.
|
|
233
|
+
const explicitChoice = channel !== 'bundled' && channel !== 'auto' && channel !== 'sidebar';
|
|
234
|
+
if (channel !== 'bundled' && channel !== 'sidebar') {
|
|
235
|
+
const detected = detectBrowser(channel);
|
|
236
|
+
if (detected === undefined) {
|
|
237
|
+
// `auto` promises to take whatever is available, so falling back is what it
|
|
238
|
+
// asked for. Naming a browser is a specific request, and quietly satisfying it
|
|
239
|
+
// with a different one turns the real problem ("no Chrome here") into a
|
|
240
|
+
// confusing error about Electron later on. Report it instead.
|
|
241
|
+
if (channel === 'auto') {
|
|
242
|
+
ctx.logger?.warn?.('dsh-builtin-browser: no installed browser was found; using the bundled browser');
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
adopt(new MissingSystemBrowserHost(channel, searchSummary(channel)), `the selected ${channel} is not installed — commands will explain how to fix it`);
|
|
246
|
+
}
|
|
180
247
|
}
|
|
181
248
|
else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
sweepAbandonedEphemeralProfiles(profileRoot, detected.kind);
|
|
208
|
-
claimEphemeralProfile(profileDir);
|
|
249
|
+
// Same profile root as settings-store, history-store and desktop-bridge-host: the
|
|
250
|
+
// `?.dsh` component used to be missing HERE only, so a desktop launched from a
|
|
251
|
+
// shortcut (no DSH_HOME in the environment) put its browser profile in
|
|
252
|
+
// `C:\Users\<user>\dsh-builtin-browser-host\` while its settings and history lived in
|
|
253
|
+
// `~/.dsh/dsh-builtin-browser-host/` — two half-profiles that never met, and a
|
|
254
|
+
// settings panel whose switches appeared not to apply.
|
|
255
|
+
const home = process.env.DSH_HOME ?? join(homedir(), '.dsh');
|
|
256
|
+
// Login state lives in the browser profile, so the cookies setting decides where
|
|
257
|
+
// that profile goes: a stable directory keeps the user signed in across restarts
|
|
258
|
+
// (the point of using their own browser), while turning persistence off gets a
|
|
259
|
+
// throwaway directory that is removed when the browser is released.
|
|
260
|
+
const persist = settings.get().cookies.persist;
|
|
261
|
+
const profileRoot = join(home, BROWSER_PROFILE_DIR);
|
|
262
|
+
adoptLegacyProfileRoot(profileRoot);
|
|
263
|
+
const profileDir = persist
|
|
264
|
+
? join(profileRoot, `${detected.kind}-profile`)
|
|
265
|
+
: join(profileRoot, ephemeralProfileName(detected.kind, randomUUID()));
|
|
266
|
+
if (!persist) {
|
|
267
|
+
// A process killed outright never runs its own cleanup (see ephemeral-profile.ts),
|
|
268
|
+
// so the sweep happens HERE — at the only moment that can still act on a profile
|
|
269
|
+
// whose owner is gone — and before this run claims a directory of its own.
|
|
270
|
+
sweepAbandonedEphemeralProfiles(profileRoot, detected.kind);
|
|
271
|
+
claimEphemeralProfile(profileDir);
|
|
272
|
+
}
|
|
273
|
+
adopt(new SystemBrowserViewHost(detected, profileDir, [], persist ? undefined : profileDir), `will drive the installed ${detected.kind} (${detected.path}${persist ? '' : ', ephemeral profile'})`);
|
|
209
274
|
}
|
|
210
|
-
adopt(new SystemBrowserViewHost(detected, profileDir, [], persist ? undefined : profileDir), `will drive the installed ${detected.kind} (${detected.path}${persist ? '' : ', ephemeral profile'})`);
|
|
211
275
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
276
|
+
// An explicit choice was already adopted above; discovering the sidebar here would
|
|
277
|
+
// replace it, which is exactly what this guard prevents.
|
|
278
|
+
//
|
|
279
|
+
// `sidebar` is explicit too, but it names the sidebar, so it is the one explicit choice that
|
|
280
|
+
// must FALL THROUGH to the discovery below instead of returning here.
|
|
281
|
+
if (explicitChoice && channel !== 'sidebar')
|
|
282
|
+
return;
|
|
283
|
+
// The shell may not be up yet. `dsh web` starts before — or entirely without — the desktop app
|
|
284
|
+
// that hosts the bridge, and a one-shot discovery meant a bridge that appeared a second later
|
|
285
|
+
// was never adopted for the rest of the process's life. Retry a handful of times over roughly
|
|
286
|
+
// half a minute, then stop: this is a convenience, not a promise, and the session the user is
|
|
287
|
+
// already driving in the self-hosted window is not worth replacing minutes into their work.
|
|
288
|
+
// `adopt` is itself one-shot (its `upgraded` guard), so the only possible effect of a retry
|
|
289
|
+
// that lands late is adopting a bridge that showed up just after startup.
|
|
290
|
+
void (async () => {
|
|
291
|
+
for (const delay of [0, 2_000, 5_000, 15_000]) {
|
|
292
|
+
if (delay > 0)
|
|
293
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
294
|
+
let sidebar;
|
|
295
|
+
try {
|
|
296
|
+
sidebar = await DesktopBridgeViewHost.discover(sessionId);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
sidebar = undefined; // discovery is documented not to throw; never let it stop the retries
|
|
300
|
+
}
|
|
301
|
+
if (sidebar === undefined)
|
|
302
|
+
continue;
|
|
303
|
+
adopt(sidebar, 'driving the desktop sidebar browser');
|
|
304
|
+
return;
|
|
234
305
|
}
|
|
235
|
-
|
|
236
|
-
|
|
306
|
+
// The user explicitly asked for the sidebar, so silence would leave them with a setting
|
|
307
|
+
// that did nothing and no way to tell why. This is reachable on `dsh web`, and on a desktop
|
|
308
|
+
// whose shell never published a bridge endpoint. An automatic attempt stays quiet (the
|
|
309
|
+
// self-hosted window is a fine outcome); an explicit request does not.
|
|
310
|
+
if (channel === 'sidebar') {
|
|
311
|
+
ctx.logger?.warn?.('dsh-builtin-browser: the sidebar browser was requested but no desktop bridge was found — '
|
|
312
|
+
+ 'this DSH has no desktop shell, or its bridge never started. Falling back to the bundled '
|
|
313
|
+
+ 'browser; set browser.channel to another value to silence this.');
|
|
237
314
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// whose shell never published a bridge endpoint. An automatic attempt stays quiet (the
|
|
246
|
-
// self-hosted window is a fine outcome); an explicit request does not.
|
|
247
|
-
if (channel === 'sidebar') {
|
|
248
|
-
ctx.logger?.warn?.('dsh-builtin-browser: the sidebar browser was requested but no desktop bridge was found — '
|
|
249
|
-
+ 'this DSH has no desktop shell, or its bridge never started. Falling back to the bundled '
|
|
250
|
-
+ 'browser; set browser.channel to another value to silence this.');
|
|
251
|
-
}
|
|
252
|
-
})();
|
|
315
|
+
})();
|
|
316
|
+
};
|
|
317
|
+
chooseCarrier();
|
|
318
|
+
// Wired AFTER the chooser exists, because the handler re-runs it: a settings write that changes
|
|
319
|
+
// `browser.channel` is a request to switch carriers, and answering `ok` without switching is the
|
|
320
|
+
// one thing a caller cannot tell apart from a write that worked.
|
|
321
|
+
installSettingsRoute(ctx, settings, () => { chooseCarrier(); });
|
|
253
322
|
}
|
|
254
323
|
/** Route the settings panel reads and writes. */
|
|
255
324
|
const SETTINGS_ROUTE = '/dsh-builtin-browser/settings';
|
|
@@ -303,7 +372,7 @@ export function adoptLegacyProfileRoot(profileRoot, legacyRoot = join(homedir(),
|
|
|
303
372
|
* @param ctx - the plugin context.
|
|
304
373
|
* @param settings - the settings document to expose.
|
|
305
374
|
*/
|
|
306
|
-
function installSettingsRoute(ctx, settings) {
|
|
375
|
+
function installSettingsRoute(ctx, settings, onChannelChange) {
|
|
307
376
|
const inject = ctx.inject;
|
|
308
377
|
if (typeof inject !== 'function')
|
|
309
378
|
return;
|
|
@@ -312,7 +381,7 @@ function installSettingsRoute(ctx, settings) {
|
|
|
312
381
|
host.effect(() => host.webServer.register({
|
|
313
382
|
kind: 'exact',
|
|
314
383
|
path: SETTINGS_ROUTE,
|
|
315
|
-
handler: (request, response) => handleSettingsRequest(request, response, settings),
|
|
384
|
+
handler: (request, response) => handleSettingsRequest(request, response, settings, onChannelChange),
|
|
316
385
|
}, 'dsh-builtin-browser: settings'));
|
|
317
386
|
});
|
|
318
387
|
}
|
|
@@ -326,8 +395,9 @@ function installSettingsRoute(ctx, settings) {
|
|
|
326
395
|
* @param request - the incoming request.
|
|
327
396
|
* @param response - the response to write.
|
|
328
397
|
* @param settings - the settings document.
|
|
398
|
+
* @param onChannelChange - re-runs the carrier choice when this write changed `browser.channel`.
|
|
329
399
|
*/
|
|
330
|
-
async function handleSettingsRequest(request, response, settings) {
|
|
400
|
+
async function handleSettingsRequest(request, response, settings, onChannelChange) {
|
|
331
401
|
const json = (status, body) => {
|
|
332
402
|
response.writeHead(status, {
|
|
333
403
|
'content-type': 'application/json; charset=utf-8',
|
|
@@ -352,7 +422,20 @@ async function handleSettingsRequest(request, response, settings) {
|
|
|
352
422
|
try {
|
|
353
423
|
const body = await readBody(request);
|
|
354
424
|
const patch = body.trim() === '' ? {} : JSON.parse(body);
|
|
355
|
-
|
|
425
|
+
const before = settings.get().browser.channel;
|
|
426
|
+
const updated = settings.update(patch);
|
|
427
|
+
// A changed `browser.channel` is a request to switch carriers, not just a value to store. The
|
|
428
|
+
// choice is re-run here so the write takes effect now — and the response says so, because a
|
|
429
|
+
// caller that cannot tell "applied" from "stored for later" has no way to know what it got,
|
|
430
|
+
// which is exactly how a write that did nothing read as a success.
|
|
431
|
+
const channelChanged = updated.browser.channel !== before;
|
|
432
|
+
if (channelChanged)
|
|
433
|
+
onChannelChange?.();
|
|
434
|
+
json(200, {
|
|
435
|
+
ok: true,
|
|
436
|
+
settings: updated,
|
|
437
|
+
...channelChanged ? { carrierRechosen: true } : {},
|
|
438
|
+
});
|
|
356
439
|
}
|
|
357
440
|
catch (error) {
|
|
358
441
|
json(400, { ok: false, error: `invalid settings patch: ${String(error)}` });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent browsing history: which pages the shared browser actually visited,
|
|
3
|
-
* kept independently from any session's operation log (`
|
|
3
|
+
* kept independently from any session's operation log (`dsh_browser_history`) and
|
|
4
4
|
* from the browser process's lifetime.
|
|
5
5
|
*
|
|
6
6
|
* It lives beside the browser profile (`$DSH_HOME/dsh-builtin-browser-host/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistent browsing history: which pages the shared browser actually visited,
|
|
3
|
-
* kept independently from any session's operation log (`
|
|
3
|
+
* kept independently from any session's operation log (`dsh_browser_history`) and
|
|
4
4
|
* from the browser process's lifetime.
|
|
5
5
|
*
|
|
6
6
|
* It lives beside the browser profile (`$DSH_HOME/dsh-builtin-browser-host/
|
|
@@ -38,7 +38,7 @@ import { dirname, join } from 'node:path';
|
|
|
38
38
|
// several Electron instances sharing Roaming\Electron fight over the GPU
|
|
39
39
|
// cache/session locks, which can leave the window without a display surface
|
|
40
40
|
// (capturePage then fails). A dedicated userData also persists cookies across
|
|
41
|
-
// host restarts (on top of
|
|
41
|
+
// host restarts (on top of dsh_browser_auth). Must run before app is ready.
|
|
42
42
|
try {
|
|
43
43
|
const base = process.env.DSH_HOME ?? app.getPath('appData');
|
|
44
44
|
app.setPath('userData', join(base, 'dsh-builtin-browser-host'));
|
|
@@ -785,7 +785,7 @@ async function handle(op, msg) {
|
|
|
785
785
|
if (win === undefined || entry === undefined)
|
|
786
786
|
throw new Error(`focus: unknown view ${viewId}`);
|
|
787
787
|
// Keys go to the view that holds web focus. A view that was never
|
|
788
|
-
// clicked holds none, so the FIRST
|
|
788
|
+
// clicked holds none, so the FIRST dsh_browser_key of a session was dropped
|
|
789
789
|
// by the renderer while CDP still answered the dispatch with success —
|
|
790
790
|
// and `document.hasFocus()` inside the page even read true.
|
|
791
791
|
const contents = entry.webContentsView.webContents;
|
|
@@ -920,7 +920,7 @@ async function handle(op, msg) {
|
|
|
920
920
|
throw new Error('capture produced no image (view not painted)');
|
|
921
921
|
}
|
|
922
922
|
// The measured size travels with the image. Without it the native path — which is the
|
|
923
|
-
// default self-hosted one — could never answer `
|
|
923
|
+
// default self-hosted one — could never answer `dsh_browser_screenshot`'s width/height,
|
|
924
924
|
// because the seam's only channel was {base64, mime} and the provider had nothing to
|
|
925
925
|
// forward. The caller's `maxWidth`/`maxHeight` are applied here, so this is the only
|
|
926
926
|
// place that knows what came back.
|
|
@@ -119,6 +119,17 @@ export interface ElectronBrowserViewHost {
|
|
|
119
119
|
* which is only correct from a teardown that is itself global.
|
|
120
120
|
*/
|
|
121
121
|
releasePage?(owner?: string): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Report that this carrier can no longer serve anything, so its owner may replace it.
|
|
124
|
+
*
|
|
125
|
+
* A carrier that borrows an outside process can lose it: the desktop shell exits, and every call
|
|
126
|
+
* after that would dial a port nobody owns. Without this signal the swap happens only at
|
|
127
|
+
* startup, so the choice made then is final for the process's life — which is what a user
|
|
128
|
+
* experiences as "the browser stopped working until I restart DSH". Optional: a carrier that
|
|
129
|
+
* owns its own lifetime has nothing to report.
|
|
130
|
+
* @param listener - called once, when the carrier concludes it is gone.
|
|
131
|
+
*/
|
|
132
|
+
onLost?(listener: () => void): void;
|
|
122
133
|
/**
|
|
123
134
|
* Fold the carrier's presentation away while keeping the page alive.
|
|
124
135
|
*
|
|
@@ -274,7 +285,7 @@ export interface ElectronBrowserProviderConfig {
|
|
|
274
285
|
*/
|
|
275
286
|
readonly contentMaxChars?: number;
|
|
276
287
|
/**
|
|
277
|
-
* Directory `
|
|
288
|
+
* Directory `dsh_browser_download` save paths must resolve inside (prevents a
|
|
278
289
|
* prompt-injected agent from writing arbitrary machine paths). Default:
|
|
279
290
|
* `~/Downloads`. NOTE: this default is a convenience, not a security
|
|
280
291
|
* boundary — on Windows it may resolve to OneDrive/redirected paths;
|
|
@@ -328,7 +339,7 @@ export declare function layoutSize(metrics: Record<string, unknown>): {
|
|
|
328
339
|
viewportHeight: number;
|
|
329
340
|
} | undefined;
|
|
330
341
|
/**
|
|
331
|
-
* Map CDP cookies onto the shape `
|
|
342
|
+
* Map CDP cookies onto the shape `dsh_browser_auth` exports.
|
|
332
343
|
*
|
|
333
344
|
* CDP reports a cookie as domain + path rather than a URL, and in seconds rather than
|
|
334
345
|
* milliseconds, so both are converted. Entries missing a name, value or domain are
|
|
@@ -441,9 +452,9 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
441
452
|
/**
|
|
442
453
|
* Page.navigate resolves at navigation COMMIT, not at load: the new document
|
|
443
454
|
* is already current (so location.href and document.title are the new page's)
|
|
444
|
-
* while its DOM is still being parsed.
|
|
455
|
+
* while its DOM is still being parsed. dsh_browser_open snapshots immediately
|
|
445
456
|
* after navigating, which is why it could report the right title with zero
|
|
446
|
-
* interactive elements — and why a separate
|
|
457
|
+
* interactive elements — and why a separate dsh_browser_snapshot right after
|
|
447
458
|
* always found them.
|
|
448
459
|
*
|
|
449
460
|
* Wait — bounded, best-effort — for the new document to settle: readyState
|
|
@@ -482,7 +493,7 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
482
493
|
*
|
|
483
494
|
* A renderer only delivers key events to the view that holds web focus. A
|
|
484
495
|
* view that was created but never clicked holds none — and `Input` events
|
|
485
|
-
* injected over CDP do not grant it — so the FIRST `
|
|
496
|
+
* injected over CDP do not grant it — so the FIRST `dsh_browser_key` of a fresh
|
|
486
497
|
* session vanished inside the renderer while the command still answered
|
|
487
498
|
* success. Hosts that cannot focus a view (a test double, a different shell
|
|
488
499
|
* adapter) simply have no `focus`, and nothing changes for them.
|
|
@@ -587,7 +598,7 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
587
598
|
* Refuse an action the settings have switched off.
|
|
588
599
|
*
|
|
589
600
|
* These switches belong to the OPERATOR. The settings document is written through the
|
|
590
|
-
* plugin's own panel and no tool can reach it, so — unlike `
|
|
601
|
+
* plugin's own panel and no tool can reach it, so — unlike `dsh_browser_restrict`, which the
|
|
591
602
|
* model owns and can lift at will — a refusal here stands for as long as the setting does.
|
|
592
603
|
* That is the point of them: a deployment can take page-script execution, downloads, or
|
|
593
604
|
* login-state writes off the table without relying on the model's cooperation.
|
|
@@ -601,7 +612,7 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
601
612
|
private assertActionAllowed;
|
|
602
613
|
/**
|
|
603
614
|
* Admit a caller-supplied save path for a file the browser writes to disk.
|
|
604
|
-
* ONE gate for both `
|
|
615
|
+
* ONE gate for both `dsh_browser_download` and `dsh_browser_screenshot`: the path
|
|
605
616
|
* must be absolute, must resolve inside `downloadDir`, and must not already
|
|
606
617
|
* exist. Without it a prompt-injected agent could write — or silently
|
|
607
618
|
* replace — any file the DSH process can reach, which also escapes the file
|
|
@@ -665,7 +676,7 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
665
676
|
*/
|
|
666
677
|
private recordVisit;
|
|
667
678
|
/**
|
|
668
|
-
* List persisted visits, newest first — what `
|
|
679
|
+
* List persisted visits, newest first — what `dsh_browser_visited` reads back.
|
|
669
680
|
* @param options - result cap and an optional hostname filter.
|
|
670
681
|
* @returns the matching visits, or an empty list when history is disabled.
|
|
671
682
|
*/
|
|
@@ -705,6 +716,19 @@ export declare class ElectronBrowserProvider implements BrowserProvider {
|
|
|
705
716
|
private activeTab;
|
|
706
717
|
/** Append a fresh tab and make it active. */
|
|
707
718
|
private newTab;
|
|
719
|
+
/**
|
|
720
|
+
* Swap a wedged view for a fresh blank one, keeping the tab's public id.
|
|
721
|
+
*
|
|
722
|
+
* A view whose renderer never commits cannot be read again: every evaluate into it runs out its
|
|
723
|
+
* whole budget. Replacing it is what turns that from permanent into one bad call — the tab id
|
|
724
|
+
* callers hold stays valid, and the next `dsh_browser_open` with the real URL re-populates it.
|
|
725
|
+
*
|
|
726
|
+
* The old view is destroyed AFTER the new one is in place, so a failure here cannot leave the
|
|
727
|
+
* session holding a handle that is already gone.
|
|
728
|
+
* @param s - the session owning the tab.
|
|
729
|
+
* @param tab - the tab to reset.
|
|
730
|
+
*/
|
|
731
|
+
private replaceTabWithBlank;
|
|
708
732
|
/** Find a session's tab by its backing view id (toolbar actions carry view ids). */
|
|
709
733
|
private tabByViewId;
|
|
710
734
|
/**
|