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
2021-07-12 23:35:30 -07:00
_del : del ,
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' ) ,
2021-07-12 23:35:30 -07:00
database = require ( './database.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' ) ,
2021-07-12 23:35:30 -07:00
safe = require ( 'safetydance' ) ,
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-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
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
2021-07-12 23:35:30 -07:00
const TASKS _FIELDS = [ 'id' , 'type' , 'argsJson' , 'percent' , 'message' , 'errorJson' , 'creationTime' , 'resultJson' , 'ts' ] ;
function postProcess ( task ) {
assert . strictEqual ( typeof task , 'object' ) ;
assert ( task . argsJson === null || typeof task . argsJson === 'string' ) ;
task . args = safe . JSON . parse ( task . argsJson ) || [ ] ;
delete task . argsJson ;
task . id = String ( task . id ) ;
task . result = JSON . parse ( task . resultJson ) ;
delete task . resultJson ;
task . error = safe . JSON . parse ( task . errorJson ) ;
delete task . errorJson ;
return task ;
}
function updateStatus ( result ) {
2019-08-27 22:39:59 -07:00
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 } ;
}
2021-07-12 23:35:30 -07:00
return result ;
2019-08-27 22:39:59 -07:00
}
2021-07-12 23:35:30 -07:00
async function get ( id ) {
2018-11-16 11:13:03 -08:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-07-12 23:35:30 -07:00
const result = await database . query ( ` SELECT ${ TASKS _FIELDS } FROM tasks WHERE id = ? ` , [ id ] ) ;
if ( result . length === 0 ) return null ;
2018-11-16 11:13:03 -08:00
2021-07-12 23:35:30 -07:00
return updateStatus ( postProcess ( result [ 0 ] ) ) ;
2018-11-16 11:13:03 -08:00
}
2021-07-12 23:35:30 -07:00
async function update ( id , task ) {
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
2018-12-08 18:50:06 -08:00
debug ( ` ${ id } : ${ JSON . stringify ( task ) } ` ) ;
2021-07-12 23:35:30 -07:00
let args = [ ] ;
let fields = [ ] ;
for ( let k in task ) {
if ( k === 'result' || k === 'error' ) {
fields . push ( ` ${ k } Json = ? ` ) ;
args . push ( JSON . stringify ( task [ k ] ) ) ;
} else {
fields . push ( k + ' = ?' ) ;
args . push ( task [ k ] ) ;
}
}
args . push ( id ) ;
2018-12-08 18:50:06 -08:00
2021-07-12 23:35:30 -07:00
const result = await database . query ( 'UPDATE tasks SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , args ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Task not found' ) ;
2018-11-29 15:16:31 -08:00
}
2021-07-12 23:35:30 -07:00
async function setCompleted ( id , task ) {
2019-09-05 11:29:46 -07:00
assert . strictEqual ( typeof id , 'string' ) ;
assert . strictEqual ( typeof task , 'object' ) ;
debug ( ` setCompleted - ${ id } : ${ JSON . stringify ( task ) } ` ) ;
2021-07-12 23:35:30 -07:00
await update ( id , _ . extend ( { percent : 100 } , task ) ) ;
2019-09-05 11:29:46 -07:00
}
2021-07-12 23:35:30 -07:00
async function setCompletedByType ( type , task ) {
2019-09-05 11:29:46 -07:00
assert . strictEqual ( typeof type , 'string' ) ;
assert . strictEqual ( typeof task , 'object' ) ;
2021-07-12 23:35:30 -07:00
const results = await listByTypePaged ( type , 1 , 1 ) ;
if ( results . length !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'No such task' ) ;
2019-09-05 11:29:46 -07:00
2021-07-12 23:35:30 -07:00
await setCompleted ( results [ 0 ] . id , task ) ;
2019-09-05 11:29:46 -07:00
}
2021-07-12 23:35:30 -07:00
async function add ( type , args ) {
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 ) ) ;
2018-11-16 11:13:03 -08:00
2021-07-12 23:35:30 -07:00
const result = await database . query ( 'INSERT INTO tasks (type, argsJson, percent, message) VALUES (?, ?, ?, ?)' , [ type , JSON . stringify ( args ) , 0 , 'Queued' ] ) ;
return String ( result . insertId ) ;
2019-08-27 22:39:59 -07:00
}
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' ) ;
2021-09-17 09:22:46 -07:00
assert ( typeof callback === 'undefined' || 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 ;
2021-07-12 23:35:30 -07:00
gTasks [ id ] = shell . sudo ( 'startTask' , [ START _TASK _CMD , id , logFile , options . nice || 0 , options . memoryLimit || 400 ] , { preserveEnv : true } , async function ( sudoError ) {
2020-08-06 22:04:46 -07:00
if ( ! gTasks [ id ] ) return ; // ignore task exit since we are shutting down. see stopAllTasks
2021-07-12 23:35:30 -07:00
const code = sudoError ? sudoError . code : 0 ;
const signal = sudoError ? sudoError . signal : 0 ;
2020-08-06 14:36:25 -07:00
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 ) ;
2021-07-12 23:35:30 -07:00
const [ getError , task ] = await safe ( get ( id ) ) ;
let taskError ;
if ( ! getError && task . percent !== 100 ) { // taskworker crashed or was killed by us
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-27 22:39:59 -07:00
}
2018-11-30 16:00:47 -08:00
2021-07-12 23:35:30 -07:00
// note that despite the update() here, we should handle the case where the box code was restarted and never got taskworker exit
await safe ( setCompleted ( id , { error : taskError } ) ) ;
} else if ( ! getError && task . error ) {
taskError = task . error ;
} else if ( ! task ) { // db got cleared in tests
taskError = new BoxError ( BoxError . NOT _FOUND , ` No such task ${ id } ` ) ;
}
2018-11-29 16:13:01 -08:00
2021-07-12 23:35:30 -07:00
delete gTasks [ id ] ;
2018-12-09 03:20:00 -08:00
2021-09-17 09:22:46 -07:00
if ( callback ) callback ( taskError , task ? task . result : null ) ;
2021-07-12 23:35:30 -07:00
2021-09-17 09:22:46 -07:00
debug ( ` startTask: ${ id } done. error: ` , taskError ) ;
2018-12-08 18:50:06 -08:00
} ) ;
2019-10-11 18:59:09 -07:00
if ( options . timeout ) {
2021-07-12 23:35:30 -07:00
killTimerId = setTimeout ( async 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 ;
2021-07-12 23:35:30 -07:00
const [ error ] = await safe ( stopTask ( id ) ) ;
if ( error ) debug ( ` startTask: error stopping task: ${ error . message } ` ) ;
2019-10-11 18:59:09 -07:00
} , options . timeout ) ;
}
2018-11-29 16:13:01 -08:00
}
2021-07-12 23:35:30 -07:00
async function stopTask ( id ) {
2018-11-29 16:13:01 -08:00
assert . strictEqual ( typeof id , 'string' ) ;
2021-07-12 23:35:30 -07:00
if ( ! gTasks [ id ] ) throw new BoxError ( BoxError . BAD _STATE , 'task is not active' ) ;
2018-11-29 16:13:01 -08:00
debug ( ` stopTask: stopping task ${ id } ` ) ;
2021-07-12 23:35:30 -07:00
await shell . promises . sudo ( 'stopTask' , [ STOP _TASK _CMD , id , ] , { } ) ;
2018-11-16 11:13:03 -08:00
}
2018-12-08 20:12:23 -08:00
2021-07-12 23:35:30 -07:00
async function stopAllTasks ( ) {
2021-07-15 09:50:11 -07:00
debug ( 'stopAllTasks: stopping all tasks' ) ;
2020-08-06 22:04:46 -07:00
gTasks = { } ; // this signals startTask() to not set completion status as "crashed"
2021-07-15 09:50:11 -07:00
const [ error ] = await safe ( shell . promises . sudo ( 'stopTask' , [ STOP _TASK _CMD , 'all' ] , { cwd : paths . baseDir ( ) } ) ) ;
if ( error ) debug ( ` stopAllTasks: error stopping stasks: ${ error . message } ` ) ;
2019-08-28 15:00:55 -07:00
}
2021-07-12 23:35:30 -07:00
async function listByTypePaged ( type , page , perPage ) {
2018-12-08 20:12:23 -08:00
assert ( typeof type === 'string' || type === null ) ;
assert . strictEqual ( typeof page , 'number' ) ;
assert . strictEqual ( typeof perPage , 'number' ) ;
2021-07-12 23:35:30 -07:00
let data = [ ] ;
let query = ` SELECT ${ TASKS _FIELDS } FROM tasks ` ;
2018-12-08 20:12:23 -08:00
2021-07-12 23:35:30 -07:00
if ( type ) {
query += ' WHERE TYPE=?' ;
data . push ( type ) ;
}
2018-12-11 16:10:38 -08:00
2021-07-12 23:35:30 -07:00
query += ' ORDER BY creationTime DESC, id DESC LIMIT ?,?' ; // put latest task first
data . push ( ( page - 1 ) * perPage ) ;
data . push ( perPage ) ;
const results = await database . query ( query , data ) ;
results . forEach ( postProcess ) ;
results . forEach ( updateStatus ) ;
return results ;
2018-12-08 20:12:23 -08:00
}
2018-12-08 21:31:55 -08:00
2021-07-12 23:35:30 -07:00
function getLogs ( taskId , options ) {
2018-12-08 21:31:55 -08:00
assert . strictEqual ( typeof taskId , 'string' ) ;
assert ( options && typeof options === 'object' ) ;
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 } ` ) ;
2021-07-12 23:35:30 -07:00
const 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
2021-07-12 23:35:30 -07:00
const cmd = '/usr/bin/tail' ;
let args = [ '--lines=' + lines ] ;
2018-12-08 21:31:55 -08:00
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 ` ) ;
2021-07-12 23:35:30 -07:00
const cp = spawn ( cmd , args ) ;
2018-12-08 21:31:55 -08:00
2021-07-12 23:35:30 -07:00
const transformStream = split ( function mapper ( line ) {
2018-12-08 21:31:55 -08:00
if ( format !== 'json' ) return line + '\n' ;
2021-07-12 23:35:30 -07:00
const data = line . split ( ' ' ) ; // logs are <ISOtimestamp> <msg>
let timestamp = ( new Date ( data [ 0 ] ) ) . getTime ( ) ;
2018-12-08 21:31:55 -08:00
if ( isNaN ( timestamp ) ) timestamp = 0 ;
2021-07-12 23:35:30 -07:00
const message = line . slice ( data [ 0 ] . length + 1 ) ;
2018-12-08 21:31:55 -08:00
// 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 ) ;
2021-07-12 23:35:30 -07:00
return transformStream ;
2018-12-08 21:31:55 -08:00
}
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 ;
}
2021-07-12 23:35:30 -07:00
async function del ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
const result = await database . query ( 'DELETE FROM tasks WHERE id = ?' , [ id ] ) ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Task not found' ) ;
}