buninu-linux 0.3.0 → 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 (34) hide show
  1. package/README.md +179 -22
  2. package/fetch-alpine.sh +9 -4
  3. package/initramfs/bin/ip +2 -1
  4. package/initramfs/bin/mount +2 -1
  5. package/initramfs/bin/poweroff +6 -27
  6. package/initramfs/bin/ps +256 -0
  7. package/initramfs/bin/reboot +10 -0
  8. package/initramfs/bin/tar +169 -0
  9. package/initramfs/bin/top +129 -0
  10. package/initramfs/bin/umount +2 -1
  11. package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
  12. package/initramfs/buninu/apps/bunmsh/README.md +7 -0
  13. package/initramfs/buninu/apps/bunmsh/package.json +1 -1
  14. package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
  15. package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
  16. package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
  17. package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
  18. package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
  19. package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
  20. package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
  21. package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
  22. package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
  23. package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
  24. package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
  25. package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
  26. package/initramfs/buninu/package.json +1 -1
  27. package/initramfs/lib/dlopen.js +0 -9
  28. package/initramfs/lib/document.js +6 -0
  29. package/initramfs/lib/power.js +35 -0
  30. package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
  31. package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
  32. package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
  33. package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
  34. package/package.json +2 -2
package/README.md CHANGED
@@ -25,14 +25,17 @@
25
25
 
26
26
  ---
27
27
 
28
+ - [Table of contents](#contents)
29
+ - .
28
30
  - Still in the early stages
29
31
  - [Video here](https://www.reddit.com/r/bun/comments/1wkpraj/buninu_linux_a_distro_with_bun_as_pid_1): Booted successfully on real x86-64 UEFI hardware with `--real`:
30
32
  * Bun reaches its interactive REPL
31
33
  * start() starts the Buninu userspace shell
32
34
  * `cfg.disk` + shell `mount` mounts local disks
33
- * `cfg.net` loads common wired NIC drivers
35
+ * `cfg.net` loads common wired NIC and Android USB-tethering drivers
36
+ * Android phone USB tethering over RNDIS has been tested successfully, allowing Buninu Linux to access the Internet through an Android phone
34
37
  * IP addresses and routes are configured manually because the image does not yet include a DHCP client.
35
- * The bundled `jmi` editor, `jsmdcui` app runtime, and local JavaScript execution also work.
38
+ * The bundled `jmi` editor, jsmdcui editor/terminal multitasking, and local JavaScript execution also work.
36
39
 
37
40
  ---
38
41
 
@@ -130,20 +133,20 @@ whitespace-only line prints the welcome and current `cfg.*` getter list again.
130
133
  Load only the subsystem needed, then enter the Buninu shell:
131
134
 
132
135
  ```js
133
- cfg.net // wired NIC drivers
136
+ cfg.net // wired NIC and Android USB-tethering drivers
134
137
  cfg.disk // SATA/NVMe/USB storage drivers and detected block devices
135
138
  cfg.power // battery, AC, button and thermal drivers
136
139
  start()
137
140
  ```
138
141
 
139
142
  There is no DHCP client yet; configure a detected wired interface with `ip` as
140
- described under [Real hardware](#real-hardware). Run `poweroff` for a synced
143
+ described under [Basic configuration](#basic-configuration). Run `poweroff` for a synced
141
144
  shutdown. After editing the initramfs, `bun ./index.js -b --real --export` rebuilds the
142
145
  image; `-h` lists all flags.
143
146
 
144
- ## Running
147
+ ## Using Buninu Linux
145
148
 
146
- ### Real hardware
149
+ ### Basic configuration
147
150
 
148
151
  Real hardware is the primary target. Boot the USB drive with Secure Boot
149
152
  disabled unless you have signed the UKI yourself. The physical display and USB
@@ -157,7 +160,7 @@ cfg.disk
157
160
  cfg.power
158
161
  ```
159
162
 
160
- `cfg.net` loads the packaged wired-network drivers and prints detected
163
+ `cfg.net` loads the packaged wired-network and Android USB-tethering drivers and prints detected
161
164
  interfaces and MAC addresses. There is no DHCP client yet, so configure the
162
165
  interface manually after entering the Buninu shell with `start()`:
163
166
 
@@ -178,6 +181,8 @@ mount -fv /dev/sda1 /mnt
178
181
 
179
182
  Run `poweroff` for a synced shutdown.
180
183
 
184
+ ### Editor quick start
185
+
181
186
  The bundled `jmi` terminal editor works without a network connection:
182
187
 
183
188
  ```sh
@@ -203,19 +208,107 @@ After leaving the editor with `Ctrl-Q`, run it locally:
203
208
  bun hlw.js
204
209
  ```
205
210
 
206
- - Sometimes `Enters` in some scenarios don't directly work; if that's the case, try `Ctrl-J` or `Ctrl-M`
211
+ If `Enter` is not recognized in a particular terminal, try `Ctrl-J` or
212
+ `Ctrl-M`.
213
+
214
+ ### Panes, terminals, and tabs
215
+
216
+ `jmi` and jsmdcui can keep editors and terminal sessions open together. Press
217
+ `Ctrl-E`, type `term`, and press `Enter` to open the default Buninu shell in a
218
+ terminal pane. `term COMMAND` starts a specific command instead. Use `vsplit`,
219
+ `hsplit`, or `tab` from the same `Ctrl-E` command prompt to create another
220
+ editor pane or tab; each command optionally accepts a filename. From an editor
221
+ pane, `Ctrl-T` is the direct shortcut for a new empty tab.
222
+
223
+ #### Pane and tab controls
224
+
225
+ | Input | Result |
226
+ | --- | --- |
227
+ | Click a pane | Focus that editor or terminal pane. |
228
+ | `Ctrl-T` in an editor pane | Open a new empty tab. |
229
+ | `Ctrl-W` once | Move to the next pane in the current tab, from either an editor or terminal pane. |
230
+ | `Alt-T` | Move to the next tab. |
231
+ | **`Ctrl-W` twice quickly** | **Important escape hatch:** move to the next tab like `Alt-T`, or create an editor tab when needed. See the detailed explanation below. |
232
+ | `Esc` in a terminal pane | Close that terminal pane and return to its previous editor buffer |
233
+ | `Ctrl-Q` or `Alt-Q` in an editor pane | Close the current editor UI. |
234
+
235
+ **Why this escape hatch is important:** A terminal pane must forward almost every key combination unchanged to the
236
+ shell or application running inside it. `Ctrl-W` is therefore the one
237
+ multitasking escape key that jsmdcui intercepts: it lets you leave the active
238
+ terminal without terminating the work inside it. Press it once to move to the
239
+ next pane in the current tab. Press it twice quickly to move to the next tab.
240
+
241
+ The double press is especially important in an all-terminal workspace. When
242
+ the current tab is the rightmost tab and every existing tab contains only
243
+ terminal panes, it creates a new editor tab instead of wrapping back to the
244
+ first terminal tab. In every other case it performs the normal next-tab cycle.
245
+ Use `Esc` only when you actually want to close the current terminal pane.
246
+
247
+ ### Using Bun Modern Shell
248
+
249
+ bunmsh is the dependency-free, mksh-inspired shell bundled with Buninu. Its
250
+ main features are:
251
+
252
+ - Lines beginning with `Bun.*` run as JavaScript; returned values are printed
253
+ and promises are awaited automatically. Use `Bun.e;` or `Bun.e,` for
254
+ arbitrary JavaScript.
255
+ - Lightweight cwd tabs keep several working directories ready. In bunmsh,
256
+ `Ctrl-T` creates or cycles right through tabs, while `Alt-T` cycles left.
257
+ * Also useful cmds: `tab n`, `tab c`(Alt-C)
258
+ - Shell variables and JavaScript share a live variable table by `$.`, and `Bun.sha`
259
+ can retain JavaScript values across commands and cwd tabs.
260
+ - Saved history, Bash/Fish history import, completions, and ghost suggestions
261
+ are available interactively.
262
+ - `serve [directory]` starts a browsable HTTP file server. Together with
263
+ Android USB tethering, it provides a simple way to transfer files between
264
+ Buninu Linux and the connected phone.
265
+ - Cross-platform builtins and PATH-fallback commands.
266
+ - `Ctrl-U` removes and saves the text before the cursor; press it again at the
267
+ beginning of the line to restore it. `Ctrl-K` independently toggles the text
268
+ after the cursor when at the end of the line.
269
+ - Unquoted pathname patterns use `Bun.Glob` and follow directory symbolic
270
+ links used as intermediate path components, as traditional shells do. This
271
+ matters for sysfs, where class entries are normally links. For example:
207
272
 
208
- ## Commands inside /bin
273
+ ```sh
274
+ ls /sys/class/net/*/device
275
+ ```
276
+
277
+ For a readable process overview, run:
278
+
279
+ ```sh
280
+ pspac
281
+ ```
282
+
283
+ `pspac` shows the real `PID COMMAND` process table and highlights each command
284
+ line as shell syntax with micro's Monokai colours. Leading directories are
285
+ dimmed so the program name stands out. Use `pspa` for the same table as plain
286
+ text.
287
+
288
+ See the [bunmsh repository](https://github.com/jjtseng93/bunmsh) for its full
289
+ syntax, interactive controls, builtins, and current compatibility details.
209
290
 
210
- Besides `bun` (and `sh`/`node` pointing at it), `/bin` includes four commands
211
- implemented as Bun scripts:
291
+ ## Commands inside /bin
212
292
 
213
293
  | command | does | manual |
214
294
  | --- | --- | --- |
215
295
  | `mount` | `mount(2)` with type detection, `-o` parsing, `LABEL=`/`UUID=`, bind/move/remount; loads required filesystem and disk modules | `mount --help` → `/usr/share/doc/buninu-linux/mount.md` |
216
296
  | `umount` | `umount2(2)` with `-l`, `-f`, `-R` | `umount --help` |
217
297
  | `ip` | iproute2 grammar over `SIOC*` ioctls and `/proc/net`: `link`, `addr`, `route`, `neigh` | `ip --help` |
298
+ | `ps` | composable process fields from `/proc`; also exports its snapshot and single-row formatter for other commands | `ps --help` |
299
+ | `top` | periodically refreshes the process snapshot and rows supplied by `ps` | `top --help` |
218
300
  | `poweroff` | sync pending writes and power off through the Linux reboot system call | `poweroff --help` |
301
+ | `reboot` | sync pending writes and restart through the shared Linux reboot logic | `reboot --help` |
302
+ | `tar` | create, extract, or list tar archives with gzip and zstd compression through `Bun.Archive` | `tar --help` |
303
+
304
+ Besides `bun` (and `sh`/`node` pointing at it), `/bin` includes eight commands
305
+ implemented as Bun scripts.
306
+
307
+ The small `tar` follows the current `Bun.Archive` boundary: extraction
308
+ restores directories and symbolic links, while hard links are skipped by Bun
309
+ 1.4.3. Creation stores regular files but not Unix metadata, links, or empty
310
+ directories, and listing reports regular files only. It supports gzip and
311
+ Bun's built-in zstd, not xz or bzip2.
219
312
 
220
313
  `--help` renders each Markdown manual in the terminal. Common examples:
221
314
 
@@ -224,6 +317,8 @@ mount /dev/sda1 /mnt --mkdir
224
317
  mount -fv /dev/sda1 /mnt
225
318
  mount -t tmpfs -o size=64M tmpfs /tmp/x
226
319
  mount -t ntfs3 -o force /dev/sda3 /mnt/windows
320
+ umount /mnt
321
+ umount -R /mnt
227
322
 
228
323
  ip link set eth0 up
229
324
  ip addr add 192.168.1.50/24 dev eth0
@@ -233,7 +328,23 @@ ip -br addr && ip route
233
328
  ip addr replace 192.168.1.50/24 dev eth0
234
329
  ip route replace default via 192.168.1.1
235
330
 
331
+ ps -eo pid,args
332
+ ps -ef
333
+ ps aux
334
+ pspac
335
+ top
336
+ top -b -n 1
337
+
236
338
  poweroff
339
+ reboot
340
+
341
+ tar cvf /tmp/buninu.tar README.md package.json
342
+ tar tvf /tmp/buninu.tar
343
+ mkdir -p /tmp/buninu-copy
344
+ tar xvf /tmp/buninu.tar -C /tmp/buninu-copy
345
+
346
+ # Add z to create a gzip-compressed archive
347
+ tar czvf /tmp/buninu.tar.gz README.md package.json
237
348
  ```
238
349
 
239
350
  ## Environment and dependencies
@@ -340,6 +451,8 @@ bun ./index.js -fb --real --export
340
451
  `index.js` is the single entry point (`npm run fetch` and `npm run pack` call
341
452
  it too). Its physical-image stages run as fetch → build:
342
453
 
454
+ #### Build flags
455
+
343
456
  | flag | runs | use it when |
344
457
  | --- | --- | --- |
345
458
  | `-f`, `--fetch` | `fetch-alpine.sh`, `fetch-bun.sh` | first clone, or after bumping a pinned version |
@@ -356,8 +469,8 @@ bun x buninu-linux -fb --real --export
356
469
 
357
470
  `--linux-lts` selects Alpine's general-purpose LTS kernel. `--real` implies
358
471
  `--linux-lts` and builds for physical hardware: it makes `tty0` the primary
359
- console and includes xHCI/USB HID, common wired network, storage and ACPI power
360
- modules needed by typical laptops and desktops.
472
+ console and includes xHCI/USB HID, common wired NIC, Android USB-tethering,
473
+ storage and ACPI power modules needed by typical laptops and desktops.
361
474
 
362
475
  For real hardware, `-fb --real` is the complete image pipeline and
363
476
  `-b --real` is the edit-and-rebuild loop. Before starting, `index.js` checks
@@ -396,6 +509,8 @@ The primary `bun ./index.js -fb --real` pipeline runs the steps below in order;
396
509
  each writes files the next one reads. When selected, `--export` runs afterward
397
510
  and copies the final `vda.img` to the invocation directory.
398
511
 
512
+ ##### Pipeline inputs and outputs
513
+
399
514
  | script | reads | writes |
400
515
  | --- | --- | --- |
401
516
  | `fetch-alpine.sh` | Alpine CDN | `kernel/vmlinuz-lts`, `kernel/linuxx64.efi.stub`, musl, and the selected network, storage, input, power and filesystem modules with trimmed module indexes |
@@ -423,6 +538,8 @@ boot.
423
538
  `build-uki.sh` appends sections to systemd's `linuxx64.efi.stub` with
424
539
  `objcopy`, at hand-picked VMAs because `objcopy` will not lay them out for you:
425
540
 
541
+ ##### UKI sections
542
+
426
543
  | section | offset from image base | contents | size |
427
544
  | --- | --- | --- | --- |
428
545
  | `.text` | +0x0 | the stub itself | 66 KB |
@@ -589,7 +706,7 @@ Which modules the image carries is the list in `fetch-alpine.sh`; the
589
706
  `select_module` helper there pulls in dependencies from Alpine's
590
707
  `modules.dep`, and `--real` adds common storage paths (`sd_mod`, `ahci`,
591
708
  `ata_generic`, `pata_acpi`, `nvme`, Intel `vmd`, `usb-storage` and `uas`),
592
- the wired NICs and the ACPI power drivers. Alpine's
709
+ the wired NICs, Android USB-tethering drivers and the ACPI power drivers. Alpine's
593
710
  kernels build all of these as modules; a self-built kernel with them `=y`
594
711
  works the same way, the loaders just find nothing to load.
595
712
 
@@ -685,8 +802,10 @@ loads the packaged SATA/PATA/SCSI, NVMe/VMD and USB storage stacks so
685
802
  their disks and partitions appear in `/dev`, then lists `/sys/class/block`. `cfg.all`
686
803
  unconditionally attempts every packaged module and recursively loads its
687
804
  declared dependencies. `--real` ships Intel `e1000`/`e1000e`/`igb`/`igc`,
688
- Realtek `r8169`, Atheros `alx`, Broadcom `tg3` and the `r8152`/`ax88179_178a`/
689
- `cdc_ether` USB dongles. `cfg.power` loads the ACPI
805
+ Realtek `r8169`, Atheros `alx`, Broadcom `tg3`, common USB dongles, and
806
+ `rndis_host`/`cdc_ether`/`cdc_ncm`/`cdc_eem`/`cdc_subset`/`zaurus` for Android
807
+ USB tethering. `cfg.net` unconditionally attempts all of these packaged
808
+ network modules. `cfg.power` loads the ACPI
690
809
  `battery`, `ac`, `button` and `thermal` modules through `/lib/modprobe.js`
691
810
  and lists `/sys/class/power_supply` (a desktop without a battery simply
692
811
  reports nothing registered). Nothing loads them at boot. PID 1 also installs
@@ -694,12 +813,48 @@ reports nothing registered). Nothing loads them at boot. PID 1 also installs
694
813
  `Promise.reject()` at the prompt makes Bun exit, which is a kernel panic
695
814
  (`Attempted to kill init!`).
696
815
 
697
- The REPL exposes a global `bunmsh()` function. Calling it creates
698
- `/tmp/bunmsh-runtime`, installs the pinned `bunmsh@0.3.6` package there, and
699
- directly runs its `src/main.js` with a small explicit environment. The calls
700
- are synchronous so bunmsh exclusively owns the terminal until `exit` returns
701
- to the Bun REPL. `start()` launches Buninu with the current process environment
702
- preserved, while overriding `PATH` and `HOME` for the userspace session.
816
+ `start()` launches the bundled Buninu userspace and its bunmsh (Bun Modern Shell) with
817
+ the current process environment preserved, while overriding `PATH` and `HOME`
818
+ for the userspace session.
819
+
820
+ ## Contents
821
+
822
+ - [Quick start](#quick-start)
823
+ * [Steps](#steps)
824
+ + [Build directly with bun x](#build-directly-with-bun-x)
825
+ + [Or build from a source checkout](#or-build-from-a-source-checkout)
826
+ + [Write the image to a USB drive](#write-the-image-to-a-usb-drive)
827
+ + [Booting from the USB drive](#booting-from-the-usb-drive)
828
+ - [Using Buninu Linux](#using-buninu-linux)
829
+ * [Basic configuration](#basic-configuration)
830
+ * [Editor quick start](#editor-quick-start)
831
+ * [Panes, terminals, and tabs](#panes-terminals-and-tabs)
832
+ + [Pane and tab controls](#pane-and-tab-controls)
833
+ * [Using Bun Modern Shell](#using-bun-modern-shell)
834
+ - [Commands inside /bin](#commands-inside-bin)
835
+ - [Environment and dependencies](#environment-and-dependencies)
836
+ * [Build environment](#build-environment)
837
+ * [Buninu Linux (Section 2)](#buninu-linux-section-2)
838
+ * [Hello world (Section 1)](#hello-world-section-1)
839
+ * [Retired native bootstrap](#retired-native-bootstrap)
840
+ - [Implementation details](#implementation-details)
841
+ * [1. Hello world](#1-hello-world)
842
+ * [2. Buninu Linux – Bun as PID 1](#2-buninu-linux-bun-as-pid-1)
843
+ + [Build flags](#build-flags)
844
+ + [Build pipeline](#build-pipeline)
845
+ - [Pipeline inputs and outputs](#pipeline-inputs-and-outputs)
846
+ + [UKI layout](#uki-layout)
847
+ - [UKI sections](#uki-sections)
848
+ + [Disk layout](#disk-layout)
849
+ + [Real-hardware boot details](#real-hardware-boot-details)
850
+ + [QEMU development](#qemu-development)
851
+ - [QEMU networking](#qemu-networking)
852
+ + [Command implementation](#command-implementation)
853
+ + [Reaching JavaScript with nothing mounted](#reaching-javascript-with-nothing-mounted)
854
+ + [What init.js does](#what-initjs-does)
855
+ - [Authorship](#authorship)
856
+ - [License](#license)
857
+ * [Bundled component licenses](#bundled-component-licenses)
703
858
 
704
859
  ## Authorship
705
860
 
@@ -721,6 +876,8 @@ The repository also commits four third-party platform binaries under
721
876
  `initramfs/lib/`, and the built image redistributes several more. Each stays
722
877
  under its own terms, with the full texts in [`LICENSES/`](LICENSES/):
723
878
 
879
+ ### Bundled component licenses
880
+
724
881
  | Component | License | Where |
725
882
  |---|---|---|
726
883
  | musl (`ld-musl-x86_64.so.1`, `libc.musl-x86_64.so.1`) | MIT | committed |
package/fetch-alpine.sh CHANGED
@@ -79,10 +79,15 @@ if [ "${REAL_MACHINE:-}" = 1 ]; then
79
79
  select_module "$module"
80
80
  done
81
81
  # Wired NICs: Intel (e1000/e1000e/igb/igc), Realtek (r8169 plus its PHY
82
- # driver), Qualcomm Atheros (alx), Broadcom (tg3), and USB dongles
83
- # (Realtek r8152, ASIX ax88179, CDC Ethernet). cfg.net in init.js
84
- # matches them to hardware through the pci:/usb: lines of modules.alias.
85
- for module in e1000 e1000e igb igc r8169 realtek alx tg3 r8152 ax88179_178a cdc_ether; do
82
+ # driver), Qualcomm Atheros (alx), Broadcom (tg3), and USB dongles.
83
+ # Android USB tethering commonly presents RNDIS, CDC ECM/NCM/EEM, or an
84
+ # older CDC subset/zaurus-compatible gadget. Selecting the leaf drivers
85
+ # recursively includes usbnet, mii, usbcore and usb-common from
86
+ # modules.dep. cfg.net unconditionally attempts every packaged
87
+ # kernel/drivers/net module, then matches USB and PCI modaliases.
88
+ for module in e1000 e1000e igb igc r8169 realtek alx tg3 \
89
+ r8152 ax88179_178a cdc_ether rndis_host cdc_ncm cdc_eem \
90
+ cdc_subset zaurus; do
86
91
  select_module "$module"
87
92
  done
88
93
  fi
package/initramfs/bin/ip CHANGED
@@ -9,7 +9,8 @@
9
9
  // shown but not configured.
10
10
 
11
11
  import { readdirSync, readFileSync } from "node:fs";
12
- import { cString, ptr, withInetSocket, Errno, SysError, showDocument } from "../lib/dlopen.js";
12
+ import { cString, ptr, withInetSocket, Errno, SysError } from "../lib/dlopen.js";
13
+ import { showDocument } from "../lib/document.js";
13
14
 
14
15
  const fail = (message, status = 1) => {
15
16
  console.error(`ip: ${message}`);
@@ -10,7 +10,8 @@
10
10
  // is not there yet.
11
11
 
12
12
  import { existsSync, mkdirSync, openSync, readSync, closeSync, readdirSync, readFileSync } from "node:fs";
13
- import { libc, cString, ptr, errno, strerror, Errno, SysError, showDocument } from "../lib/dlopen.js";
13
+ import { libc, cString, ptr, errno, strerror, Errno, SysError } from "../lib/dlopen.js";
14
+ import { showDocument } from "../lib/document.js";
14
15
  import { modprobe, tryModprobe, modulesDirectory, ModuleNotFound } from "../lib/modprobe.js";
15
16
 
16
17
  const fail = (message, status = 32) => {
@@ -1,31 +1,10 @@
1
1
  #!/bin/bun
2
2
  // poweroff — sync filesystems and power off through Linux reboot(2).
3
3
 
4
- import { libc, check, showDocument } from "../lib/dlopen.js";
4
+ import { LINUX_REBOOT_CMD_POWER_OFF, runPowerCommand } from "../lib/power.js";
5
5
 
6
- const LINUX_REBOOT_CMD_POWER_OFF = 0x4321fedc;
7
- const arguments_ = process.argv.slice(2);
8
-
9
- if (arguments_.length === 1 && (arguments_[0] === "-h" || arguments_[0] === "--help")) {
10
- await showDocument("poweroff");
11
- process.exit(0);
12
- }
13
-
14
- if (arguments_.length !== 0) {
15
- console.error("Usage: poweroff [-h | --help]");
16
- process.exit(1);
17
- }
18
-
19
- console.log("poweroff: syncing filesystems");
20
- libc.symbols.sync();
21
- console.log("poweroff: powering off");
22
- try {
23
- check("reboot(LINUX_REBOOT_CMD_POWER_OFF)", libc.symbols.reboot(LINUX_REBOOT_CMD_POWER_OFF));
24
- } catch (error) {
25
- console.error(`poweroff: ${error.message}`);
26
- process.exit(1);
27
- }
28
-
29
- // A successful reboot(2) never returns.
30
- console.error("poweroff: reboot returned without powering off");
31
- process.exit(1);
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
+ }