buninu-linux 0.3.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSES/Apache-2.0.txt +202 -0
- package/LICENSES/BSD-3-Clause-Skia.txt +29 -0
- package/LICENSES/Bitstream-Vera.txt +44 -0
- package/LICENSES/MIT-xterm.js.txt +21 -0
- package/LICENSES/OFL-1.1.txt +91 -0
- package/NOTICE.md +18 -1
- package/README.md +101 -9
- package/fetch-alpine.sh +6 -0
- package/graphics.md +218 -0
- package/initramfs/bin/bunterm +70 -0
- package/initramfs/bin/chroot +213 -0
- package/initramfs/bin/stripansi +27 -0
- package/initramfs/bin/tar +189 -14
- package/initramfs/buninu/CHANGELOG.md +40 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +4 -3
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +80 -1
- package/initramfs/lib/bunterm/glyphs.js +446 -0
- package/initramfs/lib/bunterm/images.js +225 -0
- package/initramfs/lib/bunterm/input.js +140 -0
- package/initramfs/lib/bunterm/mouse.js +222 -0
- package/initramfs/lib/bunterm/renderer.js +469 -0
- package/initramfs/lib/bunterm/terminal.js +324 -0
- package/initramfs/lib/canvas.js +181 -0
- package/initramfs/lib/canvaskit/LICENSE +29 -0
- package/initramfs/lib/canvaskit/canvaskit.js +264 -0
- package/initramfs/lib/canvaskit/canvaskit.wasm +0 -0
- package/initramfs/lib/dlopen.js +18 -2
- package/initramfs/lib/document.js +5 -1
- package/initramfs/lib/fbdev.js +581 -0
- package/initramfs/lib/xterm/LICENSE +21 -0
- package/initramfs/lib/xterm/addon-unicode-graphemes.mjs +44 -0
- package/initramfs/lib/xterm/xterm-headless.mjs +46 -0
- package/initramfs/usr/share/doc/buninu-linux/bunterm.md +112 -0
- package/initramfs/usr/share/doc/buninu-linux/canvas.md +91 -0
- package/initramfs/usr/share/doc/buninu-linux/chroot.md +105 -0
- package/initramfs/usr/share/doc/buninu-linux/fbdev.md +113 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +30 -6
- package/initramfs/usr/share/fonts/DejaVuSansMono-Bold.ttf +0 -0
- package/initramfs/usr/share/fonts/DejaVuSansMono.ttf +0 -0
- package/initramfs/usr/share/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/initramfs/usr/share/fonts/LICENSE-DejaVu.txt +44 -0
- package/initramfs/usr/share/fonts/LICENSE-OFL-1.1.txt +91 -0
- package/initramfs/usr/share/fonts/NotoColorEmoji.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoColorEmojiFlags.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoSansCJK-Regular.ttc +0 -0
- package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted.ttf +0 -0
- package/initramfs/usr/share/fonts/NotoSansSymbols-Regular-Subsetted2.ttf +0 -0
- package/initramfs/usr/share/fonts/Roboto-Regular.ttf +0 -0
- package/initramfs/usr/share/fonts/fonts.json +28 -0
- package/package.json +2 -2
- package/run-qemu.sh +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// /lib/bunterm/input.js — keyboard bytes from a Linux virtual console.
|
|
2
|
+
//
|
|
3
|
+
// The console is read in raw mode so every key reaches the terminal
|
|
4
|
+
// emulator unchanged (Ctrl-C included; the program in the PTY decides what
|
|
5
|
+
// it means). The kernel keeps translating keys to bytes (K_XLATE), and its
|
|
6
|
+
// "linux" console sequences for a few keys are rewritten into the xterm
|
|
7
|
+
// forms that programs seeing TERM=xterm-256color expect.
|
|
8
|
+
|
|
9
|
+
import { closeSync, openSync, read } from "node:fs";
|
|
10
|
+
import { libc, ptr, errno, strerror } from "../dlopen.js";
|
|
11
|
+
|
|
12
|
+
const TCGETS = 0x5401;
|
|
13
|
+
const TCSETS = 0x5402;
|
|
14
|
+
// struct termios (kernel ABI): 4 x u32 flags, c_line, c_cc[19].
|
|
15
|
+
const TERMIOS_LENGTH = 36;
|
|
16
|
+
const IFLAG = 0;
|
|
17
|
+
const OFLAG = 4;
|
|
18
|
+
const CFLAG = 8;
|
|
19
|
+
const LFLAG = 12;
|
|
20
|
+
const CC = 17;
|
|
21
|
+
const VTIME = 5;
|
|
22
|
+
const VMIN = 6;
|
|
23
|
+
|
|
24
|
+
const IGNBRK = 0o1, BRKINT = 0o2, PARMRK = 0o10, ISTRIP = 0o40, INLCR = 0o100,
|
|
25
|
+
IGNCR = 0o200, ICRNL = 0o400, IXON = 0o2000;
|
|
26
|
+
const OPOST = 0o1;
|
|
27
|
+
const CSIZE = 0o60, CS8 = 0o60, PARENB = 0o400;
|
|
28
|
+
const ISIG = 0o1, ICANON = 0o2, ECHO = 0o10, ECHONL = 0o100, IEXTEN = 0o100000;
|
|
29
|
+
|
|
30
|
+
const fail = (operation) => {
|
|
31
|
+
const code = errno();
|
|
32
|
+
throw new Error(`${operation}: ${strerror(code)}`);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getTermios = (fd) => {
|
|
36
|
+
const buffer = new Uint8Array(TERMIOS_LENGTH);
|
|
37
|
+
if (libc.symbols.ioctl(fd, TCGETS, ptr(buffer)) === -1) fail("TCGETS");
|
|
38
|
+
return buffer;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const setTermios = (fd, buffer) => {
|
|
42
|
+
if (libc.symbols.ioctl(fd, TCSETS, ptr(buffer)) === -1) fail("TCSETS");
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// cfmakeraw() on a copy of the current settings.
|
|
46
|
+
const rawTermios = (current) => {
|
|
47
|
+
const raw = new Uint8Array(current);
|
|
48
|
+
const view = new DataView(raw.buffer);
|
|
49
|
+
const clear = (offset, mask) => view.setUint32(offset, view.getUint32(offset, true) & ~mask, true);
|
|
50
|
+
clear(IFLAG, IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
|
|
51
|
+
clear(OFLAG, OPOST);
|
|
52
|
+
clear(LFLAG, ECHO | ECHONL | ICANON | ISIG | IEXTEN);
|
|
53
|
+
clear(CFLAG, CSIZE | PARENB);
|
|
54
|
+
view.setUint32(CFLAG, view.getUint32(CFLAG, true) | CS8, true);
|
|
55
|
+
raw[CC + VMIN] = 1;
|
|
56
|
+
raw[CC + VTIME] = 0;
|
|
57
|
+
return raw;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Linux console sequences → xterm sequences (keys programs commonly bind).
|
|
61
|
+
const consoleToXterm = new Map([
|
|
62
|
+
["\u001b[[A", "\u001bOP"], ["\u001b[[B", "\u001bOQ"], ["\u001b[[C", "\u001bOR"],
|
|
63
|
+
["\u001b[[D", "\u001bOS"], ["\u001b[[E", "\u001b[15~"],
|
|
64
|
+
["\u001b[1~", "\u001b[H"], ["\u001b[4~", "\u001b[F"],
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const cursorKeys = new Map([
|
|
68
|
+
["\u001b[A", "\u001bOA"], ["\u001b[B", "\u001bOB"], ["\u001b[C", "\u001bOC"], ["\u001b[D", "\u001bOD"],
|
|
69
|
+
["\u001b[H", "\u001bOH"], ["\u001b[F", "\u001bOF"],
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
// Rewrites console key sequences in `text`. With `applicationCursorKeys`
|
|
73
|
+
// (DECCKM set by the program) the arrow keys use the SS3 forms.
|
|
74
|
+
export const translateKeys = (text, { applicationCursorKeys = false } = {}) => {
|
|
75
|
+
let output = "";
|
|
76
|
+
let index = 0;
|
|
77
|
+
while (index < text.length) {
|
|
78
|
+
if (text[index] !== "\u001b") {
|
|
79
|
+
output += text[index++];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
let matched = false;
|
|
83
|
+
for (const [from, to] of consoleToXterm) {
|
|
84
|
+
if (text.startsWith(from, index)) {
|
|
85
|
+
output += applicationCursorKeys ? (cursorKeys.get(to) ?? to) : to;
|
|
86
|
+
index += from.length;
|
|
87
|
+
matched = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (matched) continue;
|
|
92
|
+
if (applicationCursorKeys) {
|
|
93
|
+
for (const [from, to] of cursorKeys) {
|
|
94
|
+
if (text.startsWith(from, index)) {
|
|
95
|
+
output += to;
|
|
96
|
+
index += from.length;
|
|
97
|
+
matched = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (matched) continue;
|
|
102
|
+
}
|
|
103
|
+
output += text[index++];
|
|
104
|
+
}
|
|
105
|
+
return output;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Opens the keyboard: `device` names a console such as /dev/tty1; without
|
|
109
|
+
// it the process's own stdin is used (it must be a terminal). `onData`
|
|
110
|
+
// receives Buffers. close() restores the previous terminal settings.
|
|
111
|
+
export const openKeyboard = ({ device = null, onData, onError = () => {} }) => {
|
|
112
|
+
const fd = device ? openSync(device, "r+") : 0;
|
|
113
|
+
const saved = getTermios(fd);
|
|
114
|
+
setTermios(fd, rawTermios(saved));
|
|
115
|
+
let open = true;
|
|
116
|
+
const chunk = Buffer.alloc(4096);
|
|
117
|
+
const pump = () => {
|
|
118
|
+
if (!open) return;
|
|
119
|
+
read(fd, chunk, 0, chunk.length, null, (error, count) => {
|
|
120
|
+
if (!open) return;
|
|
121
|
+
if (error) {
|
|
122
|
+
if (error.code === "EAGAIN") { setTimeout(pump, 10); return; }
|
|
123
|
+
onError(error);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (count > 0) onData(Buffer.from(chunk.subarray(0, count)));
|
|
127
|
+
pump();
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
pump();
|
|
131
|
+
return {
|
|
132
|
+
fd,
|
|
133
|
+
close() {
|
|
134
|
+
if (!open) return;
|
|
135
|
+
open = false;
|
|
136
|
+
try { setTermios(fd, saved); } catch {}
|
|
137
|
+
if (device) closeSync(fd);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// /lib/bunterm/mouse.js — pointing devices through the Linux input layer.
|
|
2
|
+
//
|
|
3
|
+
// Read from /dev/input/event*, where the kernel reports one 24-byte
|
|
4
|
+
// input_event per axis, button or wheel notch, ending each batch with an
|
|
5
|
+
// EV_SYN. Relative devices (a mouse) move the pointer by their deltas;
|
|
6
|
+
// absolute ones (QEMU's USB tablet, a touchscreen) place it directly, which
|
|
7
|
+
// needs the axis range from EVIOCGABS. This module is only loaded when
|
|
8
|
+
// bunterm is asked for a mouse.
|
|
9
|
+
|
|
10
|
+
import { closeSync, existsSync, openSync, read, readFileSync, readdirSync } from "node:fs";
|
|
11
|
+
import { libc, ptr } from "../dlopen.js";
|
|
12
|
+
|
|
13
|
+
const EVENT_SIZE = 24;
|
|
14
|
+
const EV_SYN = 0x00;
|
|
15
|
+
const EV_KEY = 0x01;
|
|
16
|
+
const EV_REL = 0x02;
|
|
17
|
+
const EV_ABS = 0x03;
|
|
18
|
+
const REL_X = 0x00;
|
|
19
|
+
const REL_Y = 0x01;
|
|
20
|
+
const REL_WHEEL = 0x08;
|
|
21
|
+
const REL_HWHEEL = 0x06;
|
|
22
|
+
const ABS_X = 0x00;
|
|
23
|
+
const ABS_Y = 0x01;
|
|
24
|
+
const BTN_LEFT = 0x110;
|
|
25
|
+
const BTN_RIGHT = 0x111;
|
|
26
|
+
const BTN_MIDDLE = 0x112;
|
|
27
|
+
const BTN_TOUCH = 0x14a;
|
|
28
|
+
|
|
29
|
+
// struct input_absinfo { s32 value, minimum, maximum, fuzz, flat, resolution }
|
|
30
|
+
const EVIOCGABS = (axis) => 0x80184540 + axis;
|
|
31
|
+
|
|
32
|
+
const buttonNames = {
|
|
33
|
+
[BTN_LEFT]: "left",
|
|
34
|
+
[BTN_RIGHT]: "right",
|
|
35
|
+
[BTN_MIDDLE]: "middle",
|
|
36
|
+
[BTN_TOUCH]: "left",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const capability = (directory, name) => {
|
|
40
|
+
try {
|
|
41
|
+
// A capability file is a space-separated list of 64-bit hex words, most
|
|
42
|
+
// significant first, so the last word holds bits 0-63.
|
|
43
|
+
const words = readFileSync(`${directory}/${name}`, "utf8").trim().split(/\s+/u).reverse();
|
|
44
|
+
return (bit) => {
|
|
45
|
+
const word = words[Math.floor(bit / 64)];
|
|
46
|
+
return word ? (BigInt(`0x${word}`) >> BigInt(bit % 64)) & 1n ? true : false : false;
|
|
47
|
+
};
|
|
48
|
+
} catch {
|
|
49
|
+
return () => false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Every /dev/input/event* that looks like a pointer, with how it reports
|
|
54
|
+
// position. A device with both relative and absolute axes counts as absolute.
|
|
55
|
+
export const findPointers = (directory = "/dev/input") => {
|
|
56
|
+
const found = [];
|
|
57
|
+
let names = [];
|
|
58
|
+
try {
|
|
59
|
+
names = readdirSync(directory).filter((name) => /^event\d+$/u.test(name));
|
|
60
|
+
} catch {
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
for (const name of names.sort()) {
|
|
64
|
+
const sysfs = `/sys/class/input/${name}/device/capabilities`;
|
|
65
|
+
if (!existsSync(sysfs)) continue;
|
|
66
|
+
const relative = capability(sysfs, "rel");
|
|
67
|
+
const absolute = capability(sysfs, "abs");
|
|
68
|
+
const keys = capability(sysfs, "key");
|
|
69
|
+
const hasRelative = relative(REL_X) && relative(REL_Y);
|
|
70
|
+
const hasAbsolute = absolute(ABS_X) && absolute(ABS_Y);
|
|
71
|
+
const hasButton = keys(BTN_LEFT) || keys(BTN_TOUCH);
|
|
72
|
+
if (!hasButton || (!hasRelative && !hasAbsolute)) continue;
|
|
73
|
+
let label = name;
|
|
74
|
+
try {
|
|
75
|
+
label = readFileSync(`/sys/class/input/${name}/device/name`, "utf8").trim() || name;
|
|
76
|
+
} catch {}
|
|
77
|
+
found.push({
|
|
78
|
+
path: `${directory}/${name}`,
|
|
79
|
+
name: label,
|
|
80
|
+
kind: hasAbsolute ? "absolute" : "relative",
|
|
81
|
+
wheel: relative(REL_WHEEL),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return found;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const absoluteRange = (fd, axis) => {
|
|
88
|
+
const info = new Int32Array(6);
|
|
89
|
+
if (libc.symbols.ioctl(fd, EVIOCGABS(axis), ptr(info)) === -1) return null;
|
|
90
|
+
const [, minimum, maximum] = info;
|
|
91
|
+
return maximum > minimum ? { minimum, maximum } : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Opens every pointer in `devices` and reports the pointer state after each
|
|
95
|
+
// EV_SYN: { x, y, buttons: { left, middle, right }, wheel, hwheel, moved,
|
|
96
|
+
// pressed, released }. Coordinates are pixels clamped to width x height.
|
|
97
|
+
// `speed` scales relative motion. close() releases the devices.
|
|
98
|
+
export const openPointer = ({
|
|
99
|
+
devices = findPointers(),
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
speed = 1,
|
|
103
|
+
x = Math.floor(width / 2),
|
|
104
|
+
y = Math.floor(height / 2),
|
|
105
|
+
onSync,
|
|
106
|
+
onError = () => {},
|
|
107
|
+
}) => {
|
|
108
|
+
const state = {
|
|
109
|
+
x, y,
|
|
110
|
+
buttons: { left: false, middle: false, right: false },
|
|
111
|
+
wheel: 0,
|
|
112
|
+
hwheel: 0,
|
|
113
|
+
moved: false,
|
|
114
|
+
pressed: [],
|
|
115
|
+
released: [],
|
|
116
|
+
};
|
|
117
|
+
const open = [];
|
|
118
|
+
|
|
119
|
+
for (const device of devices) {
|
|
120
|
+
let fd = -1;
|
|
121
|
+
try {
|
|
122
|
+
fd = openSync(device.path, "r");
|
|
123
|
+
const ranges = device.kind === "absolute"
|
|
124
|
+
? { x: absoluteRange(fd, ABS_X), y: absoluteRange(fd, ABS_Y) }
|
|
125
|
+
: null;
|
|
126
|
+
// An absolute device without a usable range is read as relative.
|
|
127
|
+
const kind = ranges?.x && ranges?.y ? "absolute" : "relative";
|
|
128
|
+
open.push({ ...device, fd, kind, ranges, buffer: Buffer.alloc(EVENT_SIZE * 64), reading: true });
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (fd >= 0) {
|
|
131
|
+
try { closeSync(fd); } catch {}
|
|
132
|
+
}
|
|
133
|
+
onError(new Error(`${device.path}: ${error.message}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const clamp = (value, maximum) => Math.max(0, Math.min(maximum - 1, value));
|
|
138
|
+
|
|
139
|
+
const apply = (entry, type, code, value) => {
|
|
140
|
+
if (type === EV_REL) {
|
|
141
|
+
if (code === REL_X) { state.x = clamp(Math.round(state.x + value * speed), width); state.moved = true; }
|
|
142
|
+
else if (code === REL_Y) { state.y = clamp(Math.round(state.y + value * speed), height); state.moved = true; }
|
|
143
|
+
else if (code === REL_WHEEL) state.wheel += value;
|
|
144
|
+
else if (code === REL_HWHEEL) state.hwheel += value;
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (type === EV_ABS && entry.kind === "absolute") {
|
|
148
|
+
const range = code === ABS_X ? entry.ranges.x : code === ABS_Y ? entry.ranges.y : null;
|
|
149
|
+
if (!range) return;
|
|
150
|
+
const span = range.maximum - range.minimum;
|
|
151
|
+
const fraction = (value - range.minimum) / span;
|
|
152
|
+
if (code === ABS_X) state.x = clamp(Math.round(fraction * width), width);
|
|
153
|
+
else state.y = clamp(Math.round(fraction * height), height);
|
|
154
|
+
state.moved = true;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (type === EV_KEY) {
|
|
158
|
+
const button = buttonNames[code];
|
|
159
|
+
if (!button) return;
|
|
160
|
+
const down = value !== 0;
|
|
161
|
+
if (state.buttons[button] === down) return; // key repeat
|
|
162
|
+
state.buttons[button] = down;
|
|
163
|
+
(down ? state.pressed : state.released).push(button);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const flush = () => {
|
|
168
|
+
const report = {
|
|
169
|
+
x: state.x,
|
|
170
|
+
y: state.y,
|
|
171
|
+
buttons: { ...state.buttons },
|
|
172
|
+
wheel: state.wheel,
|
|
173
|
+
hwheel: state.hwheel,
|
|
174
|
+
moved: state.moved,
|
|
175
|
+
pressed: state.pressed,
|
|
176
|
+
released: state.released,
|
|
177
|
+
};
|
|
178
|
+
state.wheel = 0;
|
|
179
|
+
state.hwheel = 0;
|
|
180
|
+
state.moved = false;
|
|
181
|
+
state.pressed = [];
|
|
182
|
+
state.released = [];
|
|
183
|
+
if (report.moved || report.wheel || report.hwheel || report.pressed.length || report.released.length) {
|
|
184
|
+
try { onSync(report); } catch (error) { onError(error); }
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const pump = (entry) => {
|
|
189
|
+
if (!entry.reading) return;
|
|
190
|
+
read(entry.fd, entry.buffer, 0, entry.buffer.length, null, (error, count) => {
|
|
191
|
+
if (!entry.reading) return;
|
|
192
|
+
if (error) {
|
|
193
|
+
if (error.code === "EAGAIN") { setTimeout(() => pump(entry), 10); return; }
|
|
194
|
+
entry.reading = false;
|
|
195
|
+
onError(new Error(`${entry.path}: ${error.message}`));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (let offset = 0; offset + EVENT_SIZE <= count; offset += EVENT_SIZE) {
|
|
199
|
+
const type = entry.buffer.readUInt16LE(offset + 16);
|
|
200
|
+
const code = entry.buffer.readUInt16LE(offset + 18);
|
|
201
|
+
const value = entry.buffer.readInt32LE(offset + 20);
|
|
202
|
+
if (type === EV_SYN) flush();
|
|
203
|
+
else apply(entry, type, code, value);
|
|
204
|
+
}
|
|
205
|
+
pump(entry);
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
for (const entry of open) pump(entry);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
devices: open.map(({ path, name, kind }) => ({ path, name, kind })),
|
|
213
|
+
state,
|
|
214
|
+
close() {
|
|
215
|
+
for (const entry of open) {
|
|
216
|
+
entry.reading = false;
|
|
217
|
+
try { closeSync(entry.fd); } catch {}
|
|
218
|
+
}
|
|
219
|
+
open.length = 0;
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
};
|