diff --git a/setup/start/sudoers b/setup/start/sudoers index f71d9d035..4c714c74b 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -52,3 +52,10 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound. Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh + +Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/starttask.sh + +Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh + diff --git a/src/scripts/starttask.sh b/src/scripts/starttask.sh new file mode 100755 index 000000000..dd7e417f6 --- /dev/null +++ b/src/scripts/starttask.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -eu -o pipefail + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "No arguments supplied" + exit 1 +fi + +if [[ "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +readonly task_id="$1" +readonly logfile="$2" +readonly nice="$3" + +readonly id=$(id -u yellowtent) +readonly service_name="cloudron-task-${task_id}" + +systemctl reset-failed "${service_name}" || true + +# keep the env vars in sync with box.service +systemd-run --unit "${service_name}" \ + -p BindsTo=box.service \ + --pipe \ + --wait \ + --property=OOMScoreAdjust=-1000 \ + --nice "${nice}" \ + --uid=${id} \ + --gid=${id} \ + -E HOME=/home/yellowtent \ + -E USER=yellowtent \ + -E DEBUG=box:* \ + -E BOX_ENV=cloudron \ + -E NODE_ENV=production \ + /home/yellowtent/box/src/taskworker.js "${task_id}" "${logfile}" + diff --git a/src/scripts/stoptask.sh b/src/scripts/stoptask.sh new file mode 100755 index 000000000..5d67b6833 --- /dev/null +++ b/src/scripts/stoptask.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -eu -o pipefail + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "No arguments supplied" + exit 1 +fi + +if [[ "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +task_id="$1" + +service_name="cloudron-task-${task_id}" + +systemctl kill --signal=SIGTERM "${service_name}" + diff --git a/src/tasks.js b/src/tasks.js index cc37f8667..fc499f6e6 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -42,7 +42,9 @@ let assert = require('assert'), BoxError = require('./boxerror.js'), child_process = require('child_process'), debug = require('debug')('box:tasks'), + path = require('path'), paths = require('./paths.js'), + shell = require('./shell.js'), spawn = require('child_process').spawn, split = require('split'), taskdb = require('./taskdb.js'), @@ -51,6 +53,8 @@ let assert = require('assert'), let gTasks = {}; // indexed by task id const NOOP_CALLBACK = function (error) { if (error) debug(error); }; +const START_TASK_CMD = path.join(__dirname, 'scripts/starttask.sh'); +const STOP_TASK_CMD = path.join(__dirname, 'scripts/stoptask.sh'); function postProcess(result) { assert.strictEqual(typeof result, 'object'); @@ -131,51 +135,52 @@ function add(type, args, callback) { }); } -function startTask(taskId, options, callback) { - assert.strictEqual(typeof taskId, 'string'); +function startTask(id, options, callback) { + assert.strictEqual(typeof id, 'string'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof callback, 'function'); - const logFile = options.logFile || `${paths.TASKS_LOG_DIR}/${taskId}.log`; - debug(`startTask - starting task ${taskId}. logs at ${logFile}`); + const logFile = options.logFile || `${paths.TASKS_LOG_DIR}/${id}.log`; + debug(`startTask - starting task ${id}. logs at ${logFile}`); let killTimerId = null, timedOut = false; - const env = Object.assign({ DEBUG: 'box:*' }, process.env); // when called from npm test, DEBUG is not set - gTasks[taskId] = child_process.fork(`${__dirname}/taskworker.js`, [ taskId, logFile ], { env }); // fork requires ipc - gTasks[taskId].once('exit', function (code, signal) { - debug(`startTask: ${taskId} completed with code ${code} and signal ${signal}`); + shell.sudo('startTask', [ START_TASK_CMD, id, logFile, options.nice || 0 ], {}, function (error) { + const code = error ? error.code : 0; + const signal = error ? error.signal : 0; + + debug(`startTask: ${id} completed with code ${code} and signal ${signal}`); if (options.timeout) clearTimeout(killTimerId); - get(taskId, function (error, task) { + get(id, function (getError, task) { let taskError; - if (!error && task.percent !== 100) { // task crashed or was killed by us + if (!getError && task.percent !== 100) { // taskworker crashed or was killed by us taskError = { - message: code === 0 ? `Task ${taskId} ${timedOut ? 'timed out' : 'stopped'}` : `Task ${taskId} crashed with code ${code} and signal ${signal}`, + message: code === 0 ? `Task ${id} ${timedOut ? 'timed out' : 'stopped'}` : `Task ${id} crashed with code ${code} and signal ${signal}`, code: code === 0 ? (timedOut ? exports.ETIMEOUT : exports.ESTOPPED) : exports.ECRASHED }; // note that despite the update() here, we should handle the case where the box code was restarted and never got taskworker exit - setCompleted(taskId, { error: taskError }, NOOP_CALLBACK); - } else if (!error && task.error) { + setCompleted(id, { error: taskError }, NOOP_CALLBACK); + } 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 ${taskId}`); + taskError = new BoxError(BoxError.NOT_FOUND, `No such task ${id}`); } - delete gTasks[taskId]; + delete gTasks[id]; callback(taskError, task ? task.result : null); - debug(`startTask: ${taskId} done`); + debug(`startTask: ${id} done`); }); }); if (options.timeout) { killTimerId = setTimeout(function () { - debug(`startTask: task ${taskId} took too long. killing`); + debug(`startTask: task ${id} took too long. killing`); timedOut = true; - stopTask(taskId, NOOP_CALLBACK); + stopTask(id, NOOP_CALLBACK); }, options.timeout); } } @@ -188,7 +193,7 @@ function stopTask(id, callback) { debug(`stopTask: stopping task ${id}`); - gTasks[id].kill('SIGTERM'); // this will end up calling the 'exit' signal handler + shell.sudo('stopTask', [ STOP_TASK_CMD, id, ], {}, NOOP_CALLBACK); callback(null); } diff --git a/src/taskworker.js b/src/taskworker.js index 1866dcaa7..b02f3b1c7 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + 'use strict'; var apptask = require('./apptask.js'), @@ -61,7 +63,7 @@ async.series([ const debug = require('debug')('box:taskworker'); // require this here so that logging handler is already setup const NOOP_CALLBACK = function (error) { if (error) debug(error); }; - process.on('SIGTERM', () => process.exit(0)); + process.on('SIGTERM', () => process.exit(0)); // sent as timeout notification debug(`Starting task ${taskId}. Logs are at ${logFile}`);