buninu-linux 0.2.1 → 0.3.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.
Files changed (42) hide show
  1. package/README.md +547 -190
  2. package/build-uki.sh +4 -2
  3. package/fetch-alpine.sh +78 -43
  4. package/index.js +4 -1
  5. package/initramfs/bin/ip +630 -0
  6. package/initramfs/bin/mount +401 -0
  7. package/initramfs/bin/poweroff +10 -0
  8. package/initramfs/bin/ps +256 -0
  9. package/initramfs/bin/reboot +10 -0
  10. package/initramfs/bin/tar +169 -0
  11. package/initramfs/bin/top +129 -0
  12. package/initramfs/bin/umount +71 -0
  13. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
  14. package/initramfs/buninu/apps/bunmsh/README.md +7 -0
  15. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  16. package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
  17. package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
  18. package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
  19. package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
  20. package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
  21. package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
  22. package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
  23. package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
  24. package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
  25. package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
  26. package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
  27. package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
  28. package/initramfs/buninu/package.json +1 -1
  29. package/initramfs/init.js +164 -11
  30. package/initramfs/lib/dlopen.js +121 -0
  31. package/initramfs/lib/document.js +6 -0
  32. package/initramfs/lib/modprobe.js +235 -0
  33. package/initramfs/lib/power.js +35 -0
  34. package/initramfs/usr/share/doc/buninu-linux/ip.md +155 -0
  35. package/initramfs/usr/share/doc/buninu-linux/mount.md +155 -0
  36. package/initramfs/usr/share/doc/buninu-linux/poweroff.md +15 -0
  37. package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
  38. package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
  39. package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
  40. package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
  41. package/initramfs/usr/share/doc/buninu-linux/umount.md +48 -0
  42. package/package.json +2 -2
@@ -0,0 +1,401 @@
1
+ #!/bin/bun
2
+ // mount — mount a filesystem, or list what is mounted. `mount --help` renders
3
+ // /usr/share/doc/buninu-linux/mount.md.
4
+ //
5
+ // A Bun script over the mount(2) binding in /lib/dlopen.js. Filesystem
6
+ // drivers and block-device drivers are kernel modules on the Alpine kernels
7
+ // this distro boots, so the command loads them through /lib/modprobe.js:
8
+ // ext4 (with jbd2/mbcache/crc16), vfat/exfat/ntfs3 with their NLS tables,
9
+ // and common virtio / SATA / NVMe / USB storage drivers when the device node
10
+ // is not there yet.
11
+
12
+ import { existsSync, mkdirSync, openSync, readSync, closeSync, readdirSync, readFileSync } from "node:fs";
13
+ import { libc, cString, ptr, errno, strerror, Errno, SysError } from "../lib/dlopen.js";
14
+ import { showDocument } from "../lib/document.js";
15
+ import { modprobe, tryModprobe, modulesDirectory, ModuleNotFound } from "../lib/modprobe.js";
16
+
17
+ const fail = (message, status = 32) => {
18
+ console.error(`mount: ${message}`);
19
+ process.exit(status);
20
+ };
21
+
22
+ // ---------------------------------------------------------------- options --
23
+
24
+ // <linux/mount.h> flags, keyed by the -o word that sets or clears them.
25
+ const MS = {
26
+ RDONLY: 1n, NOSUID: 2n, NODEV: 4n, NOEXEC: 8n, SYNCHRONOUS: 16n, REMOUNT: 32n,
27
+ MANDLOCK: 64n, DIRSYNC: 128n, NOSYMFOLLOW: 256n, NOATIME: 1024n, NODIRATIME: 2048n,
28
+ BIND: 4096n, MOVE: 8192n, REC: 16384n, SILENT: 32768n, UNBINDABLE: 1n << 17n,
29
+ PRIVATE: 1n << 18n, SLAVE: 1n << 19n, SHARED: 1n << 20n, RELATIME: 1n << 21n,
30
+ STRICTATIME: 1n << 24n, LAZYTIME: 1n << 25n,
31
+ };
32
+
33
+ const flagWords = {
34
+ ro: [MS.RDONLY, true], rw: [MS.RDONLY, false],
35
+ nosuid: [MS.NOSUID, true], suid: [MS.NOSUID, false],
36
+ nodev: [MS.NODEV, true], dev: [MS.NODEV, false],
37
+ noexec: [MS.NOEXEC, true], exec: [MS.NOEXEC, false],
38
+ sync: [MS.SYNCHRONOUS, true], async: [MS.SYNCHRONOUS, false],
39
+ dirsync: [MS.DIRSYNC, true],
40
+ mand: [MS.MANDLOCK, true], nomand: [MS.MANDLOCK, false],
41
+ noatime: [MS.NOATIME, true], atime: [MS.NOATIME, false],
42
+ nodiratime: [MS.NODIRATIME, true], diratime: [MS.NODIRATIME, false],
43
+ relatime: [MS.RELATIME, true], norelatime: [MS.RELATIME, false],
44
+ strictatime: [MS.STRICTATIME, true], nostrictatime: [MS.STRICTATIME, false],
45
+ lazytime: [MS.LAZYTIME, true], nolazytime: [MS.LAZYTIME, false],
46
+ nosymfollow: [MS.NOSYMFOLLOW, true], symfollow: [MS.NOSYMFOLLOW, false],
47
+ silent: [MS.SILENT, true], loud: [MS.SILENT, false],
48
+ remount: [MS.REMOUNT, true],
49
+ bind: [MS.BIND, true], rbind: [MS.BIND | MS.REC, true],
50
+ move: [MS.MOVE, true],
51
+ shared: [MS.SHARED, true], rshared: [MS.SHARED | MS.REC, true],
52
+ private: [MS.PRIVATE, true], rprivate: [MS.PRIVATE | MS.REC, true],
53
+ slave: [MS.SLAVE, true], rslave: [MS.SLAVE | MS.REC, true],
54
+ unbindable: [MS.UNBINDABLE, true], runbindable: [MS.UNBINDABLE | MS.REC, true],
55
+ defaults: [0n, true], auto: [0n, true], noauto: [0n, true], user: [0n, true],
56
+ nouser: [0n, true], nofail: [0n, true], _netdev: [0n, true],
57
+ };
58
+
59
+ // Splits "-o ro,noatime,size=64M" into mount(2) flags plus the data string the
60
+ // filesystem driver parses itself.
61
+ const parseOptions = (list, state) => {
62
+ for (const word of list.split(",")) {
63
+ if (!word) continue;
64
+ const key = word.split("=")[0];
65
+ if (word in flagWords) {
66
+ const [bit, set] = flagWords[word];
67
+ state.flags = set ? state.flags | bit : state.flags & ~bit;
68
+ } else if (key === "loop") {
69
+ fail("loop devices are not supported yet");
70
+ } else {
71
+ state.data.push(word);
72
+ }
73
+ }
74
+ };
75
+
76
+ const usage = () => {
77
+ console.error(`Usage:
78
+ mount [-l]
79
+ mount [-fv] [-t TYPE] [-o OPTIONS] [-r|-w] [--mkdir] SOURCE TARGET
80
+ mount -L LABEL|-U UUID [-t TYPE] [-o OPTIONS] TARGET
81
+ mount --bind|--rbind|--move OLD NEW
82
+ mount -o remount[,ro|rw] TARGET
83
+ mount -h | --help full manual (mount.md)`);
84
+ process.exit(1);
85
+ };
86
+
87
+ const parseArguments = (argv) => {
88
+ const state = { flags: 0n, data: [], positional: [], type: null, mkdir: false, verbose: false, fake: false };
89
+ const takeValue = (i, flag) => {
90
+ if (i + 1 >= argv.length) fail(`option ${flag} needs an argument`, 1);
91
+ return argv[i + 1];
92
+ };
93
+ for (let i = 0; i < argv.length; i++) {
94
+ const argument = argv[i];
95
+ if (argument === "-h" || argument === "--help") state.help = true;
96
+ else if (argument === "-t" || argument === "--types") state.type = argv[++i] ?? usage();
97
+ else if (argument.startsWith("--types=")) state.type = argument.slice(8);
98
+ else if (argument.startsWith("-t") && argument.length > 2) state.type = argument.slice(2);
99
+ else if (argument === "-o" || argument === "--options") parseOptions(takeValue(i++, "-o"), state);
100
+ else if (argument.startsWith("--options=")) parseOptions(argument.slice(10), state);
101
+ else if (argument.startsWith("-o") && argument.length > 2) parseOptions(argument.slice(2), state);
102
+ else if (argument === "-L" || argument === "--label") state.source = `LABEL=${takeValue(i++, "-L")}`;
103
+ else if (argument === "-U" || argument === "--uuid") state.source = `UUID=${takeValue(i++, "-U")}`;
104
+ else if (argument === "-r" || argument === "--read-only") state.flags |= MS.RDONLY;
105
+ else if (argument === "-w" || argument === "--rw" || argument === "--read-write") state.flags &= ~MS.RDONLY;
106
+ else if (argument === "-B" || argument === "--bind") state.flags |= MS.BIND;
107
+ else if (argument === "-R" || argument === "--rbind") state.flags |= MS.BIND | MS.REC;
108
+ else if (argument === "-M" || argument === "--move") state.flags |= MS.MOVE;
109
+ else if (argument === "--make-shared") state.flags |= MS.SHARED;
110
+ else if (argument === "--make-private") state.flags |= MS.PRIVATE;
111
+ else if (argument === "--make-rshared") state.flags |= MS.SHARED | MS.REC;
112
+ else if (argument === "--make-rprivate") state.flags |= MS.PRIVATE | MS.REC;
113
+ else if (argument === "--mkdir") state.mkdir = true;
114
+ else if (/^-[fv]{2,}$/.test(argument)) {
115
+ if (argument.includes("f")) state.fake = true;
116
+ if (argument.includes("v")) state.verbose = true;
117
+ }
118
+ else if (argument === "-v" || argument === "--verbose") state.verbose = true;
119
+ else if (argument === "-f" || argument === "--fake") state.fake = true;
120
+ else if (argument === "-l" || argument === "-n" || argument === "--no-mtab" || argument === "-i" || argument === "--no-canonicalize") {}
121
+ else if (argument === "-a" || argument === "--all") fail("there is no /etc/fstab on this system, so -a does nothing", 1);
122
+ else if (argument === "--") state.positional.push(...argv.slice(i + 1)), i = argv.length;
123
+ else if (argument.startsWith("-") && argument.length > 1) console.error(`mount: unknown option ${argument}`), usage();
124
+ else state.positional.push(argument);
125
+ }
126
+ return state;
127
+ };
128
+
129
+ // -------------------------------------------------------------- listing ----
130
+
131
+ const listMounts = () => {
132
+ const table = readFileSync("/proc/self/mounts", "utf8");
133
+ const unescape = (field) => field.replace(/\\([0-7]{3})/g, (_, code) => String.fromCharCode(parseInt(code, 8)));
134
+ for (const line of table.split("\n")) {
135
+ if (!line) continue;
136
+ const [source, target, type, options] = line.split(" ").map(unescape);
137
+ console.log(`${source} on ${target} type ${type} (${options})`);
138
+ }
139
+ };
140
+
141
+ // ---------------------------------------------------------------- probing --
142
+
143
+ // Reads the first 128 KiB of a block device: enough for every superblock
144
+ // magic checked below (btrfs sits at 64 KiB, ISO 9660 at 32 KiB).
145
+ const readHeader = (device) => {
146
+ const fd = openSync(device, "r");
147
+ try {
148
+ const buffer = Buffer.alloc(128 * 1024);
149
+ const length = readSync(fd, buffer, 0, buffer.length, 0);
150
+ return buffer.subarray(0, length);
151
+ } finally {
152
+ closeSync(fd);
153
+ }
154
+ };
155
+
156
+ const ascii = (buffer, offset, length) =>
157
+ buffer.subarray(offset, offset + length).toString("latin1").replace(/\0.*$/s, "").trim();
158
+
159
+ const hex = (buffer, offset, length, reverse = false) => {
160
+ const bytes = [...buffer.subarray(offset, offset + length)];
161
+ if (reverse) bytes.reverse();
162
+ return bytes.map((byte) => byte.toString(16).padStart(2, "0")).join("").toUpperCase();
163
+ };
164
+
165
+ // { type, label, uuid } for the volume at `device`, or null when unknown.
166
+ // Identifies what the shipped drivers can mount, plus a few more so the
167
+ // error for an unsupported volume can at least name it.
168
+ const probe = (device) => {
169
+ let header;
170
+ try {
171
+ header = readHeader(device);
172
+ } catch {
173
+ return null;
174
+ }
175
+ const at = (offset, text) =>
176
+ header.length >= offset + text.length && header.subarray(offset, offset + text.length).toString("latin1") === text;
177
+ const u16 = (offset) => header.readUInt16LE(offset);
178
+ const u32 = (offset) => header.readUInt32LE(offset);
179
+
180
+ if (header.length >= 0x800 && u16(0x438) === 0xef53) {
181
+ // ext2/3/4 share the superblock; a journal means ext3+, extents or
182
+ // 64-bit or metadata_csum mean ext4. The ext4 driver handles ext3 too.
183
+ const compat = u32(0x45c), incompat = u32(0x460), roCompat = u32(0x464);
184
+ const type = incompat & 0x2c0 || roCompat & 0x478 ? "ext4" : compat & 0x4 ? "ext3" : "ext2";
185
+ const raw = hex(header, 0x468, 16);
186
+ const uuid = `${raw.slice(0, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}-${raw.slice(16, 20)}-${raw.slice(20)}`.toLowerCase();
187
+ return { type, label: ascii(header, 0x478, 16), uuid };
188
+ }
189
+ if (at(3, "NTFS ")) return { type: "ntfs3", label: "", uuid: hex(header, 0x48, 8, true) };
190
+ if (at(3, "EXFAT ")) return { type: "exfat", label: "", uuid: `${hex(header, 0x66, 2, true)}-${hex(header, 0x64, 2, true)}` };
191
+ if (header.length >= 512 && u16(510) === 0xaa55 && (at(0x52, "FAT") || at(0x36, "FAT"))) {
192
+ const fat32 = at(0x52, "FAT32");
193
+ const serialOffset = fat32 ? 0x43 : 0x27;
194
+ const labelOffset = fat32 ? 0x47 : 0x2b;
195
+ let label = ascii(header, labelOffset, 11);
196
+ if (label === "NO NAME") label = "";
197
+ return { type: "vfat", label, uuid: `${hex(header, serialOffset + 2, 2, true)}-${hex(header, serialOffset, 2, true)}` };
198
+ }
199
+ if (at(0, "XFSB")) return { type: "xfs", label: ascii(header, 0x6c, 12), uuid: "" };
200
+ if (at(0x10040, "_BHRfS_M")) return { type: "btrfs", label: ascii(header, 0x1012b, 256), uuid: "" };
201
+ if (at(0, "hsqs")) return { type: "squashfs", label: "", uuid: "" };
202
+ if (at(0x8001, "CD001")) return { type: "iso9660", label: ascii(header, 0x8028, 32), uuid: "" };
203
+ if (header.length >= 0x404 && u32(0x400) === 0xf2f52010) return { type: "f2fs", label: "", uuid: "" };
204
+ if (header.length >= 4096 && at(4096 - 10, "SWAPSPACE2")) return { type: "swap", label: "", uuid: "" };
205
+ return null;
206
+ };
207
+
208
+ const blockDevices = () => {
209
+ try {
210
+ return readdirSync("/sys/class/block")
211
+ .filter((name) => !/^(loop|ram|zram|sr)\d/.test(name))
212
+ .sort()
213
+ .map((name) => `/dev/${name}`)
214
+ .filter((path) => existsSync(path));
215
+ } catch {
216
+ return [];
217
+ }
218
+ };
219
+
220
+ const findVolume = (key, value) => {
221
+ for (const device of blockDevices()) {
222
+ const info = probe(device);
223
+ if (!info) continue;
224
+ const candidate = key === "LABEL" ? info.label : info.uuid;
225
+ if (candidate.toUpperCase() === value.toUpperCase()) return device;
226
+ }
227
+ return null;
228
+ };
229
+
230
+ // LABEL=/UUID= sources need udev's /dev/disk/by-*; this scans instead.
231
+ const resolveSource = (source) => {
232
+ const match = /^(LABEL|UUID)=(.+)$/.exec(source);
233
+ if (!match) return source;
234
+ ensureBlockDrivers(null);
235
+ const device = findVolume(match[1], match[2]);
236
+ if (!device) fail(`can't find ${source}`);
237
+ return device;
238
+ };
239
+
240
+ // ---------------------------------------------------------------- drivers --
241
+
242
+ const sleep = (milliseconds) => Bun.sleepSync(milliseconds);
243
+
244
+ // The device name says which driver creates it. Loading is skipped when the
245
+ // node already exists, so this costs nothing after the first mount.
246
+ const ensureBlockDrivers = (device) => {
247
+ if (device && existsSync(device)) return;
248
+ const drivers = device === null
249
+ ? ["virtio_blk", "vmd", "nvme", "ahci", "ata_generic", "pata_acpi", "usb_storage", "uas", "sd_mod"]
250
+ : /^\/dev\/vd/.test(device) ? ["virtio_blk"]
251
+ : /^\/dev\/sd/.test(device) ? ["ahci", "ata_generic", "pata_acpi", "usb_storage", "uas", "sd_mod"]
252
+ : /^\/dev\/nvme/.test(device) ? ["vmd", "nvme"]
253
+ : [];
254
+ let loaded = 0;
255
+ for (const driver of drivers) loaded += tryModprobe(driver)?.length ?? 0;
256
+ if (device === null) {
257
+ if (loaded) sleep(500);
258
+ return;
259
+ }
260
+ // devtmpfs creates the node once the driver has probed the disk.
261
+ for (let waited = 0; !existsSync(device) && waited < 3000; waited += 100) sleep(100);
262
+ };
263
+
264
+ const kernelFilesystems = () => {
265
+ try {
266
+ return new Set(
267
+ readFileSync("/proc/filesystems", "utf8").split("\n").map((line) => line.split("\t").pop()).filter(Boolean),
268
+ );
269
+ } catch {
270
+ return new Set();
271
+ }
272
+ };
273
+
274
+ // The kernel's own request_module() has no /sbin/modprobe to run here, so the
275
+ // filesystem driver and the NLS tables FAT/exFAT/NTFS ask for are loaded
276
+ // before mount(2). Names come from the fs-* aliases (ntfs -> ntfs3, ...).
277
+ // A type with no module is still handed to mount(2): the kernel knows about
278
+ // built-in filesystems that neither /proc/filesystems (unreadable) nor
279
+ // modules.builtin (tmpfs, proc, ...) may list, and answers ENODEV otherwise.
280
+ let driverHint = "";
281
+ const ensureFilesystemDriver = (type, data) => {
282
+ const registered = kernelFilesystems();
283
+ const loaded = [];
284
+ if (!registered.has(type)) {
285
+ try {
286
+ loaded.push(...modprobe(`fs-${type}`));
287
+ } catch (error) {
288
+ if (!(error instanceof ModuleNotFound)) throw error;
289
+ driverHint = ` (no ${type} module in ${modulesDirectory()})`;
290
+ }
291
+ }
292
+ if (["vfat", "msdos", "exfat", "ntfs3", "iso9660", "udf"].includes(type)) {
293
+ const charsets = new Set(["utf8", "cp437", "iso8859-1"]);
294
+ for (const option of data) {
295
+ const [key, value] = option.split("=");
296
+ if (key === "iocharset" || key === "nls") charsets.add(value);
297
+ if (key === "codepage") charsets.add(`cp${value}`);
298
+ }
299
+ for (const charset of charsets) loaded.push(...(tryModprobe(`nls_${charset}`) ?? []));
300
+ }
301
+ return loaded;
302
+ };
303
+
304
+ // ---------------------------------------------------------------- mounting -
305
+
306
+ const explain = (code, source, target, type) => {
307
+ switch (code) {
308
+ case Errno.EPERM: return "must be superuser to use mount";
309
+ case Errno.ENOENT: return existsSync(target) ? `special device ${source} does not exist` : `mount point ${target} does not exist`;
310
+ case Errno.ENOTDIR: return `mount point ${target} is not a directory`;
311
+ case Errno.EBUSY: return `${source} already mounted or ${target} busy`;
312
+ case Errno.ENODEV: return `unknown filesystem type '${type}'${driverHint}`;
313
+ case Errno.EROFS: return `${source} is write-protected; try -o ro`;
314
+ case Errno.EACCES: return `${source} is not readable`;
315
+ case Errno.ENXIO: return `${source}: no such device or the partition has not been detected`;
316
+ case Errno.EINVAL: return `wrong fs type, bad option, bad superblock on ${source}, missing codepage or helper program, or other error (dmesg may say more)`;
317
+ default: return `${strerror(code)}`;
318
+ }
319
+ };
320
+
321
+ const doMount = (source, target, type, flags, data, { verbose, fake }) => {
322
+ if (verbose || fake) {
323
+ const words = [];
324
+ for (const [name, bit] of Object.entries(MS)) if (flags & bit) words.push(name.toLowerCase());
325
+ console.log(`mount(${source}, ${target}, ${type ?? "(none)"}, [${words.join(",")}], "${data}")`);
326
+ }
327
+ if (fake) return;
328
+ const dataBytes = data ? cString(data) : null;
329
+ const typeBytes = type ? cString(type) : null;
330
+ const result = libc.symbols.mount(
331
+ ptr(cString(source)),
332
+ ptr(cString(target)),
333
+ typeBytes ? ptr(typeBytes) : null,
334
+ flags,
335
+ dataBytes ? ptr(dataBytes) : null,
336
+ );
337
+ if (result === 0) return;
338
+ const code = errno();
339
+ // The kernel demotes a dirty NTFS or a FAT with errors to read-only rather
340
+ // than fail; when it does refuse outright, tell the user the escape hatch.
341
+ const hint = type === "ntfs3" && code === Errno.EINVAL
342
+ ? " — a Windows volume left dirty by hibernation or Fast Startup needs -o force, or -o ro" : "";
343
+ fail(explain(code, source, target, type) + hint);
344
+ };
345
+
346
+ const main = async () => {
347
+ const state = parseArguments(process.argv.slice(2));
348
+ if (state.help) return showDocument("mount");
349
+
350
+ if (state.source) state.positional.unshift(state.source);
351
+ if (state.positional.length === 0) return listMounts();
352
+
353
+ const pure = state.flags & (MS.BIND | MS.MOVE | MS.REMOUNT | MS.SHARED | MS.PRIVATE | MS.SLAVE | MS.UNBINDABLE);
354
+ let [source, target] = state.positional;
355
+ if (state.positional.length === 1) {
356
+ // `mount -o remount,rw /` and `mount --make-private /` name the target only.
357
+ if (!(state.flags & MS.REMOUNT) && !(state.flags & (MS.SHARED | MS.PRIVATE | MS.SLAVE | MS.UNBINDABLE))) usage();
358
+ target = source;
359
+ source = "none";
360
+ } else if (state.positional.length > 2) usage();
361
+
362
+ if (state.mkdir) mkdirSync(target, { recursive: true });
363
+
364
+ let type = state.type;
365
+ const loaded = [];
366
+ if (!pure) {
367
+ source = resolveSource(source);
368
+ if (source.startsWith("/dev/")) {
369
+ ensureBlockDrivers(source);
370
+ if (!existsSync(source)) fail(`special device ${source} does not exist`);
371
+ }
372
+ if (!type || type === "auto") {
373
+ const info = source.startsWith("/dev/") ? probe(source) : null;
374
+ if (!info) fail(`can't identify the filesystem on ${source}; use -t TYPE`);
375
+ if (info.type === "swap") fail(`${source} is swap space, not a filesystem`);
376
+ type = info.type;
377
+ if (state.verbose) console.log(`${source}: ${info.type}${info.label ? ` label "${info.label}"` : ""}${info.uuid ? ` uuid ${info.uuid}` : ""}`);
378
+ }
379
+ if (type === "ntfs") type = "ntfs3";
380
+ if (type === "ext3") type = "ext4";
381
+ loaded.push(...ensureFilesystemDriver(type, state.data));
382
+ }
383
+ if (state.verbose && loaded.length) console.log(`loaded modules: ${loaded.join(" ")}`);
384
+
385
+ doMount(source, target, pure ? null : type, state.flags, state.data.join(","), state);
386
+ // mount(2) ignores every other flag on a bind, so `--bind -o ro` takes a
387
+ // second, remount pass the way util-linux does it.
388
+ if (state.flags & MS.BIND && state.flags & MS.RDONLY && !(state.flags & MS.REMOUNT)) {
389
+ doMount("none", target, null, MS.REMOUNT | MS.BIND | MS.RDONLY | (state.flags & MS.REC), "", state);
390
+ }
391
+ if (state.fake) console.log("(DRY RUN)");
392
+ else if (state.verbose) console.log(`${source} mounted on ${target}.`);
393
+ };
394
+
395
+ try {
396
+ await main();
397
+ } catch (error) {
398
+ if (error instanceof SysError) fail(error.message);
399
+ if (error?.code === "ENOENT" || error?.code === "EACCES") fail(`${error.path ?? ""}: ${error.message}`);
400
+ throw error;
401
+ }
@@ -0,0 +1,10 @@
1
+ #!/bin/bun
2
+ // poweroff — sync filesystems and power off through Linux reboot(2).
3
+
4
+ import { LINUX_REBOOT_CMD_POWER_OFF, runPowerCommand } from "../lib/power.js";
5
+
6
+ await runPowerCommand({
7
+ name: "poweroff",
8
+ command: { name: "LINUX_REBOOT_CMD_POWER_OFF", value: LINUX_REBOOT_CMD_POWER_OFF },
9
+ action: "powering off",
10
+ });
@@ -0,0 +1,256 @@
1
+ #!/bin/bun
2
+ // ps — a /proc process snapshot, formatter, and command-line frontend.
3
+
4
+ import { readdirSync, readFileSync } from "node:fs";
5
+ import { showDocument } from "../lib/document.js";
6
+
7
+ export const CLOCK_TICKS = 100;
8
+
9
+ const read = (path) => readFileSync(path, "utf8");
10
+ const numberFrom = (text, pattern) => Number(pattern.exec(text)?.[1] ?? 0);
11
+
12
+ const ttyName = (number) => {
13
+ if (!number) return "?";
14
+ const device = number >>> 0;
15
+ const major = (device >>> 8) & 0xfff;
16
+ const minor = (device & 0xff) | ((device >>> 12) & 0xfff00);
17
+ if (major >= 136 && major <= 143) return `pts/${minor}`;
18
+ if (major === 4 && minor < 64) return `tty${minor}`;
19
+ if (major === 5 && minor === 0) return "tty";
20
+ return `${major}:${minor}`;
21
+ };
22
+
23
+ const cpuTime = (seconds) => {
24
+ const whole = Math.max(0, Math.floor(seconds));
25
+ const hours = Math.floor(whole / 3600);
26
+ const minutes = Math.floor(whole % 3600 / 60);
27
+ const secs = whole % 60;
28
+ return hours
29
+ ? `${hours}:${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`
30
+ : `${String(minutes).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
31
+ };
32
+
33
+ const elapsedTime = (seconds) => {
34
+ const whole = Math.max(0, Math.floor(seconds));
35
+ const days = Math.floor(whole / 86400);
36
+ const clock = [Math.floor(whole % 86400 / 3600), Math.floor(whole % 3600 / 60), whole % 60]
37
+ .map((part) => String(part).padStart(2, "0")).join(":");
38
+ return days ? `${days}-${clock}` : clock;
39
+ };
40
+
41
+ const startText = (started, short = false) => {
42
+ const date = new Date(started);
43
+ const now = new Date();
44
+ if (date.toDateString() === now.toDateString())
45
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
46
+ if (short || date.getFullYear() === now.getFullYear())
47
+ return date.toLocaleString("en-US", { month: "short", day: "2-digit" }).replace(" ", "");
48
+ return String(date.getFullYear());
49
+ };
50
+
51
+ const readUsers = () => {
52
+ const users = new Map();
53
+ try {
54
+ for (const line of read("/etc/passwd").split("\n")) {
55
+ const fields = line.split(":");
56
+ if (/^\d+$/.test(fields[2] ?? "") && !users.has(Number(fields[2])))
57
+ users.set(Number(fields[2]), fields[0]);
58
+ }
59
+ } catch {}
60
+ return users;
61
+ };
62
+
63
+ const readMemory = () => {
64
+ const result = {};
65
+ try {
66
+ for (const match of read("/proc/meminfo").matchAll(/^(\w+):\s+(\d+)/gm))
67
+ result[match[1]] = Number(match[2]);
68
+ } catch {}
69
+ return result;
70
+ };
71
+
72
+ const readCpu = () => {
73
+ try {
74
+ const values = read("/proc/stat").match(/^cpu\s+(.+)$/m)?.[1].trim().split(/\s+/).map(Number) ?? [];
75
+ const total = values.reduce((sum, value) => sum + value, 0);
76
+ return { total, idle: (values[3] ?? 0) + (values[4] ?? 0), count: Math.max(1, read("/proc/stat").match(/^cpu\d+\s/gm)?.length ?? 1) };
77
+ } catch { return { total: 0, idle: 0, count: 1 }; }
78
+ };
79
+
80
+ export function readProcessSnapshot() {
81
+ const users = readUsers();
82
+ const memory = readMemory();
83
+ let uptime = 0;
84
+ let bootSeconds = 0;
85
+ let loadAverage = [0, 0, 0];
86
+ try { uptime = Number(read("/proc/uptime").split(/\s+/)[0]); } catch {}
87
+ try { bootSeconds = numberFrom(read("/proc/stat"), /^btime\s+(\d+)/m); } catch {}
88
+ try { loadAverage = read("/proc/loadavg").trim().split(/\s+/).slice(0, 3).map(Number); } catch {}
89
+ const bootTime = bootSeconds ? bootSeconds * 1000 : Date.now() - uptime * 1000;
90
+ let pids = [];
91
+ try {
92
+ pids = readdirSync("/proc", { withFileTypes: true })
93
+ .filter((entry) => entry.isDirectory() && /^\d+$/.test(entry.name))
94
+ .map((entry) => Number(entry.name)).sort((a, b) => a - b);
95
+ } catch (error) { throw new Error(`cannot read /proc: ${error.message}`); }
96
+
97
+ const processes = [];
98
+ for (const pid of pids) {
99
+ try {
100
+ let args = readFileSync(`/proc/${pid}/cmdline`).toString("utf8").replace(/\0/g, " ").replace(/[\r\n]+/g, " ").trimEnd();
101
+ if (!args) args = `[${read(`/proc/${pid}/comm`).trim().replace(/[\r\n]+/g, " ")}]`;
102
+ const status = read(`/proc/${pid}/status`);
103
+ const stat = read(`/proc/${pid}/stat`);
104
+ const close = stat.lastIndexOf(")");
105
+ if (close < 0) continue;
106
+ const open = stat.indexOf("(");
107
+ const fields = stat.slice(close + 2).trim().split(/\s+/);
108
+ const command = stat.slice(open + 1, close);
109
+ const uid = numberFrom(status, /^Uid:\s+(\d+)/m);
110
+ const vmSize = numberFrom(status, /^VmSize:\s+(\d+)/m);
111
+ const rss = numberFrom(status, /^VmRSS:\s+(\d+)/m);
112
+ const ppid = Number(fields[1]);
113
+ const session = Number(fields[3]);
114
+ const ttyNumber = Number(fields[4]);
115
+ // /proc/PID/stat field 9 is flags. `fields` starts at field 3, so
116
+ // index 6 contains PF_KTHREAD and does not rely on guessing from argv.
117
+ const kernelThread = (Number(fields[6]) & 0x00200000) !== 0;
118
+ const ticks = Number(fields[11]) + Number(fields[12]);
119
+ const nice = Number(fields[16]);
120
+ const threads = Number(fields[17]);
121
+ const started = bootTime + Number(fields[19]) / CLOCK_TICKS * 1000;
122
+ const elapsed = Math.max(0.01, (Date.now() - started) / 1000);
123
+ let state = fields[0];
124
+ if (nice < 0) state += "<"; else if (nice > 0) state += "N";
125
+ if (session === pid) state += "s";
126
+ if (threads > 1) state += "l";
127
+ if (Number(fields[5]) > 0 && Number(fields[5]) === Number(fields[2])) state += "+";
128
+ processes.push({
129
+ pid, ppid, uid, user: users.get(uid) ?? String(uid), args, command,
130
+ tty: ttyName(ttyNumber), ttyNumber, session, state, kernelThread,
131
+ processor: Number(fields[36] ?? 0), priority: Number(fields[15]), nice, threads, ticks,
132
+ cpu: ticks / CLOCK_TICKS / elapsed * 100,
133
+ memory: memory.MemTotal ? rss / memory.MemTotal * 100 : 0,
134
+ vmSize, rss, size: Math.ceil(Number(fields[20]) / 4096),
135
+ time: cpuTime(ticks / CLOCK_TICKS), elapsed, started,
136
+ });
137
+ } catch {}
138
+ }
139
+ return { processes, memory, cpu: readCpu(), uptime, loadAverage, at: Date.now() };
140
+ }
141
+
142
+ export const processFieldDefinitions = {
143
+ pid: { header: "PID", numeric: true, minimum: 5, value: (r) => r.pid },
144
+ ppid: { header: "PPID", numeric: true, minimum: 5, value: (r) => r.ppid },
145
+ uid: { header: "UID", numeric: true, minimum: 5, value: (r) => r.uid },
146
+ user: { header: "USER", value: (r) => r.user }, euser: { header: "EUSER", value: (r) => r.user },
147
+ comm: { header: "COMMAND", value: (r) => r.command }, args: { header: "COMMAND", value: (r) => r.args },
148
+ cmd: { header: "CMD", value: (r) => r.args }, command: { header: "COMMAND", value: (r) => r.args },
149
+ tty: { header: "TT", value: (r) => r.tty }, tt: { header: "TT", value: (r) => r.tty },
150
+ stat: { header: "STAT", value: (r) => r.state }, state: { header: "S", value: (r) => r.state[0] },
151
+ c: { header: "C", numeric: true, value: (r) => Math.floor(r.cpu) },
152
+ "%cpu": { header: "%CPU", numeric: true, value: (r) => r.cpu.toFixed(1) }, pcpu: { header: "%CPU", numeric: true, value: (r) => r.cpu.toFixed(1) },
153
+ "%mem": { header: "%MEM", numeric: true, value: (r) => r.memory.toFixed(1) }, pmem: { header: "%MEM", numeric: true, value: (r) => r.memory.toFixed(1) },
154
+ vsz: { header: "VSZ", numeric: true, value: (r) => r.vmSize }, rss: { header: "RSS", numeric: true, value: (r) => r.rss },
155
+ sz: { header: "SZ", numeric: true, value: (r) => r.size }, time: { header: "TIME", numeric: true, value: (r) => r.time },
156
+ etime: { header: "ELAPSED", numeric: true, value: (r) => elapsedTime(r.elapsed) }, etimes: { header: "ELAPSED", numeric: true, value: (r) => Math.floor(r.elapsed) },
157
+ start: { header: "STARTED", value: (r) => startText(r.started) }, stime: { header: "STIME", value: (r) => startText(r.started, true) },
158
+ lstart: { header: "STARTED", value: (r) => new Date(r.started).toString().slice(0, 24) },
159
+ ni: { header: "NI", numeric: true, value: (r) => r.nice }, nice: { header: "NI", numeric: true, value: (r) => r.nice },
160
+ pri: { header: "PRI", numeric: true, value: (r) => r.priority }, psr: { header: "PSR", numeric: true, value: (r) => r.processor },
161
+ };
162
+
163
+ export function prepareProcessFormat(rows, fields) {
164
+ const names = Array.isArray(fields) ? fields : fields.toLowerCase().split(/[\s,]+/).filter(Boolean);
165
+ if (!names.length) throw new Error("empty -o format");
166
+ return names.map((name) => {
167
+ const definition = processFieldDefinitions[name];
168
+ if (!definition) throw new Error(`unsupported output field '${name}'`);
169
+ const width = Math.max(definition.minimum ?? 0, definition.header.length,
170
+ ...rows.map((row) => String(definition.value(row)).length));
171
+ return { name, ...definition, width };
172
+ });
173
+ }
174
+
175
+ export const formatProcessHeader = (format) => format.map((f) =>
176
+ f.numeric ? f.header.padStart(f.width) : f.header.padEnd(f.width)).join(" ").trimEnd();
177
+
178
+ export const formatProcessLine = (row, format) => format.map((f, index) => {
179
+ const value = String(f.value(row));
180
+ if (index === format.length - 1 && !f.numeric) return value;
181
+ return f.numeric ? value.padStart(f.width) : value.padEnd(f.width);
182
+ }).join(" ").trimEnd();
183
+
184
+ export function formatProcessTable(rows, fields) {
185
+ const format = prepareProcessFormat(rows, fields);
186
+ return [formatProcessHeader(format), ...rows.map((row) => formatProcessLine(row, format))];
187
+ }
188
+
189
+ const formatPresetTable = (rows, fields, headers = {}) => {
190
+ const format = prepareProcessFormat(rows, fields).map((field) => {
191
+ const header = headers[field.name] ?? field.header;
192
+ return { ...field, header, width: Math.max(field.width, header.length) };
193
+ });
194
+ return [formatProcessHeader(format), ...rows.map((row) => formatProcessLine(row, format))];
195
+ };
196
+
197
+ export function selectProcesses(rows, { all = false, selection = "default" } = {}) {
198
+ if (all) return rows;
199
+ const own = rows.find((row) => row.pid === process.pid);
200
+ const ownUid = own?.uid ?? process.geteuid?.() ?? process.getuid?.() ?? 0;
201
+ const ownTty = own?.ttyNumber ?? 0;
202
+ return rows.filter((row) => {
203
+ // A boot-console shell and kernel threads can all be UID 0 and have no
204
+ // controlling TTY. Plain ps must not accidentally behave like ps -e.
205
+ if (row.kernelThread) return false;
206
+ if (selection === "unix-a") return row.ttyNumber !== 0 && row.session !== row.pid;
207
+ if (selection === "any-with-tty") return row.ttyNumber !== 0;
208
+ if (selection === "own-any-tty") return row.uid === ownUid;
209
+ return row.uid === ownUid && row.ttyNumber === ownTty && row.session !== row.pid;
210
+ });
211
+ }
212
+
213
+ const usage = () => { throw new Error("Usage: ps | ps -e|-A [-o fields] | ps -ef | ps aux | ps ax\nTry 'ps --help' for the full manual."); };
214
+
215
+ async function main(argv) {
216
+ let fields = null, view = "default", all = false, selection = "default";
217
+ for (let index = 0; index < argv.length; index++) {
218
+ const argument = argv[index];
219
+ if (argument === "-h" || argument === "--help") return showDocument("ps");
220
+ if (["-e", "-A", "--all"].includes(argument)) { all = true; continue; }
221
+ if (["-f", "-ef", "-Af"].includes(argument)) { view = "full"; if (argument !== "-f") all = true; continue; }
222
+ if (["-F", "-eF", "-AF"].includes(argument)) { view = "extra-full"; if (argument !== "-F") all = true; continue; }
223
+ if (["aux", "-aux"].includes(argument)) { view = "aux"; all = true; continue; }
224
+ if (["ax", "-ax"].includes(argument)) { view = "bsd"; all = true; continue; }
225
+ if (argument === "a") { view = "bsd"; selection = "any-with-tty"; continue; }
226
+ if (argument === "x") { view = "bsd"; selection = "own-any-tty"; continue; }
227
+ if (argument === "-a") { selection = "unix-a"; continue; }
228
+ if (argument === "-x") { selection = "own-any-tty"; continue; }
229
+ if (argument === "-o" || argument === "--format") { fields = argv[++index] ?? usage(); view = "basic"; continue; }
230
+ if (argument.startsWith("--format=")) { fields = argument.slice(9); view = "basic"; continue; }
231
+ const combined = /^-([eA]*)o(.*)$/.exec(argument);
232
+ if (combined) { fields = combined[2] || argv[++index] || usage(); view = "basic"; if (combined[1]) all = true; continue; }
233
+ usage();
234
+ }
235
+ const rows = selectProcesses(readProcessSnapshot().processes, { all, selection });
236
+ const presets = {
237
+ full: ["user", "pid", "ppid", "c", "stime", "tty", "time", "args"],
238
+ "extra-full": ["user", "pid", "ppid", "c", "sz", "rss", "psr", "stime", "tty", "time", "args"],
239
+ aux: ["user", "pid", "%cpu", "%mem", "vsz", "rss", "tty", "stat", "start", "time", "args"],
240
+ bsd: ["pid", "tty", "stat", "time", "args"], default: ["pid", "tty", "time", "comm"],
241
+ };
242
+ try {
243
+ const headers = view === "default" ? { tty: "TTY", comm: "CMD" }
244
+ : view === "full" || view === "extra-full" ? { user: "UID", tty: "TTY", args: "CMD" }
245
+ : view === "aux" ? { tty: "TTY", start: "START" }
246
+ : view === "bsd" ? { tty: "TTY" } : {};
247
+ for (const line of fields
248
+ ? formatProcessTable(rows, fields)
249
+ : formatPresetTable(rows, presets[view], headers)) console.log(line);
250
+ } catch (error) { throw new Error(`ps: ${error.message}`); }
251
+ }
252
+
253
+ if (import.meta.main) {
254
+ try { await main(process.argv.slice(2)); }
255
+ catch (error) { console.error(error.message); process.exit(1); }
256
+ }