From eb05fbd2dc4877553b5118a069a9afdc32bea137 Mon Sep 17 00:00:00 2001 From: KernelDeimos Date: Sat, 8 Jun 2024 19:26:19 -0400 Subject: [PATCH] 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. --- packages/backend/src/CoreModule.js | 12 ++- packages/backend/src/Kernel.js | 45 +++++++- .../backend/src/boot/RuntimeEnvironment.js | 44 +++++++- .../backend/src/services/WebServerService.js | 5 + .../database/BaseDatabaseAccessService.js | 3 + packages/useapi/main.js | 100 ++++++++++++++++++ packages/useapi/package.json | 8 ++ 7 files changed, 213 insertions(+), 4 deletions(-) create mode 100644 packages/useapi/main.js create mode 100644 packages/useapi/package.json diff --git a/packages/backend/src/CoreModule.js b/packages/backend/src/CoreModule.js index 920bd6e1..e8bcea17 100644 --- a/packages/backend/src/CoreModule.js +++ b/packages/backend/src/CoreModule.js @@ -24,7 +24,8 @@ class CoreModule extends AdvancedBase { async install (context) { const services = context.get('services'); const app = context.get('app'); - await install({ services, app }); + const useapi = context.get('useapi'); + await install({ services, app, useapi }); } // Some services were created before the BaseService @@ -40,9 +41,16 @@ class CoreModule extends AdvancedBase { module.exports = CoreModule; -const install = async ({ services, app }) => { +const install = async ({ services, app, useapi }) => { const config = require('./config'); + useapi.withuse(() => { + def('Service', require('./services/BaseService')); + def('Module', AdvancedBase); + + def('puter.middlewares.auth', require('./middleware/auth2')); + }); + // /!\ IMPORTANT /!\ // For new services, put the import immediate above the // call to services.registerService. We'll clean this up diff --git a/packages/backend/src/Kernel.js b/packages/backend/src/Kernel.js index 920897eb..a544cd9a 100644 --- a/packages/backend/src/Kernel.js +++ b/packages/backend/src/Kernel.js @@ -18,12 +18,20 @@ */ const { AdvancedBase } = require("@heyputer/puter-js-common"); const { Context } = require('./util/context'); +const BaseService = require("./services/BaseService"); +const useapi = require('useapi'); class Kernel extends AdvancedBase { constructor () { super(); this.modules = []; + this.useapi = useapi(); + + this.useapi.withuse(() => { + def('Module', AdvancedBase); + def('Service', BaseService); + }); } add_module (module) { @@ -48,7 +56,8 @@ class Kernel extends AdvancedBase { const runtimeEnv = new RuntimeEnvironment({ logger: bootLogger, }); - runtimeEnv.init(); + const environment = runtimeEnv.init(); + this.environment = environment; // polyfills require('./polyfill/to-string-higher-radix'); @@ -89,6 +98,8 @@ class Kernel extends AdvancedBase { // app.set('services', services); const root_context = Context.create({ + environment: this.environment, + useapi: this.useapi, services, config, logger: this.bootLogger, @@ -108,10 +119,14 @@ class Kernel extends AdvancedBase { async _install_modules () { const { services } = this; + // Internal modules for ( const module of this.modules ) { await module.install(Context.get()); } + // External modules + await this.install_extern_mods_(); + try { await services.init(); } catch (e) { @@ -173,6 +188,34 @@ class Kernel extends AdvancedBase { await services.emit('boot.activation'); await services.emit('boot.ready'); } + + async install_extern_mods_ () { + const path_ = require('path'); + const fs = require('fs'); + + const mod_paths = this.environment.mod_paths; + for ( const mods_dirpath of mod_paths ) { + const mod_dirnames = fs.readdirSync(mods_dirpath); + for ( const mod_dirname of mod_dirnames ) { + const mod_path = path_.join(mods_dirpath, mod_dirname); + if ( ! fs.lstatSync(mod_path).isDirectory() ) { + continue; + } + + const mod_class = this.useapi.withuse(() => require(mod_path)); + const mod = new mod_class(); + if ( ! mod ) { + continue; + } + + if ( mod.install ) { + this.useapi.awithuse(async () => { + await mod.install(Context.get()); + }); + } + } + } + } } module.exports = { Kernel }; diff --git a/packages/backend/src/boot/RuntimeEnvironment.js b/packages/backend/src/boot/RuntimeEnvironment.js index 28e55058..67c41766 100644 --- a/packages/backend/src/boot/RuntimeEnvironment.js +++ b/packages/backend/src/boot/RuntimeEnvironment.js @@ -162,6 +162,30 @@ const runtime_paths = ({ path_checks }) => ({ path_ }) => [ }, ]; +// Suitable mod paths in order of precedence. +const mod_paths = ({ path_checks }) => ({ path_ }) => [ + { + label: '$MOD_PATH', + get path () { return process.env.MOD_PATH }, + checks: [ + path_checks.require_if_not_undefined, + ], + }, + { + path: '/var/puter/mods', + checks: [ + path_checks.skip_if_not_exists, + path_checks.env_not_set('NO_VAR_MODS'), + ], + }, + { + get path () { + return path_.join(original_cwd, 'mods'); + }, + checks: [ path_checks.skip_if_not_exists ], + }, +]; + class RuntimeEnvironment extends AdvancedBase { static MODULES = { fs: require('node:fs'), @@ -175,11 +199,12 @@ class RuntimeEnvironment extends AdvancedBase { this.path_checks = path_checks(this)(this.modules); this.config_paths = config_paths(this)(this.modules); this.runtime_paths = runtime_paths(this)(this.modules); + this.mod_paths = mod_paths(this)(this.modules); } init () { try { - this.init_(); + return this.init_(); } catch (e) { this.logger.error(e); print_error_help(e); @@ -203,6 +228,12 @@ class RuntimeEnvironment extends AdvancedBase { [ this.path_checks.require_write_permission ] ); + const mods_path_entry = this.get_first_suitable_path_( + { pathFor: 'mods', optional: true }, + this.mod_paths, + [ this.path_checks.require_read_permission ], + ); + process.chdir(pwd_path_entry.path); // Check for a valid config file in the config path @@ -266,6 +297,16 @@ class RuntimeEnvironment extends AdvancedBase { // console.log(config.services); // console.log(Object.keys(config.services)); // console.log({ ...config.services }); + + const mod_paths = []; + + if ( mods_path_entry ) { + mod_paths.push(mods_path_entry.path); + } + + return { + mod_paths, + }; } get_first_suitable_path_ (meta, paths, last_checks) { @@ -295,6 +336,7 @@ class RuntimeEnvironment extends AdvancedBase { return entry; } + if ( meta.optional ) return; throw new TechnicalError(`No suitable path found for ${meta.pathFor}.`); } } diff --git a/packages/backend/src/services/WebServerService.js b/packages/backend/src/services/WebServerService.js index 6f941979..b758b297 100644 --- a/packages/backend/src/services/WebServerService.js +++ b/packages/backend/src/services/WebServerService.js @@ -28,6 +28,7 @@ const fs = require('fs'); const auth = require('../middleware/auth'); const { osclink } = require('../util/strutil'); const { surrounding_box, es_import_promise } = require('../fun/dev-console-ui-utils'); +const auth2 = require('../middleware/auth2.js'); class WebServerService extends BaseService { static MODULES = { @@ -393,6 +394,10 @@ class WebServerService extends BaseService { app.options('/*', (_, res) => { return res.sendStatus(200); }); + + this.router_user = express.Router(); + this.router_user.use(auth2); + app.use(this.router_user); } _register_commands (commands) { diff --git a/packages/backend/src/services/database/BaseDatabaseAccessService.js b/packages/backend/src/services/database/BaseDatabaseAccessService.js index 2421b5b3..4396d16f 100644 --- a/packages/backend/src/services/database/BaseDatabaseAccessService.js +++ b/packages/backend/src/services/database/BaseDatabaseAccessService.js @@ -18,8 +18,11 @@ */ const { AdvancedBase } = require("@heyputer/puter-js-common"); const BaseService = require("../BaseService"); +const { DB_WRITE, DB_READ } = require("./consts"); class BaseDatabaseAccessService extends BaseService { + static DB_WRITE = DB_WRITE; + static DB_READ = DB_READ; case ( choices ) { const engine_name = this.constructor.ENGINE_NAME; if ( choices.hasOwnProperty(engine_name) ) { diff --git a/packages/useapi/main.js b/packages/useapi/main.js new file mode 100644 index 00000000..9a6c1e13 --- /dev/null +++ b/packages/useapi/main.js @@ -0,0 +1,100 @@ +const globalwith = (vars, fn) => { + const original_values = {}; + const keys = Object.keys(vars); + + for ( const key of keys ) { + if ( key in globalThis ) { + original_values[key] = globalThis[key]; + } + globalThis[key] = vars[key]; + } + + try { + return fn(); + } finally { + for ( const key of keys ) { + if ( key in original_values ) { + globalThis[key] = original_values[key]; + } else { + delete globalThis[key]; + } + } + } +}; + +const aglobalwith = async (vars, fn) => { + const original_values = {}; + const keys = Object.keys(vars); + + for ( const key of keys ) { + if ( key in globalThis ) { + original_values[key] = globalThis[key]; + } + globalThis[key] = vars[key]; + } + + try { + return await fn(); + } finally { + for ( const key of keys ) { + if ( key in original_values ) { + globalThis[key] = original_values[key]; + } else { + delete globalThis[key]; + } + } + } +}; + +let default_fn = () => { + const use = name => { + const parts = name.split('.'); + let obj = use; + for ( const part of parts ) { + if ( ! obj[part] ) { + obj[part] = {}; + } + obj = obj[part]; + } + + return obj; + }; + const library = { + use, + def: (name, value) => { + const parts = name.split('.'); + let obj = use; + for ( const part of parts.slice(0, -1) ) { + if ( ! obj[part] ) { + obj[part] = {}; + } + obj = obj[part]; + } + + obj[parts[parts.length - 1]] = value; + }, + withuse: fn => { + return globalwith({ + use, + def: library.def, + }, fn); + }, + awithuse: async fn => { + return await aglobalwith({ + use, + def: library.def, + }, fn); + } + }; + + return library; +}; + +const useapi = function useapi () { + return default_fn(); +}; + +// We export some things on the function itself +useapi.globalwith = globalwith; + +module.exports = useapi; diff --git a/packages/useapi/package.json b/packages/useapi/package.json new file mode 100644 index 00000000..2240f7fc --- /dev/null +++ b/packages/useapi/package.json @@ -0,0 +1,8 @@ +{ + "name": "useapi", + "version": "1.0.0", + "author": "Puter Technologies Inc.", + "license": "AGPL-3.0-only", + "description": "Dynamic import interface for Puter mods", + "main": "main.js" +}