buninu-linux 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +226 -22
- package/fetch-alpine.sh +9 -4
- package/graphics.md +213 -0
- package/initramfs/bin/bunterm +66 -0
- package/initramfs/bin/ip +2 -1
- package/initramfs/bin/mount +2 -1
- package/initramfs/bin/poweroff +6 -27
- package/initramfs/bin/ps +256 -0
- package/initramfs/bin/reboot +10 -0
- package/initramfs/bin/tar +169 -0
- package/initramfs/bin/top +129 -0
- package/initramfs/bin/umount +2 -1
- package/initramfs/buninu/CHANGELOG.md +40 -0
- package/initramfs/buninu/apps/bunmsh/CHANGELOG.md +36 -0
- package/initramfs/buninu/apps/bunmsh/README.md +7 -0
- package/initramfs/buninu/apps/bunmsh/package.json +1 -1
- package/initramfs/buninu/apps/bunmsh/src/fancy-ls.js +29 -14
- package/initramfs/buninu/apps/bunmsh/src/line-edit.js +43 -0
- package/initramfs/buninu/apps/bunmsh/src/main.js +15 -1
- package/initramfs/buninu/apps/bunmsh/src/mouse.js +29 -4
- package/initramfs/buninu/apps/bunmsh/src/shell.js +9 -1
- package/initramfs/buninu/apps/bunmsh/test/shell1.test.js +70 -0
- package/initramfs/buninu/apps/jsgotty/gotty.js +4 -3
- package/initramfs/buninu/apps/jsgotty/package.json +1 -1
- package/initramfs/buninu/apps/jsmdcui/CHANGELOG.md +10 -0
- package/initramfs/buninu/apps/jsmdcui/README.md +2 -0
- package/initramfs/buninu/apps/jsmdcui/package.json +1 -1
- package/initramfs/buninu/apps/jsmdcui/runtime/help/commands.md +4 -1
- package/initramfs/buninu/apps/jsmdcui/runtime/help/help.md +2 -0
- package/initramfs/buninu/apps/jsmdcui/src/index.js +40 -13
- package/initramfs/buninu/package.json +1 -1
- package/initramfs/init.js +6 -0
- 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/renderer.js +424 -0
- package/initramfs/lib/bunterm/terminal.js +203 -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 +14 -11
- package/initramfs/lib/document.js +6 -0
- package/initramfs/lib/fbdev.js +581 -0
- package/initramfs/lib/power.js +35 -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 +83 -0
- package/initramfs/usr/share/doc/buninu-linux/canvas.md +91 -0
- package/initramfs/usr/share/doc/buninu-linux/fbdev.md +113 -0
- package/initramfs/usr/share/doc/buninu-linux/ps.md +94 -0
- package/initramfs/usr/share/doc/buninu-linux/reboot.md +15 -0
- package/initramfs/usr/share/doc/buninu-linux/tar.md +73 -0
- package/initramfs/usr/share/doc/buninu-linux/top.md +23 -0
- 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,66 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// bunterm — a graphical terminal emulator on the Linux framebuffer.
|
|
3
|
+
|
|
4
|
+
import { showDocument } from "../lib/document.js";
|
|
5
|
+
import { runTerminal } from "../lib/bunterm/terminal.js";
|
|
6
|
+
|
|
7
|
+
const usage = () => {
|
|
8
|
+
console.error(`Usage:
|
|
9
|
+
bunterm [/dev/ttyN] [--font-size N] [--fb /dev/fb0] [-e command [argument...]]
|
|
10
|
+
bunterm -h | --help`);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const main = async () => {
|
|
14
|
+
const options = { argv: [] };
|
|
15
|
+
const arguments_ = process.argv.slice(2);
|
|
16
|
+
for (let index = 0; index < arguments_.length; index++) {
|
|
17
|
+
const argument = arguments_[index];
|
|
18
|
+
if (argument === "-h" || argument === "--help") {
|
|
19
|
+
await showDocument("bunterm");
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
if (argument === "-e") {
|
|
23
|
+
if (index + 1 >= arguments_.length) {
|
|
24
|
+
usage();
|
|
25
|
+
throw new Error("-e needs a command");
|
|
26
|
+
}
|
|
27
|
+
options.command = arguments_[index + 1];
|
|
28
|
+
options.argv = arguments_.slice(index + 2);
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const valued = (name) => {
|
|
32
|
+
const value = argument.startsWith(`${name}=`) ? argument.slice(name.length + 1) : arguments_[++index];
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
usage();
|
|
35
|
+
throw new Error(`${name} needs a value`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
if (argument === "--font-size" || argument.startsWith("--font-size=")) {
|
|
40
|
+
options.fontSize = Number(valued("--font-size"));
|
|
41
|
+
if (!Number.isFinite(options.fontSize) || options.fontSize < 6) throw new Error("--font-size must be a number of at least 6");
|
|
42
|
+
} else if (argument === "--line-height" || argument.startsWith("--line-height=")) {
|
|
43
|
+
options.lineHeight = Number(valued("--line-height"));
|
|
44
|
+
if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new Error("--line-height must be a positive number");
|
|
45
|
+
} else if (argument === "--fb" || argument.startsWith("--fb=")) {
|
|
46
|
+
options.device = valued("--fb");
|
|
47
|
+
} else if (argument === "--no-blink") {
|
|
48
|
+
options.cursorBlink = false;
|
|
49
|
+
} else if (argument.startsWith("/dev/")) {
|
|
50
|
+
options.console = argument;
|
|
51
|
+
} else {
|
|
52
|
+
usage();
|
|
53
|
+
throw new Error(`unexpected argument: ${argument}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return runTerminal(options);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let status = 1;
|
|
60
|
+
try {
|
|
61
|
+
status = await main();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`bunterm: ${error?.message ?? error}`);
|
|
64
|
+
}
|
|
65
|
+
// A pending console read would otherwise keep the process alive.
|
|
66
|
+
process.exit(status);
|
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
|
|
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}`);
|
package/initramfs/bin/mount
CHANGED
|
@@ -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
|
|
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) => {
|
package/initramfs/bin/poweroff
CHANGED
|
@@ -1,31 +1,10 @@
|
|
|
1
1
|
#!/bin/bun
|
|
2
2
|
// poweroff — sync filesystems and power off through Linux reboot(2).
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { LINUX_REBOOT_CMD_POWER_OFF, runPowerCommand } from "../lib/power.js";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
+
});
|
package/initramfs/bin/ps
ADDED
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// reboot — thin wrapper over the shared sync-and-reboot implementation.
|
|
3
|
+
|
|
4
|
+
import { LINUX_REBOOT_CMD_RESTART, runPowerCommand } from "../lib/power.js";
|
|
5
|
+
|
|
6
|
+
await runPowerCommand({
|
|
7
|
+
name: "reboot",
|
|
8
|
+
command: { name: "LINUX_REBOOT_CMD_RESTART", value: LINUX_REBOOT_CMD_RESTART },
|
|
9
|
+
action: "restarting",
|
|
10
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/bin/bun
|
|
2
|
+
// tar — a small tar/gzip/zstd CLI over Bun.Archive.
|
|
3
|
+
|
|
4
|
+
import { lstat, readdir } from "node:fs/promises";
|
|
5
|
+
import { basename, join, resolve } from "node:path";
|
|
6
|
+
import { showDocument } from "../lib/document.js";
|
|
7
|
+
|
|
8
|
+
const fail = (message) => {
|
|
9
|
+
console.error(`tar: ${message}`);
|
|
10
|
+
process.exit(2);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const usage = () => {
|
|
14
|
+
console.error(`Usage:
|
|
15
|
+
tar -cf ARCHIVE FILE...
|
|
16
|
+
tar -xf ARCHIVE [-C DIR] [MEMBER...]
|
|
17
|
+
tar -tf ARCHIVE [MEMBER...]
|
|
18
|
+
tar -h | --help
|
|
19
|
+
|
|
20
|
+
Try 'tar --help' for the full manual.`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const options = { operation: null, archive: null, directory: ".", compression: null, verbose: false, operands: [] };
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
if (argv.length === 0) usage();
|
|
27
|
+
const take = (index, option) => {
|
|
28
|
+
if (index + 1 >= argv.length) fail(`${option} requires an argument`);
|
|
29
|
+
return argv[index + 1];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
for (let index = 0; index < argv.length; index++) {
|
|
33
|
+
const argument = argv[index];
|
|
34
|
+
const oldStyleOptions = index === 0 && /^[cxtrvfz]+$/.test(argument)
|
|
35
|
+
&& /[cxtr]/.test(argument);
|
|
36
|
+
if (argument === "--") { options.operands.push(...argv.slice(index + 1)); break; }
|
|
37
|
+
if (argument === "-h" || argument === "--help") {
|
|
38
|
+
await showDocument("tar");
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (argument === "--zstd") { options.compression = "zstd"; continue; }
|
|
42
|
+
if (argument === "--gzip") { options.compression = "gzip"; continue; }
|
|
43
|
+
if (argument === "-C" || argument === "--directory") {
|
|
44
|
+
options.directory = take(index++, argument);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (argument.startsWith("--directory=")) {
|
|
48
|
+
options.directory = argument.slice(12);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (argument === "-f" || argument === "--file") {
|
|
52
|
+
options.archive = take(index++, argument);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (argument.startsWith("--file=")) {
|
|
56
|
+
options.archive = argument.slice(7);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if ((argument.startsWith("-") && argument !== "-") || oldStyleOptions) {
|
|
60
|
+
const flags = oldStyleOptions ? argument : argument.slice(1);
|
|
61
|
+
for (let offset = 0; offset < flags.length; offset++) {
|
|
62
|
+
const flag = flags[offset];
|
|
63
|
+
if ("cxt".includes(flag)) {
|
|
64
|
+
if (options.operation && options.operation !== flag) fail("only one of -c, -x, or -t may be used");
|
|
65
|
+
options.operation = flag;
|
|
66
|
+
} else if (flag === "r") {
|
|
67
|
+
fail("-r append is not supported by Bun.Archive");
|
|
68
|
+
} else if (flag === "v") options.verbose = true;
|
|
69
|
+
else if (flag === "z") options.compression = "gzip";
|
|
70
|
+
else if (flag === "f") {
|
|
71
|
+
const attached = flags.slice(offset + 1);
|
|
72
|
+
options.archive = attached || take(index++, "-f");
|
|
73
|
+
break;
|
|
74
|
+
} else fail(`unknown option -- ${flag}`);
|
|
75
|
+
}
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
options.operands.push(argument);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!options.operation) fail("one of -c, -x, or -t is required");
|
|
82
|
+
if (!options.archive) fail("an archive must be specified with -f");
|
|
83
|
+
|
|
84
|
+
const archiveBytes = async () => {
|
|
85
|
+
const bytes = options.archive === "-"
|
|
86
|
+
? await Bun.stdin.bytes()
|
|
87
|
+
: await Bun.file(options.archive).bytes();
|
|
88
|
+
const zstd = options.compression === "zstd" || /\.(?:zst|tzst)$/i.test(options.archive);
|
|
89
|
+
return zstd ? Bun.zstdDecompressSync(bytes) : bytes;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const patterns = () => options.operands.flatMap((name) => {
|
|
93
|
+
const clean = name.replace(/^\.\//, "").replace(/\/$/, "");
|
|
94
|
+
return [clean, `${clean}/**`];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const verboseListing = (name, file) => {
|
|
98
|
+
const modified = Number.isFinite(file.lastModified) && file.lastModified > 0
|
|
99
|
+
? new Date(file.lastModified).toISOString().replace("T", " ").slice(0, 16)
|
|
100
|
+
: "---- -- -- --:--";
|
|
101
|
+
return `${String(file.size).padStart(12)} ${modified} ${name}`;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (options.operation === "x" || options.operation === "t") {
|
|
105
|
+
let archive;
|
|
106
|
+
try {
|
|
107
|
+
archive = new Bun.Archive(await archiveBytes());
|
|
108
|
+
} catch (error) {
|
|
109
|
+
fail(error?.message || String(error));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.operation === "t") {
|
|
113
|
+
try {
|
|
114
|
+
const files = await archive.files(options.operands.length ? patterns() : undefined);
|
|
115
|
+
for (const [name, file] of files) {
|
|
116
|
+
console.log(options.verbose ? verboseListing(name, file) : name);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
fail(error?.message || String(error));
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
try {
|
|
123
|
+
if (options.verbose) {
|
|
124
|
+
const files = await archive.files(options.operands.length ? patterns() : undefined);
|
|
125
|
+
for (const name of files.keys()) console.log(name);
|
|
126
|
+
}
|
|
127
|
+
await archive.extract(options.directory, options.operands.length ? { glob: patterns() } : undefined);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
fail(error?.message || String(error));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.operands.length === 0) fail("refusing to create an empty archive");
|
|
136
|
+
const root = resolve(options.directory);
|
|
137
|
+
const inputs = {};
|
|
138
|
+
|
|
139
|
+
const add = async (diskPath, archivePath) => {
|
|
140
|
+
const info = await lstat(diskPath);
|
|
141
|
+
if (info.isSymbolicLink()) fail(`${archivePath}: creating symbolic links is not supported by Bun.Archive`);
|
|
142
|
+
if (info.isDirectory()) {
|
|
143
|
+
for (const entry of await readdir(diskPath)) await add(join(diskPath, entry), `${archivePath}/${entry}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!info.isFile()) fail(`${archivePath}: unsupported file type`);
|
|
147
|
+
const name = archivePath.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
148
|
+
if (!name || name === ".." || name.startsWith("../") || name.includes("/../")) fail(`${archivePath}: unsafe archive path`);
|
|
149
|
+
// Bun 1.4.3 accepts Blob in ArchiveInput, but a lazy BunFile is currently
|
|
150
|
+
// archived as zero bytes. Materialise it until Bun.Archive handles BunFile
|
|
151
|
+
// contents directly.
|
|
152
|
+
inputs[name] = await Bun.file(diskPath).bytes();
|
|
153
|
+
if (options.verbose) console.error(name);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
for (const operand of options.operands) {
|
|
158
|
+
const diskPath = resolve(root, operand);
|
|
159
|
+
const name = operand.startsWith("/") ? basename(operand) : operand.replace(/\/$/, "");
|
|
160
|
+
await add(diskPath, name);
|
|
161
|
+
}
|
|
162
|
+
const archive = new Bun.Archive(inputs, options.compression === "gzip" ? { compress: "gzip" } : undefined);
|
|
163
|
+
const output = options.compression === "zstd"
|
|
164
|
+
? Bun.zstdCompressSync(await archive.bytes())
|
|
165
|
+
: archive;
|
|
166
|
+
await Bun.write(options.archive === "-" ? Bun.stdout : options.archive, output);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
fail(error?.message || String(error));
|
|
169
|
+
}
|