2018-11-16 11:13:03 -08:00
'use strict' ;
exports = module . exports = {
2018-11-30 14:16:00 -08:00
get : get ,
2018-12-08 18:50:06 -08:00
update : update ,
2018-12-11 16:10:38 -08:00
listByTypePaged : listByTypePaged ,
2018-11-16 11:13:03 -08:00
2018-12-08 21:31:55 -08:00
getLogs : getLogs ,
2018-11-29 16:13:01 -08:00
startTask : startTask ,
2018-11-16 11:13:03 -08:00
stopTask : stopTask ,
2018-12-11 16:20:48 -08:00
removePrivateFields : removePrivateFields ,
2018-11-16 11:13:03 -08:00
TaskError : TaskError ,
2018-12-09 03:20:00 -08:00
// task types. if you add a task here, fill up the function table in taskworker
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' ,
2018-12-10 21:05:46 -08:00
TASK _RENEW _CERTS : 'renewcerts' ,
2018-12-15 15:27:16 -08:00
TASK _PREPARE _DASHBOARD _DOMAIN : 'prepareDashboardDomain' ,
2019-01-10 16:00:49 -08:00
TASK _CLEAN _BACKUPS : 'cleanBackups' ,
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
} ;
let assert = require ( 'assert' ) ,
2018-11-29 16:13:01 -08:00
child _process = require ( 'child_process' ) ,
2018-11-16 11:13:03 -08:00
DatabaseError = require ( './databaseerror.js' ) ,
debug = require ( 'debug' ) ( 'box:tasks' ) ,
2018-12-09 03:20:00 -08:00
EventEmitter = require ( 'events' ) ,
2018-11-29 16:13:01 -08:00
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
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
util = require ( 'util' ) ,
_ = require ( 'underscore' ) ;
2018-11-16 11:13:03 -08:00
2018-11-29 16:13:01 -08:00
const NOOP _CALLBACK = function ( error ) { if ( error ) debug ( error ) ; } ;
2018-12-08 18:50:06 -08:00
let gTasks = { } ; // indexed by task id
2018-11-29 16:13:01 -08:00
2018-11-16 11:13:03 -08:00
function TaskError ( reason , errorOrMessage ) {
assert . strictEqual ( typeof reason , 'string' ) ;
assert ( errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined' ) ;
Error . call ( this ) ;
Error . captureStackTrace ( this , this . constructor ) ;
this . name = this . constructor . name ;
this . reason = reason ;
if ( typeof errorOrMessage === 'undefined' ) {
this . message = reason ;
} else if ( typeof errorOrMessage === 'string' ) {
this . message = errorOrMessage ;
} else {
this . message = 'Internal error' ;
this . nestedError = errorOrMessage ;
}
}
util . inherits ( TaskError , Error ) ;
TaskError . INTERNAL _ERROR = 'Internal Error' ;
TaskError . BAD _STATE = 'Bad State' ;
TaskError . NOT _FOUND = 'Not Found' ;
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 ) {
2018-11-16 11:13:03 -08:00
if ( error && error . reason == DatabaseError . NOT _FOUND ) return callback ( new TaskError ( TaskError . NOT _FOUND ) ) ;
if ( error ) return callback ( new TaskError ( TaskError . INTERNAL _ERROR , error ) ) ;
2019-08-27 21:36:52 -07:00
// add some virtual fields
2018-12-11 16:20:48 -08:00
task . active = ! ! gTasks [ id ] ;
2019-08-27 21:36:52 -07:00
task . success = task . percent === 100 && ! task . errorMessage ;
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 ) {
if ( error && error . reason == DatabaseError . NOT _FOUND ) return callback ( new TaskError ( TaskError . NOT _FOUND ) ) ;
if ( error ) return callback ( new TaskError ( TaskError . INTERNAL _ERROR , error ) ) ;
callback ( ) ;
} ) ;
2018-11-29 15:16:31 -08:00
}
2019-08-27 11:38:12 -07:00
function startTask ( type , args , options ) {
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 11:38:12 -07:00
assert ( ! options || typeof options === 'object' ) ;
2018-11-16 11:13:03 -08:00
2018-12-09 03:20:00 -08:00
let events = new EventEmitter ( ) ;
2018-11-16 11:13:03 -08:00
2019-08-27 11:38:12 -07:00
options = options || { } ;
2018-12-08 18:50:06 -08:00
taskdb . add ( { type : type , percent : 0 , message : 'Starting' , args : args } , function ( error , taskId ) {
2018-12-09 03:20:00 -08:00
if ( error ) return events . emit ( 'error' , new TaskError ( TaskError . INTERNAL _ERROR , error ) ) ;
2018-11-29 16:13:01 -08:00
2019-08-27 11:38:12 -07:00
const logFile = options . logFile || ` ${ paths . TASKS _LOG _DIR } / ${ taskId } .log ` ;
2018-12-14 18:21:22 -08:00
let fd = safe . fs . openSync ( logFile , 'w' ) ; // will autoclose
2018-12-08 21:31:55 -08:00
if ( ! fd ) {
debug ( ` startTask: unable to get log filedescriptor ${ safe . error . message } ` ) ;
2018-12-09 03:20:00 -08:00
return events . emit ( 'error' , new TaskError ( TaskError . INTERNAL _ERROR , error . message ) ) ;
2018-12-08 21:31:55 -08:00
}
2018-12-09 03:20:00 -08:00
debug ( ` startTask - starting task ${ type } . logs at ${ logFile } id ${ taskId } ` ) ;
2018-11-29 16:13:01 -08:00
2018-12-09 03:20:00 -08:00
gTasks [ taskId ] = child _process . fork ( ` ${ _ _dirname } /taskworker.js ` , [ taskId ] , { stdio : [ 'pipe' , fd , fd , 'ipc' ] } ) ; // fork requires ipc
2018-12-08 18:50:06 -08:00
gTasks [ taskId ] . once ( 'exit' , function ( code , signal ) {
debug ( ` startTask: ${ taskId } completed with code ${ code } and signal ${ signal } ` ) ;
2018-11-29 16:13:01 -08:00
2018-12-08 18:50:06 -08:00
get ( taskId , function ( error , task ) {
if ( ! error && task . percent !== 100 ) { // task crashed or was killed by us (code 50)
2019-03-25 15:14:40 -07:00
error = code === 0 ? new Error ( ` Task ${ taskId } stopped ` ) : new Error ( ` Task ${ taskId } crashed with code ${ code } and signal ${ signal } ` ) ;
2018-12-08 18:50:06 -08:00
update ( taskId , { percent : 100 , errorMessage : error . message } , NOOP _CALLBACK ) ;
} else if ( ! error && task . errorMessage ) {
error = new Error ( task . errorMessage ) ;
2018-12-10 20:55:56 -08:00
} else if ( ! task ) { // db got cleared in tests
error = new Error ( ` No such task ${ taskId } ` ) ;
2018-12-08 18:50:06 -08:00
}
2018-11-30 16:00:47 -08:00
2018-12-08 18:50:06 -08:00
gTasks [ taskId ] = null ;
2018-11-29 16:13:01 -08:00
2018-12-10 20:55:56 -08:00
events . emit ( 'finish' , error , task ? task . result : null ) ;
2018-12-09 03:20:00 -08:00
2018-12-08 18:50:06 -08:00
debug ( ` startTask: ${ taskId } done ` ) ;
} ) ;
2018-11-29 16:13:01 -08:00
} ) ;
2019-03-04 17:52:31 -08:00
events . id = taskId ;
2018-12-09 03:20:00 -08:00
events . emit ( 'start' , taskId ) ;
2018-12-08 18:50:06 -08:00
} ) ;
2018-12-09 03:20:00 -08:00
return events ;
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' ) ;
if ( ! gTasks [ id ] ) return callback ( new TaskError ( TaskError . BAD _STATE , 'task is not active' ) ) ;
debug ( ` stopTask: stopping task ${ id } ` ) ;
gTasks [ id ] . kill ( 'SIGTERM' ) ; // this will end up calling the 'exit' signal handler
callback ( null ) ;
2018-11-16 11:13:03 -08:00
}
2018-12-08 20:12:23 -08: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 ) {
2018-12-08 20:12:23 -08:00
if ( error ) return callback ( new TaskError ( TaskError . INTERNAL _ERROR , error ) ) ;
2018-12-11 16:10:38 -08:00
tasks . forEach ( ( task ) => { task . active = ! ! gTasks [ task . id ] ; } ) ;
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 ) ;
}
2018-12-11 16:20:48 -08:00
// removes all fields that are strictly private and should never be returned by API calls
function removePrivateFields ( task ) {
var result = _ . pick ( task , 'id' , 'type' , 'percent' , 'message' , 'errorMessage' , 'active' , 'creationTime' , 'result' , 'ts' ) ;
return result ;
}