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.
Files changed (249) hide show
  1. package/LICENSES/jsmdcui.txt +22 -0
  2. package/README.md +102 -2
  3. package/frontends/wayland/LICENSE-land +21 -0
  4. package/frontends/wayland/README.md +123 -0
  5. package/frontends/wayland/casty/LICENSE +21 -0
  6. package/frontends/wayland/casty/bookmarks.js +32 -0
  7. package/frontends/wayland/casty/click.js +96 -0
  8. package/frontends/wayland/casty/commands.js +61 -0
  9. package/frontends/wayland/casty/config.js +45 -0
  10. package/frontends/wayland/casty/download-directory.js +28 -0
  11. package/frontends/wayland/casty/download.js +49 -0
  12. package/frontends/wayland/casty/hints.js +334 -0
  13. package/frontends/wayland/casty/keys.js +56 -0
  14. package/frontends/wayland/casty/page-commands.js +80 -0
  15. package/frontends/wayland/casty/prompt-width.js +148 -0
  16. package/frontends/wayland/casty/urlbar.js +606 -0
  17. package/frontends/wayland/client.js +88 -0
  18. package/frontends/wayland/clipboard.js +80 -0
  19. package/frontends/wayland/index.js +116 -0
  20. package/frontends/wayland/input.js +56 -0
  21. package/frontends/wayland/protocol.js +55 -0
  22. package/frontends/wayland/protocols/wayland.xml +3248 -0
  23. package/frontends/wayland/protocols/xdg-shell.xml +1415 -0
  24. package/frontends/wayland/transport.js +120 -0
  25. package/frontends/wayland/urlbar.js +158 -0
  26. package/index.js +28 -1
  27. package/lib/clipboard-command.js +10 -0
  28. package/lib/clipboard-powershell.js +22 -0
  29. package/lib/clipboard.js +57 -0
  30. package/lib/document-types.js +11 -0
  31. package/lib/headless-shell.js +78 -13
  32. package/lib/layout/text-layout.js +17 -9
  33. package/lib/paint/display-list.js +5 -1
  34. package/lib/paint/frame.js +35 -3
  35. package/lib/paint/raw-frames.js +47 -0
  36. package/lib/qml/README.md +971 -0
  37. package/lib/qml/builtins.js +700 -0
  38. package/lib/qml/canvas-dom.js +115 -0
  39. package/lib/qml/document.js +1176 -0
  40. package/lib/qml/format.js +9 -0
  41. package/lib/qml/host.js +324 -0
  42. package/lib/qml/images.js +199 -0
  43. package/lib/qml/launcher.js +132 -0
  44. package/lib/qml/loader.js +517 -0
  45. package/lib/qml/media-renderer.js +49 -0
  46. package/lib/qml/media.js +86 -0
  47. package/lib/qml/models.js +622 -0
  48. package/lib/qml/parser.js +381 -0
  49. package/lib/qml/qs +5 -0
  50. package/lib/qml/qscasty +3 -0
  51. package/lib/qml/qsheadless +3 -0
  52. package/lib/qml/qswayland +3 -0
  53. package/lib/qml/reactive.js +159 -0
  54. package/lib/qml/runtime.js +3381 -0
  55. package/lib/qml/shapes.js +243 -0
  56. package/lib/qml/text.js +109 -0
  57. package/lib/qml/textures.js +123 -0
  58. package/lib/qml/wayland-clipboard.js +32 -0
  59. package/lib/qml/worker-thread.js +70 -0
  60. package/lib/render-tree/index.js +16 -1
  61. package/lib/renderer/bindings.js +11 -0
  62. package/lib/renderer/canvas-2d.js +13 -0
  63. package/lib/renderer/compositor-channel.js +103 -0
  64. package/lib/renderer/compositor.js +25 -33
  65. package/lib/renderer/flags.js +1 -1
  66. package/lib/renderer/host.js +128 -92
  67. package/lib/renderer/main.js +38 -4
  68. package/lib/renderer/page-realm.js +32 -1
  69. package/lib/renderer/page-renderer.js +250 -21
  70. package/lib/renderer/text-input-paint.js +54 -0
  71. package/lib/renderer/text-input.js +57 -0
  72. package/lib/style/computed-style.js +2 -2
  73. package/package.json +1 -1
  74. package/test/clipboard.test.js +54 -0
  75. package/test/fixtures/browser-raw-demo.html +36 -0
  76. package/test/fixtures/flea-helpers/Dockerfile +36 -0
  77. package/test/fixtures/flea-helpers/bat.mjs +43 -0
  78. package/test/fixtures/flea-helpers/bin/bat +3 -0
  79. package/test/fixtures/flea-helpers/bin/bwrap +10 -0
  80. package/test/fixtures/flea-helpers/bin/bwrap-real-filter +9 -0
  81. package/test/fixtures/flea-helpers/bin/glow +3 -0
  82. package/test/fixtures/flea-helpers/bin/qs +3 -0
  83. package/test/fixtures/flea-helpers/bin/unsupported +3 -0
  84. package/test/fixtures/flea-helpers/check-highlighting.mjs +47 -0
  85. package/test/fixtures/flea-helpers/check-media.mjs +29 -0
  86. package/test/fixtures/flea-helpers/check-preview.mjs +27 -0
  87. package/test/fixtures/flea-helpers/check.mjs +36 -0
  88. package/test/fixtures/flea-helpers/highlight-cases.json +227 -0
  89. package/test/fixtures/flea-helpers/source-mime.xml +28 -0
  90. package/test/fixtures/flea-helpers/syntax-compat.mjs +23 -0
  91. package/test/fixtures/flea-thumbnail-stub/flea-placeholder.thumbnailer +4 -0
  92. package/test/fixtures/flea-thumbnail-stub/flea-thumbnail-stub +3 -0
  93. package/test/fixtures/qml/flea/EscapeHint.qml +11 -0
  94. package/test/fixtures/qml/flea/LICENSE +21 -0
  95. package/test/fixtures/qml/flea/README.md +25 -0
  96. package/test/fixtures/qml/flea/Theme.qml +8 -0
  97. package/test/fixtures/qml/flea/demo.qml +24 -0
  98. package/test/fixtures/qml/flea/qmldir +3 -0
  99. package/test/fixtures/qml/flea-theme/LICENSE-Flea +21 -0
  100. package/test/fixtures/qml/flea-theme/LICENSE-Omarchy +20 -0
  101. package/test/fixtures/qml/flea-theme/README.md +70 -0
  102. package/test/fixtures/qml/flea-theme/button.qml +17 -0
  103. package/test/fixtures/qml/flea-theme/demo.qml +27 -0
  104. package/test/fixtures/qml/flea-theme/field.qml +21 -0
  105. package/test/fixtures/qml/flea-theme/imports/qs/Commons/BorderGeometry.js +373 -0
  106. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Color.qml +258 -0
  107. package/test/fixtures/qml/flea-theme/imports/qs/Commons/I18n.qml +118 -0
  108. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Numbers.js +19 -0
  109. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Plural.js +91 -0
  110. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Style.qml +561 -0
  111. package/test/fixtures/qml/flea-theme/imports/qs/Commons/Util.qml +188 -0
  112. package/test/fixtures/qml/flea-theme/imports/qs/Commons/qmldir +13 -0
  113. package/test/fixtures/qml/flea-theme/segment.qml +17 -0
  114. package/test/fixtures/qml/flea-theme/settings.qml +58 -0
  115. package/test/fixtures/qml/flea-theme/text-metrics.qml +10 -0
  116. package/test/fixtures/qml/flea-theme/ui/DialogButton.qml +150 -0
  117. package/test/fixtures/qml/flea-theme/ui/DialogField.qml +147 -0
  118. package/test/fixtures/qml/flea-theme/ui/EscapeHint.qml +11 -0
  119. package/test/fixtures/qml/flea-theme/ui/Glass.qml +114 -0
  120. package/test/fixtures/qml/flea-theme/ui/Glyph.qml +54 -0
  121. package/test/fixtures/qml/flea-theme/ui/GlyphSolid.qml +22 -0
  122. package/test/fixtures/qml/flea-theme/ui/Reveal.qml +54 -0
  123. package/test/fixtures/qml/flea-theme/ui/SettingsRuler.qml +85 -0
  124. package/test/fixtures/qml/flea-theme/ui/SettingsSegment.qml +85 -0
  125. package/test/fixtures/qml/flea-theme/ui/StableModel.qml +89 -0
  126. package/test/fixtures/qml/flea-theme/ui/Theme.qml +460 -0
  127. package/test/fixtures/qml/flea-theme/ui/ThemeColors.qml +43 -0
  128. package/test/fixtures/qml/flea-theme/ui/ViewState.qml +565 -0
  129. package/test/fixtures/qml/flea-theme/ui/js/Anchor.js +379 -0
  130. package/test/fixtures/qml/flea-theme/ui/js/AnchorHold.js +162 -0
  131. package/test/fixtures/qml/flea-theme/ui/js/Archive.js +157 -0
  132. package/test/fixtures/qml/flea-theme/ui/js/BackReturn.js +110 -0
  133. package/test/fixtures/qml/flea-theme/ui/js/Buttons.js +54 -0
  134. package/test/fixtures/qml/flea-theme/ui/js/ColumnMenu.js +111 -0
  135. package/test/fixtures/qml/flea-theme/ui/js/Columns.js +298 -0
  136. package/test/fixtures/qml/flea-theme/ui/js/Contrast.js +157 -0
  137. package/test/fixtures/qml/flea-theme/ui/js/Density.js +44 -0
  138. package/test/fixtures/qml/flea-theme/ui/js/DirSizes.js +129 -0
  139. package/test/fixtures/qml/flea-theme/ui/js/ExtThumbs.js +115 -0
  140. package/test/fixtures/qml/flea-theme/ui/js/Filter.js +284 -0
  141. package/test/fixtures/qml/flea-theme/ui/js/FolderSorts.js +108 -0
  142. package/test/fixtures/qml/flea-theme/ui/js/Format.js +305 -0
  143. package/test/fixtures/qml/flea-theme/ui/js/Icons.js +206 -0
  144. package/test/fixtures/qml/flea-theme/ui/js/Input.js +39 -0
  145. package/test/fixtures/qml/flea-theme/ui/js/KeyName.js +64 -0
  146. package/test/fixtures/qml/flea-theme/ui/js/Keymap.js +370 -0
  147. package/test/fixtures/qml/flea-theme/ui/js/Kinds.js +145 -0
  148. package/test/fixtures/qml/flea-theme/ui/js/Lists.js +11 -0
  149. package/test/fixtures/qml/flea-theme/ui/js/MakeDefault.js +186 -0
  150. package/test/fixtures/qml/flea-theme/ui/js/Match.js +27 -0
  151. package/test/fixtures/qml/flea-theme/ui/js/Material.js +109 -0
  152. package/test/fixtures/qml/flea-theme/ui/js/Menu.js +513 -0
  153. package/test/fixtures/qml/flea-theme/ui/js/Motion.js +16 -0
  154. package/test/fixtures/qml/flea-theme/ui/js/Mounts.js +320 -0
  155. package/test/fixtures/qml/flea-theme/ui/js/MouseNav.js +13 -0
  156. package/test/fixtures/qml/flea-theme/ui/js/Nav.js +328 -0
  157. package/test/fixtures/qml/flea-theme/ui/js/Palette.js +33 -0
  158. package/test/fixtures/qml/flea-theme/ui/js/Places.js +236 -0
  159. package/test/fixtures/qml/flea-theme/ui/js/Protocols.js +296 -0
  160. package/test/fixtures/qml/flea-theme/ui/js/RecentMode.js +154 -0
  161. package/test/fixtures/qml/flea-theme/ui/js/Search.js +396 -0
  162. package/test/fixtures/qml/flea-theme/ui/js/Settings.js +476 -0
  163. package/test/fixtures/qml/flea-theme/ui/js/SettingsAbout.js +99 -0
  164. package/test/fixtures/qml/flea-theme/ui/js/SettingsShelf.js +52 -0
  165. package/test/fixtures/qml/flea-theme/ui/js/Sort.js +127 -0
  166. package/test/fixtures/qml/flea-theme/ui/js/StateWords.js +42 -0
  167. package/test/fixtures/qml/flea-theme/ui/js/Swap.js +166 -0
  168. package/test/fixtures/qml/flea-theme/ui/js/TextSize.js +107 -0
  169. package/test/fixtures/qml/flea-theme/ui/js/Thumbs.js +158 -0
  170. package/test/fixtures/qml/flea-theme/ui/js/UiState.js +438 -0
  171. package/test/fixtures/qml/flea-theme/ui/js/Update.js +132 -0
  172. package/test/fixtures/qml/flea-theme/ui/js/UserDirs.js +56 -0
  173. package/test/fixtures/qml/flea-theme/ui/qmldir +227 -0
  174. package/test/fixtures/qml/hello.qml +26 -0
  175. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabel.qml +2 -0
  176. package/test/fixtures/qml/imports/Demo/Toolkit/VersionLabelNew.qml +2 -0
  177. package/test/fixtures/qml/imports/Demo/Toolkit/qmldir +3 -0
  178. package/test/fixtures/qml/imports/components/Badge.qml +15 -0
  179. package/test/fixtures/qml/imports/logic/base.js +2 -0
  180. package/test/fixtures/qml/imports/logic/counter.js +4 -0
  181. package/test/fixtures/qml/imports/logic/math.js +3 -0
  182. package/test/fixtures/qml/imports/logic/shared.js +3 -0
  183. package/test/fixtures/qml/imports/main.qml +21 -0
  184. package/test/fixtures/qml/imports/theme/Theme.qml +8 -0
  185. package/test/fixtures/qml/imports/theme/qmldir +2 -0
  186. package/test/fixtures/qml/multiply.qml +61 -0
  187. package/test/fixtures/qml-canvas-dom.js +90 -0
  188. package/test/fixtures/qml-media-audio.js +39 -0
  189. package/test/fixtures/raw-capture-benchmark.json +76 -0
  190. package/test/helpers/flea-sandbox.js +48 -0
  191. package/test/helpers/quickshell-fixture.js +125 -0
  192. package/test/html-clipboard-wayland.test.js +47 -0
  193. package/test/index.test.js +13 -0
  194. package/test/manual-casty-without-qml.js +87 -0
  195. package/test/manual-flea-application.js +49 -0
  196. package/test/manual-flea-binary.js +62 -0
  197. package/test/manual-flea-qml.js +9 -0
  198. package/test/manual-flea-settings.js +40 -0
  199. package/test/manual-flea-theme.js +40 -0
  200. package/test/manual-qml-removal.js +46 -0
  201. package/test/manual-without-qml.js +45 -0
  202. package/test/qml-canvas-dom.test.js +37 -0
  203. package/test/qml-clipboard-bridge.test.js +25 -0
  204. package/test/qml-clipboard-wayland.test.js +40 -0
  205. package/test/qml-connections-timing.test.js +73 -0
  206. package/test/qml-dynamic.test.js +401 -0
  207. package/test/qml-flea-app.test.js +146 -0
  208. package/test/qml-flea-backend.test.js +78 -0
  209. package/test/qml-flea-columns-backend.test.js +38 -0
  210. package/test/qml-flea-compile.test.js +78 -0
  211. package/test/qml-flea-launcher.test.js +88 -0
  212. package/test/qml-flea-markdown-backend.test.js +31 -0
  213. package/test/qml-flea-media.test.js +28 -0
  214. package/test/qml-flea-navigation-backend.test.js +35 -0
  215. package/test/qml-flea-navigation-timing.test.js +147 -0
  216. package/test/qml-flea-preview-backend.test.js +53 -0
  217. package/test/qml-flea-previews.test.js +163 -0
  218. package/test/qml-flea-rows.test.js +101 -0
  219. package/test/qml-flea-textures.test.js +59 -0
  220. package/test/qml-flea-view-switch.test.js +33 -0
  221. package/test/qml-flea-wayland.test.js +68 -0
  222. package/test/qml-flea.test.js +34 -0
  223. package/test/qml-host.test.js +238 -0
  224. package/test/qml-imports.test.js +102 -0
  225. package/test/qml-inherited-handlers.test.js +51 -0
  226. package/test/qml-input-selection.test.js +66 -0
  227. package/test/qml-interactions.test.js +309 -0
  228. package/test/qml-language.test.js +167 -0
  229. package/test/qml-launcher.test.js +54 -0
  230. package/test/qml-layout-text.test.js +205 -0
  231. package/test/qml-media.test.js +63 -0
  232. package/test/qml-persisted-delegates.test.js +45 -0
  233. package/test/qml-quickshell-controls.test.js +65 -0
  234. package/test/qml-quickshell-settings.test.js +62 -0
  235. package/test/qml-repeater.test.js +72 -0
  236. package/test/qml-style-updates.test.js +124 -0
  237. package/test/qml-textures.test.js +66 -0
  238. package/test/qml-views.test.js +332 -0
  239. package/test/qml-worker.test.js +69 -0
  240. package/test/qml.test.js +83 -0
  241. package/test/raw-capture.test.js +152 -0
  242. package/test/renderer-host.test.js +11 -0
  243. package/test/text-input-pointer.test.js +31 -0
  244. package/test/text-input-scroll.test.js +29 -0
  245. package/test/text-input.test.js +47 -0
  246. package/test/wayland-frontend.test.js +83 -0
  247. package/test/wayland-urlbar.test.js +38 -0
  248. package/test/window-resize.test.js +36 -0
  249. 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: fd 3, a socket shared with the controller,
10
- * newline-delimited JSON (it does not go through the main thread's IPC):
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 { cropViewport, encodeImage, encodeImageBase64 } from "../paint/frame.js";
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
- const CHANNEL_FD = 3;
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
- const channel = Bun.file(CHANNEL_FD).writer();
46
-
44
+ let channel;
47
45
  function send(message) {
48
- channel.write(`${JSON.stringify(message)}\n`);
49
- channel.flush();
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
- const key = `${frameSerial}:${viewport.x}:${viewport.y}:${format}:${quality}:${optimizeForSpeed}`;
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 } = cropViewport(composited, viewport.x, viewport.y, viewport.width, viewport.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 handleLine(line) {
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);
@@ -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
- /** fd 3 is the controller's compositor channel; start the compositor Worker. */
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). */
@@ -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
- * channel (fd 3) to its compositor thread, which answers screenshots and
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 { closeSync } from "node:fs";
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
- // The compositor's channel is an extra stdio pipe used as fd 3, which
109
- // Windows does not have (Bun hands out libuv pipes there, not file
110
- // descriptors): there the main thread answers screenshots and scrolling.
111
- compositor = entry === DEFAULT_ENTRY && process.platform !== "win32",
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 child = Bun.spawn({
137
- cmd: [process.execPath, this.#entry, ...this.#args,
138
- ...(logging ? [RENDERER_LOG_FLAG] : []), ...(compositor ? [COMPOSITOR_FLAG] : []),
139
- ...(process.env.BUNINU_VERIFY_STYLE ? [VERIFY_STYLE_FLAG] : []), ...(process.env.BUNINU_FULL_STYLE ? [FULL_STYLE_FLAG] : [])],
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: process.platform === "win32" && process.env.SystemRoot ? { SystemRoot: process.env.SystemRoot } : {},
143
- // With BUNINU_LOG, output goes through the controller into the log file.
144
- // fd 3 is a socket to the renderer's compositor thread.
145
- stdio: ["ignore", logging ? "pipe" : "inherit", logging ? "pipe" : "inherit", ...(compositor ? ["pipe"] : [])],
146
- serialization: "advanced",
147
- ipc: (message) => {
148
- if (message?.type === "ready") {
149
- this.#sandbox = sanitizeSandboxReport(message.sandbox);
150
- resolveReady();
151
- }
152
- else this.#handleMessage(child, message);
153
- },
154
- onExit: (_process, exitCode, signalCode) => {
155
- this.#closeChannel(channel);
156
- this.#closeSockets(child);
157
- this.#closeAudio(child);
158
- this.#closePageFetches(child);
159
- if (this.#child !== child) return;
160
- console.error(`buninu-browser: renderer ${child.pid} exited unexpectedly (${signalCode ?? `code ${exitCode}`})`);
161
- this.#child = null;
162
- this.#ready = null;
163
- rejectReady(new Error("renderer exited during startup"));
164
- for (const { reject } of this.#pending.values()) reject(new Error("renderer exited"));
165
- this.#pending.clear();
166
- },
167
- });
168
- this.#child = child;
169
- const channel = compositor && Number.isInteger(child.stdio[3]) ? this.#openChannel(child.stdio[3]) : null;
170
- this.#channel = channel;
171
- if (logging) {
172
- pipeToLog(child.stdout, `renderer ${child.pid}`, (chunk) => process.stdout.write(chunk));
173
- pipeToLog(child.stderr, `renderer ${child.pid}`, (chunk) => process.stderr.write(chunk));
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.writer.write(`${JSON.stringify({ id, method, params })}\n`);
239
- channel.writer.flush();
271
+ try { channel.transport.send({id,method,params}); }
272
+ catch(error) { channel.pending.get(id).reject(error); channel.pending.delete(id); }
240
273
  });
241
- const sanitized = sanitizeCompositorResult(method, result);
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
- #openChannel(fd) {
247
- const channel = { writer: Bun.file(fd).writer(), pending: new Map(), nextId: 1, closed: false };
248
- (async () => {
249
- const decoder = new TextDecoder();
250
- let buffer = "";
251
- try {
252
- for await (const chunk of Bun.file(fd).stream()) {
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
- #handleCompositorLine(channel, line) {
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.writer.write(`${JSON.stringify({ type: "audio-timing", audioId: id, seconds: payload.seconds })}\n`);
499
- this.#channel.writer.flush();
535
+ this.#channel.transport.send({type:"audio-timing",audioId:id,seconds:payload.seconds});
500
536
  }
501
537
  }
502
538
  };
@@ -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 fd 3 while this thread is busy; frames go to it with
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
- const allowHostJs = process.argv.includes(DANGEROUSLY_ALLOW_HOST_JS);
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: `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}` }
299
+ ? { installed: false, reason: hostReason }
273
300
  : installSeccomp();
274
- const ses = allowHostJs ? { installed: false, reason: `disabled by ${DANGEROUSLY_ALLOW_HOST_JS}` } : lockdownRealm();
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;