buninu-browser 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/jsmdcui.txt +22 -0
- package/README.md +102 -2
- package/frontends/wayland/LICENSE-land +21 -0
- package/frontends/wayland/README.md +123 -0
- package/frontends/wayland/casty/LICENSE +21 -0
- package/frontends/wayland/casty/bookmarks.js +32 -0
- package/frontends/wayland/casty/click.js +96 -0
- package/frontends/wayland/casty/commands.js +61 -0
- package/frontends/wayland/casty/config.js +45 -0
- package/frontends/wayland/casty/download-directory.js +28 -0
- package/frontends/wayland/casty/download.js +49 -0
- package/frontends/wayland/casty/hints.js +334 -0
- package/frontends/wayland/casty/keys.js +56 -0
- package/frontends/wayland/casty/page-commands.js +80 -0
- package/frontends/wayland/casty/prompt-width.js +148 -0
- package/frontends/wayland/casty/urlbar.js +606 -0
- package/frontends/wayland/client.js +88 -0
- package/frontends/wayland/clipboard.js +80 -0
- package/frontends/wayland/index.js +116 -0
- package/frontends/wayland/input.js +56 -0
- package/frontends/wayland/protocol.js +55 -0
- package/frontends/wayland/protocols/wayland.xml +3248 -0
- package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
- package/frontends/wayland/transport.js +120 -0
- package/frontends/wayland/urlbar.js +158 -0
- package/index.js +28 -1
- package/lib/clipboard-command.js +10 -0
- package/lib/clipboard-powershell.js +22 -0
- package/lib/clipboard.js +57 -0
- package/lib/document-types.js +11 -0
- package/lib/headless-shell.js +78 -13
- package/lib/layout/text-layout.js +17 -9
- package/lib/paint/display-list.js +5 -1
- package/lib/paint/frame.js +35 -3
- package/lib/paint/raw-frames.js +47 -0
- package/lib/qml/README.md +971 -0
- package/lib/qml/builtins.js +700 -0
- package/lib/qml/canvas-dom.js +115 -0
- package/lib/qml/document.js +1176 -0
- package/lib/qml/format.js +9 -0
- package/lib/qml/host.js +324 -0
- package/lib/qml/images.js +199 -0
- package/lib/qml/launcher.js +132 -0
- package/lib/qml/loader.js +517 -0
- package/lib/qml/media-renderer.js +49 -0
- package/lib/qml/media.js +86 -0
- package/lib/qml/models.js +622 -0
- package/lib/qml/parser.js +381 -0
- package/lib/qml/qs +5 -0
- package/lib/qml/qscasty +3 -0
- package/lib/qml/qsheadless +3 -0
- package/lib/qml/qswayland +3 -0
- package/lib/qml/reactive.js +159 -0
- package/lib/qml/runtime.js +3381 -0
- package/lib/qml/shapes.js +243 -0
- package/lib/qml/text.js +109 -0
- package/lib/qml/textures.js +123 -0
- package/lib/qml/wayland-clipboard.js +32 -0
- package/lib/qml/worker-thread.js +70 -0
- package/lib/render-tree/index.js +16 -1
- package/lib/renderer/bindings.js +11 -0
- package/lib/renderer/canvas-2d.js +13 -0
- package/lib/renderer/compositor-channel.js +103 -0
- package/lib/renderer/compositor.js +25 -33
- package/lib/renderer/flags.js +1 -1
- package/lib/renderer/host.js +128 -92
- package/lib/renderer/main.js +38 -4
- package/lib/renderer/page-realm.js +32 -1
- package/lib/renderer/page-renderer.js +250 -21
- package/lib/renderer/text-input-paint.js +54 -0
- package/lib/renderer/text-input.js +57 -0
- package/lib/style/computed-style.js +2 -2
- package/package.json +1 -1
- package/test/clipboard.test.js +54 -0
- package/test/fixtures/browser-raw-demo.html +36 -0
- package/test/fixtures/flea-helpers/Dockerfile +36 -0
- package/test/fixtures/flea-helpers/bat.mjs +43 -0
- package/test/fixtures/flea-helpers/bin/bat +3 -0
- package/test/fixtures/flea-helpers/bin/bwrap +10 -0
- package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
- package/test/fixtures/flea-helpers/bin/glow +3 -0
- package/test/fixtures/flea-helpers/bin/qs +3 -0
- package/test/fixtures/flea-helpers/bin/unsupported +3 -0
- package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
- package/test/fixtures/flea-helpers/check-media.mjs +29 -0
- package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
- package/test/fixtures/flea-helpers/check.mjs +36 -0
- package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
- package/test/fixtures/flea-helpers/source-mime.xml +28 -0
- package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
- package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
- package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
- package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea/LICENSE +21 -0
- package/test/fixtures/qml/flea/README.md +25 -0
- package/test/fixtures/qml/flea/Theme.qml +8 -0
- package/test/fixtures/qml/flea/demo.qml +24 -0
- package/test/fixtures/qml/flea/qmldir +3 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
- package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
- package/test/fixtures/qml/flea-theme/README.md +70 -0
- package/test/fixtures/qml/flea-theme/button.qml +17 -0
- package/test/fixtures/qml/flea-theme/demo.qml +27 -0
- package/test/fixtures/qml/flea-theme/field.qml +21 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
- package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
- package/test/fixtures/qml/flea-theme/segment.qml +17 -0
- package/test/fixtures/qml/flea-theme/settings.qml +58 -0
- package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
- package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
- package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
- package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
- package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
- package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
- package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
- package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
- package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
- package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
- package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
- package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
- package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
- package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
- package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
- package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
- package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
- package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
- package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
- package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
- package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
- package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
- package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
- package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
- package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
- package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
- package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
- package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
- package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
- package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
- package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
- package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
- package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
- package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
- package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
- package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
- package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
- package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
- package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
- package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
- package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
- package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
- package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
- package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
- package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
- package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
- package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
- package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
- package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
- package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
- package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
- package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
- package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
- package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
- package/test/fixtures/qml/hello.qml +26 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
- package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
- package/test/fixtures/qml/imports/components/Badge.qml +15 -0
- package/test/fixtures/qml/imports/logic/base.js +2 -0
- package/test/fixtures/qml/imports/logic/counter.js +4 -0
- package/test/fixtures/qml/imports/logic/math.js +3 -0
- package/test/fixtures/qml/imports/logic/shared.js +3 -0
- package/test/fixtures/qml/imports/main.qml +21 -0
- package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
- package/test/fixtures/qml/imports/theme/qmldir +2 -0
- package/test/fixtures/qml/multiply.qml +61 -0
- package/test/fixtures/qml-canvas-dom.js +90 -0
- package/test/fixtures/qml-media-audio.js +39 -0
- package/test/fixtures/raw-capture-benchmark.json +76 -0
- package/test/helpers/flea-sandbox.js +48 -0
- package/test/helpers/quickshell-fixture.js +125 -0
- package/test/html-clipboard-wayland.test.js +47 -0
- package/test/index.test.js +13 -0
- package/test/manual-casty-without-qml.js +87 -0
- package/test/manual-flea-application.js +49 -0
- package/test/manual-flea-binary.js +62 -0
- package/test/manual-flea-qml.js +9 -0
- package/test/manual-flea-settings.js +40 -0
- package/test/manual-flea-theme.js +40 -0
- package/test/manual-qml-removal.js +46 -0
- package/test/manual-without-qml.js +45 -0
- package/test/qml-canvas-dom.test.js +37 -0
- package/test/qml-clipboard-bridge.test.js +25 -0
- package/test/qml-clipboard-wayland.test.js +40 -0
- package/test/qml-connections-timing.test.js +73 -0
- package/test/qml-dynamic.test.js +401 -0
- package/test/qml-flea-app.test.js +146 -0
- package/test/qml-flea-backend.test.js +78 -0
- package/test/qml-flea-columns-backend.test.js +38 -0
- package/test/qml-flea-compile.test.js +78 -0
- package/test/qml-flea-launcher.test.js +88 -0
- package/test/qml-flea-markdown-backend.test.js +31 -0
- package/test/qml-flea-media.test.js +28 -0
- package/test/qml-flea-navigation-backend.test.js +35 -0
- package/test/qml-flea-navigation-timing.test.js +147 -0
- package/test/qml-flea-preview-backend.test.js +53 -0
- package/test/qml-flea-previews.test.js +163 -0
- package/test/qml-flea-rows.test.js +101 -0
- package/test/qml-flea-textures.test.js +59 -0
- package/test/qml-flea-view-switch.test.js +33 -0
- package/test/qml-flea-wayland.test.js +68 -0
- package/test/qml-flea.test.js +34 -0
- package/test/qml-host.test.js +238 -0
- package/test/qml-imports.test.js +102 -0
- package/test/qml-inherited-handlers.test.js +51 -0
- package/test/qml-input-selection.test.js +66 -0
- package/test/qml-interactions.test.js +309 -0
- package/test/qml-language.test.js +167 -0
- package/test/qml-launcher.test.js +54 -0
- package/test/qml-layout-text.test.js +205 -0
- package/test/qml-media.test.js +63 -0
- package/test/qml-persisted-delegates.test.js +45 -0
- package/test/qml-quickshell-controls.test.js +65 -0
- package/test/qml-quickshell-settings.test.js +62 -0
- package/test/qml-repeater.test.js +72 -0
- package/test/qml-style-updates.test.js +124 -0
- package/test/qml-textures.test.js +66 -0
- package/test/qml-views.test.js +332 -0
- package/test/qml-worker.test.js +69 -0
- package/test/qml.test.js +83 -0
- package/test/raw-capture.test.js +152 -0
- package/test/renderer-host.test.js +11 -0
- package/test/text-input-pointer.test.js +31 -0
- package/test/text-input-scroll.test.js +29 -0
- package/test/text-input.test.js +47 -0
- package/test/wayland-frontend.test.js +83 -0
- package/test/wayland-urlbar.test.js +38 -0
- package/test/window-resize.test.js +36 -0
- package/tools/bench-raw-capture.js +32 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Private loopback transport, connected before renderer seccomp. No inherited
|
|
2
|
+
// numeric descriptors: node:net also supports Windows sockets.
|
|
3
|
+
import {createServer, createConnection} from 'node:net';
|
|
4
|
+
import {randomBytes} from 'node:crypto';
|
|
5
|
+
const MAX_JSON = 96 * 1024 * 1024;
|
|
6
|
+
const MAX_PIXELS = 64 * 1024 * 1024;
|
|
7
|
+
const MAX_QUEUE = 128 * 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
// Length-prefixed JSON followed by optional binary pixels. Copy once into an
|
|
10
|
+
// owned packet so a raster lease may be released before the socket drains.
|
|
11
|
+
export function encodePacket(message, pixels = null) {
|
|
12
|
+
const json = Buffer.from(JSON.stringify(message));
|
|
13
|
+
const size = pixels?.byteLength ?? 0;
|
|
14
|
+
if (json.length > MAX_JSON || size > MAX_PIXELS || json.length + size > MAX_QUEUE) throw Error('Compositor packet too large');
|
|
15
|
+
const packet = Buffer.allocUnsafe(8 + json.length + size);
|
|
16
|
+
packet.writeUInt32LE(json.length, 0); packet.writeUInt32LE(size, 4);
|
|
17
|
+
packet.set(json, 8); if (size) packet.set(pixels, 8 + json.length);
|
|
18
|
+
return packet;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class PacketReader {
|
|
22
|
+
constructor(receive, limit = MAX_QUEUE) { this.receive = receive; this.limit = limit; this.header = Buffer.alloc(8); this.used = 0; this.packet = null; }
|
|
23
|
+
push(chunk) {
|
|
24
|
+
let offset = 0;
|
|
25
|
+
while (offset < chunk.length) {
|
|
26
|
+
const target = this.packet ?? this.header;
|
|
27
|
+
const count = Math.min(target.length - this.used, chunk.length - offset);
|
|
28
|
+
target.set(chunk.subarray(offset, offset + count), this.used);
|
|
29
|
+
this.used += count; offset += count;
|
|
30
|
+
if (this.used !== target.length) continue;
|
|
31
|
+
this.used = 0;
|
|
32
|
+
if (!this.packet) {
|
|
33
|
+
this.jsonLength = this.header.readUInt32LE(0);
|
|
34
|
+
const binaryLength = this.header.readUInt32LE(4);
|
|
35
|
+
const total = this.jsonLength + binaryLength;
|
|
36
|
+
if (!this.jsonLength || this.jsonLength > MAX_JSON || binaryLength > MAX_PIXELS || total > this.limit) throw Error('Invalid compositor packet size');
|
|
37
|
+
this.packet = Buffer.allocUnsafe(total);
|
|
38
|
+
} else {
|
|
39
|
+
const packet = this.packet; this.packet = null;
|
|
40
|
+
const message = JSON.parse(packet.subarray(0, this.jsonLength).toString());
|
|
41
|
+
const pixels = packet.subarray(this.jsonLength);
|
|
42
|
+
this.receive(message, pixels);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function attach(socket, receive, closed, limit) {
|
|
49
|
+
socket.setNoDelay(true);
|
|
50
|
+
const reader = new PacketReader(receive, limit);
|
|
51
|
+
socket.on('data', chunk => { try { reader.push(chunk); } catch { socket.destroy(); } });
|
|
52
|
+
socket.on('error', () => {});
|
|
53
|
+
socket.once('close', closed);
|
|
54
|
+
return {
|
|
55
|
+
reader,
|
|
56
|
+
send(message, pixels) {
|
|
57
|
+
if (socket.destroyed) throw Error('Compositor channel closed');
|
|
58
|
+
const packet = encodePacket(message, pixels);
|
|
59
|
+
if (socket.writableLength + packet.length > MAX_QUEUE) { socket.destroy(); throw Error('Compositor channel backpressure limit'); }
|
|
60
|
+
socket.write(packet);
|
|
61
|
+
},
|
|
62
|
+
close() { socket.destroy(); },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function listenCompositor(receive, closed) {
|
|
67
|
+
const token = randomBytes(32).toString('hex');
|
|
68
|
+
const sockets = new Set(); let peer = null, ended = false;
|
|
69
|
+
const server = createServer(socket => {
|
|
70
|
+
if (ended || peer || sockets.size >= 4) { socket.destroy(); return; }
|
|
71
|
+
sockets.add(socket);
|
|
72
|
+
const timer = setTimeout(() => socket.destroy(), 3000);
|
|
73
|
+
const candidate = attach(socket, (message, pixels) => {
|
|
74
|
+
if (!peer) {
|
|
75
|
+
if (pixels.length || message?.type !== 'hello' || message.token !== token) { socket.destroy(); return; }
|
|
76
|
+
peer = candidate; clearTimeout(timer); candidate.reader.limit = MAX_QUEUE;
|
|
77
|
+
for (const other of sockets) if (other !== socket) other.destroy();
|
|
78
|
+
server.close(); candidate.send({type:'accepted'});
|
|
79
|
+
} else if (peer === candidate) receive(message, pixels);
|
|
80
|
+
}, () => { clearTimeout(timer); sockets.delete(socket); if (peer === candidate && !ended) closed(); }, 4096);
|
|
81
|
+
});
|
|
82
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
83
|
+
return {
|
|
84
|
+
endpoint: {port:server.address().port, token},
|
|
85
|
+
send(message) { if (!peer || ended) throw Error('Compositor not connected'); peer.send(message); },
|
|
86
|
+
close() { ended = true; server.close(); for (const socket of sockets) socket.destroy(); },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function connectCompositor(endpoint, receive, closed) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const socket = createConnection({host:'127.0.0.1', port:endpoint.port});
|
|
93
|
+
let accepted = false;
|
|
94
|
+
const timer = setTimeout(() => socket.destroy(), 5000);
|
|
95
|
+
const peer = attach(socket, (message, pixels) => {
|
|
96
|
+
if (!accepted) {
|
|
97
|
+
if (message?.type !== 'accepted' || pixels.length) { socket.destroy(); return; }
|
|
98
|
+
accepted = true; clearTimeout(timer); resolve(peer);
|
|
99
|
+
} else receive(message, pixels);
|
|
100
|
+
}, () => { clearTimeout(timer); if (!accepted) reject(Error('Compositor connection failed')); else closed(); });
|
|
101
|
+
socket.once('connect', () => peer.send({type:'hello', token:endpoint.token}));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* screenshots and scrolling on its own channel, so the page stays visible and
|
|
7
7
|
* scrollable while the main thread works.
|
|
8
8
|
*
|
|
9
|
-
* Channel to the controller:
|
|
10
|
-
*
|
|
9
|
+
* Channel to the controller: authenticated loopback socket, with length-prefixed
|
|
10
|
+
* JSON and optional binary pixels (does not go through main-thread IPC):
|
|
11
11
|
* controller -> compositor { id, method: "screenshot" | "scroll" | "scrollOffset", params }
|
|
12
12
|
* (screenshot params: [{ format?: "png" | "jpeg" | "webp", quality? }])
|
|
13
13
|
* compositor -> controller { id, result } | { id, error }
|
|
@@ -22,14 +22,13 @@
|
|
|
22
22
|
* a fragment link, a key press or a new document.
|
|
23
23
|
*/
|
|
24
24
|
import { ScrollViewport } from "../layout/scroll-viewport.js";
|
|
25
|
-
import {
|
|
25
|
+
import { rawFrame, captureViewport, encodeImage, encodeImageBase64 } from "../paint/frame.js";
|
|
26
26
|
import { FrameCaptureCache } from "../paint/frame-cache.js";
|
|
27
27
|
import { FrameLeaseTracker } from "../paint/shared-frame-pool.js";
|
|
28
28
|
import wasmpeg from "../../vendor/wasmpeg/src/js/index.js";
|
|
29
29
|
import { VideoPresentation, compositeVideo } from "./video-presentation.js";
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
const MAX_LINE_BYTES = 1024 * 1024;
|
|
31
|
+
import {connectCompositor} from './compositor-channel.js';
|
|
33
32
|
|
|
34
33
|
const viewport = new ScrollViewport(800, 600);
|
|
35
34
|
let tile = null;
|
|
@@ -42,12 +41,18 @@ const video = new VideoPresentation(wasmpeg, () => {
|
|
|
42
41
|
send({ type: "frame" });
|
|
43
42
|
}, report => postMessage({ type: "video-report", ...report }));
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
let channel;
|
|
47
45
|
function send(message) {
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const result = message.result;
|
|
47
|
+
if (result?.pixels instanceof Uint8Array) {
|
|
48
|
+
const {pixels,...metadata} = result;
|
|
49
|
+
channel.send({...message,result:metadata},pixels);
|
|
50
|
+
} else channel.send(message);
|
|
50
51
|
}
|
|
52
|
+
const endpoint = await new Promise(resolve => {
|
|
53
|
+
self.onmessage = ({data}) => { if (data?.type === 'connect') resolve(data.endpoint); };
|
|
54
|
+
});
|
|
55
|
+
channel = await connectCompositor(endpoint, request => { void handleRequest(request); }, () => process.exit(1));
|
|
51
56
|
|
|
52
57
|
self.onmessage = ({ data }) => {
|
|
53
58
|
if (data?.type === "video-open") { void video.open(data); return; }
|
|
@@ -77,12 +82,19 @@ self.onmessage = ({ data }) => {
|
|
|
77
82
|
};
|
|
78
83
|
|
|
79
84
|
const methods = {
|
|
80
|
-
async screenshot({ format = "png", quality = 80, optimizeForSpeed = false } = {}) {
|
|
85
|
+
async screenshot({ format = "png", quality = 80, optimizeForSpeed = false, clip } = {}) {
|
|
81
86
|
if (!tile) return "";
|
|
82
|
-
|
|
87
|
+
if (format === 'rgba' || format === 'bgra') {
|
|
88
|
+
return frameLeases.capture(frame => {
|
|
89
|
+
const composited = frame.videoLayer?.id === video.config?.id ? compositeVideo(frame, video.frame) : frame;
|
|
90
|
+
const {pixels,width,height} = captureViewport(composited,viewport.x,viewport.y,viewport.width,viewport.height,clip);
|
|
91
|
+
return rawFrame(pixels,width,height,format);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const key = `${frameSerial}:${viewport.x}:${viewport.y}:${format}:${quality}:${optimizeForSpeed}:${JSON.stringify(clip)}`;
|
|
83
95
|
return captures.get(key, () => frameLeases.capture(frame => {
|
|
84
96
|
const composited = frame.videoLayer?.id === video.config?.id ? compositeVideo(frame, video.frame) : frame;
|
|
85
|
-
const { pixels, width, height } =
|
|
97
|
+
const { pixels, width, height } = captureViewport(composited, viewport.x, viewport.y, viewport.width, viewport.height,clip);
|
|
86
98
|
return encodeImageBase64(pixels, width, height, { format, quality, optimizeForSpeed });
|
|
87
99
|
}));
|
|
88
100
|
},
|
|
@@ -98,13 +110,7 @@ const methods = {
|
|
|
98
110
|
},
|
|
99
111
|
};
|
|
100
112
|
|
|
101
|
-
async function
|
|
102
|
-
let request;
|
|
103
|
-
try {
|
|
104
|
-
request = JSON.parse(line);
|
|
105
|
-
} catch {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
113
|
+
async function handleRequest(request) {
|
|
108
114
|
if (request?.type === "audio-timing" && Number.isInteger(request.audioId)) {
|
|
109
115
|
video.timing(request.audioId, request.seconds);
|
|
110
116
|
return;
|
|
@@ -118,20 +124,6 @@ async function handleLine(line) {
|
|
|
118
124
|
}
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
(async () => {
|
|
122
|
-
const decoder = new TextDecoder();
|
|
123
|
-
let pending = "";
|
|
124
|
-
for await (const chunk of Bun.file(CHANNEL_FD).stream()) {
|
|
125
|
-
pending += decoder.decode(chunk, { stream: true });
|
|
126
|
-
let newline;
|
|
127
|
-
while ((newline = pending.indexOf("\n")) >= 0) {
|
|
128
|
-
handleLine(pending.slice(0, newline));
|
|
129
|
-
pending = pending.slice(newline + 1);
|
|
130
|
-
}
|
|
131
|
-
if (pending.length > MAX_LINE_BYTES) pending = "";
|
|
132
|
-
}
|
|
133
|
-
})();
|
|
134
|
-
|
|
135
127
|
// Bun.Image initialises its codecs on first use; do that before the
|
|
136
128
|
// renderer installs seccomp, which forbids opening files.
|
|
137
129
|
await encodeImage(new Uint8Array(4), 1, 1);
|
package/lib/renderer/flags.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Command-line flags the controller passes to renderer processes. */
|
|
2
2
|
import { CHROMIUM_VERSION } from "../user-agent.js";
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/** Start the compositor Worker with its pre-sandbox loopback channel. */
|
|
5
5
|
export const COMPOSITOR_FLAG = "--compositor";
|
|
6
6
|
|
|
7
7
|
/** BUNINU_VERIFY_STYLE: check every incremental restyle against a full one (development). */
|
package/lib/renderer/host.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {writeClipboard, readClipboard} from '../clipboard.js';
|
|
1
2
|
/**
|
|
2
3
|
* Controller-side handle for a renderer process.
|
|
3
4
|
*
|
|
@@ -9,10 +10,11 @@
|
|
|
9
10
|
* Everything the renderer sends is treated as untrusted input.
|
|
10
11
|
*
|
|
11
12
|
* Besides IPC with the renderer's main thread, each renderer gets a second
|
|
12
|
-
*
|
|
13
|
+
* authenticated loopback channel to its compositor thread, which answers screenshots and
|
|
13
14
|
* scrolling even while the main thread is busy (see compositor.js).
|
|
14
15
|
*/
|
|
15
|
-
import {
|
|
16
|
+
import {listenCompositor} from "./compositor-channel.js";
|
|
17
|
+
import {RawFrames} from "../paint/raw-frames.js";
|
|
16
18
|
import { PulseClient } from "../audio/pulse-client.js";
|
|
17
19
|
import { logEnabled, pipeToLog, RENDERER_LOG_FLAG, writeLog } from "../log.js";
|
|
18
20
|
import { NetworkService } from "../network/network-service.js";
|
|
@@ -47,13 +49,20 @@ const MAX_WEB_SOCKETS = 64;
|
|
|
47
49
|
const MAX_WEB_SOCKET_MESSAGE = 16 * 1024 * 1024;
|
|
48
50
|
const COMPOSITOR_METHODS = new Set(["screenshot", "scroll", "scrollOffset"]);
|
|
49
51
|
const COMPOSITOR_TIMEOUT = 10_000;
|
|
50
|
-
const MAX_CHANNEL_LINE = 96 * 1024 * 1024;
|
|
51
52
|
const LIFECYCLE_NAMES = new Set(["DOMContentLoaded", "load"]);
|
|
52
53
|
|
|
53
54
|
// A file path, not a URL's pathname ("/C:/..." on Windows).
|
|
54
55
|
const DEFAULT_ENTRY = Bun.fileURLToPath(new URL("./main.js", import.meta.url));
|
|
55
56
|
|
|
56
57
|
export class RendererHost {
|
|
58
|
+
#clipboardRead = readClipboard;
|
|
59
|
+
#clipboardWrite = writeClipboard;
|
|
60
|
+
#clipboardUntil = 0;
|
|
61
|
+
#clipboardPending = false;
|
|
62
|
+
#rawFrames = null;
|
|
63
|
+
#startEpoch = 0;
|
|
64
|
+
#closed = false;
|
|
65
|
+
#rejectStartup = null;
|
|
57
66
|
#entry;
|
|
58
67
|
#args;
|
|
59
68
|
#timeout;
|
|
@@ -105,11 +114,13 @@ export class RendererHost {
|
|
|
105
114
|
entry = DEFAULT_ENTRY, args = [], timeout = 30_000, onRestart = null, onNavigate = null, onDownload = null,
|
|
106
115
|
fetch: fetchImpl = fetch,
|
|
107
116
|
network = new NetworkService({ fetch: fetchImpl }), onFrame = null, onLifecycle = null,
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
// A dedicated loopback socket works independently of the main thread on all platforms.
|
|
118
|
+
compositor = entry === DEFAULT_ENTRY,
|
|
119
|
+
clipboardRead = readClipboard,
|
|
120
|
+
clipboardWrite = writeClipboard,
|
|
112
121
|
} = {}) {
|
|
122
|
+
this.#clipboardRead = clipboardRead;
|
|
123
|
+
this.#clipboardWrite = clipboardWrite;
|
|
113
124
|
this.#onFrame = onFrame;
|
|
114
125
|
this.#onLifecycle = onLifecycle;
|
|
115
126
|
this.#compositor = compositor;
|
|
@@ -124,6 +135,7 @@ export class RendererHost {
|
|
|
124
135
|
|
|
125
136
|
/** Starts the renderer if it is not running and waits until it is ready. */
|
|
126
137
|
start() {
|
|
138
|
+
if (this.#closed) return Promise.reject(Error("Renderer is closed"));
|
|
127
139
|
if (this.#ready) return this.#ready;
|
|
128
140
|
let resolveReady;
|
|
129
141
|
let rejectReady;
|
|
@@ -131,47 +143,67 @@ export class RendererHost {
|
|
|
131
143
|
resolveReady = resolve;
|
|
132
144
|
rejectReady = reject;
|
|
133
145
|
});
|
|
146
|
+
this.#rejectStartup = rejectReady;
|
|
134
147
|
const logging = logEnabled();
|
|
135
148
|
const compositor = this.#compositor;
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
149
|
+
const epoch = ++this.#startEpoch;
|
|
150
|
+
void (async () => {
|
|
151
|
+
const channel = compositor ? {pending:new Map(),nextId:1,closed:false,transport:null} : null;
|
|
152
|
+
if (channel) channel.transport = await listenCompositor(
|
|
153
|
+
(message,pixels)=>this.#handleCompositorMessage(channel,message,pixels),
|
|
154
|
+
()=>this.#closeChannel(channel));
|
|
155
|
+
if (epoch !== this.#startEpoch) { channel?.transport.close(); throw Error('Renderer startup cancelled'); }
|
|
156
|
+
this.#channel = channel;
|
|
140
157
|
// No inherited environment: nothing from the controller leaks in.
|
|
141
158
|
// Windows programs need SystemRoot to load system libraries.
|
|
142
|
-
env
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
159
|
+
let env = process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {};
|
|
160
|
+
|
|
161
|
+
// qml handle begin: applications need HOME/PATH/runtime paths, web pages do not
|
|
162
|
+
if (this.#args.includes("--buninu-qml-host")) env = { ...process.env };
|
|
163
|
+
// qml handle end
|
|
164
|
+
|
|
165
|
+
const child = Bun.spawn({
|
|
166
|
+
cmd: [process.execPath, this.#entry, ...this.#args,
|
|
167
|
+
...(logging ? [RENDERER_LOG_FLAG] : []), ...(compositor ? [COMPOSITOR_FLAG, `--compositor-endpoint=${JSON.stringify(channel.transport.endpoint)}`] : []),
|
|
168
|
+
...(process.env.BUNINU_VERIFY_STYLE ? [VERIFY_STYLE_FLAG] : []), ...(process.env.BUNINU_FULL_STYLE ? [FULL_STYLE_FLAG] : [])],
|
|
169
|
+
env,
|
|
170
|
+
// With BUNINU_LOG, output goes through the controller into the log file.
|
|
171
|
+
stdio: ["ignore", logging ? "pipe" : "inherit", logging ? "pipe" : "inherit"],
|
|
172
|
+
serialization: "advanced",
|
|
173
|
+
ipc: (message) => {
|
|
174
|
+
if (message?.type === "ready") {
|
|
175
|
+
this.#sandbox = sanitizeSandboxReport(message.sandbox);
|
|
176
|
+
this.#rejectStartup=null;
|
|
177
|
+
clearTimeout(startupTimer);
|
|
178
|
+
resolveReady();
|
|
179
|
+
}
|
|
180
|
+
else this.#handleMessage(child, message);
|
|
181
|
+
},
|
|
182
|
+
onExit: (_process, exitCode, signalCode) => {
|
|
183
|
+
clearTimeout(startupTimer);
|
|
184
|
+
this.#closeChannel(channel);
|
|
185
|
+
this.#closeSockets(child);
|
|
186
|
+
this.#closeAudio(child);
|
|
187
|
+
this.#closePageFetches(child);
|
|
188
|
+
rejectReady(new Error("renderer exited during startup"));
|
|
189
|
+
if (this.#child !== child) return;
|
|
190
|
+
console.error(`buninu-browser: renderer ${child.pid} exited unexpectedly (${signalCode ?? `code ${exitCode}`})`);
|
|
191
|
+
this.#child = null;
|
|
192
|
+
this.#ready = null;
|
|
193
|
+
this.#rawFrames?.close(); this.#rawFrames=null;
|
|
194
|
+
for (const { reject } of this.#pending.values()) reject(new Error("renderer exited"));
|
|
195
|
+
this.#pending.clear();
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
this.#child = child;
|
|
199
|
+
const startupTimer = setTimeout(() => {
|
|
200
|
+
this.#closeChannel(channel); child.kill(9); rejectReady(Error('Renderer startup timed out'));
|
|
201
|
+
}, this.#timeout);
|
|
202
|
+
if (logging) {
|
|
203
|
+
pipeToLog(child.stdout, `renderer ${child.pid}`, (chunk) => process.stdout.write(chunk));
|
|
204
|
+
pipeToLog(child.stderr, `renderer ${child.pid}`, (chunk) => process.stderr.write(chunk));
|
|
205
|
+
}
|
|
206
|
+
})().catch(error=>{ if(epoch===this.#startEpoch){this.#closeChannel(this.#channel);this.#ready=null;} rejectReady(error); });
|
|
175
207
|
return this.#ready;
|
|
176
208
|
}
|
|
177
209
|
|
|
@@ -213,12 +245,13 @@ export class RendererHost {
|
|
|
213
245
|
* waiting for the renderer's main thread or its call queue.
|
|
214
246
|
*/
|
|
215
247
|
async composite(method, ...params) {
|
|
216
|
-
if (!this.#compositor || !COMPOSITOR_METHODS.has(method)) return this.call(method, ...params);
|
|
248
|
+
if (!this.#compositor || !COMPOSITOR_METHODS.has(method)) return this.#captureResult(method, await this.call(method, ...params), params);
|
|
217
249
|
// A dead renderer is restarted (and its page restored) through the call path.
|
|
218
250
|
if (!this.#child) await this.call("url");
|
|
219
251
|
await this.start();
|
|
220
252
|
const channel = this.#channel;
|
|
221
|
-
if (!channel) return this.call(method, ...params);
|
|
253
|
+
if (!channel) return this.#captureResult(method, await this.call(method, ...params), params);
|
|
254
|
+
const child = this.#child;
|
|
222
255
|
const result = await new Promise((resolve, reject) => {
|
|
223
256
|
const id = channel.nextId++;
|
|
224
257
|
const timer = setTimeout(() => {
|
|
@@ -235,56 +268,25 @@ export class RendererHost {
|
|
|
235
268
|
reject(error);
|
|
236
269
|
},
|
|
237
270
|
});
|
|
238
|
-
channel.
|
|
239
|
-
channel.
|
|
271
|
+
try { channel.transport.send({id,method,params}); }
|
|
272
|
+
catch(error) { channel.pending.get(id).reject(error); channel.pending.delete(id); }
|
|
240
273
|
});
|
|
241
|
-
|
|
274
|
+
if (channel.closed || this.#child !== child) throw Error("Renderer closed during capture");
|
|
275
|
+
const sanitized = this.#captureResult(method, result, params);
|
|
242
276
|
if (method === "scroll") this.#pendingScroll = sanitized;
|
|
243
277
|
return sanitized;
|
|
244
278
|
}
|
|
245
279
|
|
|
246
|
-
#
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
254
|
-
let newline;
|
|
255
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
256
|
-
this.#handleCompositorLine(channel, buffer.slice(0, newline));
|
|
257
|
-
buffer = buffer.slice(newline + 1);
|
|
258
|
-
}
|
|
259
|
-
if (buffer.length > MAX_CHANNEL_LINE) buffer = "";
|
|
260
|
-
}
|
|
261
|
-
} catch {
|
|
262
|
-
// The renderer went away.
|
|
263
|
-
}
|
|
264
|
-
this.#closeChannel(channel);
|
|
265
|
-
// Close everything that refers to the socket once, after end of
|
|
266
|
-
// stream, so no descriptor number can have been reused meanwhile.
|
|
267
|
-
try {
|
|
268
|
-
channel.writer.end();
|
|
269
|
-
} catch {
|
|
270
|
-
// Already closed.
|
|
271
|
-
}
|
|
272
|
-
try {
|
|
273
|
-
closeSync(fd);
|
|
274
|
-
} catch {
|
|
275
|
-
// Already closed.
|
|
276
|
-
}
|
|
277
|
-
})();
|
|
278
|
-
return channel;
|
|
280
|
+
#captureResult(method,result,params) {
|
|
281
|
+
if (method === 'screenshot' && ['rgba','bgra'].includes(params[0]?.format)) {
|
|
282
|
+
if (result?.format !== params[0].format) throw Error('Raw screenshot format mismatch');
|
|
283
|
+
this.#rawFrames ??= new RawFrames();
|
|
284
|
+
return this.#rawFrames.write(result.pixels,result.width,result.height,result.format);
|
|
285
|
+
}
|
|
286
|
+
return sanitizeCompositorResult(method,result);
|
|
279
287
|
}
|
|
280
288
|
|
|
281
|
-
#
|
|
282
|
-
let message;
|
|
283
|
-
try {
|
|
284
|
-
message = JSON.parse(line);
|
|
285
|
-
} catch {
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
289
|
+
#handleCompositorMessage(channel, message, pixels) {
|
|
288
290
|
if (channel.closed || typeof message !== "object" || message === null) return;
|
|
289
291
|
if (message.type === "frame") {
|
|
290
292
|
if (this.#channel === channel) this.#onFrame?.();
|
|
@@ -292,19 +294,24 @@ export class RendererHost {
|
|
|
292
294
|
const pending = channel.pending.get(message.id);
|
|
293
295
|
channel.pending.delete(message.id);
|
|
294
296
|
if (Object.hasOwn(message, "error")) pending.reject(new Error(`compositor: ${String(message.error).slice(0, 200)}`));
|
|
295
|
-
else pending.resolve(message.result);
|
|
297
|
+
else pending.resolve(pixels.length ? {...message.result,pixels} : message.result);
|
|
296
298
|
}
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
#closeChannel(channel) {
|
|
300
302
|
if (!channel || channel.closed) return;
|
|
301
303
|
channel.closed = true;
|
|
304
|
+
channel.transport?.close();
|
|
302
305
|
for (const { reject } of channel.pending.values()) reject(new Error("compositor closed"));
|
|
303
306
|
channel.pending.clear();
|
|
304
307
|
if (this.#channel === channel) this.#channel = null;
|
|
305
308
|
}
|
|
306
309
|
|
|
307
310
|
close() {
|
|
311
|
+
this.#closed = true;
|
|
312
|
+
this.#startEpoch++;
|
|
313
|
+
this.#rejectStartup?.(Error("Renderer closed during startup")); this.#rejectStartup=null;
|
|
314
|
+
this.#rawFrames?.close(); this.#rawFrames=null;
|
|
308
315
|
this.#closeChannel(this.#channel);
|
|
309
316
|
const child = this.#child;
|
|
310
317
|
this.#closeSockets(child);
|
|
@@ -312,13 +319,18 @@ export class RendererHost {
|
|
|
312
319
|
this.#closePageFetches(child);
|
|
313
320
|
this.#child = null;
|
|
314
321
|
this.#ready = null;
|
|
322
|
+
for (const {reject} of this.#pending.values()) reject(Error("Renderer closed"));
|
|
323
|
+
this.#pending.clear();
|
|
315
324
|
child?.kill();
|
|
316
325
|
}
|
|
317
326
|
|
|
318
327
|
async #call(method, params, timeout = this.#timeout) {
|
|
328
|
+
if (this.#closed) throw Error("Renderer is closed");
|
|
319
329
|
if (!this.#child) await this.#restart();
|
|
320
330
|
await this.start();
|
|
321
331
|
const child = this.#child;
|
|
332
|
+
if (method === 'click' || method === 'press') this.#clipboardUntil = Date.now()+5000;
|
|
333
|
+
if (method === 'loadDocument') this.#clipboardUntil = 0;
|
|
322
334
|
const id = this.#nextId++;
|
|
323
335
|
return new Promise((resolve, reject) => {
|
|
324
336
|
const timer = setTimeout(() => {
|
|
@@ -327,6 +339,7 @@ export class RendererHost {
|
|
|
327
339
|
this.#child = null;
|
|
328
340
|
this.#ready = null;
|
|
329
341
|
console.error(`buninu-browser: renderer ${child.pid} missed the ${timeout}ms deadline for ${method}; killing it`);
|
|
342
|
+
this.#rawFrames?.close(); this.#rawFrames=null;
|
|
330
343
|
child.kill(9);
|
|
331
344
|
reject(new Error(`renderer ${method} timed out after ${timeout}ms`));
|
|
332
345
|
}, timeout);
|
|
@@ -365,6 +378,30 @@ export class RendererHost {
|
|
|
365
378
|
pending.reject(new Error(`renderer: ${String(message.error).split("\n")[0] || "unknown error"}`));
|
|
366
379
|
}
|
|
367
380
|
else pending.resolve(message.result);
|
|
381
|
+
} else if (['clipboard-read','clipboard-read-input'].includes(message.type) && Number.isInteger(message.id)) {
|
|
382
|
+
const reply = (text, error) => {if(this.#child===child) child.send({type:'clipboard-result',id:message.id,text,error});};
|
|
383
|
+
let secure=false;
|
|
384
|
+
try {const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname));} catch {}
|
|
385
|
+
if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending) {
|
|
386
|
+
reply(undefined,'Clipboard read requires a recent user action in a secure document'); return;
|
|
387
|
+
}
|
|
388
|
+
this.#clipboardUntil=0; this.#clipboardPending=true;
|
|
389
|
+
Promise.resolve().then(()=>this.#clipboardRead()).then(text=>{
|
|
390
|
+
if(typeof text!=='string'||Buffer.byteLength(text)>1024*1024)throw Error('Clipboard text exceeds limit');
|
|
391
|
+
reply(text);
|
|
392
|
+
}).catch(()=>reply(undefined,'Unable to read system clipboard')).finally(()=>{this.#clipboardPending=false;});
|
|
393
|
+
} else if (['clipboard-write','clipboard-write-input'].includes(message.type) && Number.isInteger(message.id)) {
|
|
394
|
+
const reply = error => { if(this.#child===child)child.send({type:'clipboard-result',id:message.id,...(error?{error}: {})}); };
|
|
395
|
+
let secure=false;
|
|
396
|
+
try { const url=new URL(this.#documentUrl); secure=url.protocol==='https:' || (url.protocol==='http:' && ['localhost','127.0.0.1','[::1]'].includes(url.hostname)); } catch {}
|
|
397
|
+
if ((!secure && !message.type.endsWith('-input')) || Date.now()>this.#clipboardUntil || this.#clipboardPending
|
|
398
|
+
|| typeof message.text!=='string' || Buffer.byteLength(message.text)>1024*1024) {
|
|
399
|
+
reply('Clipboard write requires a recent user action in a secure document'); return;
|
|
400
|
+
}
|
|
401
|
+
this.#clipboardUntil=0; this.#clipboardPending=true;
|
|
402
|
+
Promise.resolve().then(()=>this.#clipboardWrite(message.text))
|
|
403
|
+
.then(()=>reply(),()=>reply('Unable to write system clipboard'))
|
|
404
|
+
.finally(()=>{this.#clipboardPending=false;});
|
|
368
405
|
} else if (message.type === "fetch" && Number.isInteger(message.id)) {
|
|
369
406
|
this.#fetchForRenderer(child, message);
|
|
370
407
|
} else if (message.type === "page-fetch" && Number.isInteger(message.id)) {
|
|
@@ -495,8 +532,7 @@ export class RendererHost {
|
|
|
495
532
|
child.send({ type: "audio-event", id, ...payload });
|
|
496
533
|
// The compositor's A/V clock must not wait behind page JavaScript.
|
|
497
534
|
if (payload.event === "timing" && this.#channel && !this.#channel.closed) {
|
|
498
|
-
this.#channel.
|
|
499
|
-
this.#channel.writer.flush();
|
|
535
|
+
this.#channel.transport.send({type:"audio-timing",audioId:id,seconds:payload.seconds});
|
|
500
536
|
}
|
|
501
537
|
}
|
|
502
538
|
};
|
package/lib/renderer/main.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* closed with sentBytes: the PCM that reached the output before audio-close)
|
|
29
29
|
*
|
|
30
30
|
* With --compositor, a compositor Worker (compositor.js) answers screenshots
|
|
31
|
-
* and scrolling on
|
|
31
|
+
* and scrolling on a dedicated socket while this thread is busy; frames go to it with
|
|
32
32
|
* postMessage.
|
|
33
33
|
*
|
|
34
34
|
* Startup order matters for the sandbox:
|
|
@@ -172,6 +172,8 @@ function compositorCaughtUp() {
|
|
|
172
172
|
|
|
173
173
|
async function startCompositor() {
|
|
174
174
|
const worker = new Worker(new URL("./compositor.js", import.meta.url).href);
|
|
175
|
+
const endpoint = JSON.parse(process.argv.find(arg=>arg.startsWith('--compositor-endpoint=')).slice('--compositor-endpoint='.length));
|
|
176
|
+
worker.postMessage({type:'connect',endpoint});
|
|
175
177
|
await new Promise((resolve, reject) => {
|
|
176
178
|
worker.addEventListener("message", resolve, { once: true });
|
|
177
179
|
worker.addEventListener("error", reject, { once: true });
|
|
@@ -237,12 +239,28 @@ async function startScriptRewriter() {
|
|
|
237
239
|
});
|
|
238
240
|
}
|
|
239
241
|
|
|
242
|
+
let nextClipboardId=1;
|
|
243
|
+
const clipboardWrites=new Map();
|
|
244
|
+
function clipboardRead() { return clipboardWrite(undefined, 'clipboard-read'); }
|
|
245
|
+
function clipboardWrite(text, type = 'clipboard-write') {
|
|
246
|
+
const id=nextClipboardId++;
|
|
247
|
+
return new Promise((resolve,reject)=>{
|
|
248
|
+
const timer=setTimeout(()=>{clipboardWrites.delete(id);reject(Error('Clipboard timed out'));},5000);
|
|
249
|
+
clipboardWrites.set(id,{resolve,reject,timer});
|
|
250
|
+
process.send({type,id,text});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
240
254
|
const renderer = await createPageRenderer({
|
|
241
255
|
fetchResource,
|
|
242
256
|
rewriteScript,
|
|
243
257
|
requestNavigation: (url, download = null, fragmentFrom = null, post = null) => process.send({ type: "navigate", url, download, fragmentFrom, post }),
|
|
244
258
|
requestDownload: ({ url, name, data }) => process.send({ type: "download-data", url, name, data }),
|
|
245
259
|
pageFetch,
|
|
260
|
+
clipboardRead,
|
|
261
|
+
clipboardWrite,
|
|
262
|
+
clipboardReadInput:()=>clipboardWrite(undefined,'clipboard-read-input'),
|
|
263
|
+
clipboardWriteInput:text=>clipboardWrite(text,'clipboard-write-input'),
|
|
246
264
|
webSockets,
|
|
247
265
|
audio,
|
|
248
266
|
setCookie: (value) => process.send({ type: "cookie", value }),
|
|
@@ -267,11 +285,20 @@ const renderer = await createPageRenderer({
|
|
|
267
285
|
videoDecoder: wasmpeg,
|
|
268
286
|
});
|
|
269
287
|
await warmUp();
|
|
270
|
-
|
|
288
|
+
let allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
|
|
289
|
+
let hostReason = `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}`;
|
|
290
|
+
|
|
291
|
+
// qml handle begin: controller-selected local QML application process
|
|
292
|
+
if (process.argv.includes("--buninu-qml-host")) {
|
|
293
|
+
allowHostJs = true;
|
|
294
|
+
hostReason = "local QML application renderer";
|
|
295
|
+
}
|
|
296
|
+
// qml handle end
|
|
297
|
+
|
|
271
298
|
const seccomp = allowHostJs
|
|
272
|
-
? { installed: false, reason:
|
|
299
|
+
? { installed: false, reason: hostReason }
|
|
273
300
|
: installSeccomp();
|
|
274
|
-
const ses = allowHostJs ? { installed: false, reason:
|
|
301
|
+
const ses = allowHostJs ? { installed: false, reason: hostReason } : lockdownRealm();
|
|
275
302
|
renderer.setScripting(allowHostJs ? "host" : ses.installed ? "ses" : "off");
|
|
276
303
|
|
|
277
304
|
/** Hardens the JavaScript realm so page scripts can run in SES compartments. */
|
|
@@ -323,6 +350,13 @@ async function warmUp() {
|
|
|
323
350
|
}
|
|
324
351
|
|
|
325
352
|
process.on("message", async (message) => {
|
|
353
|
+
if(message?.type==='clipboard-result') {
|
|
354
|
+
const pending=clipboardWrites.get(message.id);if(!pending)return;
|
|
355
|
+
clipboardWrites.delete(message.id);clearTimeout(pending.timer);
|
|
356
|
+
if(message.error)pending.reject(Error(String(message.error)));else pending.resolve(message.text);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
326
360
|
if (message?.type === "page-fetch-head") {
|
|
327
361
|
const pending = pendingFetches.get(message.id);
|
|
328
362
|
if (!pending?.page || pending.head) return;
|