mirror of
https://github.com/HeyPuter/puter.git
synced 2026-05-12 20:40:52 +00:00
Ns/simplify 1 (#2764)
* Rid devconsole * lots of removals * more commandservice
This commit is contained in:
committed by
GitHub
parent
b0bd8a12a7
commit
649db33763
@@ -87,7 +87,3 @@ See [events.md](./events.md)
|
||||
## Definitions
|
||||
|
||||
See [definitions.md](./definitions.md)
|
||||
|
||||
## Bundled extensions
|
||||
|
||||
- [dev-console](./dev-console.md) – Dev socket for running backend commands locally (opt-in via `DEVCONSOLE=1`).
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# dev-console extension
|
||||
|
||||
The **dev-console** extension provides a **dev socket** so you can run backend commands on a local Puter instance (e.g. commands registered in [CommandService](../../../src/backend/src/services/CommandService.js)).
|
||||
|
||||
## Enabling
|
||||
|
||||
The extension is **opt-in**. Set the environment variable `DEVCONSOLE=1` when starting Puter. The `npm run dev` script already does this:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
With `DEVCONSOLE=1`, the extension registers a `dev-socket` service that creates a UNIX socket and runs command lines through CommandService.
|
||||
|
||||
## Usage
|
||||
|
||||
See [Backend – dev socket](../../../src/backend/doc/dev_socket.md) for how to connect (e.g. `rlwrap nc -U ./dev.sock`) and run commands like `help`, `logs:indent`, etc.
|
||||
|
||||
## Location
|
||||
|
||||
The extension lives in `extensions/dev-console/`. It only registers the dev-socket service when `DEVCONSOLE=1`; otherwise the extension loads but does nothing, so it does not affect default runs.
|
||||
@@ -5,10 +5,6 @@
|
||||
### Here
|
||||
Documentation for extensions is [here](src/backend/doc/extensions/README.md).
|
||||
|
||||
### Bundled extensions
|
||||
|
||||
- **dev-console** (`extensions/dev-console/`) – Dev socket for running backend commands locally. Opt-in via `DEVCONSOLE=1` (e.g. `npm run dev`). See [Backend – dev socket](src/backend/doc/dev_socket.md).
|
||||
|
||||
### Not Here
|
||||
|
||||
Outdated documentation for extensions is [here](../doc/contributors/extensions/README.md).
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present 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/>.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import net from 'node:net';
|
||||
import path from 'node:path';
|
||||
|
||||
const SOCKET_NAME = 'dev.sock';
|
||||
const WELCOME = [
|
||||
'Puter dev socket – enter a command (e.g. help) and press Enter.',
|
||||
'Close the connection with Ctrl+C or by typing exit.',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
function getSocketDir () {
|
||||
if ( process.env.PUTER_DEV_SOCKET_DIR ) {
|
||||
return process.env.PUTER_DEV_SOCKET_DIR;
|
||||
}
|
||||
const volatileRuntime = path.join(process.cwd(), 'volatile', 'runtime');
|
||||
if ( fs.existsSync(volatileRuntime) ) {
|
||||
return volatileRuntime;
|
||||
}
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
extension.on('init', async () => {
|
||||
if ( process.env.DEVCONSOLE !== '1' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = extension.import('service:commands');
|
||||
const socketDir = getSocketDir();
|
||||
const socketPath = path.join(socketDir, SOCKET_NAME);
|
||||
|
||||
try {
|
||||
if ( fs.existsSync(socketPath) ) {
|
||||
fs.unlinkSync(socketPath);
|
||||
}
|
||||
fs.mkdirSync(socketDir, { recursive: true });
|
||||
} catch ( err ) {
|
||||
console.warn('dev-socket: could not prepare socket path', socketPath, err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const server = net.createServer((socket) => {
|
||||
socket.setEncoding('utf8');
|
||||
socket.write(`${WELCOME }\n> `);
|
||||
let buffer = '';
|
||||
socket.on('data', (chunk) => {
|
||||
buffer += chunk;
|
||||
const lines = buffer.split(/\r?\n/);
|
||||
buffer = lines.pop() ?? '';
|
||||
for ( const line of lines ) {
|
||||
const trimmed = line.trim();
|
||||
if ( trimmed === '' ) continue;
|
||||
if ( trimmed.toLowerCase() === 'exit' ) {
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
const log = {
|
||||
log: (msg) => {
|
||||
socket.write(`${String(msg) }\n`);
|
||||
},
|
||||
error: (msg) => {
|
||||
socket.write(`${String(msg) }\n`);
|
||||
},
|
||||
};
|
||||
commands.executeRawCommand(trimmed, log).then(() => {
|
||||
socket.write('> ');
|
||||
}).catch((err) => {
|
||||
log.error(err?.message ?? err);
|
||||
socket.write('> ');
|
||||
});
|
||||
}
|
||||
});
|
||||
socket.on('end', () => {
|
||||
});
|
||||
socket.on('error', () => {
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(socketPath, () => {
|
||||
console.log('dev-socket: socket listening at', socketPath);
|
||||
});
|
||||
server.on('error', (err) => {
|
||||
console.warn('dev-socket: socket error', err.message);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "@heyputer/extension-dev-console",
|
||||
"version": "1.0.0",
|
||||
"description": "Dev socket for running backend commands locally (opt-in via DEVCONSOLE=1)",
|
||||
"main": "main.js",
|
||||
"type": "module",
|
||||
"private": true
|
||||
}
|
||||
@@ -26,52 +26,27 @@ class CustomPuterService extends use.Service {
|
||||
const svc_puterHomepage = this.services.get('puter-homepage');
|
||||
svc_puterHomepage.register_script('/custom-gui/main.js');
|
||||
}
|
||||
['__on_install.routes'] (_, { app }) {
|
||||
'__on_install.routes' (_, { app }) {
|
||||
const require = this.require;
|
||||
const express = require('express');
|
||||
const path_ = require('path');
|
||||
|
||||
app.use('/custom-gui',
|
||||
express.static(path.join(__dirname, 'gui')));
|
||||
app.use(
|
||||
'/custom-gui',
|
||||
express.static(path.join(__dirname, 'gui')),
|
||||
);
|
||||
}
|
||||
async ['__on_boot.consolidation'] () {
|
||||
const then = Date.now();
|
||||
this.tod_widget = () => {
|
||||
const s = 5 - Math.floor((Date.now() - then) / 1000);
|
||||
const lines = [
|
||||
'\x1B[36;1mKDMOD ENABLED\x1B[0m' +
|
||||
` (👁️ ${s}s)`,
|
||||
];
|
||||
// It would be super cool to be able to use this here
|
||||
// surrounding_box('33;1', lines);
|
||||
return lines;
|
||||
};
|
||||
|
||||
const svc_devConsole = this.services.get('dev-console', { optional: true });
|
||||
if ( ! svc_devConsole ) return;
|
||||
svc_devConsole.add_widget(this.tod_widget);
|
||||
|
||||
setTimeout(() => {
|
||||
svc_devConsole.remove_widget(this.tod_widget);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
commands.registerCommands('o', [
|
||||
{
|
||||
id: 'k',
|
||||
description: '',
|
||||
handler: async (_, log) => {
|
||||
const svc_devConsole = this.services.get('dev-console', { optional: true });
|
||||
if ( ! svc_devConsole ) return;
|
||||
svc_devConsole.remove_widget(this.tod_widget);
|
||||
const lines = this.tod_widget();
|
||||
for ( const line of lines ) log.log(line);
|
||||
this.tod_widget = null;
|
||||
log.log('kdmod is enabled');
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { CustomPuterService };
|
||||
module.exports = { CustomPuterService };
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@
|
||||
"start=gui": "nodemon --exec \"node dev-server.js\" ",
|
||||
"start": "node ./tools/run-selfhosted.js",
|
||||
"prestart": "npm run build:ts",
|
||||
"dev": "npm run build:ts && DEVCONSOLE=1 node ./tools/run-selfhosted.js",
|
||||
"dev": "npm run build:ts && node ./tools/run-selfhosted.js",
|
||||
"build": "npx eslint --quiet -c eslint/mandatory.eslint.config.js src/backend/src extensions && npm run build:ts && cd src/gui && node ./build.js",
|
||||
"check-translations": "node tools/check-translations.js",
|
||||
"prepare": "husky",
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
## Backend - dev socket
|
||||
|
||||
The "dev socket" allows you to interact with Puter's backend by running commands.
|
||||
It's a UNIX socket that lets you run commands registered with
|
||||
[CommandService](../../src/services/CommandService.js) (e.g. `help`, `logs:indent`, `params:get`, etc.).
|
||||
|
||||
### Enabling the dev socket
|
||||
|
||||
The dev socket is provided by the **dev-console extension** and is **opt-in**. To enable it:
|
||||
|
||||
1. Set the environment variable `DEVCONSOLE=1` when starting Puter (e.g. `npm run dev` already does this).
|
||||
2. The extension lives in `extensions/dev-console/` and registers a `dev-socket` service when `DEVCONSOLE=1`.
|
||||
|
||||
### Socket location
|
||||
|
||||
The socket is created in a directory chosen as follows (in order):
|
||||
|
||||
- `PUTER_DEV_SOCKET_DIR` if set
|
||||
- `./volatile/runtime` if it exists (typical local dev)
|
||||
- otherwise the process current working directory
|
||||
|
||||
The socket file is named `dev.sock`.
|
||||
|
||||
### Connecting
|
||||
|
||||
When in that directory, connect with your tool of choice. For example, using `nc` and `rlwrap` for readline history:
|
||||
|
||||
```
|
||||
rlwrap nc -U ./dev.sock
|
||||
```
|
||||
|
||||
If it is successful you will see a message with instructions. Enter a command (e.g. `help`) and press Enter.
|
||||
@@ -72,10 +72,6 @@ key-value store, and in-memory cache.
|
||||
### Adding Features to Puter
|
||||
- [Implementing Drivers](./pages/drivers.md)
|
||||
|
||||
### Bundled extensions
|
||||
|
||||
- **dev-console** – When `DEVCONSOLE=1` is set (e.g. `npm run dev`), the dev-console extension registers a UNIX socket (`dev.sock`) so you can run backend commands (see [CommandService](../../src/services/CommandService.js)) from a terminal. See [Backend – dev socket](../dev_socket.md).
|
||||
|
||||
## Extensions - Planned Features
|
||||
|
||||
Extensions are under refactor currently. This is the checklist:
|
||||
|
||||
@@ -33,6 +33,7 @@ const { redisClient } = require('./clients/redis/redisSingleton');
|
||||
const { kv } = require('./util/kvSingleton');
|
||||
const { s3ClientProvider } = require('./clients/s3/s3ClientProvider');
|
||||
const { PuterS3Service } = require('./deprecated/filesystem/PuterS3Service');
|
||||
const BaseService = require('./services/BaseService');
|
||||
|
||||
/**
|
||||
* @footgun - real install method is defined above
|
||||
@@ -121,7 +122,6 @@ const install = async ({ context, services, app, useapi, modapi }) => {
|
||||
|
||||
// TODO: move these to top level imports or await imports and esm this file
|
||||
|
||||
const { CommandService } = require('./services/CommandService');
|
||||
const { RateLimitService } = require('./services/sla/RateLimitService');
|
||||
const { AuthService } = require('./services/auth/AuthService');
|
||||
const { SLAService } = require('./services/sla/SLAService');
|
||||
@@ -164,7 +164,6 @@ const install = async ({ context, services, app, useapi, modapi }) => {
|
||||
services.registerService('dynamo', DDBClientWrapper);
|
||||
|
||||
services.registerService('system-validation', SystemValidationService);
|
||||
services.registerService('commands', CommandService);
|
||||
services.registerService('__api-filesystem', FilesystemAPIService);
|
||||
services.registerService('__api', PuterAPIService);
|
||||
services.registerService('__gui', ServeGUIService);
|
||||
@@ -395,14 +394,11 @@ const install = async ({ context, services, app, useapi, modapi }) => {
|
||||
const install_legacy = async ({ services }) => {
|
||||
const { OperationTraceService } = require('./services/OperationTraceService');
|
||||
const { ClientOperationService } = require('./services/ClientOperationService');
|
||||
const { EngPortalService } = require('./services/EngPortalService');
|
||||
|
||||
// === Services which do not yet extend BaseService ===
|
||||
// services.registerService('filesystem', FilesystemService);
|
||||
services.registerService('operationTrace', OperationTraceService);
|
||||
services.registerService('client-operation', ClientOperationService);
|
||||
services.registerService('engineering-portal', EngPortalService);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,14 +16,17 @@
|
||||
* 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');
|
||||
|
||||
import { AdvancedBase } from '@heyputer/putility';
|
||||
import { StrategizedService } from './services/StrategizedService.js';
|
||||
import { SqliteDatabaseAccessService } from './services/database/SqliteDatabaseAccessService.js';
|
||||
|
||||
// import {BaseService} from './services/BaseService.js';
|
||||
|
||||
class DatabaseModule extends AdvancedBase {
|
||||
async install (context) {
|
||||
const services = context.get('services');
|
||||
|
||||
const { StrategizedService } = require('./services/StrategizedService');
|
||||
const { SqliteDatabaseAccessService } = require('./services/database/SqliteDatabaseAccessService');
|
||||
services.registerService('database', StrategizedService, {
|
||||
strategy_key: 'engine',
|
||||
strategies: {
|
||||
@@ -33,4 +36,4 @@ class DatabaseModule extends AdvancedBase {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DatabaseModule;
|
||||
export default DatabaseModule;
|
||||
|
||||
@@ -213,7 +213,6 @@ class Kernel extends AdvancedBase {
|
||||
deployment_type: globalThis.deployment_type,
|
||||
});
|
||||
})();
|
||||
|
||||
await services.emit('boot.activation');
|
||||
await services.emit('boot.ready');
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ class AlarmService extends BaseService {
|
||||
* the '_init' method of CommandService may not have been called yet.
|
||||
*/
|
||||
'__on_boot.consolidation' () {
|
||||
this._register_commands(this.services.get('commands'));
|
||||
}
|
||||
|
||||
adapt_id_ (id) {
|
||||
@@ -361,137 +360,6 @@ class AlarmService extends BaseService {
|
||||
get_alarm (id) {
|
||||
return this.alarms[id] ?? this.alarm_aliases[id];
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
// Function to handle a specific alarm event.
|
||||
// This comment can be added above line 320.
|
||||
// This function is responsible for processing specific events related to alarms.
|
||||
// It can be used for tasks such as updating alarm status, sending notifications, or triggering actions.
|
||||
// This function is called internally by the AlarmService class.
|
||||
|
||||
// /*
|
||||
// * handleAlarmEvent - Handles a specific alarm event.
|
||||
// *
|
||||
// * @param {Object} alarm - The alarm object containing relevant information.
|
||||
// * @param {Function} callback - Optional callback function to be called when the event is handled.
|
||||
// */
|
||||
// function handleAlarmEvent(alarm, callback) {
|
||||
// // Implementation goes here.
|
||||
// }
|
||||
const completeAlarmID = (args) => {
|
||||
// The alarm ID is the first argument, so return no results if we're on the second or later.
|
||||
if ( args.length > 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
const lastArg = args[args.length - 1];
|
||||
|
||||
const results = [];
|
||||
for ( const alarm of Object.values(this.alarms) ) {
|
||||
if ( alarm.id.startsWith(lastArg) ) {
|
||||
results.push(alarm.id);
|
||||
}
|
||||
if ( alarm.short_id?.startsWith(lastArg) ) {
|
||||
results.push(alarm.short_id);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
commands.registerCommands('alarm', [
|
||||
{
|
||||
id: 'list',
|
||||
description: 'list alarms',
|
||||
handler: async (args, log) => {
|
||||
for ( const alarm of Object.values(this.alarms) ) {
|
||||
log.log(`${alarm.id_string}: ${alarm.message} (${alarm.count})`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'info',
|
||||
description: 'show info about an alarm',
|
||||
handler: async (args, log) => {
|
||||
const [id] = args;
|
||||
const alarm = this.get_alarm(id);
|
||||
if ( ! alarm ) {
|
||||
log.log(`no alarm with id ${id}`);
|
||||
return;
|
||||
}
|
||||
log.log(`\x1B[33;1m${alarm.id_string}\x1B[0m :: ${alarm.message} (${alarm.count})`);
|
||||
log.log(`started: ${new Date(alarm.started).toISOString()}`);
|
||||
log.log(`short id: ${alarm.short_id}`);
|
||||
log.log(`original id: ${alarm.id}`);
|
||||
|
||||
// print stack trace of alarm error
|
||||
if ( alarm.error ) {
|
||||
log.log(alarm.error.stack);
|
||||
}
|
||||
// print other fields
|
||||
for ( const [key, value] of Object.entries(alarm.fields) ) {
|
||||
log.log(`- ${key}: ${util.inspect(value)}`);
|
||||
}
|
||||
},
|
||||
completer: completeAlarmID,
|
||||
},
|
||||
{
|
||||
id: 'clear',
|
||||
description: 'clear an alarm',
|
||||
handler: async (args, log) => {
|
||||
const [id] = args;
|
||||
const alarm = this.get_alarm(id);
|
||||
if ( ! alarm ) {
|
||||
log.log(`no alarm with id ${id}; ` +
|
||||
`but calling clear(${JSON.stringify(id)}) anyway.`);
|
||||
}
|
||||
this.clear(id);
|
||||
},
|
||||
completer: completeAlarmID,
|
||||
},
|
||||
{
|
||||
id: 'clear-all',
|
||||
description: 'clear all alarms',
|
||||
handler: async (_args, _log) => {
|
||||
const alarms = Object.values(this.alarms);
|
||||
this.alarms = {};
|
||||
for ( const alarm of alarms ) {
|
||||
this.handle_alarm_off_(alarm);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'sound',
|
||||
description: 'sound an alarm',
|
||||
handler: async (args, _log) => {
|
||||
const [id, message] = args;
|
||||
this.create(id ?? 'test', message, {});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'inspect',
|
||||
description: 'show logs that happened an alarm',
|
||||
handler: async (args, log) => {
|
||||
const [id, occurance_idx] = args;
|
||||
const alarm = this.get_alarm(id);
|
||||
if ( ! alarm ) {
|
||||
log.log(`no alarm with id ${id}`);
|
||||
return;
|
||||
}
|
||||
const occurance = alarm.occurrences[occurance_idx];
|
||||
if ( ! occurance ) {
|
||||
log.log(`no occurance with index ${occurance_idx}`);
|
||||
return;
|
||||
}
|
||||
log.log(`┏━━ Logs before: ${alarm.id_string} ━━━━`);
|
||||
for ( const lg of occurance.logs ) {
|
||||
log.log(`┃ ${ this.logutil.stringify_log_entry(lg)}`);
|
||||
}
|
||||
log.log(`┗━━ Logs before: ${alarm.id_string} ━━━━`);
|
||||
},
|
||||
completer: completeAlarmID,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -410,61 +410,6 @@ class LogService extends BaseService {
|
||||
* Registers logging commands with the command service.
|
||||
*/
|
||||
'__on_boot.consolidation' () {
|
||||
const commands = this.services.get('commands');
|
||||
commands.registerCommands('logs', [
|
||||
{
|
||||
id: 'show',
|
||||
description: 'toggle log output',
|
||||
handler: async () => {
|
||||
this.devlogger && (this.devlogger.off = !this.devlogger.off);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'rec',
|
||||
description: 'start recording to a file via dev logger',
|
||||
handler: async (args, ctx) => {
|
||||
const [name] = args;
|
||||
const { log } = ctx;
|
||||
if ( ! this.devlogger ) {
|
||||
log('no dev logger; what are you doing?');
|
||||
}
|
||||
this.devlogger.recto = name;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stop',
|
||||
description: 'stop recording to a file via dev logger',
|
||||
handler: async ([_name], log) => {
|
||||
if ( ! this.devlogger ) {
|
||||
log('no dev logger; what are you doing?');
|
||||
}
|
||||
this.devlogger.recto = null;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'indent',
|
||||
description: 'toggle log indentation',
|
||||
handler: async () => {
|
||||
globalThis.dev_console_indent_on =
|
||||
!globalThis.dev_console_indent_on;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'get-level',
|
||||
description: 'get the current log level for displayed logs',
|
||||
handler: async (args, log) => {
|
||||
log.log(`${display_log_level} (${display_log_level_label[display_log_level] ?? '?'})`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'set-level',
|
||||
description: 'set the new log level for displayed logs',
|
||||
handler: async (args, log) => {
|
||||
display_log_level = Number(args[0]);
|
||||
log.log(`${display_log_level} (${display_log_level_label[display_log_level] ?? '?'})`);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* Registers logging commands with the command service.
|
||||
|
||||
@@ -45,7 +45,6 @@ class PagerService extends BaseService {
|
||||
* the '_init' method of CommandService may not have been called yet.
|
||||
*/
|
||||
'__on_boot.consolidation' () {
|
||||
this._register_commands(this.services.get('commands'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,24 +138,6 @@ class PagerService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
commands.registerCommands('pager', [
|
||||
{
|
||||
id: 'test-alert',
|
||||
description: 'create a test alert',
|
||||
handler: async (args, log) => {
|
||||
const [severity] = args;
|
||||
await this.alert({
|
||||
id: 'test-alert',
|
||||
message: 'test alert',
|
||||
source: 'test',
|
||||
severity,
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -98,7 +98,6 @@ class ParameterService extends BaseService {
|
||||
* @private
|
||||
*/
|
||||
'__on_boot.consolidation' () {
|
||||
this._registerCommands(this.services.get('commands'));
|
||||
}
|
||||
|
||||
createParameters (serviceName, parameters, opt_instance) {
|
||||
@@ -109,9 +108,11 @@ class ParameterService extends BaseService {
|
||||
id: `${serviceName}:${parameter.id}`,
|
||||
}));
|
||||
if ( opt_instance ) {
|
||||
this.bindToInstance(`${serviceName}:${parameter.id}`,
|
||||
opt_instance,
|
||||
parameter.id);
|
||||
this.bindToInstance(
|
||||
`${serviceName}:${parameter.id}`,
|
||||
opt_instance,
|
||||
parameter.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,71 +145,6 @@ class ParameterService extends BaseService {
|
||||
}
|
||||
return parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers parameter-related commands with the command service
|
||||
* @param {Object} commands - The command service instance to register with
|
||||
*/
|
||||
_registerCommands (commands) {
|
||||
const completeParameterName = (args) => {
|
||||
// The parameter name is the first argument, so return no results if we're on the second or later.
|
||||
if ( args.length > 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
const lastArg = args[args.length - 1];
|
||||
|
||||
return this.parameters_
|
||||
.map(parameter => parameter.spec_.id)
|
||||
.filter(parameterName => parameterName.startsWith(lastArg));
|
||||
};
|
||||
|
||||
commands.registerCommands('params', [
|
||||
{
|
||||
id: 'get',
|
||||
description: 'get a parameter',
|
||||
handler: async (args, log) => {
|
||||
const [name] = args;
|
||||
const value = await this.get(name);
|
||||
log.log(value);
|
||||
},
|
||||
completer: completeParameterName,
|
||||
},
|
||||
{
|
||||
id: 'set',
|
||||
description: 'set a parameter',
|
||||
handler: async (args, log) => {
|
||||
const [name, value] = args;
|
||||
const parameter = this._get_param(name);
|
||||
parameter.set(value);
|
||||
log.log(value);
|
||||
},
|
||||
completer: completeParameterName,
|
||||
},
|
||||
{
|
||||
id: 'list',
|
||||
description: 'list parameters',
|
||||
handler: async (args, log) => {
|
||||
const [prefix] = args;
|
||||
let parameters = this.parameters_;
|
||||
if ( prefix ) {
|
||||
parameters = parameters
|
||||
.filter(p => p.spec_.id.startsWith(prefix));
|
||||
}
|
||||
log.log(`available parameters${
|
||||
prefix ? ` (starting with: ${prefix})` : ''
|
||||
}:`);
|
||||
for ( const parameter of parameters ) {
|
||||
// log.log(`- ${parameter.spec_.id}: ${parameter.spec_.description}`);
|
||||
// Log parameter description and value
|
||||
const value = await parameter.get();
|
||||
log.log(`- ${parameter.spec_.id} = ${value}`);
|
||||
log.log(` ${parameter.spec_.description}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -23,19 +23,6 @@ class DomainVerificationService extends BaseService {
|
||||
return await get_user({ username: 'admin' });
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
const svc_commands = this.services.get('commands');
|
||||
svc_commands.registerCommands('domain', [
|
||||
{
|
||||
id: 'user',
|
||||
description: '',
|
||||
handler: async (args, log) => {
|
||||
const res = await this.get_controlling_user({ domain: args[0] });
|
||||
log.log(res);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -42,35 +42,6 @@ class SizeService extends BaseService {
|
||||
}
|
||||
|
||||
'__on_boot.consolidate' () {
|
||||
const svc_commands = this.services.get('commands');
|
||||
svc_commands.registerCommands('size', [
|
||||
{
|
||||
id: 'get-usage',
|
||||
description: 'get usage for a user',
|
||||
handler: async (args, log) => {
|
||||
const user = await UserParameter.adapt(args[0]);
|
||||
const usage = await this.get_usage(user.id);
|
||||
log.log(`usage: ${usage} bytes`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'get-capacity',
|
||||
description: 'get storage capacity for a user',
|
||||
handler: async (args, log) => {
|
||||
const user = await UserParameter.adapt(args[0]);
|
||||
const capacity = await this.get_storage_capacity(user);
|
||||
log.log(`capacity: ${capacity} bytes`);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'get-cache-size',
|
||||
description: 'get the number of cached users',
|
||||
handler: async (args, log) => {
|
||||
const size = Object.keys(this.usages).length;
|
||||
log.log(`cache size: ${size}`);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
async get_usage (user_id) {
|
||||
|
||||
@@ -36,7 +36,6 @@ const DEFAULT_FILES = {};
|
||||
|
||||
class DefaultUserService extends BaseService {
|
||||
async _init () {
|
||||
this._register_commands(this.services.get('commands'));
|
||||
}
|
||||
async '__on_ready.webserver' () {
|
||||
// check if a user named `admin` exists
|
||||
@@ -221,19 +220,6 @@ class DefaultUserService extends BaseService {
|
||||
return tmp_password;
|
||||
});
|
||||
}
|
||||
_register_commands (commands) {
|
||||
commands.registerCommands('default-user', [
|
||||
{
|
||||
id: 'reset-password',
|
||||
handler: async (args, ctx) => {
|
||||
const [username] = args;
|
||||
const user = await get_user({ username });
|
||||
const tmp_pwd = await this.force_tmp_password_(user);
|
||||
ctx.log(`New password for ${quot(username)} is: ${tmp_pwd}`);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DefaultUserService;
|
||||
|
||||
@@ -25,57 +25,6 @@ class SelfhostedService extends BaseService {
|
||||
`;
|
||||
|
||||
async _init () {
|
||||
this._register_commands(this.services.get('commands'));
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
const db = this.services.get('database').get(DB_WRITE, 'selfhosted');
|
||||
commands.registerCommands('app', [
|
||||
{
|
||||
id: 'godmode-on',
|
||||
description: 'Toggle godmode for an app',
|
||||
handler: async (args, _log) => {
|
||||
const svc_su = this.services.get('su');
|
||||
await await svc_su.sudo(async () => {
|
||||
const [app_uid] = args;
|
||||
const es_app = await this.services.get('es:app');
|
||||
const app = await es_app.read(app_uid);
|
||||
if ( ! app ) {
|
||||
throw new Error(`App ${app_uid} not found`);
|
||||
}
|
||||
await db.write('UPDATE apps SET godmode = 1 WHERE uid = ?', [app_uid]);
|
||||
const svc_event = this.services.get('event');
|
||||
await svc_event.emit('app.changed', {
|
||||
app_uid,
|
||||
action: 'updated',
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
commands.registerCommands('app', [
|
||||
{
|
||||
id: 'godmode-off',
|
||||
description: 'Toggle godmode for an app',
|
||||
handler: async (args, _log) => {
|
||||
const svc_su = this.services.get('su');
|
||||
await await svc_su.sudo(async () => {
|
||||
const [app_uid] = args;
|
||||
const es_app = await this.services.get('es:app');
|
||||
const app = await es_app.read(app_uid);
|
||||
if ( ! app ) {
|
||||
throw new Error(`App ${app_uid} not found`);
|
||||
}
|
||||
await db.write('UPDATE apps SET godmode = 0 WHERE uid = ?', [app_uid]);
|
||||
const svc_event = this.services.get('event');
|
||||
await svc_event.emit('app.changed', {
|
||||
app_uid,
|
||||
action: 'updated',
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ export class TestCoreModule {
|
||||
services.registerService('su', SUService);
|
||||
services.registerService('alarm', AlarmService);
|
||||
services.registerService('event', EventService);
|
||||
services.registerService('commands', CommandService);
|
||||
services.registerService('meteringService', MeteringServiceWrapper);
|
||||
services.registerService('puter-kvstore', DynamoKVStoreWrapper);
|
||||
services.registerService('permission', PermissionService);
|
||||
|
||||
@@ -136,6 +136,7 @@ class WebServerService extends BaseService {
|
||||
* @returns {Promise<void>} A promise that resolves once the server is started.
|
||||
*/
|
||||
async '__on_boot.activation' () {
|
||||
console.log('starting webser');
|
||||
const services = this.services;
|
||||
await services.emit('start.webserver');
|
||||
await services.emit('ready.webserver');
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present 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 { Context } = require('../util/context');
|
||||
const BaseService = require('./BaseService');
|
||||
|
||||
/**
|
||||
* Represents a Command class that encapsulates command execution functionality.
|
||||
* Each Command instance contains a specification (spec) that defines its ID,
|
||||
* name, description, handler function, and optional argument completer.
|
||||
* The class provides methods for executing commands and handling command
|
||||
* argument completion.
|
||||
*/
|
||||
class Command {
|
||||
constructor (spec) {
|
||||
this.spec_ = spec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the unique identifier for this command
|
||||
* @returns {string} The command's ID as specified in the constructor
|
||||
*/
|
||||
get id () {
|
||||
return this.spec_.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command with given arguments and logging
|
||||
* @param {Array} args - Command arguments to pass to the handler
|
||||
* @param {Object} [log=console] - Logger object for output, defaults to console
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} Logs any errors that occur during command execution
|
||||
*/
|
||||
async execute (args, log) {
|
||||
log = log ?? console;
|
||||
const { id, name, description, handler } = this.spec_;
|
||||
try {
|
||||
await handler(args, log);
|
||||
} catch ( err ) {
|
||||
log.error(`command ${name ?? id} failed: ${err.message}`);
|
||||
log.error(err.stack);
|
||||
}
|
||||
}
|
||||
|
||||
completeArgument (args) {
|
||||
const completer = this.spec_.completer;
|
||||
if ( completer )
|
||||
{
|
||||
return completer(args);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CommandService class manages the registration, execution, and handling of commands in the Puter system.
|
||||
* Extends BaseService to provide command-line interface functionality. Maintains a collection of Command
|
||||
* objects, supports command registration with namespaces, command execution with arguments, and provides
|
||||
* command lookup capabilities. Includes built-in help command functionality.
|
||||
* @extends BaseService
|
||||
*/
|
||||
class CommandService extends BaseService {
|
||||
/**
|
||||
* Initializes the command service's internal state
|
||||
* Called during service construction to set up the empty commands array
|
||||
*/
|
||||
async _construct () {
|
||||
this.commands_ = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the help command to the list of commands on init
|
||||
*/
|
||||
async _init () {
|
||||
this.commands_.push(new Command({
|
||||
id: 'help',
|
||||
description: 'show this help',
|
||||
handler: (args, log) => {
|
||||
log.log('available commands:');
|
||||
for ( const command of this.commands_ ) {
|
||||
log.log(`- ${command.spec_.id}: ${command.spec_.description}`);
|
||||
}
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async '__on_boot.consolidation' () {
|
||||
const svc_event = this.services.get('event');
|
||||
const svc_command = this;
|
||||
const event = {
|
||||
createCommand (name, command) {
|
||||
const serviceName = Context.get('extension_name') ?? '%missing%';
|
||||
const commandSpec = typeof command === 'function'
|
||||
? { handler: command }
|
||||
: command;
|
||||
if ( typeof commandSpec !== 'object' ) {
|
||||
throw new Error('command must be either a function or an object');
|
||||
}
|
||||
if ( ! (typeof command.handler === 'function') ) {
|
||||
throw new Error('command should have a handler function');
|
||||
}
|
||||
svc_command.registerCommands(serviceName, [{
|
||||
id: name,
|
||||
...commandSpec,
|
||||
}]);
|
||||
},
|
||||
};
|
||||
svc_event.emit('create.commands', event);
|
||||
}
|
||||
|
||||
registerCommands (serviceName, commands) {
|
||||
if ( ! this.log ) {
|
||||
/* eslint-disable */
|
||||
console.error(
|
||||
'CommandService.registerCommands was called before a logger ' +
|
||||
'was initialied. This happens when calling registerCommands ' +
|
||||
'in the "construct" phase instead of the "init" phase. If ' +
|
||||
'you are migrating a legacy service that does not extend ' +
|
||||
'BaseService, maybe the _construct hook is calling init()'
|
||||
);
|
||||
/* eslint-enable */
|
||||
process.exit(1);
|
||||
}
|
||||
for ( const command of commands ) {
|
||||
this.log.debug(`registering command ${serviceName}:${command.id}`);
|
||||
this.commands_.push(new Command({
|
||||
...command,
|
||||
id: `${serviceName}:${command.id}`,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command with the given arguments and logging context
|
||||
* @param {string[]} args - Array of command arguments where first element is command name
|
||||
* @param {Object} log - Logger object for output (defaults to console if not provided)
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If command execution fails
|
||||
*/
|
||||
async executeCommand (args, log) {
|
||||
const [commandName, ...commandArgs] = args;
|
||||
const command = this.commands_.find(c => c.spec_.id === commandName);
|
||||
if ( ! command ) {
|
||||
log.error(`unknown command: ${commandName}`);
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Executes a command with the given arguments in a global context
|
||||
* @param {string[]} args - Array of command arguments where first element is command name
|
||||
* @param {Object} log - Logger object for output
|
||||
* @returns {Promise<void>}
|
||||
* @throws {Error} If command execution fails
|
||||
*/
|
||||
await globalThis.root_context.sub({
|
||||
injected_logger: log,
|
||||
}).arun(async () => {
|
||||
await command.execute(commandArgs, log);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a raw command string by splitting it into arguments and executing the command
|
||||
* @param {string} text - Raw command string to execute
|
||||
* @param {object} log - Logger object for output (defaults to console if not provided)
|
||||
* @returns {Promise<void>}
|
||||
* @todo Replace basic whitespace splitting with proper tokenizer (obvious-json)
|
||||
*/
|
||||
async executeRawCommand (text, log) {
|
||||
// TODO: add obvious-json as a tokenizer
|
||||
const args = text.split(/\s+/);
|
||||
await this.executeCommand(args, log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all registered command names/IDs
|
||||
* @returns {string[]} Array of command identifier strings
|
||||
*/
|
||||
get commandNames () {
|
||||
return this.commands_.map(command => command.id);
|
||||
}
|
||||
|
||||
getCommand (id) {
|
||||
return this.commands_.find(command => command.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CommandService,
|
||||
};
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createTestKernel } from '../../tools/test.mjs';
|
||||
import { CommandService } from './CommandService';
|
||||
|
||||
describe('CommandService', async () => {
|
||||
const testKernel = await createTestKernel({
|
||||
serviceMap: {
|
||||
commands: CommandService,
|
||||
},
|
||||
initLevelString: 'init',
|
||||
});
|
||||
|
||||
const commandService = testKernel.services!.get('commands') as CommandService;
|
||||
|
||||
it('should be instantiated', () => {
|
||||
expect(commandService).toBeInstanceOf(CommandService);
|
||||
});
|
||||
|
||||
it('should have help command registered by default', () => {
|
||||
expect(commandService.commandNames).toContain('help');
|
||||
});
|
||||
|
||||
it('should register commands', () => {
|
||||
commandService.registerCommands('test-service', [
|
||||
{
|
||||
id: 'test-cmd',
|
||||
description: 'A test command',
|
||||
handler: async () => {},
|
||||
},
|
||||
]);
|
||||
expect(commandService.commandNames).toContain('test-service:test-cmd');
|
||||
});
|
||||
|
||||
it('should execute registered commands', async () => {
|
||||
let executed = false;
|
||||
commandService.registerCommands('exec-test', [
|
||||
{
|
||||
id: 'exec-cmd',
|
||||
description: 'Execute test',
|
||||
handler: async () => { executed = true; },
|
||||
},
|
||||
]);
|
||||
|
||||
const mockLog = { error: vi.fn(), log: vi.fn() };
|
||||
await commandService.executeCommand(['exec-test:exec-cmd'], mockLog);
|
||||
expect(executed).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass arguments to command handler', async () => {
|
||||
let receivedArgs: string[] = [];
|
||||
commandService.registerCommands('args-test', [
|
||||
{
|
||||
id: 'args-cmd',
|
||||
description: 'Args test',
|
||||
handler: async (args) => { receivedArgs = args; },
|
||||
},
|
||||
]);
|
||||
|
||||
const mockLog = { error: vi.fn(), log: vi.fn() };
|
||||
await commandService.executeCommand(['args-test:args-cmd', 'arg1', 'arg2'], mockLog);
|
||||
expect(receivedArgs).toEqual(['arg1', 'arg2']);
|
||||
});
|
||||
|
||||
it('should handle unknown commands', async () => {
|
||||
const mockLog = { error: vi.fn(), log: vi.fn() };
|
||||
await commandService.executeCommand(['unknown-command'], mockLog);
|
||||
expect(mockLog.error).toHaveBeenCalledWith('unknown command: unknown-command');
|
||||
});
|
||||
|
||||
it('should execute raw commands', async () => {
|
||||
let executed = false;
|
||||
commandService.registerCommands('raw-test', [
|
||||
{
|
||||
id: 'raw-cmd',
|
||||
description: 'Raw test',
|
||||
handler: async () => { executed = true; },
|
||||
},
|
||||
]);
|
||||
|
||||
const mockLog = { error: vi.fn(), log: vi.fn() };
|
||||
await commandService.executeRawCommand('raw-test:raw-cmd', mockLog);
|
||||
expect(executed).toBe(true);
|
||||
});
|
||||
|
||||
it('should get command by id', () => {
|
||||
commandService.registerCommands('get-test', [
|
||||
{
|
||||
id: 'get-cmd',
|
||||
description: 'Get test',
|
||||
handler: async () => {},
|
||||
},
|
||||
]);
|
||||
|
||||
const cmd = commandService.getCommand('get-test:get-cmd');
|
||||
expect(cmd).toBeDefined();
|
||||
expect(cmd?.id).toBe('get-test:get-cmd');
|
||||
});
|
||||
|
||||
it('should execute help command', async () => {
|
||||
const mockLog = { error: vi.fn(), log: vi.fn() };
|
||||
await commandService.executeCommand(['help'], mockLog);
|
||||
expect(mockLog.log).toHaveBeenCalledWith('available commands:');
|
||||
});
|
||||
|
||||
it('should support command completers', () => {
|
||||
commandService.registerCommands('complete-test', [
|
||||
{
|
||||
id: 'complete-cmd',
|
||||
description: 'Complete test',
|
||||
handler: async () => {},
|
||||
completer: (args) => ['option1', 'option2'],
|
||||
},
|
||||
]);
|
||||
|
||||
const cmd = commandService.getCommand('complete-test:complete-cmd');
|
||||
const completions = cmd?.completeArgument([]);
|
||||
expect(completions).toEqual(['option1', 'option2']);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -211,7 +211,17 @@ class Container {
|
||||
for ( const k in this.instances_ ) {
|
||||
try {
|
||||
if ( PARALLEL ) promises.push(this.instances_[k].init());
|
||||
else await this.instances_[k].init();
|
||||
else {
|
||||
// Logic to get name of a service, unused but
|
||||
// if you ever need to log the name
|
||||
// this will be accurate
|
||||
let name = this.instances_[k].constructor.name;
|
||||
if ( name === 'ExtensionService' ) {
|
||||
name = this.instances_[k].args.state.extension.runtime.name;
|
||||
}
|
||||
|
||||
await this.instances_[k].init();
|
||||
}
|
||||
} catch (e) {
|
||||
init_failures.push({ k, e });
|
||||
}
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2024-present 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');
|
||||
|
||||
/**
|
||||
* @class EngPortalService
|
||||
* @extends {AdvancedBase}
|
||||
*
|
||||
* EngPortalService is a class that provides services for managing and accessing various operations, alarms, and statistics
|
||||
* within a system. It inherits from the AdvancedBase class and utilizes multiple dependencies such as socket.io for communication
|
||||
* and uuidv4 for generating unique identifiers. The class includes methods for listing operations, serializing frames, listing alarms,
|
||||
* fetching server statistics, and registering command handlers. This class is integral to maintaining and monitoring system health
|
||||
* and operations efficiently.
|
||||
*/
|
||||
class EngPortalService extends AdvancedBase {
|
||||
static MODULES = {
|
||||
uuidv4: require('uuid').v4,
|
||||
};
|
||||
|
||||
constructor ({ services }) {
|
||||
super();
|
||||
this.services = services;
|
||||
this.commands = services.get('commands');
|
||||
this._registerCommands(this.commands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all ongoing operations.
|
||||
* This method retrieves all ongoing operations from the 'operationTrace' service,
|
||||
* serializes them, and returns the serialized list.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Array>} A list of serialized operation frames.
|
||||
*/
|
||||
async list_operations () {
|
||||
const svc_operationTrace = this.services.get('operationTrace');
|
||||
const ls = [];
|
||||
for ( const id in svc_operationTrace.ongoing ) {
|
||||
const op = svc_operationTrace.ongoing[id];
|
||||
ls.push(this._serialize_frame(op));
|
||||
}
|
||||
|
||||
return ls;
|
||||
}
|
||||
|
||||
_serialize_frame (frame) {
|
||||
const out = {
|
||||
id: frame.id,
|
||||
label: frame.label,
|
||||
status: frame.status,
|
||||
async: frame.async,
|
||||
checkpoint: frame.checkpoint,
|
||||
// tags: frame.tags,
|
||||
// attributes: frame.attributes,
|
||||
// messages: frame.messages,
|
||||
// error: frame.error_ ? frame.error_.message || true : null,
|
||||
children: [],
|
||||
attributes: {},
|
||||
};
|
||||
|
||||
for ( const k in frame.attributes ) {
|
||||
out.attributes[k] = frame.attributes[k];
|
||||
}
|
||||
|
||||
for ( const child of frame.children ) {
|
||||
out.children.push(this._serialize_frame(child));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of alarms.
|
||||
*
|
||||
* This method fetches all active alarms from the 'alarm' service and returns a serialized array of alarm objects.
|
||||
*
|
||||
* @returns {Promise<Array>} A promise that resolves to an array of serialized alarm objects.
|
||||
*/
|
||||
async list_alarms () {
|
||||
const svc_alarm = this.services.get('alarm');
|
||||
const ls = [];
|
||||
for ( const id in svc_alarm.alarms ) {
|
||||
const alarm = svc_alarm.alarms[id];
|
||||
ls.push(this._serialize_alarm(alarm));
|
||||
}
|
||||
|
||||
return ls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the system statistics.
|
||||
*
|
||||
* This method retrieves the system statistics from the server-health service and returns them.
|
||||
*
|
||||
* @async
|
||||
* @returns {Promise<Object>} A promise that resolves to the system statistics.
|
||||
*/
|
||||
async get_stats () {
|
||||
const svc_health = this.services.get('server-health');
|
||||
return await svc_health.get_stats();
|
||||
}
|
||||
|
||||
_serialize_alarm (alarm) {
|
||||
const out = {
|
||||
id: alarm.id,
|
||||
short_id: alarm.short_id,
|
||||
started: alarm.started,
|
||||
occurrances: alarm.occurrences.map(this._serialize_occurance.bind(this)),
|
||||
...(alarm.error ? {
|
||||
error: {
|
||||
message: alarm.error.message,
|
||||
stack: alarm.error.stack,
|
||||
},
|
||||
} : {}),
|
||||
};
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
_serialize_occurance (occurance) {
|
||||
const out = {
|
||||
message: occurance.message,
|
||||
timestamp: occurance.timestamp,
|
||||
fields: occurance.fields,
|
||||
};
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
_registerCommands (commands) {
|
||||
this.commands.registerCommands('eng', [
|
||||
{
|
||||
id: 'list-operations',
|
||||
description: 'testing',
|
||||
handler: async (args, log) => {
|
||||
const ops = await this.list_operations();
|
||||
log.log(JSON.stringify(ops, null, 2));
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
EngPortalService,
|
||||
};
|
||||
@@ -45,39 +45,6 @@ class LockService extends BaseService {
|
||||
* initialization is complete.
|
||||
*/
|
||||
async _init () {
|
||||
const svc_commands = this.services.get('commands');
|
||||
svc_commands.registerCommands('lock', [
|
||||
{
|
||||
id: 'locks',
|
||||
description: 'lists locks',
|
||||
handler: async (args, log) => {
|
||||
for ( const name in this.locks ) {
|
||||
let line = `${name }: `;
|
||||
if ( this.locks[name].effective_mode === RWLock.TYPE_READ ) {
|
||||
line += `READING (${this.locks[name].readers_})`;
|
||||
log.log(line);
|
||||
}
|
||||
else
|
||||
if ( this.locks[name].effective_mode === RWLock.TYPE_WRITE ) {
|
||||
line += 'WRITING';
|
||||
log.log(line);
|
||||
}
|
||||
else {
|
||||
line += 'UNKNOWN';
|
||||
log.log(line);
|
||||
|
||||
// log the lock's internal state
|
||||
const lines = JSON.stringify(this.locks[name],
|
||||
null,
|
||||
2).split('\n');
|
||||
for ( const line of lines ) {
|
||||
log.log(` -> ${ line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -70,34 +70,7 @@ class ScriptService extends BaseService {
|
||||
* @returns {Promise<void>} A promise that resolves when the initialization is complete.
|
||||
*/
|
||||
async _init () {
|
||||
const svc_commands = this.services.get('commands');
|
||||
svc_commands.registerCommands('script', [
|
||||
{
|
||||
id: 'run',
|
||||
description: 'run a script',
|
||||
handler: async (args, ctx) => {
|
||||
const script_name = args.shift();
|
||||
const script = this.scripts.find(s => s.name === script_name);
|
||||
if ( ! script ) {
|
||||
ctx.error(`script not found: ${script_name}`);
|
||||
return;
|
||||
}
|
||||
await script.run(ctx, args);
|
||||
},
|
||||
completer: (args) => {
|
||||
// The script name is the first argument, so return no results if we're on the second or later.
|
||||
if ( args.length > 1 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
const scriptName = args[args.length - 1];
|
||||
|
||||
return this.scripts
|
||||
.filter(script => scriptName.startsWith(scriptName))
|
||||
.map(script => script.name);
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
register (name, fn) {
|
||||
|
||||
+41
-40
@@ -158,47 +158,48 @@ export class OpenRouterProvider implements IChatProvider {
|
||||
}
|
||||
|
||||
async models () {
|
||||
let models = kv.get('openrouterChat:models');
|
||||
if ( ! models ) {
|
||||
try {
|
||||
const resp = await axios.request({
|
||||
method: 'GET',
|
||||
url: `${this.#apiBaseUrl}/models`,
|
||||
});
|
||||
return [] as IChatModel[];
|
||||
// let models = kv.get('openrouterChat:models');
|
||||
// if ( ! models ) {
|
||||
// try {
|
||||
// const resp = await axios.request({
|
||||
// method: 'GET',
|
||||
// url: `${this.#apiBaseUrl}/models`,
|
||||
// });
|
||||
|
||||
models = resp.data.data;
|
||||
kv.set('openrouterChat:models', models);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
const coerced_models: IChatModel[] = [];
|
||||
for ( const model of models ) {
|
||||
if ( (model.id as string).includes('openrouter/auto') ) {
|
||||
continue;
|
||||
}
|
||||
const overridenModel = OPEN_ROUTER_MODEL_OVERRIDES.find(m => m.id === `openrouter:${model.id}`);
|
||||
const microcentCosts = Object.fromEntries(Object.entries(model.pricing).map(([k, v]) => [k, Math.round((v as number < 0 ? 1 : v as number) * 1_000_000 * 100)])) ;
|
||||
if ( ! microcentCosts.request ) {
|
||||
microcentCosts.request = 0;
|
||||
}
|
||||
coerced_models.push({
|
||||
id: `openrouter:${model.id}`,
|
||||
name: `${model.name} (OpenRouter)`,
|
||||
aliases: [model.id, model.name, `openrouter/${model.id}`, model.id.split('/').slice(1).join('/')],
|
||||
context: model.context_length,
|
||||
max_tokens: model.top_provider.max_completion_tokens,
|
||||
costs_currency: 'usd-cents',
|
||||
input_cost_key: 'prompt',
|
||||
output_cost_key: 'completion',
|
||||
costs: {
|
||||
tokens: 1_000_000,
|
||||
...microcentCosts,
|
||||
},
|
||||
...overridenModel,
|
||||
});
|
||||
}
|
||||
return coerced_models;
|
||||
// models = resp.data.data;
|
||||
// kv.set('openrouterChat:models', models);
|
||||
// } catch (e) {
|
||||
// console.log(e);
|
||||
// }
|
||||
// }
|
||||
// const coerced_models: IChatModel[] = [];
|
||||
// for ( const model of models ) {
|
||||
// if ( (model.id as string).includes('openrouter/auto') ) {
|
||||
// continue;
|
||||
// }
|
||||
// const overridenModel = OPEN_ROUTER_MODEL_OVERRIDES.find(m => m.id === `openrouter:${model.id}`);
|
||||
// const microcentCosts = Object.fromEntries(Object.entries(model.pricing).map(([k, v]) => [k, Math.round((v as number < 0 ? 1 : v as number) * 1_000_000 * 100)])) ;
|
||||
// if ( ! microcentCosts.request ) {
|
||||
// microcentCosts.request = 0;
|
||||
// }
|
||||
// coerced_models.push({
|
||||
// id: `openrouter:${model.id}`,
|
||||
// name: `${model.name} (OpenRouter)`,
|
||||
// aliases: [model.id, model.name, `openrouter/${model.id}`, model.id.split('/').slice(1).join('/')],
|
||||
// context: model.context_length,
|
||||
// max_tokens: model.top_provider.max_completion_tokens,
|
||||
// costs_currency: 'usd-cents',
|
||||
// input_cost_key: 'prompt',
|
||||
// output_cost_key: 'completion',
|
||||
// costs: {
|
||||
// tokens: 1_000_000,
|
||||
// ...microcentCosts,
|
||||
// },
|
||||
// ...overridenModel,
|
||||
// });
|
||||
// }
|
||||
// return coerced_models;
|
||||
}
|
||||
checkModeration (_text: string): ReturnType<IChatProvider['checkModeration']> {
|
||||
throw new Error('Method not implemented.');
|
||||
|
||||
@@ -71,7 +71,6 @@ class PermissionService extends BaseService {
|
||||
*/
|
||||
this.kvService = this.services.get('puter-kvstore').as('puter-kvstore');
|
||||
this.db = this.services.get('database').get(DB_WRITE, 'permissions');
|
||||
this._register_commands(this.services.get('commands'));
|
||||
this.kvAvgTimes = { count: 0, avg: 0, max: 0 };
|
||||
this.dbAvgTimes = { count: 0, avg: 0, max: 0 };
|
||||
}
|
||||
@@ -1272,62 +1271,6 @@ class PermissionService extends BaseService {
|
||||
|
||||
this._permission_exploders.push(exploder);
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
commands.registerCommands('perms', [
|
||||
{
|
||||
id: 'grant-user-app',
|
||||
handler: async (args, _log) => {
|
||||
const [username, app_uid, permission, extra] = args;
|
||||
|
||||
// actor from username
|
||||
const actor = new Actor({
|
||||
type: new UserActorType({
|
||||
user: await get_user({ username }),
|
||||
}),
|
||||
});
|
||||
|
||||
await this.grant_user_app_permission(actor, app_uid, permission, extra);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scan',
|
||||
handler: async (args, ctx) => {
|
||||
const [username, permission] = args;
|
||||
|
||||
// actor from username
|
||||
const actor = new Actor({
|
||||
type: new UserActorType({
|
||||
user: await get_user({ username }),
|
||||
}),
|
||||
});
|
||||
|
||||
let reading = await this.scan(actor, permission);
|
||||
// reading = PermissionUtil.reading_to_options(reading);
|
||||
ctx.log(JSON.stringify(reading, undefined, ' '));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scan-app',
|
||||
handler: async (args, ctx) => {
|
||||
const [username, app_name, permission] = args;
|
||||
const app = await get_app({ name: app_name });
|
||||
|
||||
// actor from username
|
||||
const actor = new Actor({
|
||||
type: new AppUnderUserActorType({
|
||||
app,
|
||||
user: await get_user({ username }),
|
||||
}),
|
||||
});
|
||||
|
||||
const reading = await this.scan(actor, permission);
|
||||
// reading = PermissionUtil.reading_to_options(reading);
|
||||
ctx.log(JSON.stringify(reading, undefined, ' '));
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -270,7 +270,6 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
}
|
||||
|
||||
async '__on_boot.consolidation' () {
|
||||
this._register_commands(this.services.get('commands'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,37 +371,6 @@ class SqliteDatabaseAccessService extends BaseDatabaseAccessService {
|
||||
await vm.runInContext(contents, context);
|
||||
}
|
||||
|
||||
_register_commands (commands) {
|
||||
commands.registerCommands('sqlite', [
|
||||
{
|
||||
id: 'execfile',
|
||||
description: 'execute a file',
|
||||
handler: async (args, log) => {
|
||||
try {
|
||||
const [filename] = args;
|
||||
const fs = require('fs');
|
||||
const contents = fs.readFileSync(filename, 'utf8');
|
||||
this.db.exec(contents);
|
||||
} catch ( err ) {
|
||||
log.error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'read',
|
||||
description: 'read a query',
|
||||
handler: async (args, log) => {
|
||||
try {
|
||||
const [query] = args;
|
||||
const rows = this._read(query, []);
|
||||
log.log(rows);
|
||||
} catch ( err ) {
|
||||
log.error(err.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -44,39 +44,6 @@ class FSLockService extends BaseService {
|
||||
* @returns {Promise<void>} A promise that resolves when the initialization is complete.
|
||||
*/
|
||||
async _init () {
|
||||
const svc_commands = this.services.get('commands');
|
||||
svc_commands.registerCommands('fslock', [
|
||||
{
|
||||
id: 'locks',
|
||||
description: 'lists locks',
|
||||
handler: async (args, log) => {
|
||||
for ( const path in this.locks ) {
|
||||
let line = `${path }: `;
|
||||
if ( this.locks[path].effective_mode === MODE_READ ) {
|
||||
line += `READING (${this.locks[path].readers_})`;
|
||||
log.log(line);
|
||||
}
|
||||
else
|
||||
if ( this.locks[path].effective_mode === MODE_WRITE ) {
|
||||
line += 'WRITING';
|
||||
log.log(line);
|
||||
}
|
||||
else {
|
||||
line += 'UNKNOWN';
|
||||
log.log(line);
|
||||
|
||||
// log the lock's internal state
|
||||
const lines = JSON.stringify(this.locks[path],
|
||||
null,
|
||||
2).split('\n');
|
||||
for ( const line of lines ) {
|
||||
log.log(` -> ${ line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -104,7 +104,8 @@ class Mig_StorePath extends Job {
|
||||
for ( ;; ) {
|
||||
const t_0 = performance.now();
|
||||
const [fsentries] = await dbrr.promise().execute(
|
||||
'SELECT id, uuid FROM fsentries WHERE path IS NULL ORDER BY accessed DESC LIMIT 50');
|
||||
'SELECT id, uuid FROM fsentries WHERE path IS NULL ORDER BY accessed DESC LIMIT 50',
|
||||
);
|
||||
|
||||
if ( fsentries.length === 0 ) {
|
||||
log.info('No more fsentries to migrate');
|
||||
@@ -127,8 +128,9 @@ class Mig_StorePath extends Job {
|
||||
log.info(`id=${fsentry.id} uuid=${fsentry.uuid} path=${path}`);
|
||||
}
|
||||
await dbrw.promise().execute(
|
||||
'UPDATE fsentries SET path=? WHERE id=?',
|
||||
[path, fsentry.id]);
|
||||
'UPDATE fsentries SET path=? WHERE id=?',
|
||||
[path, fsentry.id],
|
||||
);
|
||||
}
|
||||
|
||||
const t_1 = performance.now();
|
||||
@@ -173,7 +175,8 @@ class Mig_IndexAccessed extends Job {
|
||||
log.info('Running update statement');
|
||||
const t_0 = performance.now();
|
||||
const [results] = await dbrr.promise().execute(
|
||||
'UPDATE fsentries SET accessed = COALESCE(accessed, created) WHERE accessed IS NULL LIMIT 10000');
|
||||
'UPDATE fsentries SET accessed = COALESCE(accessed, created) WHERE accessed IS NULL LIMIT 10000',
|
||||
);
|
||||
log.info(`Updated ${results.affectedRows} rows`);
|
||||
|
||||
if ( results.affectedRows === 0 ) {
|
||||
@@ -369,43 +372,19 @@ class Mig_AuditInitialStorage extends Job {
|
||||
* independently.
|
||||
*/
|
||||
class FSEntryMigrateService {
|
||||
constructor ({ services }) {
|
||||
const mysql = services.get('mysql');
|
||||
const dbrr = mysql.get(DB_MODE_READ, 'fsentry-migrate');
|
||||
const dbrw = mysql.get(DB_MODE_WRITE, 'fsentry-migrate');
|
||||
const log = services.get('log-service').create('fsentry-migrate');
|
||||
constructor (_) {
|
||||
// const mysql = services.get('mysql');
|
||||
// const dbrr = mysql.get(DB_MODE_READ, 'fsentry-migrate');
|
||||
// const dbrw = mysql.get(DB_MODE_WRITE, 'fsentry-migrate');
|
||||
// const log = services.get('log-service').create('fsentry-migrate');
|
||||
|
||||
const migrations = {
|
||||
'store-path': new Mig_StorePath({ dbrr, dbrw, log }),
|
||||
'index-accessed': new Mig_IndexAccessed({ dbrr, dbrw, log }),
|
||||
'fix-trash': new Mig_FixTrash({ dbrr, dbrw, log }),
|
||||
'gen-referral-codes': new Mig_AddReferralCodes({ dbrr, dbrw, log }),
|
||||
};
|
||||
// const migrations = {
|
||||
// 'store-path': new Mig_StorePath({ dbrr, dbrw, log }),
|
||||
// 'index-accessed': new Mig_IndexAccessed({ dbrr, dbrw, log }),
|
||||
// 'fix-trash': new Mig_FixTrash({ dbrr, dbrw, log }),
|
||||
// 'gen-referral-codes': new Mig_AddReferralCodes({ dbrr, dbrw, log }),
|
||||
// };
|
||||
|
||||
services.get('commands').registerCommands('fsentry-migrate', [
|
||||
{
|
||||
id: 'start',
|
||||
description: 'start a migration',
|
||||
handler: async (args, log) => {
|
||||
const [migration] = args;
|
||||
if ( ! migrations[migration] ) {
|
||||
throw new Error(`unknown migration: ${migration}`);
|
||||
}
|
||||
migrations[migration].start(args.slice(1));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'stop',
|
||||
description: 'stop a migration',
|
||||
handler: async (args, log) => {
|
||||
const [migration] = args;
|
||||
if ( ! migrations[migration] ) {
|
||||
throw new Error(`unknown migration: ${migration}`);
|
||||
}
|
||||
migrations[migration].stop();
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user