buninu-browser 0.6.0 → 0.9.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/AGENTS.md +5 -0
- package/README.md +94 -4
- package/buninu-browser.js +1 -1
- package/frontends/wayland/README.md +35 -2
- package/frontends/wayland/client.js +8 -6
- package/frontends/wayland/clipboard.js +1 -1
- package/frontends/wayland/index.js +1 -1
- package/frontends/wayland/input.js +1 -1
- package/frontends/wayland/resource-io.js +6 -0
- package/frontends/wayland/transport-posix.js +120 -0
- package/frontends/wayland/transport-win32.js +9 -0
- package/frontends/wayland/transport.js +3 -120
- package/frontends/wayland/win32-resources.js +80 -0
- package/frontends/wayland/win32-wire.js +62 -0
- package/index.js +10 -6
- package/lib/audio/biquad.js +25 -0
- package/lib/audio/piano.js +6 -3
- package/lib/audio/playback-buffer.js +5 -5
- package/lib/audio/pulse-client.js +24 -5
- package/lib/audio/web-audio.js +570 -0
- package/lib/browser-dialog.js +116 -0
- package/lib/cdp-server.js +128 -72
- package/lib/file-picker.js +48 -0
- package/lib/headless-shell.js +34 -12
- package/lib/layout/text-layout.js +112 -19
- package/lib/media/mp4-frame-times.js +93 -0
- package/lib/paint/direct-canvas.ts +39 -0
- package/lib/paint/display-list.js +37 -12
- package/lib/paint/raster-readback.js +16 -7
- package/lib/renderer/animation-frames.js +10 -5
- package/lib/renderer/bindings.js +69 -16
- package/lib/renderer/canvas-2d.js +265 -15
- package/lib/renderer/canvas-round-rect.js +36 -0
- package/lib/renderer/css-animations.js +173 -0
- package/lib/renderer/dialog-client.js +30 -0
- package/lib/renderer/dialog-worker.js +35 -0
- package/lib/renderer/eval-environment.js +28 -0
- package/lib/renderer/file-input.js +49 -0
- package/lib/renderer/host.js +33 -4
- package/lib/renderer/html-drag.js +77 -0
- package/lib/renderer/image-loading.js +45 -2
- package/lib/renderer/main.js +4 -0
- package/lib/renderer/module-transform.js +5 -3
- package/lib/renderer/page-builtins.js +59 -0
- package/lib/renderer/page-realm.js +177 -40
- package/lib/renderer/page-renderer.js +397 -49
- package/lib/renderer/piano-input.js +3 -1
- package/lib/renderer/raster-surface.js +18 -0
- package/lib/renderer/script-rewrite.js +165 -9
- package/lib/renderer/video-presentation.js +5 -4
- package/lib/renderer/webgl/clip.js +24 -0
- package/lib/renderer/webgl/context.js +158 -0
- package/lib/renderer/webgl/cpu-fragment.js +211 -0
- package/lib/renderer/webgl/draw-cache.js +22 -0
- package/lib/renderer/webgl/pipeline.js +70 -0
- package/lib/renderer/webgl/shaders.js +58 -0
- package/lib/renderer/webgl/textures.js +43 -0
- package/lib/renderer/window-named-properties.js +46 -0
- package/lib/screenshot-cli.js +4 -7
- package/lib/style/animation-values.js +29 -0
- package/lib/style/computed-style.js +109 -13
- package/lib/style/transform.js +44 -0
- package/lib/window-size.js +10 -0
- package/package.json +1 -1
- package/patches/happy-dom/0007-scope-content-models-to-html-namespace.patch +29 -0
- package/patches/happy-dom/README.md +16 -0
- package/test/animation-frames.test.js +15 -0
- package/test/canvas-2d.test.js +252 -4
- package/test/canvas-frame-pacing.test.js +7 -0
- package/test/cdp-viewers.test.js +79 -0
- package/test/classic-games-patches/0001-fix-game-initialization.patch +177 -0
- package/test/classic-games-patches/README.md +156 -0
- package/test/classic-games-scenarios.js +133 -0
- package/test/clipboard.test.js +16 -0
- package/test/computed-style.test.js +18 -0
- package/test/css-animation-lifecycle.test.js +28 -0
- package/test/css-animations.test.js +83 -0
- package/test/direct-canvas.test.js +39 -0
- package/test/dynamic-inline-handlers.test.js +25 -0
- package/test/eval-declarations.test.js +22 -0
- package/test/file-picker.test.js +26 -0
- package/test/fixtures/canvas-damage.js +23 -0
- package/test/fixtures/canvas-frame-pacing.js +35 -0
- package/test/fixtures/direct-canvas.js +96 -0
- package/test/fixtures/javascript-dialogs.html +8 -0
- package/test/fixtures/media-audio-lifetime.js +65 -0
- package/test/fixtures/renderer-memory.js +7 -5
- package/test/fixtures/video-black-replay.js +8 -6
- package/test/fixtures/video-slow-end.js +39 -0
- package/test/fixtures/web-audio-renderer.js +45 -0
- package/test/flex.test.js +26 -0
- package/test/grid.test.js +13 -0
- package/test/headless-shell.test.js +9 -0
- package/test/headless-window-size.test.js +28 -0
- package/test/html-rcdata.test.js +35 -0
- package/test/image-decode.test.js +18 -0
- package/test/index.test.js +1 -0
- package/test/javascript-dialog.test.js +83 -0
- package/test/manual-breakout-paddle.js +42 -0
- package/test/manual-breakout-static.js +35 -0
- package/test/manual-classic-games.js +41 -0
- package/test/manual-clumsy-bird-three-pipes.js +81 -0
- package/test/manual-clumsy-bird.js +52 -0
- package/test/manual-hextris.js +30 -0
- package/test/manual-numbered-music.js +24 -0
- package/test/manual-t-rex-runner.js +49 -0
- package/test/manual-threejs-scene.js +64 -0
- package/test/manual-webgl-benchmark.js +27 -0
- package/test/manual-webgl-cpu.html +26 -0
- package/test/media-audio-lifetime.test.js +8 -0
- package/test/mp4-frame-times.test.js +33 -0
- package/test/page-scripts.test.js +96 -1
- package/test/piano-input.test.js +15 -2
- package/test/piano.test.js +11 -2
- package/test/playback-buffer.test.js +11 -0
- package/test/pointer-drag.test.js +33 -0
- package/test/positioning.test.js +82 -0
- package/test/pulse-client.test.js +51 -2
- package/test/raster-readback.test.js +14 -0
- package/test/script-rewrite.test.js +61 -3
- package/test/video-slow-end.test.js +6 -0
- package/test/viewport-keyboard-overflow.test.js +25 -0
- package/test/wayland-win32-resources.test.js +47 -0
- package/test/wayland-win32-wire.test.js +59 -0
- package/test/web-audio-renderer.test.js +8 -0
- package/test/web-audio.test.js +264 -0
- package/test/webgl-clip.test.js +9 -0
- package/test/webgl-cpu-fragment.test.js +43 -0
- package/test/window-named-properties.test.js +41 -0
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfig.ts +2 -2
- package/vendor/happy-dom/packages/happy-dom/src/config/HTMLElementConfigContentModelEnum.ts +1 -0
- package/vendor/happy-dom/packages/happy-dom/src/html-parser/HTMLParser.ts +23 -3
- package/vendor/happy-dom/packages/happy-dom/src/query-selector/SelectorParser.ts +1 -1
- package/vendor/land-renderer/LICENSE +21 -0
- package/vendor/land-renderer/README.md +14 -0
- package/vendor/land-renderer/direct-compositor.ts +443 -0
- package/vendor/land-renderer/frame.js +88 -0
- package/vendor/land-renderer/png-fast.js +85 -0
- package/vendor/land-renderer/raw-frames.js +66 -0
- package/vendor/land-renderer/render-types.ts +7 -0
- package/vendor/land-renderer/renderer.ts +134 -0
- package/vendor/land-renderer/snapshot.js +37 -0
- package/vendor/land-renderer/viewport.js +18 -0
- package/vendor/ses/dist/ses.mjs +1 -1
package/AGENTS.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# AGENTS
|
|
2
2
|
|
|
3
3
|
## General
|
|
4
|
+
- Never EVER EVER EVER EVER use hardlinks.
|
|
5
|
+
- Hardlinks are not allowed.
|
|
6
|
+
- Using hardlinks is utterly harmful.
|
|
7
|
+
- .
|
|
4
8
|
- This project only uses pure JavaScript +/- WASM.
|
|
5
9
|
- Native bindings are generally not allowed in the headless shell itself
|
|
6
10
|
* In the future when it is wrapped into a full browser on each platform, the frontend should stay as thin wrappers just enough to draw rendered content from the headlesss shell
|
|
@@ -14,6 +18,7 @@
|
|
|
14
18
|
## Implementing the browser
|
|
15
19
|
- Do not treat any website specially when refining browser features like layout css js. Fix the actual problem that all websites can benefit from.
|
|
16
20
|
- If you don't know how to fix a specific bug, look at ../chromium implementation but don't copy LGPL code.
|
|
21
|
+
- The graphics rendering of this browser should ensure CPU runs first. GPU acceleration must only be optional.
|
|
17
22
|
|
|
18
23
|
## Viewing files
|
|
19
24
|
- No file is unviewable: every response the browser navigates to must show something, never a "cannot be shown" page
|
package/README.md
CHANGED
|
@@ -50,13 +50,17 @@ npx buninu-browser github.com \
|
|
|
50
50
|
# The engine alone, as a CDP
|
|
51
51
|
# endpoint for any client
|
|
52
52
|
npx buninu-browser --headless \
|
|
53
|
-
--remote-debugging-port=9222
|
|
53
|
+
--remote-debugging-port=9222 --window-size=1280,800
|
|
54
54
|
|
|
55
55
|
# capture a page and exit
|
|
56
56
|
npx buninu-browser --headless --screenshot=page.png \
|
|
57
57
|
--window-size=1280,800 https://example.com
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
`--window-size=WIDTH,HEIGHT` sets the initial viewport for both headless CDP
|
|
61
|
+
and screenshot modes (default 800×600 CSS pixels, DPR 1). CDP clients can
|
|
62
|
+
subsequently change it with `Emulation.setDeviceMetricsOverride`.
|
|
63
|
+
|
|
60
64
|
Screenshot mode waits for the page's `load` event before capture. For content
|
|
61
65
|
that appears later, add `--screenshot-wait=1000` (real milliseconds). The output
|
|
62
66
|
format follows the filename extension: PNG, JPEG, or WebP. Chromium's
|
|
@@ -96,9 +100,10 @@ the rest is passed on:
|
|
|
96
100
|
argument goes to casty)
|
|
97
101
|
- `--headless`: the engine alone, as
|
|
98
102
|
a CDP endpoint
|
|
99
|
-
- `--win32
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
- `--win32`: casty with a native Windows
|
|
104
|
+
window (Windows only)
|
|
105
|
+
- `--gtk`, `--appkit`, `--qt`: native
|
|
106
|
+
windowed frontends (reserved, not implemented yet)
|
|
102
107
|
- `--help`/`-h`, `--version`/`-V`,
|
|
103
108
|
`--readme`: this launcher's help,
|
|
104
109
|
version and backend details, and
|
|
@@ -107,6 +112,27 @@ the rest is passed on:
|
|
|
107
112
|
casty's and the engine's options:
|
|
108
113
|
`--casty --help`, `--headless --help`.
|
|
109
114
|
|
|
115
|
+
### JavaScript dialogs
|
|
116
|
+
|
|
117
|
+
`alert()`, `confirm()` and `prompt()` show a browser-owned modal dialog in the
|
|
118
|
+
page image, including through Casty and the Wayland frontend. For example,
|
|
119
|
+
enter `javascript:alert(1+2)` in the address bar. Alert has an OK button;
|
|
120
|
+
confirm and prompt also have Cancel. Enter accepts, Escape dismisses, and
|
|
121
|
+
prompt initially selects its default text. Long messages scroll and their
|
|
122
|
+
text can be selected and copied. Page scripts wait synchronously for the
|
|
123
|
+
answer; confirm returns a boolean and a cancelled prompt returns `null`.
|
|
124
|
+
The dialog lives in a separate renderer, outside the page's DOM and realm.
|
|
125
|
+
|
|
126
|
+
CDP exposes `Page.javascriptDialogOpening`, `Page.javascriptDialogClosed`
|
|
127
|
+
and `Page.handleJavaScriptDialog` for automation. Ordinary pointer and
|
|
128
|
+
keyboard input can also operate the displayed dialog.
|
|
129
|
+
|
|
130
|
+
File inputs also use a browser-owned chooser: click folders or enter a folder
|
|
131
|
+
path, select files, then Open (or Cancel/Escape). Multiple selection and
|
|
132
|
+
asynchronous `FileReader` are supported. Only selected file contents and names
|
|
133
|
+
reach the page, not their host paths. The current chooser accepts at most
|
|
134
|
+
32 MiB per selection; it does not yet filter the listing by `accept`.
|
|
135
|
+
|
|
110
136
|
### Websites with self-signed certificates
|
|
111
137
|
|
|
112
138
|
To browse a website with an untrusted self-signed HTTPS certificate, set
|
|
@@ -359,6 +385,36 @@ Documents load in stages:
|
|
|
359
385
|
are reported over CDP when the page reaches them. Every visible change
|
|
360
386
|
pushes a screencast frame.
|
|
361
387
|
|
|
388
|
+
### Experimental CPU WebGL
|
|
389
|
+
|
|
390
|
+
`canvas.getContext('webgl')` has a software-only triangle rendering subset for
|
|
391
|
+
basic Three.js scenes. It uses the bundled CanvasKit shader compiler, with a
|
|
392
|
+
bounded numeric JavaScript shader path for supported expressions. Unsupported
|
|
393
|
+
numeric syntax keeps the CanvasKit path. It does not require a GPU or a new
|
|
394
|
+
native dependency. Depth testing, perspective varyings and basic 2D image/canvas
|
|
395
|
+
textures are supported; this is not a conformant or complete WebGL implementation
|
|
396
|
+
(framebuffers, cube maps, mipmaps and general blending remain incomplete).
|
|
397
|
+
|
|
398
|
+
The renderer defers unobserved draws, caches exact geometry/shading results while
|
|
399
|
+
reapplying current depth, and skips polygon clipping for meshes wholly inside
|
|
400
|
+
the view. CSS rotation, canvas-local pointer coordinates, HTML drag events and
|
|
401
|
+
detached image loading use the ordinary browser paths rather than Three.js
|
|
402
|
+
special cases.
|
|
403
|
+
|
|
404
|
+
The interactive reference is the [Three.js stereo scene](https://drjohnjstest.blogspot.com/2023/11/3dsbsthreejsdemo.html).
|
|
405
|
+
Run `bun test/manual-threejs-scene.js` for its buttons, prompt, orientation and
|
|
406
|
+
drag checks. Pass a folder containing `floor.jpg` and `wall.jpg` to also exercise
|
|
407
|
+
the file chooser and room textures; optional `小凱L.png` / `小凱R.png` exercise
|
|
408
|
+
stereo-image import. A second argument sets the output directory. This network
|
|
409
|
+
check is separate from `bun test`; Copy was also checked through the existing
|
|
410
|
+
browser clipboard callback without replacing the user's clipboard.
|
|
411
|
+
|
|
412
|
+
For completed-frame timings, run
|
|
413
|
+
`bun test/manual-webgl-benchmark.js buninu /path/to/assets`; replace `buninu`
|
|
414
|
+
with `chromium` for a reference (select a software Vulkan ICD when comparing CPU
|
|
415
|
+
rendering). It records both canvas sizes, frame times and a screenshot. These
|
|
416
|
+
timings exclude CDP screenshot encoding and terminal presentation.
|
|
417
|
+
|
|
362
418
|
### Mobile presentation
|
|
363
419
|
|
|
364
420
|
Pages see a desktop browser unless `--mobile` is given. With it, requests
|
|
@@ -438,6 +494,40 @@ additional unreported downstream latency, such as Bluetooth output latency.
|
|
|
438
494
|
With `BUNINU_LOG`, `media sync` records reported audio progress, the latest
|
|
439
495
|
rendered video position, and the output queue/latency for diagnosis.
|
|
440
496
|
|
|
497
|
+
### Web Audio synthesis and sample playback
|
|
498
|
+
|
|
499
|
+
The initial [Web Audio](https://www.w3.org/TR/webaudio-1.1/) subset provides
|
|
500
|
+
`AudioContext` (`webkitAudioContext` is an alias), `OscillatorNode`, `GainNode`
|
|
501
|
+
and `AudioParam` scheduling. Sine, square, sawtooth and triangle oscillators
|
|
502
|
+
are synthesized in JavaScript in 128-sample blocks. Connected voices share
|
|
503
|
+
one stereo PCM stream per context through the existing audio output above;
|
|
504
|
+
no GPU or additional native library is needed. Suspend, close and navigation
|
|
505
|
+
release the context's output. `AudioBuffer`, `createBuffer()`,
|
|
506
|
+
`decodeAudioData()` and `AudioBufferSourceNode` support decoded sample playback,
|
|
507
|
+
offsets, duration, looping, rate/detune and ended events. Compressed audio uses
|
|
508
|
+
the existing WASM decoder and is resampled to the context rate; the current
|
|
509
|
+
software graph downmixes buffer channels before stereo output. Offline contexts,
|
|
510
|
+
filters and AudioWorklets are not implemented yet.
|
|
511
|
+
|
|
512
|
+
[Clumsy Bird](https://ellisonleao.github.io/clumsy-bird/) is an interactive
|
|
513
|
+
reference for sample playback, Canvas image patterns, detached image dimensions
|
|
514
|
+
and page-local primitive prototype extensions. Start with a click or Space;
|
|
515
|
+
Space also makes the bird fly. The page remains under SES and the renderer's
|
|
516
|
+
seccomp policy; shared host prototypes are not unfrozen. Prototype extension
|
|
517
|
+
methods belong to the page and are discarded on navigation. The live check
|
|
518
|
+
`bun test/manual-clumsy-bird.js buninu /tmp/clumsy-bird` records menu and gameplay
|
|
519
|
+
screenshots and verifies pointer/Space input with the ordinary headless backend.
|
|
520
|
+
|
|
521
|
+
The interactive reference is the [text-to-numbered musical notation page](https://drjohnjstest.blogspot.com/2023/10/text-to-numbered-musical-notation.html).
|
|
522
|
+
Its Play button and two-part playback use changing oscillator frequencies.
|
|
523
|
+
To run the live reference test, including a screenshot:
|
|
524
|
+
|
|
525
|
+
```sh
|
|
526
|
+
bun test/manual-numbered-music.js buninu /tmp/numbered-music
|
|
527
|
+
# Optional comparison; requires /usr/local/bin/chromium-headless-shell.
|
|
528
|
+
bun test/manual-numbered-music.js chromium /tmp/numbered-music-chromium
|
|
529
|
+
```
|
|
530
|
+
|
|
441
531
|
### Render scheduling
|
|
442
532
|
|
|
443
533
|
Set `BUNINU_RENDER_MIN_INTERVAL` to adjust the minimum delay before coalesced
|
package/buninu-browser.js
CHANGED
|
@@ -50,7 +50,7 @@ Options:
|
|
|
50
50
|
--chromium Use the previous desktop Chromium compatibility identity
|
|
51
51
|
Last desktop identity flag wins; --mobile takes priority
|
|
52
52
|
--screenshot[=FILE] Capture one PNG and exit (default screenshot.png)
|
|
53
|
-
--window-size=WIDTH,HEIGHT
|
|
53
|
+
--window-size=WIDTH,HEIGHT Initial viewport size in CSS pixels (default 800,600)
|
|
54
54
|
--screenshot-wait=MILLISECONDS Wait after page load before capturing
|
|
55
55
|
--version, -V Print the version
|
|
56
56
|
--help, -h Print this help
|
|
@@ -8,8 +8,8 @@ bun index.js --wayland https://example.com --window-size=800,600
|
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
Uses the existing browser controller and renderer. No Qt, libwayland, native addon,
|
|
11
|
-
new dependency, PNG encoding, or base64.
|
|
12
|
-
|
|
11
|
+
new dependency, PNG encoding, or base64. Linux/Android 64-bit uses libc for
|
|
12
|
+
SCM_RIGHTS ancillary file descriptors. Windows uses the named mapping transport below.
|
|
13
13
|
The default launcher remains Casty. External integration blocks in index.js are
|
|
14
14
|
marked `wayland handle`; the frontend lives entirely in this directory.
|
|
15
15
|
|
|
@@ -150,3 +150,36 @@ The page includes vertical/horizontal/two-axis scroll boxes, button and double-c
|
|
|
150
150
|
counters, a drag canvas, text controls and bounded event logs. It reports received
|
|
151
151
|
modifiers without swapping wheel axes itself. 頁面不自行轉換滾輪方向,可從紀錄
|
|
152
152
|
確認外層是否真的送來水平軸與修飾鍵;預設 Casty 路徑可省略 `--wayland`。
|
|
153
|
+
|
|
154
|
+
## Windows named mappings / Windows 具名共享記憶體 (unstable)
|
|
155
|
+
|
|
156
|
+
**unstable.** Experimental Windows-to-Windows transport; native Windows verification is still
|
|
157
|
+
required. Start Land with `bun index.js --win32` in the Land directory, then run
|
|
158
|
+
`bun index.js --wayland URL` here with `WAYLAND_DISPLAY=wayland-buninu` (or the
|
|
159
|
+
socket name printed by Land). Both processes must use the same Windows session
|
|
160
|
+
and runtime directory: `XDG_RUNTIME_DIR`, then `TMPDIR`, then system temp.
|
|
161
|
+
For example, in PowerShell before starting each process:
|
|
162
|
+
|
|
163
|
+
```powershell
|
|
164
|
+
$env:XDG_RUNTIME_DIR = $env:TEMP
|
|
165
|
+
$env:WAYLAND_DISPLAY = "wayland-buninu"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Only `process.platform === 'win32'` imports the Windows backend and loads
|
|
169
|
+
`kernel32.dll`. `CreateFileMappingW` / `OpenFileMappingW` / `MapViewOfFile` provide
|
|
170
|
+
pagefile-backed `Local\buninu-wayland-UUID` resources. No per-frame pixel file,
|
|
171
|
+
PNG, base64 or pixel socket upload is used. Wayland bytes and FD argument order
|
|
172
|
+
stay unchanged inside a versioned socket envelope carrying mapping descriptors.
|
|
173
|
+
An ACK retains the sender's mapping handle until the receiver has opened it.
|
|
174
|
+
Existing commit/release synchronization protects reusable pixel buffers. Keymaps
|
|
175
|
+
use byte mappings; bounded clipboard pipes use mappings with an atomic length
|
|
176
|
+
and EOF header. Disconnection releases pending handles and queued resources.
|
|
177
|
+
This envelope is private to these two Windows implementations; it is not the
|
|
178
|
+
standard SCM_RIGHTS wire transport. Linux/Android continue to use that original
|
|
179
|
+
transport, and Windows does not advertise DMA-BUF support.
|
|
180
|
+
|
|
181
|
+
Windows 驗證:在本專案執行 `bun test test/wayland-win32-resources.test.js`,
|
|
182
|
+
測試跨程序共享像素、剪貼板 pipe 的 EOF,以及原本 socket 路徑上的具名資源傳遞。
|
|
183
|
+
其他平台會略過這些原生測試;封裝、分段收送、ACK 和清理測試可在各平台執行:
|
|
184
|
+
`bun test test/wayland-win32-wire.test.js`。PowerShell 剪貼板沿用原有 jsmdcui
|
|
185
|
+
函式,仍優先嘗試 xclip,失敗才 fallback。
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {join,isAbsolute} from 'node:path';
|
|
2
2
|
import {tmpdir} from 'node:os';
|
|
3
|
-
import {openSync,
|
|
4
|
-
import {
|
|
3
|
+
import {openSync,ftruncateSync,unlinkSync} from 'node:fs';
|
|
4
|
+
import {closeSync,writeSync} from './resource-io.js';
|
|
5
|
+
import {connectUnix,libc} from './transport.js';
|
|
5
6
|
import {interfaces,decode,encodeRequest} from './protocol.js';
|
|
6
7
|
|
|
7
8
|
export class WaylandClient {
|
|
8
9
|
constructor(path) {
|
|
9
10
|
const display=process.env.WAYLAND_DISPLAY || 'wayland-0';
|
|
10
|
-
this.connection=connectUnix(path || (isAbsolute(display)?display:join(process.env.XDG_RUNTIME_DIR||tmpdir(),display)));
|
|
11
|
+
this.connection=connectUnix(path || (isAbsolute(display)?display:join(process.env.XDG_RUNTIME_DIR||process.env.TMPDIR||tmpdir(),display)));
|
|
11
12
|
this.objects=new Map();this.nextId=2;this.globals=new Map();this.waiters=new Set();
|
|
12
13
|
this.objects.set(1,{iface:'wl_display',event:(name,args)=>{
|
|
13
14
|
if(name==='error')throw Error(`Wayland error ${args.join(': ')}`);
|
|
@@ -70,9 +71,10 @@ export class WaylandWindow {
|
|
|
70
71
|
for(const b of [...this.buffers])if(!b.busy&&(b.width!==width||b.height!==height))this.disposeBuffer(b);
|
|
71
72
|
let b=this.buffers.find(b=>!b.busy);
|
|
72
73
|
if(!b){
|
|
73
|
-
const path=join(process.env.XDG_RUNTIME_DIR||tmpdir(),`buninu-shm-${process.pid}-${crypto.randomUUID()}`);
|
|
74
|
-
const fd=openSync(path,'wx+',0o600);
|
|
75
|
-
|
|
74
|
+
const path=join(process.env.XDG_RUNTIME_DIR||process.env.TMPDIR||tmpdir(),`buninu-shm-${process.pid}-${crypto.randomUUID()}`);
|
|
75
|
+
const fd=process.platform==='win32'?libc.createMapping(size):openSync(path,'wx+',0o600);
|
|
76
|
+
if(process.platform!=='win32')unlinkSync(path);
|
|
77
|
+
try{if(process.platform!=='win32')ftruncateSync(fd,size);
|
|
76
78
|
b={fd,width,height,busy:false};const pool=c.object('wl_shm_pool');c.request(c.shm,'create_pool',pool,fd,size);
|
|
77
79
|
b.id=c.object('wl_buffer',(event)=>{if(event==='release'){b.busy=false;this.onReady?.();}});
|
|
78
80
|
c.request(pool,'create_buffer',b.id,0,width,height,width*4,1);c.request(pool,'destroy');this.buffers.push(b);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A standard wl_data_device text client. Only explicit copy/paste transfers bytes.
|
|
2
|
-
import {readSync, writeSync} from '
|
|
2
|
+
import {readSync, writeSync} from './resource-io.js';
|
|
3
3
|
import {libc} from './transport.js';
|
|
4
4
|
const mimeTypes=['text/plain;charset=utf-8','text/plain','UTF8_STRING','STRING'];
|
|
5
5
|
const limit=1024*1024;
|
|
@@ -121,7 +121,7 @@ export async function launch({url='about:blank',width=800,height=600,socket,chro
|
|
|
121
121
|
}
|
|
122
122
|
if(import.meta.main){
|
|
123
123
|
const args=Bun.argv.slice(2).filter(a=>a!=='--');
|
|
124
|
-
if(args.some(a=>['-h','--help'].includes(a))){console.log('Usage: buninu-browser --wayland [URL or file] [--window-size=800,600]\nUses WAYLAND_DISPLAY
|
|
124
|
+
if(args.some(a=>['-h','--help'].includes(a))){console.log('Usage: buninu-browser --wayland [URL or file] [--window-size=800,600]\nUses WAYLAND_DISPLAY; socket directory: XDG_RUNTIME_DIR, TMPDIR, system temp. Experimental wl_shm frontend.');process.exit(0);}
|
|
125
125
|
let width=800,height=600,url='about:blank';
|
|
126
126
|
for(const arg of args){if(arg.startsWith('--window-size=')){[width,height]=arg.slice(14).split(',').map(Number);}else if(arg.startsWith('-'))throw Error(`Unknown Wayland option: ${arg}`);else url=arg;}
|
|
127
127
|
if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width>8192||height>8192)throw Error('Invalid window size');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Native fs descriptors on POSIX; named-mapping resource IDs on Windows.
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
const mapping = process.platform === 'win32' ? await import('./win32-resources.js') : null;
|
|
4
|
+
export const readSync = mapping?.readResource ?? fs.readSync;
|
|
5
|
+
export const writeSync = mapping?.writeResource ?? fs.writeSync;
|
|
6
|
+
export const closeSync = mapping?.closeResource ?? fs.closeSync;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Adapted from buninu-land/lib/transport.js (MIT; LICENSE-land).
|
|
2
|
+
// Linux 64-bit ABI. Only libc is loaded. recvmsg is required for SCM_RIGHTS;
|
|
3
|
+
// Bun's stream sockets do not expose ancillary file descriptors.
|
|
4
|
+
import { dlopen, ptr } from 'bun:ffi';
|
|
5
|
+
|
|
6
|
+
if (!['arm64', 'x64'].includes(process.arch) || !['linux', 'android'].includes(process.platform)) throw Error('Requires 64-bit Linux or Android');
|
|
7
|
+
const signature = (args, returns = 'i32') => ({ args, returns });
|
|
8
|
+
let library;
|
|
9
|
+
let errnoSymbol;
|
|
10
|
+
// Same architecture-specific libc names as buninu-linux's /lib/dlopen.js.
|
|
11
|
+
const muslLibc = `libc.musl-${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}.so.1`;
|
|
12
|
+
const candidates = process.platform === 'android'
|
|
13
|
+
? [['libc.so', '__errno']]
|
|
14
|
+
: [
|
|
15
|
+
['libc.so.6', '__errno_location'],
|
|
16
|
+
[muslLibc, '__errno_location'],
|
|
17
|
+
[`/lib/${muslLibc}`, '__errno_location'],
|
|
18
|
+
['libc.so', '__errno_location'],
|
|
19
|
+
['libc.so', '__errno'],
|
|
20
|
+
];
|
|
21
|
+
const failures = [];
|
|
22
|
+
for (const [name, symbol] of candidates) {
|
|
23
|
+
try { library = dlopen(name, {
|
|
24
|
+
connect: signature(['i32','ptr','u32']), socket: signature(['i32','i32','i32']),
|
|
25
|
+
recvmsg: signature(['i32','ptr','i32'], 'i64'), sendmsg: signature(['i32','ptr','i32'], 'i64'),
|
|
26
|
+
dup: signature(['i32']), send: signature(['i32','ptr','u64','i32'], 'i64'), close: signature(['i32']),
|
|
27
|
+
|
|
28
|
+
// Standard Wayland clipboard FD transfer.
|
|
29
|
+
pipe2: signature(['ptr','i32']), fcntl: signature(['i32','i32','i32']),
|
|
30
|
+
|
|
31
|
+
[symbol]: signature([], 'ptr'),
|
|
32
|
+
}); errnoSymbol = symbol; break; }
|
|
33
|
+
catch (error) { failures.push(`${name} (${symbol}): ${error.message}`); }
|
|
34
|
+
}
|
|
35
|
+
if (!library) throw Error(`Cannot load libc on ${process.platform}:\n${failures.join('\n')}`);
|
|
36
|
+
export const libc = library.symbols;
|
|
37
|
+
import { toArrayBuffer } from 'bun:ffi';
|
|
38
|
+
export const errno = () => new Int32Array(toArrayBuffer(libc[errnoSymbol](), 0, 4))[0];
|
|
39
|
+
const retry = () => [4,11].includes(errno());
|
|
40
|
+
export class Connection {
|
|
41
|
+
constructor(fd, removed) {
|
|
42
|
+
this.fd = fd; this.removed = removed; this.fds = []; this.input = Buffer.alloc(0); this.output = []; this.queuedBytes = 0;
|
|
43
|
+
this.data = Buffer.alloc(65536); this.control = Buffer.alloc(4096); this.iov = Buffer.alloc(16); this.msg = Buffer.alloc(56);
|
|
44
|
+
this.iov.writeBigUInt64LE(BigInt(ptr(this.data))); this.iov.writeBigUInt64LE(BigInt(this.data.length), 8);
|
|
45
|
+
this.msg.writeBigUInt64LE(BigInt(ptr(this.iov)), 16); this.msg.writeBigUInt64LE(1n, 24);
|
|
46
|
+
this.msg.writeBigUInt64LE(BigInt(ptr(this.control)), 32);
|
|
47
|
+
}
|
|
48
|
+
send(bytes, fds = []) {
|
|
49
|
+
if (this.closed) return;
|
|
50
|
+
const owned = [];
|
|
51
|
+
try { for (const fd of fds) { const copy = libc.dup(fd); if (copy < 0) throw Error('Cannot duplicate outgoing FD'); owned.push(copy); } }
|
|
52
|
+
catch (e) { for (const fd of owned) libc.close(fd); throw e; }
|
|
53
|
+
this.output.push({ bytes, fds: owned }); this.queuedBytes += bytes.length;
|
|
54
|
+
if (this.queuedBytes > 4*1024*1024) this.close();
|
|
55
|
+
}
|
|
56
|
+
flush() {
|
|
57
|
+
for (let round = 0; round < 64 && this.output.length; round++) {
|
|
58
|
+
const item = this.output[0];
|
|
59
|
+
let n;
|
|
60
|
+
if (item.fds.length) {
|
|
61
|
+
const iov = Buffer.alloc(16), msg = Buffer.alloc(56);
|
|
62
|
+
const size = 16 + item.fds.length * 4, control = Buffer.alloc(Math.ceil(size / 8) * 8);
|
|
63
|
+
iov.writeBigUInt64LE(BigInt(ptr(item.bytes))); iov.writeBigUInt64LE(BigInt(item.bytes.length), 8);
|
|
64
|
+
control.writeBigUInt64LE(BigInt(size)); control.writeInt32LE(1,8); control.writeInt32LE(1,12);
|
|
65
|
+
item.fds.forEach((fd,i) => control.writeInt32LE(fd,16+i*4));
|
|
66
|
+
msg.writeBigUInt64LE(BigInt(ptr(iov)),16); msg.writeBigUInt64LE(1n,24);
|
|
67
|
+
msg.writeBigUInt64LE(BigInt(ptr(control)),32); msg.writeBigUInt64LE(BigInt(control.length),40);
|
|
68
|
+
n = Number(libc.sendmsg(this.fd,msg,0x4000));
|
|
69
|
+
} else n = Number(libc.send(this.fd,item.bytes,item.bytes.length,0x4000));
|
|
70
|
+
if (n < 0) { if (!retry()) this.close(); return; }
|
|
71
|
+
if (!n) return this.close();
|
|
72
|
+
// SCM_RIGHTS is delivered with the first successfully sent byte, even on partial writes.
|
|
73
|
+
for (const fd of item.fds) libc.close(fd); item.fds = [];
|
|
74
|
+
this.queuedBytes -= n;
|
|
75
|
+
if (n === item.bytes.length) this.output.shift(); else item.bytes = item.bytes.subarray(n);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
poll() {
|
|
79
|
+
if (this.closed) return;
|
|
80
|
+
try {
|
|
81
|
+
for (let round = 0; round < 16; round++) {
|
|
82
|
+
this.msg.writeBigUInt64LE(BigInt(this.control.length), 40); this.msg.writeUInt32LE(0, 48);
|
|
83
|
+
const n = Number(libc.recvmsg(this.fd, this.msg, 0x40000000));
|
|
84
|
+
if (n < 0) { if (retry()) break; return this.close(); }
|
|
85
|
+
if (!n) return this.close();
|
|
86
|
+
const end = Number(this.msg.readBigUInt64LE(40));
|
|
87
|
+
for (let p = 0; p + 16 <= end;) {
|
|
88
|
+
const size = Number(this.control.readBigUInt64LE(p)); if (size < 16 || p + size > end) throw Error('Invalid ancillary data');
|
|
89
|
+
if (this.control.readInt32LE(p+8) === 1 && this.control.readInt32LE(p+12) === 1)
|
|
90
|
+
for (let q = p + 16; q + 4 <= p + size; q += 4) this.fds.push(this.control.readInt32LE(q));
|
|
91
|
+
p += Math.ceil(size / 8) * 8;
|
|
92
|
+
}
|
|
93
|
+
if (this.msg.readUInt32LE(48) & 8) throw Error('Truncated ancillary data');
|
|
94
|
+
this.input = Buffer.concat([this.input, this.data.subarray(0,n)]);
|
|
95
|
+
if (this.input.length > 1024*1024 || this.fds.length > 256) throw Error('Client queue limit');
|
|
96
|
+
this.onData();
|
|
97
|
+
if (this.closed) return;
|
|
98
|
+
}
|
|
99
|
+
this.flush();
|
|
100
|
+
} catch (e) { console.error(`Wayland client: ${e.message}`);
|
|
101
|
+
// Flush queued requests before disconnecting.
|
|
102
|
+
this.flush();
|
|
103
|
+
this.close(); }
|
|
104
|
+
}
|
|
105
|
+
close() {
|
|
106
|
+
if (this.closed) return; this.closed = true; libc.close(this.fd);
|
|
107
|
+
for (const item of this.output) for (const fd of item.fds) libc.close(fd);
|
|
108
|
+
this.output = []; this.queuedBytes = 0;
|
|
109
|
+
for (const fd of this.fds) libc.close(fd); this.fds = []; this.onClose?.(); this.removed();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function connectUnix(path) {
|
|
114
|
+
if (Buffer.byteLength(path)>107 || path.includes('\0')) throw Error('Invalid Wayland socket path');
|
|
115
|
+
const fd=libc.socket(1,1|0x800|0x80000,0);
|
|
116
|
+
if(fd<0)throw Error(`socket: errno ${errno()}`);
|
|
117
|
+
const address=Buffer.alloc(110); address.writeUInt16LE(1);address.write(path,2);
|
|
118
|
+
if(libc.connect(fd,address,Buffer.byteLength(path)+3)<0){const code=errno();libc.close(fd);throw Error(`connect ${path}: errno ${code}`);}
|
|
119
|
+
return new Connection(fd,()=>{});
|
|
120
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as resources from './win32-resources.js';
|
|
2
|
+
import {MappingConnection} from './win32-wire.js';
|
|
3
|
+
export const {libc,errno}=resources;
|
|
4
|
+
export const Connection=MappingConnection;
|
|
5
|
+
export function connectUnix(path){
|
|
6
|
+
const c=new MappingConnection(resources);
|
|
7
|
+
Bun.connect({unix:path,socket:{open:s=>c.attach(s),data:(_,b)=>c.receive(b),drain:()=>c.flush(),close:()=>c.close(),error:(_,e)=>{c.error=e;c.close()}}}).catch(e=>{c.error=e;c.close()});
|
|
8
|
+
return c;
|
|
9
|
+
}
|
|
@@ -1,120 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import { dlopen, ptr } from 'bun:ffi';
|
|
5
|
-
|
|
6
|
-
if (!['arm64', 'x64'].includes(process.arch) || !['linux', 'android'].includes(process.platform)) throw Error('Requires 64-bit Linux or Android');
|
|
7
|
-
const signature = (args, returns = 'i32') => ({ args, returns });
|
|
8
|
-
let library;
|
|
9
|
-
let errnoSymbol;
|
|
10
|
-
// Same architecture-specific libc names as buninu-linux's /lib/dlopen.js.
|
|
11
|
-
const muslLibc = `libc.musl-${process.arch === 'arm64' ? 'aarch64' : 'x86_64'}.so.1`;
|
|
12
|
-
const candidates = process.platform === 'android'
|
|
13
|
-
? [['libc.so', '__errno']]
|
|
14
|
-
: [
|
|
15
|
-
['libc.so.6', '__errno_location'],
|
|
16
|
-
[muslLibc, '__errno_location'],
|
|
17
|
-
[`/lib/${muslLibc}`, '__errno_location'],
|
|
18
|
-
['libc.so', '__errno_location'],
|
|
19
|
-
['libc.so', '__errno'],
|
|
20
|
-
];
|
|
21
|
-
const failures = [];
|
|
22
|
-
for (const [name, symbol] of candidates) {
|
|
23
|
-
try { library = dlopen(name, {
|
|
24
|
-
connect: signature(['i32','ptr','u32']), socket: signature(['i32','i32','i32']),
|
|
25
|
-
recvmsg: signature(['i32','ptr','i32'], 'i64'), sendmsg: signature(['i32','ptr','i32'], 'i64'),
|
|
26
|
-
dup: signature(['i32']), send: signature(['i32','ptr','u64','i32'], 'i64'), close: signature(['i32']),
|
|
27
|
-
|
|
28
|
-
// Standard Wayland clipboard FD transfer.
|
|
29
|
-
pipe2: signature(['ptr','i32']), fcntl: signature(['i32','i32','i32']),
|
|
30
|
-
|
|
31
|
-
[symbol]: signature([], 'ptr'),
|
|
32
|
-
}); errnoSymbol = symbol; break; }
|
|
33
|
-
catch (error) { failures.push(`${name} (${symbol}): ${error.message}`); }
|
|
34
|
-
}
|
|
35
|
-
if (!library) throw Error(`Cannot load libc on ${process.platform}:\n${failures.join('\n')}`);
|
|
36
|
-
export const libc = library.symbols;
|
|
37
|
-
import { toArrayBuffer } from 'bun:ffi';
|
|
38
|
-
export const errno = () => new Int32Array(toArrayBuffer(libc[errnoSymbol](), 0, 4))[0];
|
|
39
|
-
const retry = () => [4,11].includes(errno());
|
|
40
|
-
export class Connection {
|
|
41
|
-
constructor(fd, removed) {
|
|
42
|
-
this.fd = fd; this.removed = removed; this.fds = []; this.input = Buffer.alloc(0); this.output = []; this.queuedBytes = 0;
|
|
43
|
-
this.data = Buffer.alloc(65536); this.control = Buffer.alloc(4096); this.iov = Buffer.alloc(16); this.msg = Buffer.alloc(56);
|
|
44
|
-
this.iov.writeBigUInt64LE(BigInt(ptr(this.data))); this.iov.writeBigUInt64LE(BigInt(this.data.length), 8);
|
|
45
|
-
this.msg.writeBigUInt64LE(BigInt(ptr(this.iov)), 16); this.msg.writeBigUInt64LE(1n, 24);
|
|
46
|
-
this.msg.writeBigUInt64LE(BigInt(ptr(this.control)), 32);
|
|
47
|
-
}
|
|
48
|
-
send(bytes, fds = []) {
|
|
49
|
-
if (this.closed) return;
|
|
50
|
-
const owned = [];
|
|
51
|
-
try { for (const fd of fds) { const copy = libc.dup(fd); if (copy < 0) throw Error('Cannot duplicate outgoing FD'); owned.push(copy); } }
|
|
52
|
-
catch (e) { for (const fd of owned) libc.close(fd); throw e; }
|
|
53
|
-
this.output.push({ bytes, fds: owned }); this.queuedBytes += bytes.length;
|
|
54
|
-
if (this.queuedBytes > 4*1024*1024) this.close();
|
|
55
|
-
}
|
|
56
|
-
flush() {
|
|
57
|
-
for (let round = 0; round < 64 && this.output.length; round++) {
|
|
58
|
-
const item = this.output[0];
|
|
59
|
-
let n;
|
|
60
|
-
if (item.fds.length) {
|
|
61
|
-
const iov = Buffer.alloc(16), msg = Buffer.alloc(56);
|
|
62
|
-
const size = 16 + item.fds.length * 4, control = Buffer.alloc(Math.ceil(size / 8) * 8);
|
|
63
|
-
iov.writeBigUInt64LE(BigInt(ptr(item.bytes))); iov.writeBigUInt64LE(BigInt(item.bytes.length), 8);
|
|
64
|
-
control.writeBigUInt64LE(BigInt(size)); control.writeInt32LE(1,8); control.writeInt32LE(1,12);
|
|
65
|
-
item.fds.forEach((fd,i) => control.writeInt32LE(fd,16+i*4));
|
|
66
|
-
msg.writeBigUInt64LE(BigInt(ptr(iov)),16); msg.writeBigUInt64LE(1n,24);
|
|
67
|
-
msg.writeBigUInt64LE(BigInt(ptr(control)),32); msg.writeBigUInt64LE(BigInt(control.length),40);
|
|
68
|
-
n = Number(libc.sendmsg(this.fd,msg,0x4000));
|
|
69
|
-
} else n = Number(libc.send(this.fd,item.bytes,item.bytes.length,0x4000));
|
|
70
|
-
if (n < 0) { if (!retry()) this.close(); return; }
|
|
71
|
-
if (!n) return this.close();
|
|
72
|
-
// SCM_RIGHTS is delivered with the first successfully sent byte, even on partial writes.
|
|
73
|
-
for (const fd of item.fds) libc.close(fd); item.fds = [];
|
|
74
|
-
this.queuedBytes -= n;
|
|
75
|
-
if (n === item.bytes.length) this.output.shift(); else item.bytes = item.bytes.subarray(n);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
poll() {
|
|
79
|
-
if (this.closed) return;
|
|
80
|
-
try {
|
|
81
|
-
for (let round = 0; round < 16; round++) {
|
|
82
|
-
this.msg.writeBigUInt64LE(BigInt(this.control.length), 40); this.msg.writeUInt32LE(0, 48);
|
|
83
|
-
const n = Number(libc.recvmsg(this.fd, this.msg, 0x40000000));
|
|
84
|
-
if (n < 0) { if (retry()) break; return this.close(); }
|
|
85
|
-
if (!n) return this.close();
|
|
86
|
-
const end = Number(this.msg.readBigUInt64LE(40));
|
|
87
|
-
for (let p = 0; p + 16 <= end;) {
|
|
88
|
-
const size = Number(this.control.readBigUInt64LE(p)); if (size < 16 || p + size > end) throw Error('Invalid ancillary data');
|
|
89
|
-
if (this.control.readInt32LE(p+8) === 1 && this.control.readInt32LE(p+12) === 1)
|
|
90
|
-
for (let q = p + 16; q + 4 <= p + size; q += 4) this.fds.push(this.control.readInt32LE(q));
|
|
91
|
-
p += Math.ceil(size / 8) * 8;
|
|
92
|
-
}
|
|
93
|
-
if (this.msg.readUInt32LE(48) & 8) throw Error('Truncated ancillary data');
|
|
94
|
-
this.input = Buffer.concat([this.input, this.data.subarray(0,n)]);
|
|
95
|
-
if (this.input.length > 1024*1024 || this.fds.length > 256) throw Error('Client queue limit');
|
|
96
|
-
this.onData();
|
|
97
|
-
if (this.closed) return;
|
|
98
|
-
}
|
|
99
|
-
this.flush();
|
|
100
|
-
} catch (e) { console.error(`Wayland client: ${e.message}`);
|
|
101
|
-
// Flush queued requests before disconnecting.
|
|
102
|
-
this.flush();
|
|
103
|
-
this.close(); }
|
|
104
|
-
}
|
|
105
|
-
close() {
|
|
106
|
-
if (this.closed) return; this.closed = true; libc.close(this.fd);
|
|
107
|
-
for (const item of this.output) for (const fd of item.fds) libc.close(fd);
|
|
108
|
-
this.output = []; this.queuedBytes = 0;
|
|
109
|
-
for (const fd of this.fds) libc.close(fd); this.fds = []; this.onClose?.(); this.removed();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function connectUnix(path) {
|
|
114
|
-
if (Buffer.byteLength(path)>107 || path.includes('\0')) throw Error('Invalid Wayland socket path');
|
|
115
|
-
const fd=libc.socket(1,1|0x800|0x80000,0);
|
|
116
|
-
if(fd<0)throw Error(`socket: errno ${errno()}`);
|
|
117
|
-
const address=Buffer.alloc(110); address.writeUInt16LE(1);address.write(path,2);
|
|
118
|
-
if(libc.connect(fd,address,Buffer.byteLength(path)+3)<0){const code=errno();libc.close(fd);throw Error(`connect ${path}: errno ${code}`);}
|
|
119
|
-
return new Connection(fd,()=>{});
|
|
120
|
-
}
|
|
1
|
+
// Platform selection must precede loading any native library.
|
|
2
|
+
const backend = await import(process.platform === 'win32' ? './transport-win32.js' : './transport-posix.js');
|
|
3
|
+
export const {libc,errno,Connection,connectUnix} = backend;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Windows-only named pagefile mappings. No DLL is loaded on other platforms.
|
|
2
|
+
import {dlopen,toArrayBuffer} from 'bun:ffi';
|
|
3
|
+
import {fstatSync,readSync,writeSync,closeSync} from 'node:fs';
|
|
4
|
+
if(process.platform!=='win32')throw Error('Named mappings require Windows');
|
|
5
|
+
const sig=(args,returns)=>({args,returns});
|
|
6
|
+
const {symbols:k}=dlopen('kernel32.dll',{
|
|
7
|
+
CreateFileMappingW:sig(['i64','ptr','u32','u32','u32','ptr'],'ptr'),
|
|
8
|
+
OpenFileMappingW:sig(['u32','i32','ptr'],'ptr'),
|
|
9
|
+
MapViewOfFile:sig(['ptr','u32','u32','u32','u64'],'ptr'),
|
|
10
|
+
UnmapViewOfFile:sig(['ptr'],'i32'),CloseHandle:sig(['ptr'],'i32'),GetLastError:sig([],'u32'),
|
|
11
|
+
});
|
|
12
|
+
const entries=new Map();let next=-1;
|
|
13
|
+
const MAX=256*1024*1024;
|
|
14
|
+
const wide=s=>Buffer.from(s+'\0','utf16le');
|
|
15
|
+
const fail=what=>Error(`${what}: Win32 error ${k.GetLastError()}`);
|
|
16
|
+
function mapped(name,size,kind,create){
|
|
17
|
+
if(!/^Local\\buninu-wayland-[\da-f-]{36}$/.test(name)||!Number.isInteger(size)||size<1||size>MAX)throw Error('Invalid named mapping');
|
|
18
|
+
const handle=create?k.CreateFileMappingW(-1n,null,4,0,size,wide(name)):k.OpenFileMappingW(0xf001f,0,wide(name));
|
|
19
|
+
if(!handle)throw fail('File mapping');
|
|
20
|
+
if(create&&k.GetLastError()===183){k.CloseHandle(handle);throw Error('Mapping name collision')}
|
|
21
|
+
const address=k.MapViewOfFile(handle,0xf001f,0,0,size);
|
|
22
|
+
if(!address){const e=fail('MapViewOfFile');k.CloseHandle(handle);throw e}
|
|
23
|
+
return {name,size,kind,handle,address,bytes:Buffer.from(toArrayBuffer(address,0,size)),refs:0,forwarded:false};
|
|
24
|
+
}
|
|
25
|
+
function add(entry,writer=false){entry.refs++;const fd=next--;entries.set(fd,{entry,writer});return fd}
|
|
26
|
+
export function createMapping(size,kind='pixels'){
|
|
27
|
+
const e=mapped('Local\\buninu-wayland-'+crypto.randomUUID(),size,kind,true);return add(e,kind==='pipe');
|
|
28
|
+
}
|
|
29
|
+
export function closeResource(fd){
|
|
30
|
+
if(fd>=0){closeSync(fd);return 0}
|
|
31
|
+
const item=entries.get(fd);if(!item)return 0;entries.delete(fd);
|
|
32
|
+
const e=item.entry;
|
|
33
|
+
if(item.writer&&!item.forwarded)Atomics.store(new Int32Array(e.bytes.buffer,e.bytes.byteOffset,2),1,1);
|
|
34
|
+
if(--e.refs===0){k.UnmapViewOfFile(e.address);k.CloseHandle(e.handle);e.bytes=null}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
export function sendResource(fd){
|
|
38
|
+
let item=entries.get(fd),temporary;
|
|
39
|
+
if(!item){const size=fstatSync(fd).size;if(size>MAX)throw Error('Resource exceeds limit');const data=Buffer.alloc(size);let at=0;while(at<size){const n=readSync(fd,data,at,size-at,at);if(!n)throw Error('Short resource file');at+=n}temporary=createMapping(Math.max(1,data.length),'bytes');item=entries.get(temporary);data.copy(item.entry.bytes)}
|
|
40
|
+
const retained=add(item.entry,false);
|
|
41
|
+
if(item.writer)item.forwarded=true;
|
|
42
|
+
const descriptor={name:item.entry.name,size:item.entry.size,kind:item.entry.kind,writer:item.writer};
|
|
43
|
+
if(temporary!==undefined)closeResource(temporary);
|
|
44
|
+
return {descriptor,release:()=>closeResource(retained)};
|
|
45
|
+
}
|
|
46
|
+
export function receiveResource(d){
|
|
47
|
+
if(!d||!['pixels','bytes','pipe'].includes(d.kind)||typeof d.writer!=='boolean'||(d.writer&&d.kind!=='pipe')||(d.kind==='pipe'&&d.size<9))throw Error('Invalid resource descriptor');
|
|
48
|
+
return add(mapped(d.name,d.size,d.kind,false),d.writer);
|
|
49
|
+
}
|
|
50
|
+
export function readResource(fd,buffer,offset=0,length=buffer.length-offset,position=null){
|
|
51
|
+
if(fd>=0)return readSync(fd,buffer,offset,length,position);
|
|
52
|
+
const item=entries.get(fd);if(!item)throw Error('Closed mapping');const e=item.entry;
|
|
53
|
+
if(e.kind==='pipe'){
|
|
54
|
+
const header=new Int32Array(e.bytes.buffer,e.bytes.byteOffset,2),available=Atomics.load(header,0),start=item.position??0;
|
|
55
|
+
if(available<start||available>e.size-8)throw Error('Invalid pipe length');
|
|
56
|
+
const n=Math.min(length,available-start);
|
|
57
|
+
if(!n&&!Atomics.load(header,1))throw Object.assign(Error('Pipe pending'),{code:'EAGAIN'});
|
|
58
|
+
e.bytes.copy(buffer,offset,8+start,8+start+n);item.position=start+n;return n;
|
|
59
|
+
}
|
|
60
|
+
const start=position??item.position??0,n=Math.max(0,Math.min(length,e.size-start));
|
|
61
|
+
e.bytes.copy(buffer,offset,start,start+n);if(position===null)item.position=start+n;return n;
|
|
62
|
+
}
|
|
63
|
+
export function writeResource(fd,buffer,offset=0,length=buffer.length-offset,position=null){
|
|
64
|
+
if(fd>=0)return writeSync(fd,buffer,offset,length,position);
|
|
65
|
+
const item=entries.get(fd);if(!item)throw Error('Closed mapping');const e=item.entry;
|
|
66
|
+
const start=e.kind==='pipe'?Atomics.load(new Int32Array(e.bytes.buffer,e.bytes.byteOffset,2),0):(position??item.position??0),header=e.kind==='pipe'?8:0;
|
|
67
|
+
if(start+length>e.size-header)throw Error('Mapping write exceeds capacity');
|
|
68
|
+
e.bytes.set(buffer.subarray(offset,offset+length),header+start);
|
|
69
|
+
if(header)Atomics.store(new Int32Array(e.bytes.buffer,e.bytes.byteOffset,2),0,start+length);else if(position===null)item.position=start+length;
|
|
70
|
+
return length;
|
|
71
|
+
}
|
|
72
|
+
let lastErrno=0;
|
|
73
|
+
export const libc={
|
|
74
|
+
close:closeResource,createMapping,fcntl:()=>0,
|
|
75
|
+
pipe2(out){const fd=createMapping(1024*1024+8,'pipe'),e=entries.get(fd).entry;out[0]=add(e);out[1]=fd;return 0},
|
|
76
|
+
pread(fd,b,n,p){try{return readResource(fd,b,0,n,Number(p))}catch{lastErrno=5;return -1}},
|
|
77
|
+
read(fd,b,n){try{return readResource(fd,b,0,n)}catch(e){lastErrno=e.code==='EAGAIN'?11:5;return -1}},
|
|
78
|
+
write(fd,b,n){try{return writeResource(fd,b,0,n)}catch{lastErrno=5;return -1}},
|
|
79
|
+
};
|
|
80
|
+
export const errno=()=>lastErrno;
|