Skip to content

Commit

Permalink
dev: experimental local-terminal service
Browse files Browse the repository at this point in the history
  • Loading branch information
KernelDeimos committed Jan 10, 2025
1 parent 7293fdb commit 2993b88
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/backend/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -69,4 +70,5 @@ module.exports = {

// Development modules
PerfMonModule,
DevelopmentModule,
};
1 change: 1 addition & 0 deletions src/backend/src/data/hardcoded-permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
39 changes: 39 additions & 0 deletions src/backend/src/modules/development/DevelopmentModule.js
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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,
};
173 changes: 173 additions & 0 deletions src/backend/src/modules/development/LocalTerminalService.js
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/

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;
1 change: 1 addition & 0 deletions src/backend/src/modules/web/WebServerService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});
Expand Down

0 comments on commit 2993b88

Please sign in to comment.