UNPKG

@d3x0r/sack-gui

Version:

SACK abstraction library exposed to JS to provide low level system services.

554 lines (512 loc) 15.2 kB
import {local} from "./local.mjs" import {sack} from "sack.vfs" const JSOX = sack.JSOX; const disk = sack.Volume(); export const config = { pwdBare:null, config:null, send:null, local : null } //import {pwdBare, config,send} from "./main.mjs"; let pendingDepends = []; export class Task { started = new Date(0); starting = false; ended = new Date(); running = false; failed = false; id = sack.Id(); name = null; stopped = false; stopping = false; #autoEndBatch = false; #log = []; #task = null; // task definition #run = null; // running service instance handle #exitCode = null; // set before clearing #run #ws = []; // task definition #restart = false; #ranOnce = false; #dependsOn = []; #dependants = []; #killed = false; #path = null; #stopTimer = null; constructor(task) { this.#task = task; this.name = task.name; this.noAutoRun = task.noAutoRun; this.#autoEndBatch = task.autoEndBatch || false; if( task.moveTo ) { task.moveTo.cb = (yesno)=>{ if( !yesno ) { console.log( "Timed out move... trying move again:", this.name ); this.move(); } else { console.log( "Moved task:", this.name ); setTimeout( ()=>this.clickWindow(), 2000 ); } } } if( task.style ) { task.style.cb = (stylesSet)=>{ if( stylesSet !== 7 ) { console.log( "Timed out styles... trying style again:", this.name ); this.style(); } else { console.log( "Styled task:", this.name ); //setTimeout( ()=>this.clickWindow(), 2000 ); } } } //if( task.work && ( task.work[0] !== '/' && task.work[1] !== ':' ) ) // this.work = config.pwdBare + "/" + task.work; //else this.work = task.work; this.#restart = task.restart || false; if( task.prePath ) { if( process.platform === "win32" ) this.#path = task.prePath + ";" + process.env.PATH; else this.#path = task.prePath + ":" + process.env.PATH; } if( task.postPath ) { if( process.platform === "win32" ) this.#path = process.env.PATH + ";" + task.postPath; else this.#path = process.env.PATH + ":" + task.postPath; } if( task.dependsOn ) { for( let dep of task.dependsOn ) { let found = false; for( let testTask of config.local.tasks ) { if( testTask.name === dep ){ testTask.#dependants.push( this ); this.#dependsOn.push( testTask ); found = true; break; } } if( !found ) { pendingDepends.push( {task:this, dep} ); } } } for( let p = 0; p < pendingDepends.length; p++ ) { const pd = pendingDepends[p]; if( pd.dep === this.name ) { pd.task.#dependsOn.push( this ); this.#dependants.push( pd.task ); pendingDepends.splice( p, 1 ); p--; } } } clickWindow() { let x, y; if( "display" in this.#task.moveTo || "monitor" in this.#task.moveTo) { const displays = sack.Task.getDisplays(); let dev; for( let device of displays.device ) { if( device.display === this.#task.moveTo.display ) dev = device; for( let monitor of displays.monitor ) { if( monitor.display === device.display ) { device.monitor = monitor; monitor.device = device; break; } } } //console.log( "Dev?", dev, this.#task.moveTo.display ); if( dev ) { x = dev.monitor.x + dev.monitor.width/2; y = dev.monitor.y + dev.monitor.width/2; } else { console.log( "Failed to match display..." ); return; } } else { x = this.#task.moveTo.x + this.#task.moveTo.width/2; y = this.#task.moveTo.y + this.#task.moveTo.height/2; } //console.log( "Generate click after move:", this.name, x, y ); sack.Mouse.clickAt( x, y ); } get task() { // get the original task configuration return this.#task; } get title() { // get current task title of main window if( this.#run && "windowTitle" in this.#run) return this.#run.windowTitle(); return "no title"; } set autoEndBatch( val ) { // this was automatic code, but shouldn't // really be done, except on demenad.... // nothing demands it. this.autoEndBatch = val; } get run() { // get run handle return this.#run; } get killed() { // get run handle return this.#killed; } get hasDepends() { return !!this.#dependsOn.length; } set restart(val) { if( !val ) this.#restart = val; if( val && this.#task.restart ) { // only enable restart if task configuration allows it this.#restart = val; if( this.running ) this.stop(); else if( !this.running ) this.start(); } else if( val && !this.running ) this.start(); } get restart() { return this.#restart; } get log() { if( this.#log.length > 20 ) return { at:this.#log.length-20, log:this.#log.slice( this.#log.length - 20, this.#log.length ) }; else return { at:0, log: this.#log }; } getLog( from ) { //console.log( "reading log from:", from, from - 20, from ); if( from > 20 ) return { at:from-20, log:this.#log.slice( from - 20, from ) }; else { return { at:0, log:this.#log.slice( 0, from ) }; } } set ws( val) { this.#ws.push( val ); val.onclose = close; const this_ = this; // log is a getter that returns the tail of the log really const log = JSOX.stringify( {op:"log", system: local.id, id:this.id, log:this.log } ); //console.log( "ws sends:", log ); val.send( log ); function close( code, reason ) { //console.log( "task websocket closed; removing self" ); const ws = this_.#ws.findIndex( ws=>ws===val ); if( ws > -1 ) this_.#ws.splice( ws, 1 ); } } stopLog( ws ) { // don't send log to this socket anymore const wsid = this_.#ws.findIndex( val=>ws===val ); if( wsid > -1 ) this_.#ws.splice( wsid, 1 ); } get noKill() { return this.#task.noKill || false } set stopTimer( val ) { this.#stopTimer = val; } get stopTimer() { return this.#stopTimer; } start() { this.stopped = false; if( this.running ) { console.log( "Already started:", this.#task.name ); return; } if( this.#task.work && !disk.isDir( this.#task.work ) ){ console.log( "Task not available (working path doesn't exist", this.#task.work ); this.running = false; this.failed = true; const msg = {op:"status", id:this.id, running: false, ended: this.ended, started: this.started, failed:true }; config.send( msg ); return; } // set starting to prevent dependancies from starting dependants this.starting = true; for( let dep of this.#dependsOn ) { if( !dep.running ) dep.start(); } let bin; if( process.platform === "linux" ) { bin = this.#task.bin; // linux will scan path for name }else if( !this.#task.bin.includes( ":" ) ) bin = this.#task.bin; else { if( this.#task.altbin ) { if( disk.exists( this.#task.bin ) ) bin = this.#task.bin; else bin = this.#task.altbin; } else bin = this.#task.bin; // linux will scan path for name } if( this.#run ) this.#run.end(); const this_ = this; console.log( "Starting:", this.#task.name ); //console.log( "Starting:", bin, this ); const env = Object.assign( {}, this.#task.env ); if( this.#path ) env.PATH = this.#path; //env.PATH = this.#path; this.#run = sack.Task( { work:this.#task.work, bin:bin, args:this.#task.args, end: stop, env, input: log, errorInput: log2, newGroup: this.#task.newGroup, noKill: this.#task.noKill, noWait: this.#task.noWait, newConsole : this.#task.newConsole, useSignal : this.#task.useSignal, useBreak : this.#task.useBreak, moveTo : this.#task.moveTo, style : this.#task.style, noInheritStdio : this.#task.noInheritStdio, } ); //console.log( "Task:", this.#task ); if( this.#run ) { this.running = true; this.starting = false; // is running, not just starting. this.started = new Date(); const msg = {op:"status", id:this_.id, running: true, ended: this_.ended, started: this_.started }; config.send( msg ); for( let dep of this.#dependants ) { if( !dep.running && !dep.starting ) { console.log( "Task Started, starting Dep:", dep.name ); dep.start(); } else { console.log( "Dependant task is still running:", dep.name, dep.starting, dep.running ); } } }else { console.log( 'failed to start? try altbin?' ); } function log(buffer) { //console.log( "Adding stdout log:", buffer ); if( this_.#autoEndBatch ) { if( buffer === "Terminate batch job (Y/N)? " ) this_.#run.write( "y\n" ); } // strip newlines - attempts to line gather... if( buffer.endsWith("\n" ) ) buffer = buffer.slice( 0, -1 ); const msg = { time:new Date(), error: false, line:buffer}; this_.#send( msg ); if( !this_.#task.temporary ) { const saneBuffer = buffer.replaceAll( '\r\r\n', '\n' ).replaceAll( "\r\n", "\n" ); const lines = saneBuffer.split('\n' ); for( let line of lines ) console.log( this_.#task.name, ":", line ); } } function log2(buffer) { //console.log( "stderr log:", buffer ); if( buffer.endsWith("\n" ) ) buffer = buffer.slice( 0, -1 ); const msg = { time:new Date(), error: true, line:buffer}; this_.#send( msg ); if( !this_.#task.temporary ) { const saneBuffer = buffer.replaceAll( '\r\r\n', '\n' ).replaceAll( "\r\n", "\n" ); const lines = saneBuffer.split('\n' ); for( let line of lines ) console.log( this_.#task.name, ":", line ); } } function stop() { this_.ended = new Date(); this_.running = false; this_.stopping = false; if( this_.#stopTimer !== null ) { clearTimeout( this_.#stopTimer ); this_.#stopTimer = null; } let exitCode = this_.#run?this_.#run.exitCode:this_.#exitCode; console.log( "Task ended:", this_.name, this_.ended, exitCode, exitCode.toString(16) ); this_.#ranOnce = true; this_.#exitCode = this_.#run.exitCode; this_.#run = null; for( let dep of this_.#dependants ) { dep.stop(); dep.#ranOnce = false; } if( this_.#restart ) { //console.log( "doing resume timeout", this_.#task.restartDelay) console.log( "this should restart?" ); if( this_.#task.restartDelay ) setTimeout( ()=>this_.start(), this_.#task.restartDelay ); else setTimeout( ()=>this_.start(), 200 ); } //console.log( "stopped:", this_.#task.name ); const msg = {op:"status", id:this_.id, running: false, ended: this_.ended, started: this_.started }; config.send( msg ); } if( this.#task.multiStart ) { const sameConfig = local.tasks.find( t=>t.#task === this.#task ); const unstarted = local.tasks.find( t=>t.#task === this.#task && !t.#run && !t.running && !t.starting ); if( !unstarted ) { const noAutoRun = this.#task.noAutoRun; this.#task.noAutoRun = true; const nextTask = new Task( this.#task ); this.#task.noAutoRun = noAutoRun; //config.tasks.push( task ); local.tasks.push( nextTask ); local.taskMap[nextTask.id] = nextTask; if( local.addTask ) local.addTask( nextTask.id, nextTask ); } } } #send( buffer ) { this.#log.push( buffer ); if( !this.#ws.length ) return; const msg = { op:"log", system:local.id, id:this.id, log: buffer }; const msg_ = JSOX.stringify( msg ) ; //console.log( "msg to send:", msg_ ); this.#ws.forEach( ws=>ws.send( msg_ ) ); } kill() { this.#killed = true; if( this.#run ) this.#run.terminate(); } stop() { console.log( "Stop command: ", this.stopped, this.#run, this.stopped ); if( this.stopped ) return; //console.trace( "STOPPED?", this.stopped, this.#run ); if( this.#run ) this.#run.end(); // stop things this depends on. for( let dep of this.#dependants ) { if( dep.#run ) { //console.log( "dep running: ", dep.name ); if( !dep.stopped ) { dep.stopped = true; dep.#run.end(); timeoutTaskStop( dep ); } } dep.#ranOnce = false; } timeoutTaskStop( this ); this.stopped = true; } update( task ) { const keys = Object.keys( task ); // update existing internal task config for( let key of keys ) { if( this.#task[key] !== task[key]){ switch( key ) { case "bin": break; case "altbin": break; } this.#task[key] = task[key]; } } this.noAutoRun = task.noAutoRun; //if( ( task.work[0] !== '/' && task.work[1] !== ':' ) ) // this.work = config.pwdBare + "/" + task.work; //else this.work = task.work; this.#restart = task.restart || false; if( task.dependsOn ) { if( ( "object" === typeof task.dependsOn ) && task.dependsOn.length ){ // depends on more than one task... for( let dep of task.dependsOn ) this.#addDep( dep ); }else { this.#addDep( task.dependsOn ); } } } #addDep( dep ){ const oldTask = findTask( dep ); if( oldTask ) { if( !oldTask.#dependants.find( t=>t === this )) { oldTask.#dependants.push( this ); } this.#dependsOn.push( oldTask ); } else { console.log( "Dependant task is not found:", dep, "for", this.name ); } } move() { this.#run.moveWindow( this.#task.moveTo ); } style() { this.#run.styleWindow( this.#task.style ); } } function findTask( name ) { for( let testTask of config.local.tasks ) { if( testTask.name === name ){ return testTask; } } return null; } export function terminateTasks() { const local = config.local; local.tasks.forEach( task=>{ if (task.running && !task.noKill){ task.restart = false; task.kill() }; } ); } export function closeAllTasks( ws ) { const local = config.local; const waits = []; local.tasks.forEach( task=>{ if( task.noKill ) return; if (task.running){ task.restart = false; task.stop() waits.push( timeoutTaskStop( task ) ); } } ); return Promise.all( waits ).then( (waits)=>{ console.log( "Reply with a close?", ws ); if( ws ) ws.close( 1000, "Tasks Stopped" ); return waits; } ); } function timeoutTaskStop( task ) { const started = Date.now(); task.stopping = true; console.log( "A stop started... and now we wait on", task.name ); config.local.connections.forEach( (conn)=> { if( conn.ws.readyState == 1 ) { try { conn.ws.send( JSOX.stringify( {op:"stopping", task } ) ) } catch(err) { console.log ("Send to connection error:", err ); } } else console.log( "Connection is still in list but closed:", conn ); }); let resolve = null; function tick() { let del; task.stopTimer = null; if( (del=Date.now()-started) > 1500 ) { if( task.running ) { //console.log( "Still waiting for task...", task.running, task.name, Date.now() -started); //console.log( "Task is stubborn - forcing kill:", task.name ); if( !task.killed ) { console.log( "Task is stubborn - forcing kill:", task.name ); task.kill(); resolve( false ); } else console.log( "Task is stubborn - forced kill (waiting for end):", task.name ); //config.local.connections.forEach( (conn)=> // conn.ws.send( JSOX.stringify( {op:"stop", task } ) )); } } if( task.running ) { console.log( "Still running...", task.name, del ); task.stopTimer = setTimeout( tick, 300 ); } else resolve( true ); } return new Promise( (res,rej)=>{ resolve = res; tick(); } ); }