Skip to content

Commit 2db5d47

Browse files
recovery environment
1 parent dd723b5 commit 2db5d47

8 files changed

Lines changed: 201 additions & 13 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ollieos",
3-
"version": "2.1.0",
3+
"version": "2.1.1",
44
"description": "",
55
"main": "server.js",
66
"scripts": {

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, maybe add /etc/safe_mode_shell file to launch ash --no-scripts or similar
101103
}
102104

103105
return final_code;

src/programs/core/recovery.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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+
// TODO: bypass the privilege agent instead
57+
let exit_code: number;
58+
const shell = kernel.spawn("ash", ["--no-scripts"], undefined, true);
59+
try {
60+
exit_code = await shell.completion;
61+
} catch (e) {
62+
exit_code = -1;
63+
term.writeln("Error in privileged shell:");
64+
term.writeln(e);
65+
}
66+
67+
shell.process.kill(exit_code)
68+
}
69+
break;
70+
case "3": {
71+
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.");
72+
term.writeln("Press Y to confirm, or any other key to cancel.");
73+
74+
const confirm_key = await term.wait_for_keypress();
75+
if (confirm_key.key.toLowerCase() !== "y") {
76+
term.writeln("Bootloader reset cancelled.");
77+
break;
78+
}
79+
80+
term.writeln(NEWLINE + "Resetting bootloader...");
81+
82+
// delete /boot/init, /etc/boot_target, /etc/default_shell, /sys/privilege_agent
83+
const fs = kernel.get_fs();
84+
try {
85+
await fs.delete_file("/boot/init");
86+
} catch (e) {
87+
term.writeln("Warning: Failed to delete /boot/init");
88+
term.writeln(e);
89+
}
90+
91+
try {
92+
await fs.delete_file("/etc/boot_target");
93+
} catch (e) {
94+
term.writeln("Warning: Failed to delete /etc/boot_target");
95+
term.writeln(e);
96+
}
97+
98+
try {
99+
await fs.delete_file("/etc/default_shell");
100+
} catch (e) {
101+
term.writeln("Warning: Failed to delete /etc/default_shell");
102+
term.writeln(e);
103+
}
104+
105+
try {
106+
await fs.delete_file("/sys/privilege_agent");
107+
} catch (e) {
108+
term.writeln("Warning: Failed to delete /sys/privilege_agent");
109+
term.writeln(e);
110+
}
111+
112+
term.writeln("Rebooting...");
113+
window.location.reload();
114+
}
115+
break;
116+
case "4": {
117+
term.writeln("Are you sure you want to erase the filesystem? This action cannot be undone.");
118+
term.writeln("Press Y to confirm, or any other key to cancel.");
119+
120+
const confirm_key = await term.wait_for_keypress();
121+
if (confirm_key.key.toLowerCase() !== "y") {
122+
term.writeln("Filesystem wipe cancelled.");
123+
break;
124+
}
125+
126+
term.writeln(NEWLINE + "Wiping filesystem...");
127+
128+
const fs = kernel.get_fs();
129+
try {
130+
await fs.erase_all();
131+
} catch (e) {
132+
term.writeln("Error: Failed to wipe filesystem.");
133+
term.writeln(e);
134+
}
135+
136+
term.writeln("Rebooting...");
137+
window.location.reload();
138+
}
139+
break;
140+
case "x":
141+
term.writeln(NEWLINE + "Exiting recovery.");
142+
running = false;
143+
break;
144+
default:
145+
// ignore other keys
146+
break;
147+
}
148+
}
149+
150+
return 0;
151+
}
152+
} as PrivilegedProgram;

0 commit comments

Comments
 (0)