mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-14 17:26:03 +00:00
394 lines
13 KiB
JavaScript
394 lines
13 KiB
JavaScript
/*
|
|
* Copyright (C) 2024 Puter Technologies Inc.
|
|
*
|
|
* This file is part of Puter.
|
|
*
|
|
* Puter is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
const { AdvancedBase } = require("@heyputer/putility");
|
|
const { quot } = require('@heyputer/putility').libs.string;
|
|
const { TechnicalError } = require("../errors/TechnicalError");
|
|
const { print_error_help } = require("../errors/error_help_details");
|
|
const default_config = require("./default_config");
|
|
const config = require("../config");
|
|
const { ConfigLoader } = require("../config/ConfigLoader");
|
|
|
|
// highlights a string
|
|
const hl = s => `\x1b[33;1m${s}\x1b[0m`;
|
|
|
|
// Save the original working directory
|
|
const original_cwd = process.cwd();
|
|
|
|
// === [ Puter Runtime Environment ] ===
|
|
// This file contains the RuntimeEnvironment class which is
|
|
// responsible for locating the configuration and runtime
|
|
// directories for the Puter Kernel.
|
|
|
|
// Depending on which path we're checking for configuration
|
|
// or runtime from config_paths, there will be different
|
|
// requirements. These are all possible requirements.
|
|
//
|
|
// Each check may result in the following:
|
|
// - false: this is not the desired path; skip it
|
|
// - true: this is the desired path, and it's valid
|
|
// - throw: this is the desired path, but it's invalid
|
|
const path_checks = ({ logger }) => ({ fs, path_ }) => ({
|
|
require_if_not_undefined: ({ path }) => {
|
|
if ( path == undefined ) return false;
|
|
|
|
const exists = fs.existsSync(path);
|
|
if ( !exists ) {
|
|
throw new Error(`Path does not exist: ${path}`);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
skip_if_not_exists: ({ path }) => {
|
|
const exists = fs.existsSync(path);
|
|
return exists;
|
|
},
|
|
skip_if_not_in_repo: ({ path }) => {
|
|
const exists = fs.existsSync(path_.join(path, '../../.is_puter_repository'));
|
|
return exists;
|
|
},
|
|
require_read_permission: ({ path }) => {
|
|
try {
|
|
fs.readdirSync(path);
|
|
} catch (e) {
|
|
throw new Error(`Cannot readdir on path: ${path}`);
|
|
}
|
|
return true;
|
|
},
|
|
require_write_permission: ({ path }) => {
|
|
try {
|
|
fs.writeFileSync(path_.join(path, '.tmp_test_write_permission'), 'test');
|
|
fs.unlinkSync(path_.join(path, '.tmp_test_write_permission'));
|
|
} catch (e) {
|
|
throw new Error(`Cannot write to path: ${path}`);
|
|
}
|
|
return true;
|
|
},
|
|
contains_config_file: ({ path }) => {
|
|
const valid_config_names = [
|
|
'config.json',
|
|
'config.json5',
|
|
];
|
|
for ( const name of valid_config_names ) {
|
|
const exists = fs.existsSync(path_.join(path, name));
|
|
if ( exists ) {
|
|
return true;
|
|
}
|
|
}
|
|
throw new Error(`No valid config file found in path: ${path}`);
|
|
},
|
|
env_not_set: name => () => {
|
|
return ! process.env[name];
|
|
}
|
|
});
|
|
|
|
// Configuration paths in order of precedence.
|
|
// We will load configuration from the first path that's suitable.
|
|
const config_paths = ({ path_checks }) => ({ path_ }) => [
|
|
{
|
|
label: '$CONFIG_PATH',
|
|
get path () { return process.env.CONFIG_PATH },
|
|
checks: [
|
|
path_checks.require_if_not_undefined,
|
|
],
|
|
},
|
|
{
|
|
path: '/etc/puter',
|
|
checks: [ path_checks.skip_if_not_exists ],
|
|
},
|
|
{
|
|
get path () {
|
|
return path_.join(original_cwd, 'volatile/config');
|
|
},
|
|
checks: [ path_checks.skip_if_not_in_repo ],
|
|
},
|
|
{
|
|
get path () {
|
|
return path_.join(original_cwd, 'config');
|
|
},
|
|
checks: [ path_checks.skip_if_not_exists ],
|
|
},
|
|
];
|
|
|
|
const valid_config_names = [
|
|
'config.json',
|
|
'config.json5',
|
|
];
|
|
|
|
// Suitable working directories in order of precedence.
|
|
// We will `process.chdir` to the first path that's suitable.
|
|
const runtime_paths = ({ path_checks }) => ({ path_ }) => [
|
|
{
|
|
label: '$RUNTIME_PATH',
|
|
get path () { return process.env.RUNTIME_PATH },
|
|
checks: [
|
|
path_checks.require_if_not_undefined,
|
|
],
|
|
},
|
|
{
|
|
path: '/var/puter',
|
|
checks: [
|
|
path_checks.skip_if_not_exists,
|
|
path_checks.env_not_set('NO_VAR_RUNTIME'),
|
|
],
|
|
},
|
|
{
|
|
get path () {
|
|
return path_.join(original_cwd, 'volatile/runtime');
|
|
},
|
|
checks: [ path_checks.skip_if_not_in_repo ],
|
|
},
|
|
{
|
|
get path () {
|
|
return path_.join(original_cwd, 'runtime');
|
|
},
|
|
checks: [ path_checks.skip_if_not_exists ],
|
|
},
|
|
];
|
|
|
|
// Suitable mod paths in order of precedence.
|
|
const mod_paths = ({ path_checks, entry_path }) => ({ 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(path_.dirname(
|
|
entry_path || require.main.filename), '../mods');
|
|
},
|
|
checks: [ path_checks.skip_if_not_exists ],
|
|
},
|
|
];
|
|
|
|
class RuntimeEnvironment extends AdvancedBase {
|
|
static MODULES = {
|
|
fs: require('node:fs'),
|
|
path_: require('node:path'),
|
|
crypto: require('node:crypto'),
|
|
format: require('string-template'),
|
|
}
|
|
|
|
constructor ({ logger, entry_path, boot_parameters }) {
|
|
super();
|
|
this.logger = logger;
|
|
this.entry_path = entry_path;
|
|
this.boot_parameters = boot_parameters;
|
|
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 {
|
|
return this.init_();
|
|
} catch (e) {
|
|
this.logger.error(e);
|
|
print_error_help(e);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
init_ () {
|
|
// This variable, called "environment", will be passed back to Kernel
|
|
// with some helpful values. A partial-population of this object later
|
|
// in this function will be used when evaluating configured paths.
|
|
const environment = {};
|
|
environment.source = this.modules.path_.dirname(
|
|
this.entry_path || require.main.filename);
|
|
|
|
const config_path_entry = this.get_first_suitable_path_(
|
|
{ pathFor: 'configuration' },
|
|
this.config_paths,
|
|
[
|
|
this.path_checks.require_read_permission,
|
|
// this.path_checks.contains_config_file,
|
|
]
|
|
);
|
|
|
|
// Note: there used to be a 'mods_path_entry' here too
|
|
// but it was never used
|
|
const pwd_path_entry = this.get_first_suitable_path_(
|
|
{ pathFor: 'working directory' },
|
|
this.runtime_paths,
|
|
[ this.path_checks.require_write_permission ]
|
|
);
|
|
|
|
process.chdir(pwd_path_entry.path);
|
|
|
|
// Check for a valid config file in the config path
|
|
let using_config;
|
|
for ( const name of valid_config_names ) {
|
|
const exists = this.modules.fs.existsSync(
|
|
this.modules.path_.join(config_path_entry.path, name)
|
|
);
|
|
if ( exists ) {
|
|
using_config = name;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const owrite_config = this.boot_parameters.args.overwriteConfig;
|
|
|
|
const { fs, path_, crypto } = this.modules;
|
|
if ( !using_config || owrite_config ) {
|
|
const generated_values = {};
|
|
generated_values.cookie_name = crypto.randomUUID();
|
|
generated_values.jwt_secret = crypto.randomUUID();
|
|
generated_values.url_signature_secret = crypto.randomUUID();
|
|
generated_values.private_uid_secret = crypto.randomBytes(24).toString('hex');
|
|
generated_values.private_uid_namespace = crypto.randomUUID();
|
|
if ( using_config ) {
|
|
this.logger.info(
|
|
`Overwriting ${quot(using_config)} because ` +
|
|
`${hl('--overwrite-config')} is set`
|
|
);
|
|
// make backup
|
|
fs.copyFileSync(
|
|
path_.join(config_path_entry.path, using_config),
|
|
path_.join(config_path_entry.path, using_config + '.bak'),
|
|
);
|
|
// preserve generated values
|
|
{
|
|
const config_raw = fs.readFileSync(
|
|
path_.join(config_path_entry.path, using_config),
|
|
'utf8',
|
|
);
|
|
const config_values = JSON.parse(config_raw);
|
|
for ( const k in generated_values ) {
|
|
if ( ! config_values[k] ) continue;
|
|
generated_values[k] = config_values[k];
|
|
}
|
|
}
|
|
}
|
|
const generated_config = {
|
|
...default_config,
|
|
...generated_values,
|
|
};
|
|
generated_config[""] = null; // for trailing comma
|
|
fs.writeFileSync(
|
|
path_.join(config_path_entry.path, 'config.json'),
|
|
JSON.stringify(generated_config, null, 4) + '\n',
|
|
);
|
|
using_config = 'config.json';
|
|
}
|
|
|
|
let config_to_load = 'config.json';
|
|
if ( process.env.PUTER_CONFIG_PROFILE ) {
|
|
this.logger.info(
|
|
hl('PROFILE') + ' ' +
|
|
quot(process.env.PUTER_CONFIG_PROFILE) + ' ' +
|
|
`because $PUTER_CONFIG_PROFILE is set`
|
|
);
|
|
config_to_load = `${process.env.PUTER_CONFIG_PROFILE}.json`
|
|
const exists = fs.existsSync(
|
|
path_.join(config_path_entry.path, config_to_load)
|
|
);
|
|
if ( ! exists ) {
|
|
fs.writeFileSync(
|
|
path_.join(config_path_entry.path, config_to_load),
|
|
JSON.stringify({
|
|
config_name: process.env.PUTER_CONFIG_PROFILE,
|
|
$imports: ['config.json'],
|
|
}, null, 4) + '\n',
|
|
);
|
|
}
|
|
}
|
|
|
|
const loader = new ConfigLoader(this.logger, config_path_entry.path, config);
|
|
loader.enable(config_to_load);
|
|
|
|
if ( ! config.config_name ) {
|
|
throw new Error('config_name is required');
|
|
}
|
|
this.logger.info(hl(`config name`) + ` ${quot(config.config_name)}`);
|
|
|
|
const mod_paths = [];
|
|
environment.mod_paths = mod_paths;
|
|
|
|
// Trying this as a default for now...
|
|
if ( ! config.mod_directories ) {
|
|
config.mod_directories = [
|
|
'{source}/../mods/mods_enabled',
|
|
'{source}/../extensions',
|
|
];
|
|
}
|
|
|
|
// If configured, add a user-specified mod path
|
|
if ( config.mod_directories ) {
|
|
for ( const dir of config.mod_directories ) {
|
|
const mods_directory = this.modules.format(
|
|
dir, environment,
|
|
);
|
|
mod_paths.push(mods_directory);
|
|
}
|
|
}
|
|
|
|
return environment;
|
|
}
|
|
|
|
get_first_suitable_path_ (meta, paths, last_checks) {
|
|
for ( const entry of paths ) {
|
|
const checks = [...(entry.checks ?? []), ...last_checks];
|
|
this.logger.info(
|
|
`Checking path ${quot(entry.label ?? entry.path)} for ${meta.pathFor}...`
|
|
);
|
|
|
|
let checks_pass = true;
|
|
for ( const check of checks ) {
|
|
this.logger.info(
|
|
`-> doing ${quot(check.name)} on path ${quot(entry.path)}...`
|
|
);
|
|
const result = check(entry);
|
|
if ( result === false ) {
|
|
this.logger.info(
|
|
`-> ${quot(check.name)} doesn't like this path`
|
|
);
|
|
checks_pass = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( ! checks_pass ) continue;
|
|
|
|
this.logger.info(
|
|
`${hl('USING')} ${quot(entry.path)} for ${meta.pathFor}.`
|
|
)
|
|
|
|
return entry;
|
|
}
|
|
|
|
if ( meta.optional ) return;
|
|
throw new TechnicalError(`No suitable path found for ${meta.pathFor}.`);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
RuntimeEnvironment,
|
|
}; |