@d3x0r/sack-gui
Version:
SACK abstraction library exposed to JS to provide low level system services.
730 lines (670 loc) • 21.6 kB
JavaScript
const myPath = import.meta.url.split(/\/|\\/g);
const tmpPath = myPath.slice();
tmpPath.splice( 0, 3 );
tmpPath.splice( tmpPath.length-1, 1 );
const appRoot = (process.platform==="win32"?"":'/')+tmpPath.slice(0,-1).join( '/' );
const parentRoot = (process.platform==="win32"?"":'/')+tmpPath.slice(0,-2).join( '/' );
import {System} from "../ui/system.mjs"
import {local} from "./local.mjs"
import os from "os";
import {sack} from "sack.vfs"
const disk = sack.Volume();
export const pwdBare = process.cwd();
let firstLoad = true;
let config = await reloadConfig();
import {openServer} from "../../http-ws/server.mjs"
import {config as taskConfig, Task, closeAllTasks} from "./task.mjs"
taskConfig.pwdBare = pwdBare;
taskConfig.send = send;
taskConfig.config = config;
taskConfig.local = local;
local.addTask = addTask;
const JSOX = sack.JSOX;
async function reloadConfig() {
const config_run = (await import( (process.platform==="win32"?"file://":"")+pwdBare+"/config.run.jsox" ).catch( err=>(console.log( "parsing error:", err),{default:null}) )).default;
const config_tasks = (await import( (process.platform==="win32"?"file://":"")+pwdBare+"/config.tasks.jsox" ).catch( err=>(console.log( "parsing error:", err),{default:null}) )).default;
const config = config_run
|| config_tasks
|| (await import( (process.platform==="win32"?"file://":"")+pwdBare+"/config.jsox" ).catch( err=>({default:null}) )).default
|| { extraModules:[]
, hostname:""
, useUpstream: false
, upstreamServer: ""
, port:0
, tasks:[]
};
if( !firstLoad )
config.tasks.forEach( loadTask );
firstLoad = false;
return config;
}
const serverOpts = {resourcePath:appRoot+"/ui"
, npmPath:parentRoot+"/.."
, port:Number(process.env.PORT) || config.port || 8080};
// start server...
console.log( "Serve on port:", serverOpts.port );
const server = openServer( serverOpts, accept, connect );
server.addHandler( (req,res)=>{
if( req.url.startsWith( "/events")){
req.url = "/../.." + req.url;
}
return false;
})
class Connection {
ws = null;
logStreams = [];
remote = null;
address = null;
system = null;
constructor( ws ) {
this.ws = ws;
this.address = ws.connection.remoteAddress;
}
}
function handleStart( ws, msg, msg_ ) {
if( (!("system" in msg ) ) || msg.system === local.id ){
const task = local.taskMap[msg.id];
if( !task ) {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
} else {
//task.restart = true;
task.start();
}
} else {
const remote = local.systems.find( system=>system.id === msg.system );
console.log( "Got remote:", remote, remote.connection );
if( remote ) remote.connection.ws.send( msg_ );
else ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
}
}
function handleRestart( ws, msg, msg_ ) {
if( (!("system" in msg ) ) || msg.system === local.id ){
const task = local.taskMap[msg.id];
if( !task ) {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
} else {
console.log( "Set task restart:", task );
task.restart = true;
console.log( "Task is running alrady?", task.running );
}
} else {
const remote = local.systems.find( system=>system.id === msg.system );
console.log( "Got remote:", remote, remote.connection );
if( remote ) remote.connection.ws.send( msg_ );
else ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
}
}
function handleStop( ws, msg, msg_ ) {
if( (!("system" in msg ) ) || msg.system === local.id ){
const task = local.taskMap[msg.id];
if( !task ) {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
} else {
task.restart = false;
task.stop();
}
} else {
const remote = local.systems.find( system=>system.id === msg.system );
if( remote ) remote.connection.ws.send( msg_ );
else {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
ws.send( JSOX.stringify( {op:"deleteSystem", id: msg.system } ) );
}
}
}
function handleInput( ws, msg, msg_ )
{
if( (!("system" in msg ) ) || msg.system === local.id ){
const task = local.taskMap[msg.id];
//console.log( "Log request:", msg_ );
if( !task ) {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
} else {
task.run.write( msg.data );
}
}
else {
const remote = local.systems.find( system=>system.id === msg.system );
//console.log( "found remote system:", remote, msg_ );
if( remote ) {
remote.connection.ws.send( msg_ );
}
else {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
ws.send( JSOX.stringify( {op:"deleteSystem", id: msg.system } ) );
}
}
}
function handleLog( ws, msg, msg_ )
{
if( (!("system" in msg ) ) || msg.system === local.id ){
const task = local.taskMap[msg.id];
//console.log( "Log request:", msg_ );
if( !task ) {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
} else if( !msg.at ) {
// this adds the socket, and sends the initial log for the task..
task.ws = ws;
// this will generate 'log' as new messages happen.
} else {
const backlog = task.getLog( msg.at );
const backLogMsg = { op:"backlog", system: local.id, id:task.id, backlog };
//console.log( "Sending backlog", backLogMsg );
ws.send( JSOX.stringify( backLogMsg ))
}
}
else {
const remote = local.systems.find( system=>system.id === msg.system );
//console.log( "found remote system:", remote, msg_ );
if( remote ) {
remote.connection.ws.send( msg_ );
// this task can wants to be on this connection.
if( local.taskMap[msg.id] )
local.taskMap[msg.id].push( ws );
else
local.taskMap[msg.id] = [ ws ];
}
else {
ws.send( JSOX.stringify( {op:"delete", id: msg.id } ) );
ws.send( JSOX.stringify( {op:"deleteSystem", id: msg.system } ) );
}
}
}
function handleTaskInfo( ws, msg, msg_ ) {
//console.log( "handleTaskInfo:", msg_ );
if( msg.system === local.id ) {
const task = local.taskMap[msg.id];
if( task ){
ws.send( JSOX.stringify( {op:"taskInfo", id:task.id, task:task.task, title:task.title }));
}else
ws.send( JSOX.stringify( {op:"taskInfo", id:task.id, task:null }));
} else {
const remote = local.systems.find( system=>system.id === msg.system );
if( remote ) {
if( local.replyMap[msg.id] ) {
local.replyMap[msg.id].push( ws );
} else local.replyMap[msg.id] = [ws];
remote.connection.ws.send( msg_ );
}
}
}
function connectToCore() {
console.log( "Connecting upstream...");
const ws = sack.WebSocket.Client( "ws://"+(config.upstreamServer|| "localhost:8089"), "task-proxy");
//console.log( "ws?", ws );
ws.onopen = ()=>{
console.log( "Sending initial tasks" );
ws.send( JSOX.stringify( {op:"extern.tasks", tasks:local.tasks
, system:config.hostname || os.hostname()
, id : local.id
, port:serverOpts.port
}
))
local.upstreamWS = ws;
}
ws.onmessage = (msg)=>handleMessage(ws,msg);
ws.onclose = (code,reason)=>{
console.log( "Disconnected from upstream" );
local.upstreamWS = null;
setTimeout( connectToCore, 5000 );
}
}
//console.log( "Upstream?", config.useUpstream, config.upstreamServer )
if( config.useUpstream )
connectToCore();
config.tasks.forEach( loadTask );
function loadTask( task ) {
const oldTask = local.tasks.find( oldTask=>oldTask.name === task.name );
if( !oldTask ) {
const newTask = new Task( task );
if( !config.tasks.find( oldTask=>oldTask.name === task.name )){
config.tasks.push( task );
}
local.tasks.push( newTask );
local.taskMap[newTask.id] = newTask;
return newTask;
}else {
oldTask.update( task );
return oldTask;
}
}
function loadModules( n ) {
if( n >= config.extraModules.length )
return startTasks();
import( config.extraModules[n].name ).then( (module)=>{
module[config.extraModules[n].function](config.extraModules[n].options).then( ()=>{
loadModules( n+1 );
} ).catch( (err)=>{
console.log( "Error loading:", config.extraModules[n].name, config.extraModules[n].function );
loadModules( n+1 );
} )
} ).catch( (err)=>{
console.log( "Error loading:", config.extraModules[n].name, config.extraModules[n].function );
loadModules( n+1 );
} );
}
function onStopAll( n ) {
if( !config.onStopAll || n >= config.onStopAll.length )
return;
return import( config.onStopAll[n].name ).then( (module)=>{
module[config.onStopAll[n].function](config.onStopAll[n].options).then( ()=>{
return loadModules( n+1 );
} ).catch( (err)=>{
console.log( "Error loading:", config.onStopAll[n].name, config.onStopAll[n].function );
return loadModules( n+1 );
} )
} ).catch( (err)=>{
console.log( "Error loading:", config.onStopAll[n].name, config.onStopAll[n].function );
return loadModules( n+1 );
} );
}
if( config.extraModules ) {
loadModules( 0 );
}
else startTasks();
function startTasks() {
local.tasks.forEach( task=>{
if (!task.running
&& !task.hasDepends
&& !task.noAutoRun){
task.start()
}} );
}
// start client interface to server.
//sack.Task( { work:programRoot+"/../ui", bin:"cmd", args:["/C", "start", "http://localhost:8080/index.html" ] } );
//setTimeout( ()=>{console.log( "Timeout closing" )}, 5000 );
export function send( msg_ ) {
// should append my system id
if( "string" === typeof msg_ ) {
if( local.upstreamWS && local.upstreamWS.readyState === 1 ) local.upstreamWS.send( msg_ );
local.connections.forEach( conn=>(conn.ws.readyState == 1) &&conn.ws.send( msg_ ) );
} else {
const msg = JSOX.stringify( msg_ );
if( local.upstreamWS && local.upstreamWS.readyState === 1 ) local.upstreamWS.send( msg );
local.connections.forEach( conn=>(conn.ws.readyState == 1) &&conn.ws.send( msg ) );
}
}
function accept( ws ) {
this.accept();
}
function connect( ws ) {
//console.log( "Connect ws:", ws.headers );
const connection = new Connection( ws );
const protocol = ws.headers["Sec-WebSocket-Protocol"];
if( protocol === "task-proxy" )
ws.onmessage = handleProxyMessage;
else if( protocol === "tasks" ) { // client UI
ws.onmessage = (msg)=>handleMessage(ws,msg);
console.log( "Adding connection...");
local.connections.push( connection );
sendTasks();
} else {
ws.close( 1020, "Bad Protocol" );
return;
}
ws.onclose = handleClose;
// this is from a peer connecting to upstream
function handleProxyMessage( msg_ ) {
const msg = JSOX.parse( msg_ );
//console.log( "Received (from proxy):", msg );
switch( msg.op ) {
case "taskInfo":
const replyTo = local.replyMap[msg.id];
//console.log( "Info reply should rely to:", replyTo, local.replyMap );
if( replyTo ) {
replyTo.forEach( ws=>(ws.readyState===1) && ws.send( msg_ ) );
delete local.replyMap[msg.id];
} else {
//ws.send( JSOX.stringify( {op:"remote disappeared?"}))
}
break;
case "log": {
// relay log forward... someone asked for logging
// to which connections?
const sendTo = local.taskMap[msg.id];
if( sendTo ) {
sendTo.forEach( ws=>(ws.readyState===1) && ws.send( msg_ ) );
} else {
//ws.send( JSOX.stringify( {op:"remote disappeared?"}))
}
}
break;
case "backlog": {
const sendTo = local.taskMap[msg.id];
if( sendTo ) {
sendTo.forEach( ws=>(ws.readyState===1) && ws.send( msg_ ) );
}else {
//ws.send( JSOX.stringify( {op:"remote disappeared?"}))
}
}break;
case "addTask": {
// received from creating a remote task
connection.system.addTask( msg.id, msg.task );
if( local.upstreamWS ) local.upstreamWS.send( JSOX.stringify( {op:msg.op, system:connection.system.id, task:msg.task } ) );
send( {op:msg.op, system:connection.system.id, task:msg.task});
}
break;
case "updateTask": {
// received from updating a remote task
connection.system.updateTask( msg.task );
if( local.upstreamWS ) local.upstreamWS.send( JSOX.stringify( {op:msg.op, system:connection.system.id, task:msg.task } ) );
send( {op:msg.op, system:connection.system.id, task:msg.task});
}
break;
case "deleteTask": {
// received from deleting a remote task
connection.system.deleteTask( msg.task );
if( local.upstreamWS ) local.upstreamWS.send( msg_ );
send( {op:msg.op, system:connection.system.id, task:msg.task});
}
break;
case "status": {
// update internal version of statuses.
for( let n = 0; n < local.systems.length; n++ ) {
const system = local.systems[n];
if( system.id === msg.system ) {
for( let task of system.tasks ) {
if( task.id === msg.id ) {
task.running = msg.running;
}
}
}
}
// send to all other connections a status update...
// msg.system should be a remote system id, but I don't hae to insert it based on
// connection.system.id
if( local.upstreamWS ) local.upstreamWS.send( msg_ );
send( msg_ );
break;
}
case "extern.tasks": {
let n;
let system = null;
//console.log( "Got external tasks...", msg );
//console.log( "looking at systems:", local.systems );
//console.log( "Connection system?", connection.system );
if( connection.system ){
if( connection.system.id === msg.id ){
console.log( "This shouldn't happen.. we should have already been connected to this system...")
system = connection.system;
}else {
for( n = 0; n < connection.systems.length; n++ ) {
const testSystem = connection.systems[n];
if( testSystem.id === msg.id ){
connection.system = testSystem; // this really shouldn't happen...
console.log( "Found existing system to replace tasks. (this probably shouldn't happen with IDs)" );
system = testSystem;
system.connection = connection;
system.tasks = msg.tasks;
break;
}
}
}
} else
for( n = 0; n < local.systems.length; n++ ) {
const testSystem = local.systems[n];
if( testSystem.id === msg.id ){
system = testSystem;
console.log( "Found existing system to replace tasks." );
system.connection = connection;
system.tasks = msg.tasks;
break;
}
}
if( !system ){
console.log( "Make a new system" );
// this is the connection that the system can be reached on...
system = new System( connection, msg.id, msg.port, msg.system, msg.tasks);
// if this already heard tasks, this is probably a chlid system of the remote
// which will go under that system's systems.
if( connection.system ){
system.upstream = connection.system;
connection.system.systems.push( system );
}
else {
connection.system = system;
// upstream is self. (null)
// another level above me would have this upstream as me...
}
// every remote sys in local.systems.
local.systems.push( system );
}
if( local.upstreamWS ) {
local.upstreamWS.send( msg_ );
}
send( msg_ );
}
break;
}
}
function sendTasks() {
const msg = {op:"tasks", system:local.id, tasks: local.tasks, systems: local.systems };
const msg_ = JSOX.stringify( msg );
ws.send( msg_ );
}
function handleClose( code, reason ) {
if( protocol === "task-proxy"){
// need to forget this system.
const systemindex = local.systems.findIndex( system=>system.connection === connection );
console.log( "did we find proxy connection?", systemindex);
if( systemindex >= 0 ) {
local.systems.splice( systemindex, 1 );
console.log( "connection too:", connection.system );
send( {op:"deleteSystem", id: connection.system.id});
}
}
console.log( "Client disconnect:", code, reason );
const id = local.connections.findIndex( conn=>conn.ws===ws );
//console.log( "Did we find the connection?", id );
if( id >=0 ) local.connections.splice( id, 1 );
}
}
function addTask( id, task ) {
if( local.upstreamWS ) local.upstreamWS.send( {op:"addTask", system:local.id, id, task } );
send( {op:"addTask", system:local.id, id, task } );
}
function updateTask( id, task ) {
if( local.upstreamWS ) local.upstreamWS.send( {op:"updateTask", system:local.id, id, task } );
send( {op:"updateTask", system:local.id, id, task } );
}
function deleteTask( id ) {
if( local.upstreamWS ) local.upstreamWS.send( {op:"deleteTask", system:local.id, id } );
send( {op:"deleteTask", system:local.id, id } );
}
function handleMessage( ws, msg_ ) {
try {
const msg = JSOX.parse( msg_ );
switch( msg.op ) {
case "shutdown": {
console.log( "received shutdown request" );
closeAllTasks(msg.close?ws:null).then( ()=>{
console.log( "Close resulted, and we're exiting now." );
setTimeout( ()=>{process.exit(msg.stop?1:0);}, 1000 );
} );
break;
}
case "stopAll": {
console.log( "Stopping all tasks", msg.close );
if( msg.close )
closeAllTasks( ws ).then( onStopAll );
else
closeAllTasks().then( onStopAll );
break;
}
case "startAll": {
console.log( "Start all tasks" );
startTasks();
if( msg.close )
ws.close( 1000, "Starting Tasks" );
break;
}
case "task_logging":
for( let task of local.tasks ) {
if( task.id === msg.id ) {
task.ws = ws;
break;
}
}
break;
case "start":
handleStart( ws, msg, msg_ );
break;
case "stop":
handleStop( ws, msg, msg_ );
break;
case "restart":
handleRestart( ws, msg, msg_ );
break;
case "log":
handleLog( ws, msg, msg_ );
break;
case "send":
handleInput( ws, msg, msg_ );
break;
case "createTask": {
if( local.system === msg.system || !msg.system ) {
const task = loadTask( msg.task );
if( !msg.task.temporary )
saveRunConfig();
addTask( task.id, task ); // sends new task
if( !task.noAutoRun ) task.start();
} else if( msg.system && msg.system != local.system ) {
local.systems.find( system=>{
if( system.id === msg.system ) {
system.createTask( msg_ );
return true;
}
return false;
});
}else {
const task = loadTask( msg.task );
if( !msg.task.temporary )
saveRunConfig();
addTask( task.id, task );
if( !task.noAutoRun ) task.start();
}
}
break;
case "updateTask": {
if( !msg.system || msg.system === local.id ) {
const task = local.taskMap[msg.id];
if( task )
{
const taskInfo = task.task;
task.update( msg.task );
for( let t = 0; t < config.tasks; t++ ) {
if( config.task[t] === taskInfo ) {
const keys = Object.keys( msg.task );
for( let key of keys ) {
taskInfo[key] = msg.task[key];
}
}
msg.task = taskInfo;
msg_ = JSOX.stringify( msg );
}
saveRunConfig();
updateTask( msg.id, task.task );
}
}else {
if( connection.system )
connection.system.updateTask( msg.id, msg.task );
else
console.log( "Told to update a task I don't know, and can't reach?", msg );
}
}
break;
case "deleteTask": {
if( !msg.system || msg.system ===local.system ) {
const task = local.taskMap[msg.id];
if( task ) {
const taskInfo = task.task;
for( let t = 0; t < config.tasks.length; t++ ) {
if( config.tasks[t] === taskInfo ) {
config.tasks.splice( t, 1 );
for( let t2 = 0; t2 < local.tasks.length; t2++ ) {
if( local.tasks[t2] === task ) {
local.tasks.splice( t2, 1 );
break;
}
}
delete local.taskMap[msg.id];
if( task.running )
task.stop();
if( !taskInfo.temporary )
saveRunConfig();
break;
}
}
send( msg_ );
}
} else {
// send to remote system...
}
}
break;
case "getDisplays": {
const displays = sack.Task.getDisplays();
for( let device of displays.device ) {
for( let monitor of displays.monitor ) {
if( monitor.display === device.display ) {
device.monitor = monitor;
monitor.device = device;
break;
}
}
}
ws.send( JSOX.stringify( { op:"displays", displays: displays } ) );
}
break;
case "getTaskInfo":
handleTaskInfo( ws, msg, msg_ );
break;
case "updateDisplay": {
const task = local.taskMap[msg.id];
if( !task ) {
// task on a remote system?
} else {
const taskInfo = task.task;
if( "moveTo" in taskInfo ) {
if( "monitor" in msg ) {
delete taskInfo.moveTo.display;
taskInfo.moveTo.monitor = msg.monitor;
} else {
delete taskInfo.moveTo.monitor;
taskInfo.moveTo.display = msg.display;
}
} else {
taskInfo.moveTo = {
display: msg.display,
monitor: msg.monitor,
timeout: 2500,
};
}
task.move();
saveRunConfig();
}
}
break;
}
} catch( err ) {
console.log( "Exception?", err );
}
}
function saveRunConfig() {
const c = Object.assign( {}, config );
c.tasks = c.tasks.reduce( (acc,task)=>{if( !task.temporary ) acc.push( task ); return acc;}, [] );
const output = JSOX.stringify( config, null, "\t" );
disk.write( "config.run.jsox", output );
}
if( "enableExitSignal" in sack.system ) {
sack.system.enableExitSignal( ()=>{
console.log( "Got exit signal... so generate exit?" );
closeAllTasks().then( ()=>{
console.log( "Took some time to shut down tasks?" );
//process.emit( "SIGINT" );
process.exit(0);
});
} );
}
//process.on( "SIGINT", ()=>{ process.stdout.write( "SIGINT\n" ); closeAllTasks().then( ()=>{ console.log( "sigint terminate finished" ); } ) } );
//process.on( "uncaughtException", (err)=>{ process.stdout.write( "uncaught Exception" + err ); } );