Skip to content

Commit 2d5a02c

Browse files
recovery environment
1 parent dd723b5 commit 2d5a02c

6 files changed

Lines changed: 197 additions & 10 deletions

File tree

src/kernel.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface SpawnResult {
2727
}
2828

2929
export interface UserspaceKernel {
30+
readonly privileged: boolean;
3031
get_program_registry(): UserspaceProgramRegistry;
3132
get_sound_registry(): SoundRegistry;
3233
get_fs(): UserspaceFileSystem;
@@ -64,6 +65,10 @@ export class Kernel {
6465

6566
#init_program_name: string | null = null;
6667

68+
get privileged(): boolean {
69+
return true;
70+
}
71+
6772
get panicked(): boolean {
6873
return this.#panicked;
6974
}
@@ -265,7 +270,9 @@ export class Kernel {
265270
// run init program
266271
try {
267272
const init = this.spawn(init_program, init_args, undefined, true);
273+
268274
this.#init_program_name = init_program;
275+
this.#term.focus();
269276

270277
if (on_init_spawned) {
271278
on_init_spawned(this).catch((e) => {
@@ -404,6 +411,7 @@ export class Kernel {
404411
const fs_proxy = AbstractFileSystem.create_userspace_proxy(kernel_fs);
405412

406413
Object.defineProperties(proxy, {
414+
privileged: { value: false, enumerable: true },
407415
get_program_registry: { value: () => prog_reg_proxy, enumerable: true },
408416
get_sound_registry: { value: () => self.get_sound_registry(), enumerable: true },
409417
get_fs: { value: () => fs_proxy, enumerable: true },

src/programs/@ALL.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { default as ignition } from "./core/ignition";
22
export { default as jetty } from "./core/jetty";
33
export { default as ash } from "./core/ash";
44
export { default as default_privilege_agent } from "./core/default_privilege_agent";
5+
export { default as recovery } from "./core/recovery";
56

67
export { default as help } from "./help";
78
export { default as shutdown } from "./shutdown";

src/programs/core/ash/index.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import {make_read_line_key_handlers, make_read_line_printable_handler} from "./k
77
export default {
88
name: "ash",
99
description: "A shell.",
10-
usage_suffix: "[--login]",
10+
usage_suffix: "[--login] [--no-scripts]",
1111
arg_descriptions: {
1212
"Arguments:": {
13-
"--login": "Start the shell as a login shell. Don't pass this flag manually, it's handled by the system."
13+
"--login": "Start the shell as a login shell. Don't pass this flag manually, it's handled by the system.",
14+
"--no-scripts": "Do not run any startup scripts like .ashrc or .ash_profile."
1415
}
1516
},
1617
compat: "2.0.0",
@@ -47,13 +48,13 @@ export default {
4748
}
4849

4950
// run .ash_profile, checking it exists again just in case (because why not)
50-
if (await fs.exists(absolute_profile)) {
51+
if (!args.includes("--no-scripts") && await fs.exists(absolute_profile)) {
5152
await shell.run_script(absolute_profile);
5253
}
5354
}
5455

5556
// run .ashrc, checking it exists again just in case (could be deleted in profile)
56-
if (await fs.exists(absolute_rc)) {
57+
if (!args.includes("--no-scripts") && await fs.exists(absolute_rc)) {
5758
await shell.run_script(absolute_rc);
5859
}
5960

@@ -67,8 +68,6 @@ export default {
6768
const read_line_key_handlers = make_read_line_key_handlers(shell, kernel);
6869
const read_line_printable_handler = make_read_line_printable_handler(shell);
6970

70-
term.focus();
71-
7271
while (running) {
7372
await shell.insert_prompt(true);
7473

src/programs/core/ignition/index.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { PrivilegedProgram } from "../../../types";
33
import {ServiceManager} from "./services";
44
import type {ProcessContext} from "../../../processes";
55

6+
import {ANSI} from "../../../term_ctl";
7+
68
interface IgnitionIPCMessageBase {
79
type: string;
810
}
@@ -60,6 +62,8 @@ export default {
6062
main: async (data) => {
6163
const { kernel, term, process } = data;
6264

65+
const {CURSOR} = ANSI;
66+
6367
// check if ignition is already running (only allowed to be PID 1)
6468
if (process.pid !== 1) {
6569
term.writeln("Cannot run ignition.");
@@ -213,13 +217,13 @@ export default {
213217
let error: Error | null = null;
214218
try {
215219
exit_code = await boot_target_proc.completion;
216-
boot_target_proc.process.kill(exit_code);
217220
} catch (e) {
218221
console.error(e);
219222
error = e as Error;
220223
exit_code = -1;
221224
}
222225

226+
boot_target_proc.process.kill(exit_code);
223227
console.log(`boot target ${boot_target} exited with code ${exit_code}`);
224228

225229
term.writeln(`Boot target ${boot_target} exited with code ${exit_code}!`);
@@ -236,9 +240,31 @@ export default {
236240
deaths_in_window++;
237241

238242
if (deaths_in_window >= 5) {
239-
term.writeln("Boot target has crashed too many times in a short period. Halting to prevent a crash loop.");
240-
term.writeln("Press any key to retry...");
241-
await term.wait_for_keypress();
243+
term.writeln("Boot target has crashed too many times in a short period.");
244+
term.writeln("Press R key to enter recovery mode, or any other key to retry...");
245+
term.write(CURSOR.invisible);
246+
247+
const key = await term.wait_for_keypress();
248+
if (key.key.toLowerCase() === "r") {
249+
term.writeln("Entering recovery mode...");
250+
251+
const recovery_proc = kernel.spawn("recovery", [], undefined, true);
252+
let recovery_exit_code: number;
253+
try {
254+
recovery_exit_code = await recovery_proc.completion;
255+
recovery_proc.process.kill(recovery_exit_code);
256+
} catch (e) {
257+
console.error(e);
258+
recovery_exit_code = -1;
259+
}
260+
261+
term.writeln(`Recovery environment exited with code ${recovery_exit_code}. Retrying boot target...`);
262+
} else {
263+
term.writeln("Retrying boot target...");
264+
}
265+
266+
term.write(CURSOR.visible);
267+
242268
deaths_in_window = 0;
243269
window_start = null;
244270
}

src/programs/core/jetty.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ export default {
9898
term.write(ANSI.CURSOR.visible);
9999

100100
term.reset();
101+
102+
// TODO: add recovery logic here too
101103
}
102104

103105
return final_code;

src/programs/core/recovery.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type {PrivilegedProgram} from "../../types";
2+
3+
import {ANSI, NEWLINE} from "../../term_ctl";
4+
5+
export default {
6+
name: "recovery",
7+
description: "Emergency recovery environment",
8+
usage_suffix: "",
9+
arg_descriptions: {},
10+
hide_from_help: true,
11+
compat: "2.0.0",
12+
main: async (data) => {
13+
const { kernel, term } = data;
14+
15+
const {CURSOR} = ANSI;
16+
17+
if (!kernel.privileged) {
18+
term.writeln("Recovery requires privileged environment.");
19+
return 1;
20+
}
21+
22+
let running = true;
23+
while (running) {
24+
term.reset();
25+
26+
term.writeln("RECOVERY ENVIRONMENT");
27+
term.writeln("===================");
28+
term.write(NEWLINE);
29+
term.writeln("1. Reboot");
30+
term.writeln("2. Privileged ash shell");
31+
term.writeln("3. Reset bootloader and reboot");
32+
term.writeln("4. Wipe filesystem and reboot");
33+
term.write(NEWLINE);
34+
term.writeln("X: Exit recovery");
35+
term.write(NEWLINE);
36+
term.writeln("Press the corresponding key to select an option.");
37+
38+
if (typeof window !== "undefined") {
39+
term.writeln(`Recovery also available at ${window.location.origin}/recover_fs`);
40+
}
41+
42+
term.write(CURSOR.invisible);
43+
44+
const key = await term.wait_for_keypress();
45+
46+
switch (key.key.toLowerCase()) {
47+
case "1":
48+
term.writeln(NEWLINE + "Rebooting...");
49+
window.location.reload();
50+
break;
51+
case "2": {
52+
term.writeln(NEWLINE + "Starting privileged ash shell...");
53+
term.write(CURSOR.visible);
54+
55+
// TODO: this doesnt make much difference being privileged as the programs are separate processes
56+
let exit_code: number;
57+
const shell = kernel.spawn("ash", ["--no-scripts"], undefined, true);
58+
try {
59+
exit_code = await shell.completion;
60+
} catch (e) {
61+
exit_code = -1;
62+
term.writeln("Error in privileged shell:");
63+
term.writeln(e);
64+
}
65+
66+
shell.process.kill(exit_code)
67+
}
68+
break;
69+
case "3": {
70+
term.writeln("Are you sure you want to reset the bootloader? This will clear your choice of init system, boot target, default shell, and privilege agent but retains your files.");
71+
term.writeln("Press Y to confirm, or any other key to cancel.");
72+
73+
const confirm_key = await term.wait_for_keypress();
74+
if (confirm_key.key.toLowerCase() !== "y") {
75+
term.writeln("Bootloader reset cancelled.");
76+
break;
77+
}
78+
79+
term.writeln(NEWLINE + "Resetting bootloader...");
80+
81+
// delete /boot/init, /etc/boot_target, /etc/default_shell, /sys/privilege_agent
82+
const fs = kernel.get_fs();
83+
try {
84+
await fs.delete_file("/boot/init");
85+
} catch (e) {
86+
term.writeln("Warning: Failed to delete /boot/init");
87+
term.writeln(e);
88+
}
89+
90+
try {
91+
await fs.delete_file("/etc/boot_target");
92+
} catch (e) {
93+
term.writeln("Warning: Failed to delete /etc/boot_target");
94+
term.writeln(e);
95+
}
96+
97+
try {
98+
await fs.delete_file("/etc/default_shell");
99+
} catch (e) {
100+
term.writeln("Warning: Failed to delete /etc/default_shell");
101+
term.writeln(e);
102+
}
103+
104+
try {
105+
await fs.delete_file("/sys/privilege_agent");
106+
} catch (e) {
107+
term.writeln("Warning: Failed to delete /sys/privilege_agent");
108+
term.writeln(e);
109+
}
110+
111+
term.writeln("Rebooting...");
112+
window.location.reload();
113+
}
114+
break;
115+
case "4": {
116+
term.writeln("Are you sure you want to erase the filesystem? This action cannot be undone.");
117+
term.writeln("Press Y to confirm, or any other key to cancel.");
118+
119+
const confirm_key = await term.wait_for_keypress();
120+
if (confirm_key.key.toLowerCase() !== "y") {
121+
term.writeln("Filesystem wipe cancelled.");
122+
break;
123+
}
124+
125+
term.writeln(NEWLINE + "Wiping filesystem...");
126+
127+
const fs = kernel.get_fs();
128+
try {
129+
await fs.erase_all();
130+
} catch (e) {
131+
term.writeln("Error: Failed to wipe filesystem.");
132+
term.writeln(e);
133+
}
134+
135+
term.writeln("Rebooting...");
136+
window.location.reload();
137+
}
138+
break;
139+
case "x":
140+
term.writeln(NEWLINE + "Exiting recovery.");
141+
running = false;
142+
break;
143+
default:
144+
// ignore other keys
145+
break;
146+
}
147+
}
148+
149+
return 0;
150+
}
151+
} as PrivilegedProgram;

0 commit comments

Comments
 (0)