2018-11-16 11:13:03 -08:00
'use strict' ;
exports = module . exports = {
2021-02-01 14:07:23 -08:00
get ,
add ,
update ,
setCompleted ,
setCompletedByType ,
listByTypePaged ,
2018-11-16 11:13:03 -08:00
2021-02-01 14:07:23 -08:00
getLogs ,
2018-12-08 21:31:55 -08:00
2021-02-01 14:07:23 -08:00
startTask ,
stopTask ,
stopAllTasks ,
2018-11-16 11:13:03 -08:00
2021-02-01 14:07:23 -08:00
removePrivateFields ,
2019-12-06 08:40:16 -08:00
2020-09-02 17:06:24 +02:00
// task types. if you add a task here, fill up the function table in taskworker and dashboard client.js
2019-08-26 15:55:57 -07:00
TASK _APP : 'app' ,
2018-11-19 20:01:02 -08:00
TASK _BACKUP : 'backup' ,
TASK _UPDATE : 'update' ,
2021-05-18 13:28:48 -07:00
TASK _CHECK _CERTS : 'checkCerts' ,
2020-08-15 23:17:29 -07:00
TASK _SETUP _DNS _AND _CERT : 'setupDnsAndCert' ,
2019-01-10 16:00:49 -08:00
TASK _CLEAN _BACKUPS : 'cleanBackups' ,
2019-08-29 17:19:51 +02:00
TASK _SYNC _EXTERNAL _LDAP : 'syncExternalLdap' ,
2020-08-15 23:17:47 -07:00
TASK _CHANGE _MAIL _LOCATION : 'changeMailLocation' ,
2021-02-24 18:42:39 -08:00
TASK _SYNC _DNS _RECORDS : 'syncDnsRecords' ,
2018-12-10 21:05:46 -08:00
2019-08-30 13:46:55 -07:00
// error codes
ESTOPPED : 'stopped' ,
ECRASHED : 'crashed' ,
2019-10-11 19:30:21 -07:00
ETIMEOUT : 'timeout' ,
2019-08-30 13:46:55 -07:00
2018-12-10 21:05:46 -08:00
// testing
_TASK _IDENTITY : '_identity' ,
_TASK _CRASH : '_crash' ,
_TASK _ERROR : '_error' ,
2018-12-10 21:42:03 -08:00
_TASK _SLEEP : '_sleep'
2018-11-16 11:13:03 -08:00
} ;
2021-05-18 13:28:48 -07:00
const assert = require ( 'assert' ) ,
2019-10-22 20:12:44 -07:00
BoxError = require ( './boxerror.js' ) ,
2018-11-16 11:13:03 -08:00
debug = require ( 'debug' ) ( 'box:tasks' ) ,
2020-08-06 14:36:25 -07:00
path = require ( 'path' ) ,
2018-11-29 16:13:01 -08:00
paths = require ( './paths.js' ) ,
2020-08-06 14:36:25 -07:00
shell = require ( './shell.js' ) ,
2018-12-08 21:31:55 -08:00
spawn = require ( 'child_process' ) . spawn ,
split = require ( 'split' ) ,
2018-11-16 11:13:03 -08:00
taskdb = require ( './taskdb.js' ) ,
2018-12-11 16:20:48 -08:00
_ = require ( 'underscore' ) ;
2018-11-16 11:13:03 -08:00
2018-12-08 18:50:06 -08:00
let gTasks = { } ; // indexed by task id
2018-11-29 16:13:01 -08:00
2019-08-30 13:46:55 -07:00
const NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
2020-08-06 14:36:25 -07:00
const START _TASK _CMD = path . join ( _ _dirname , 'scripts/starttask.sh' ) ;
const STOP _TASK _CMD = path . join ( _ _dirname , 'scripts/stoptask.sh' ) ;
2019-08-30 13:46:55 -07:00
2019-08-27 22:39:59 -07:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
result . active = ! ! gTasks [ result . id ] ;
2020-08-19 16:39:49 +02:00
2019-08-30 13:46:55 -07:00
// we rely on 'percent' to determine success. maybe this can become a db field
result . success = result . percent === 100 && ! result . error ;
2020-08-19 16:39:49 +02:00
// we rely on 'percent' to determine pending. maybe this can become a db field
result . pending = result . percent === 1 ;
2019-08-30 13:46:55 -07:00
// the error in db will be empty if we didn't get a chance to handle task exit
if ( ! result . active && result . percent !== 100 && ! result . error ) {
result . error = { message : 'Cloudron crashed/stopped' , code : exports . ECRASHED } ;
}
2019-08-27 22:39:59 -07:00
}
2018-11-30 14:16:00 -08:00
function get ( id , callback ) {
2018-11-16 11:13:03 -08:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-11 16:20:48 -08:00
taskdb . get ( id , function ( error , task ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2018-11-16 11:13:03 -08:00
2019-08-27 22:39:59 -07:00
postProcess ( task ) ;
2018-11-29 23:12:03 -08:00
2018-12-11 16:20:48 -08:00
callback ( null , task ) ;
2018-11-16 11:13:03 -08:00
} ) ;
}
2018-12-08 18:50:06 -08:00
function update ( id , task , callback ) {
2018-11-29 15:16:31 -08:00
assert . strictEqual ( typeof id , 'string' ) ;
2018-12-08 18:50:06 -08:00
assert . strictEqual ( typeof task , 'object' ) ;
2018-11-29 15:16:31 -08:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-08 18:50:06 -08:00
debug ( ` ${ id } : ${ JSON . stringify ( task ) } ` ) ;
taskdb . update ( id , task , function ( error ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2018-12-08 18:50:06 -08:00
callback ( ) ;
} ) ;
2018-11-29 15:16:31 -08:00
}
2019-09-05 11:29:46 -07:00
function setCompleted ( id , task , callback ) {
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof task , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
debug ( ` setCompleted - ${ id } : ${ JSON . stringify ( task ) } ` ) ;
2019-09-05 11:42:32 -07:00
update ( id , _ . extend ( { percent : 100 } , task ) , callback ) ;
2019-09-05 11:29:46 -07:00
}
function setCompletedByType ( type , task , callback ) {
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof task , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
listByTypePaged ( type , 1 , 1 , function ( error , results ) {
2019-10-22 20:12:44 -07:00
if ( error ) return callback ( error ) ;
if ( results . length !== 1 ) return callback ( new BoxError ( BoxError . NOT _FOUND ) ) ;
2019-09-05 11:29:46 -07:00
setCompleted ( results [ 0 ] . id , task , function ( error ) {
2019-10-22 20:12:44 -07:00
if ( error ) return callback ( error ) ;
2019-09-05 11:29:46 -07:00
callback ( ) ;
} ) ;
} ) ;
}
2019-08-27 22:39:59 -07:00
function add ( type , args , callback ) {
2018-12-08 18:50:06 -08:00
assert . strictEqual ( typeof type , 'string' ) ;
2018-12-09 03:20:00 -08:00
assert ( Array . isArray ( args ) ) ;
2019-08-27 22:39:59 -07:00
assert . strictEqual ( typeof callback , 'function' ) ;
2018-11-16 11:13:03 -08:00
2020-08-19 16:39:49 +02:00
taskdb . add ( { type : type , percent : 0 , message : 'Queued' , args : args } , function ( error , taskId ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2018-11-16 11:13:03 -08:00
2019-08-27 22:39:59 -07:00
callback ( null , taskId ) ;
} ) ;
}
2019-08-27 11:38:12 -07:00
2020-08-06 14:36:25 -07:00
function startTask ( id , options , callback ) {
assert . strictEqual ( typeof id , 'string' ) ;
2019-08-27 22:39:59 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-11-29 16:13:01 -08:00
2020-08-06 14:36:25 -07:00
const logFile = options . logFile || ` ${ paths . TASKS _LOG _DIR } / ${ id } .log ` ;
2021-06-16 14:21:19 -07:00
debug ( ` startTask - starting task ${ id } with options ${ JSON . stringify ( options ) } . logs at ${ logFile } ` ) ;
2018-11-29 16:13:01 -08:00
2019-10-11 18:59:09 -07:00
let killTimerId = null , timedOut = false ;
2020-08-10 21:53:07 -07:00
gTasks [ id ] = shell . sudo ( 'startTask' , [ START _TASK _CMD , id , logFile , options . nice || 0 , options . memoryLimit || 400 ] , { preserveEnv : true } , function ( error ) {
2020-08-06 22:04:46 -07:00
if ( ! gTasks [ id ] ) return ; // ignore task exit since we are shutting down. see stopAllTasks
2020-08-06 14:36:25 -07:00
const code = error ? error . code : 0 ;
const signal = error ? error . signal : 0 ;
debug ( ` startTask: ${ id } completed with code ${ code } and signal ${ signal } ` ) ;
2018-11-29 16:13:01 -08:00
2019-10-11 18:59:09 -07:00
if ( options . timeout ) clearTimeout ( killTimerId ) ;
2020-08-06 14:36:25 -07:00
get ( id , function ( getError , task ) {
2019-08-30 13:46:55 -07:00
let taskError ;
2020-08-06 14:36:25 -07:00
if ( ! getError && task . percent !== 100 ) { // taskworker crashed or was killed by us
2020-12-21 18:06:20 -08:00
if ( code === 0 ) {
taskError = {
message : ` Task ${ id } ${ timedOut ? 'timed out' : 'stopped' } ` ,
code : timedOut ? exports . ETIMEOUT : exports . ESTOPPED
} ;
} else { // task crashed
taskError = {
message : signal === 9 ? ` Task ${ id } crashed as it ran out of memory ` : ` Task ${ id } crashed with code ${ code } and signal ${ signal } ` ,
code : exports . ECRASHED
} ;
}
2019-08-30 13:46:55 -07:00
// note that despite the update() here, we should handle the case where the box code was restarted and never got taskworker exit
2020-08-06 14:36:25 -07:00
setCompleted ( id , { error : taskError } , NOOP _CALLBACK ) ;
} else if ( ! getError && task . error ) {
2019-08-30 13:46:55 -07:00
taskError = task . error ;
2019-08-27 22:39:59 -07:00
} else if ( ! task ) { // db got cleared in tests
2020-08-06 14:36:25 -07:00
taskError = new BoxError ( BoxError . NOT _FOUND , ` No such task ${ id } ` ) ;
2019-08-27 22:39:59 -07:00
}
2018-11-30 16:00:47 -08:00
2020-08-06 14:36:25 -07:00
delete gTasks [ id ] ;
2018-11-29 16:13:01 -08:00
2019-08-30 13:46:55 -07:00
callback ( taskError , task ? task . result : null ) ;
2018-12-09 03:20:00 -08:00
2020-08-06 14:36:25 -07:00
debug ( ` startTask: ${ id } done ` ) ;
2018-11-29 16:13:01 -08:00
} ) ;
2018-12-08 18:50:06 -08:00
} ) ;
2019-10-11 18:59:09 -07:00
if ( options . timeout ) {
killTimerId = setTimeout ( function ( ) {
2020-08-06 14:36:25 -07:00
debug ( ` startTask: task ${ id } took too long. killing ` ) ;
2019-10-11 18:59:09 -07:00
timedOut = true ;
2020-08-06 14:36:25 -07:00
stopTask ( id , NOOP _CALLBACK ) ;
2019-10-11 18:59:09 -07:00
} , options . timeout ) ;
}
2018-11-29 16:13:01 -08:00
}
2018-12-11 09:22:13 -08:00
function stopTask ( id , callback ) {
2018-11-29 16:13:01 -08:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-10-22 20:12:44 -07:00
if ( ! gTasks [ id ] ) return callback ( new BoxError ( BoxError . BAD _STATE , 'task is not active' ) ) ;
2018-11-29 16:13:01 -08:00
debug ( ` stopTask: stopping task ${ id } ` ) ;
2020-08-06 14:36:25 -07:00
shell . sudo ( 'stopTask' , [ STOP _TASK _CMD , id , ] , { } , NOOP _CALLBACK ) ;
2018-11-29 16:13:01 -08:00
callback ( null ) ;
2018-11-16 11:13:03 -08:00
}
2018-12-08 20:12:23 -08:00
2019-08-28 15:00:55 -07:00
function stopAllTasks ( callback ) {
assert . strictEqual ( typeof callback , 'function' ) ;
2020-08-06 22:04:46 -07:00
debug ( 'stopTask: stopping all tasks' ) ;
gTasks = { } ; // this signals startTask() to not set completion status as "crashed"
2021-03-02 22:26:39 -08:00
shell . sudo ( 'stopTask' , [ STOP _TASK _CMD , 'all' ] , { cwd : paths . baseDir ( ) } , callback ) ;
2019-08-28 15:00:55 -07:00
}
2018-12-11 16:10:38 -08:00
function listByTypePaged ( type , page , perPage , callback ) {
2018-12-08 20:12:23 -08:00
assert ( typeof type === 'string' || type === null ) ;
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2018-12-11 16:10:38 -08:00
taskdb . listByTypePaged ( type , page , perPage , function ( error , tasks ) {
2019-10-24 11:13:48 -07:00
if ( error ) return callback ( error ) ;
2018-12-08 20:12:23 -08:00
2019-08-27 22:39:59 -07:00
tasks . forEach ( postProcess ) ;
2018-12-11 16:10:38 -08:00
2018-12-08 20:12:23 -08:00
callback ( null , tasks ) ;
} ) ;
}
2018-12-08 21:31:55 -08:00
function getLogs ( taskId , options , callback ) {
assert . strictEqual ( typeof taskId , 'string' ) ;
assert ( options && typeof options === 'object' ) ;
assert . strictEqual ( typeof callback , 'function' ) ;
2019-01-08 12:10:53 -08:00
assert . strictEqual ( typeof options . lines , 'number' ) ;
assert . strictEqual ( typeof options . format , 'string' ) ;
assert . strictEqual ( typeof options . follow , 'boolean' ) ;
2018-12-08 21:31:55 -08:00
debug ( ` Getting logs for ${ taskId } ` ) ;
2019-01-08 12:10:53 -08:00
var lines = options . lines === - 1 ? '+1' : options . lines ,
2018-12-08 21:31:55 -08:00
format = options . format || 'json' ,
2019-01-08 12:10:53 -08:00
follow = options . follow ;
2018-12-08 21:31:55 -08:00
let cmd = '/usr/bin/tail' ;
var args = [ '--lines=' + lines ] ;
if ( follow ) args . push ( '--follow' , '--retry' , '--quiet' ) ; // same as -F. to make it work if file doesn't exist, --quiet to not output file headers, which are no logs
args . push ( ` ${ paths . TASKS _LOG _DIR } / ${ taskId } .log ` ) ;
var cp = spawn ( cmd , args ) ;
var transformStream = split ( function mapper ( line ) {
if ( format !== 'json' ) return line + '\n' ;
var data = line . split ( ' ' ) ; // logs are <ISOtimestamp> <msg>
var timestamp = ( new Date ( data [ 0 ] ) ) . getTime ( ) ;
if ( isNaN ( timestamp ) ) timestamp = 0 ;
var message = line . slice ( data [ 0 ] . length + 1 ) ;
// ignore faulty empty logs
if ( ! timestamp && ! message ) return ;
return JSON . stringify ( {
realtimeTimestamp : timestamp * 1000 ,
message : message ,
source : taskId
} ) + '\n' ;
} ) ;
transformStream . close = cp . kill . bind ( cp , 'SIGKILL' ) ; // closing stream kills the child process
cp . stdout . pipe ( transformStream ) ;
callback ( null , transformStream ) ;
}
2019-12-06 08:40:16 -08:00
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields ( task ) {
2020-08-19 16:39:49 +02:00
var result = _ . pick ( task , 'id' , 'type' , 'percent' , 'message' , 'error' , 'active' , 'pending' , 'creationTime' , 'result' , 'ts' , 'success' ) ;
2019-12-06 08:40:16 -08:00
return result ;
}