Skip to content

Commit eb05fbd

Browse files
committed
feat: add external mod loading
A package called "useapi" is introduced to provide a dynamic import system. This import system, rather than relying on the state of the filesystem, is populated as modules are installed into Puter's kernel. The "useapi" package is then used to add support for loading external mod directories as Puter kernel modules, making it possible to mod puter without any tooling.
1 parent fa7bec3 commit eb05fbd

File tree

7 files changed

+213
-4
lines changed

7 files changed

+213
-4
lines changed

packages/backend/src/CoreModule.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ class CoreModule extends AdvancedBase {
2424
async install (context) {
2525
const services = context.get('services');
2626
const app = context.get('app');
27-
await install({ services, app });
27+
const useapi = context.get('useapi');
28+
await install({ services, app, useapi });
2829
}
2930

3031
// Some services were created before the BaseService
@@ -40,9 +41,16 @@ class CoreModule extends AdvancedBase {
4041

4142
module.exports = CoreModule;
4243

43-
const install = async ({ services, app }) => {
44+
const install = async ({ services, app, useapi }) => {
4445
const config = require('./config');
4546

47+
useapi.withuse(() => {
48+
def('Service', require('./services/BaseService'));
49+
def('Module', AdvancedBase);
50+
51+
def('puter.middlewares.auth', require('./middleware/auth2'));
52+
});
53+
4654
// /!\ IMPORTANT /!\
4755
// For new services, put the import immediate above the
4856
// call to services.registerService. We'll clean this up

packages/backend/src/Kernel.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,20 @@
1818
*/
1919
const { AdvancedBase } = require("@heyputer/puter-js-common");
2020
const { Context } = require('./util/context');
21+
const BaseService = require("./services/BaseService");
22+
const useapi = require('useapi');
2123

2224
class Kernel extends AdvancedBase {
2325
constructor () {
2426
super();
2527

2628
this.modules = [];
29+
this.useapi = useapi();
30+
31+
this.useapi.withuse(() => {
32+
def('Module', AdvancedBase);
33+
def('Service', BaseService);
34+
});
2735
}
2836

2937
add_module (module) {
@@ -48,7 +56,8 @@ class Kernel extends AdvancedBase {
4856
const runtimeEnv = new RuntimeEnvironment({
4957
logger: bootLogger,
5058
});
51-
runtimeEnv.init();
59+
const environment = runtimeEnv.init();
60+
this.environment = environment;
5261

5362
// polyfills
5463
require('./polyfill/to-string-higher-radix');
@@ -89,6 +98,8 @@ class Kernel extends AdvancedBase {
8998
// app.set('services', services);
9099

91100
const root_context = Context.create({
101+
environment: this.environment,
102+
useapi: this.useapi,
92103
services,
93104
config,
94105
logger: this.bootLogger,
@@ -108,10 +119,14 @@ class Kernel extends AdvancedBase {
108119
async _install_modules () {
109120
const { services } = this;
110121

122+
// Internal modules
111123
for ( const module of this.modules ) {
112124
await module.install(Context.get());
113125
}
114126

127+
// External modules
128+
await this.install_extern_mods_();
129+
115130
try {
116131
await services.init();
117132
} catch (e) {
@@ -173,6 +188,34 @@ class Kernel extends AdvancedBase {
173188
await services.emit('boot.activation');
174189
await services.emit('boot.ready');
175190
}
191+
192+
async install_extern_mods_ () {
193+
const path_ = require('path');
194+
const fs = require('fs');
195+
196+
const mod_paths = this.environment.mod_paths;
197+
for ( const mods_dirpath of mod_paths ) {
198+
const mod_dirnames = fs.readdirSync(mods_dirpath);
199+
for ( const mod_dirname of mod_dirnames ) {
200+
const mod_path = path_.join(mods_dirpath, mod_dirname);
201+
if ( ! fs.lstatSync(mod_path).isDirectory() ) {
202+
continue;
203+
}
204+
205+
const mod_class = this.useapi.withuse(() => require(mod_path));
206+
const mod = new mod_class();
207+
if ( ! mod ) {
208+
continue;
209+
}
210+
211+
if ( mod.install ) {
212+
this.useapi.awithuse(async () => {
213+
await mod.install(Context.get());
214+
});
215+
}
216+
}
217+
}
218+
}
176219
}
177220

178221
module.exports = { Kernel };

packages/backend/src/boot/RuntimeEnvironment.js

+43-1
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,30 @@ const runtime_paths = ({ path_checks }) => ({ path_ }) => [
162162
},
163163
];
164164

165+
// Suitable mod paths in order of precedence.
166+
const mod_paths = ({ path_checks }) => ({ path_ }) => [
167+
{
168+
label: '$MOD_PATH',
169+
get path () { return process.env.MOD_PATH },
170+
checks: [
171+
path_checks.require_if_not_undefined,
172+
],
173+
},
174+
{
175+
path: '/var/puter/mods',
176+
checks: [
177+
path_checks.skip_if_not_exists,
178+
path_checks.env_not_set('NO_VAR_MODS'),
179+
],
180+
},
181+
{
182+
get path () {
183+
return path_.join(original_cwd, 'mods');
184+
},
185+
checks: [ path_checks.skip_if_not_exists ],
186+
},
187+
];
188+
165189
class RuntimeEnvironment extends AdvancedBase {
166190
static MODULES = {
167191
fs: require('node:fs'),
@@ -175,11 +199,12 @@ class RuntimeEnvironment extends AdvancedBase {
175199
this.path_checks = path_checks(this)(this.modules);
176200
this.config_paths = config_paths(this)(this.modules);
177201
this.runtime_paths = runtime_paths(this)(this.modules);
202+
this.mod_paths = mod_paths(this)(this.modules);
178203
}
179204

180205
init () {
181206
try {
182-
this.init_();
207+
return this.init_();
183208
} catch (e) {
184209
this.logger.error(e);
185210
print_error_help(e);
@@ -203,6 +228,12 @@ class RuntimeEnvironment extends AdvancedBase {
203228
[ this.path_checks.require_write_permission ]
204229
);
205230

231+
const mods_path_entry = this.get_first_suitable_path_(
232+
{ pathFor: 'mods', optional: true },
233+
this.mod_paths,
234+
[ this.path_checks.require_read_permission ],
235+
);
236+
206237
process.chdir(pwd_path_entry.path);
207238

208239
// Check for a valid config file in the config path
@@ -266,6 +297,16 @@ class RuntimeEnvironment extends AdvancedBase {
266297
// console.log(config.services);
267298
// console.log(Object.keys(config.services));
268299
// console.log({ ...config.services });
300+
301+
const mod_paths = [];
302+
303+
if ( mods_path_entry ) {
304+
mod_paths.push(mods_path_entry.path);
305+
}
306+
307+
return {
308+
mod_paths,
309+
};
269310
}
270311

271312
get_first_suitable_path_ (meta, paths, last_checks) {
@@ -295,6 +336,7 @@ class RuntimeEnvironment extends AdvancedBase {
295336
return entry;
296337
}
297338

339+
if ( meta.optional ) return;
298340
throw new TechnicalError(`No suitable path found for ${meta.pathFor}.`);
299341
}
300342
}

packages/backend/src/services/WebServerService.js

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const fs = require('fs');
2828
const auth = require('../middleware/auth');
2929
const { osclink } = require('../util/strutil');
3030
const { surrounding_box, es_import_promise } = require('../fun/dev-console-ui-utils');
31+
const auth2 = require('../middleware/auth2.js');
3132

3233
class WebServerService extends BaseService {
3334
static MODULES = {
@@ -393,6 +394,10 @@ class WebServerService extends BaseService {
393394
app.options('/*', (_, res) => {
394395
return res.sendStatus(200);
395396
});
397+
398+
this.router_user = express.Router();
399+
this.router_user.use(auth2);
400+
app.use(this.router_user);
396401
}
397402

398403
_register_commands (commands) {

packages/backend/src/services/database/BaseDatabaseAccessService.js

+3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@
1818
*/
1919
const { AdvancedBase } = require("@heyputer/puter-js-common");
2020
const BaseService = require("../BaseService");
21+
const { DB_WRITE, DB_READ } = require("./consts");
2122

2223
class BaseDatabaseAccessService extends BaseService {
24+
static DB_WRITE = DB_WRITE;
25+
static DB_READ = DB_READ;
2326
case ( choices ) {
2427
const engine_name = this.constructor.ENGINE_NAME;
2528
if ( choices.hasOwnProperty(engine_name) ) {

packages/useapi/main.js

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const globalwith = (vars, fn) => {
2+
const original_values = {};
3+
const keys = Object.keys(vars);
4+
5+
for ( const key of keys ) {
6+
if ( key in globalThis ) {
7+
original_values[key] = globalThis[key];
8+
}
9+
globalThis[key] = vars[key];
10+
}
11+
12+
try {
13+
return fn();
14+
} finally {
15+
for ( const key of keys ) {
16+
if ( key in original_values ) {
17+
globalThis[key] = original_values[key];
18+
} else {
19+
delete globalThis[key];
20+
}
21+
}
22+
}
23+
};
24+
25+
const aglobalwith = async (vars, fn) => {
26+
const original_values = {};
27+
const keys = Object.keys(vars);
28+
29+
for ( const key of keys ) {
30+
if ( key in globalThis ) {
31+
original_values[key] = globalThis[key];
32+
}
33+
globalThis[key] = vars[key];
34+
}
35+
36+
try {
37+
return await fn();
38+
} finally {
39+
for ( const key of keys ) {
40+
if ( key in original_values ) {
41+
globalThis[key] = original_values[key];
42+
} else {
43+
delete globalThis[key];
44+
}
45+
}
46+
}
47+
};
48+
49+
let default_fn = () => {
50+
const use = name => {
51+
const parts = name.split('.');
52+
let obj = use;
53+
for ( const part of parts ) {
54+
if ( ! obj[part] ) {
55+
obj[part] = {};
56+
}
57+
obj = obj[part];
58+
}
59+
60+
return obj;
61+
};
62+
const library = {
63+
use,
64+
def: (name, value) => {
65+
const parts = name.split('.');
66+
let obj = use;
67+
for ( const part of parts.slice(0, -1) ) {
68+
if ( ! obj[part] ) {
69+
obj[part] = {};
70+
}
71+
obj = obj[part];
72+
}
73+
74+
obj[parts[parts.length - 1]] = value;
75+
},
76+
withuse: fn => {
77+
return globalwith({
78+
use,
79+
def: library.def,
80+
}, fn);
81+
},
82+
awithuse: async fn => {
83+
return await aglobalwith({
84+
use,
85+
def: library.def,
86+
}, fn);
87+
}
88+
};
89+
90+
return library;
91+
};
92+
93+
const useapi = function useapi () {
94+
return default_fn();
95+
};
96+
97+
// We export some things on the function itself
98+
useapi.globalwith = globalwith;
99+
100+
module.exports = useapi;

packages/useapi/package.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "useapi",
3+
"version": "1.0.0",
4+
"author": "Puter Technologies Inc.",
5+
"license": "AGPL-3.0-only",
6+
"description": "Dynamic import interface for Puter mods",
7+
"main": "main.js"
8+
}

0 commit comments

Comments
 (0)