diff --git a/src/backend/exports.js b/src/backend/exports.js
index 46cef0e86..2ff8500d8 100644
--- a/src/backend/exports.js
+++ b/src/backend/exports.js
@@ -33,6 +33,7 @@ const { TemplateModule } = require("./src/modules/template/TemplateModule.js");
const { PuterFSModule } = require("./src/modules/puterfs/PuterFSModule.js");
const { PerfMonModule } = require("./src/modules/perfmon/PerfMonModule.js");
const { AppsModule } = require("./src/modules/apps/AppsModule.js");
+const { DevelopmentModule } = require("./src/modules/development/DevelopmentModule.js");
module.exports = {
@@ -69,4 +70,5 @@ module.exports = {
// Development modules
PerfMonModule,
+ DevelopmentModule,
};
diff --git a/src/backend/src/data/hardcoded-permissions.js b/src/backend/src/data/hardcoded-permissions.js
index 2dbfe0a19..d75685c31 100644
--- a/src/backend/src/data/hardcoded-permissions.js
+++ b/src/backend/src/data/hardcoded-permissions.js
@@ -97,6 +97,7 @@ const hardcoded_user_group_permissions = {
'service': {},
'feature': {},
'kernel-info': {},
+ 'local-terminal:access': {},
},
'b7220104-7905-4985-b996-649fdcdb3c8f': {
'service:hello-world:ii:hello-world': policy_perm('temp.es'),
diff --git a/src/backend/src/modules/development/DevelopmentModule.js b/src/backend/src/modules/development/DevelopmentModule.js
new file mode 100644
index 000000000..409f9dea3
--- /dev/null
+++ b/src/backend/src/modules/development/DevelopmentModule.js
@@ -0,0 +1,39 @@
+/*
+ * 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 .
+ */
+
+const { AdvancedBase } = require("@heyputer/putility");
+
+/**
+ * Enable this module when you want performance monitoring.
+ *
+ * Performance monitoring requires additional setup. Jaegar should be installed
+ * and running.
+ */
+class DevelopmentModule extends AdvancedBase {
+ async install (context) {
+ const services = context.get('services');
+
+ const LocalTerminalService = require("./LocalTerminalService");
+ services.registerService('local-terminal', LocalTerminalService);
+ }
+}
+
+module.exports = {
+ DevelopmentModule,
+};
diff --git a/src/backend/src/modules/development/LocalTerminalService.js b/src/backend/src/modules/development/LocalTerminalService.js
new file mode 100644
index 000000000..3fc2016a2
--- /dev/null
+++ b/src/backend/src/modules/development/LocalTerminalService.js
@@ -0,0 +1,173 @@
+/*
+ * 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 .
+ */
+
+const { spawn } = require("child_process");
+const APIError = require("../../api/APIError");
+const configurable_auth = require("../../middleware/configurable_auth");
+const { Endpoint } = require("../../util/expressutil");
+
+
+const PERM_LOCAL_TERMINAL = 'local-terminal:access';
+
+const path_ = require('path');
+const { Actor } = require("../../services/auth/Actor");
+const BaseService = require("../../services/BaseService");
+const { Context } = require("../../util/context");
+
+class LocalTerminalService extends BaseService {
+ _construct () {
+ this.sessions_ = {};
+ }
+ get_profiles () {
+ return {
+ ['api-test']: {
+ cwd: path_.join(
+ __dirname,
+ '../../../../../',
+ 'tools/api-tester',
+ ),
+ shell: ['/usr/bin/env', 'node', 'apitest.js'],
+ allow_args: true,
+ },
+ };
+ };
+ ['__on_install.routes'] (_, { app }) {
+ const r_group = (() => {
+ const require = this.require;
+ const express = require('express');
+ return express.Router()
+ })();
+ app.use('/local-terminal', r_group);
+
+ Endpoint({
+ route: '/new',
+ methods: ['POST'],
+ mw: [configurable_auth()],
+ handler: async (req, res) => {
+ const term_uuid = require('uuid').v4();
+
+ const svc_permission = this.services.get('permission');
+ const actor = Context.get('actor');
+ const can_access = actor &&
+ await svc_permission.check(actor, PERM_LOCAL_TERMINAL);
+
+ if ( ! can_access ) {
+ throw APIError.create('permission_denied', null, {
+ permission: PERM_LOCAL_TERMINAL,
+ });
+ }
+
+ const profiles = this.get_profiles();
+ if ( ! profiles[req.body.profile] ) {
+ throw APIError.create('invalid_profile', null, {
+ profile: req.body.profile,
+ });
+ }
+
+ const profile = profiles[req.body.profile];
+
+ const args = profile.shell.slice(1);
+ if ( ! profile.allow_args && req.body.args ) {
+ args.push(...req.body.args);
+ }
+ const proc = spawn(profile.shell[0], args, {
+ shell: true,
+ env: {
+ ...process.env,
+ ...(profile.env ?? {}),
+ },
+ cwd: profile.cwd,
+ });
+
+ console.log('process??', proc);
+
+ // stdout to websocket
+ {
+ const svc_socketio = req.services.get('socketio');
+ proc.stdout.on('data', data => {
+ const base64 = data.toString('base64');
+ console.log('---------------------- CHUNK?', base64);
+ svc_socketio.send(
+ { room: req.user.id },
+ 'local-terminal.stdout',
+ {
+ term_uuid,
+ base64,
+ },
+ );
+ });
+ proc.stderr.on('data', data => {
+ const base64 = data.toString('base64');
+ console.log('---------------------- CHUNK?', base64);
+ svc_socketio.send(
+ { room: req.user.id },
+ 'local-terminal.stderr',
+ {
+ term_uuid,
+ base64,
+ },
+ );
+ });
+ }
+
+ proc.on('exit', () => {
+ this.log.noticeme(`[${term_uuid}] Process exited (${proc.exitCode})`);
+ delete this.sessions_[term_uuid];
+ });
+
+ this.sessions_[term_uuid] = {
+ uuid: term_uuid,
+ proc,
+ };
+
+ res.json({ term_uuid });
+ },
+ }).attach(r_group);
+ }
+ async _init () {
+ const svc_event = this.services.get('event');
+ svc_event.on('web.socket.user-connected', async (_, {
+ socket,
+ user,
+ }) => {
+ const svc_permission = this.services.get('permission');
+ const actor = Actor.adapt(user);
+ const can_access = actor &&
+ await svc_permission.check(actor, PERM_LOCAL_TERMINAL);
+
+ if ( ! can_access ) {
+ return;
+ }
+
+ socket.on('local-terminal.stdin', async msg => {
+ console.log('local term message', msg);
+
+ const session = this.sessions_[msg.term_uuid];
+ if ( ! session ) {
+ return;
+ }
+
+ const base64 = Buffer.from(msg.data, 'base64');
+ session.proc.stdin.write(base64);
+ })
+ });
+ }
+}
+
+module.exports = LocalTerminalService;
diff --git a/src/backend/src/modules/web/WebServerService.js b/src/backend/src/modules/web/WebServerService.js
index ee6d10fe7..3f910ba9e 100644
--- a/src/backend/src/modules/web/WebServerService.js
+++ b/src/backend/src/modules/web/WebServerService.js
@@ -275,6 +275,7 @@ class WebServerService extends BaseService {
actor: socket.actor,
}).arun(async () => {
await svc_event.emit('web.socket.user-connected', {
+ socket,
user: socket.user
});
});