diff --git a/src/apps.js b/src/apps.js index 40cbb4413..b6c3be73c 100644 --- a/src/apps.js +++ b/src/apps.js @@ -531,7 +531,7 @@ async function checkStorage(app, volumeId, prefix) { const rel = path.relative(sourceDir, targetDir); if (!rel.startsWith('../') && rel.split('/').length > 1) throw new BoxError(BoxError.BAD_FIELD, 'Only one level subdirectory moves are supported'); - const [error] = await safe(shell.promises.sudo([ CHECKVOLUME_CMD, targetDir, sourceDir ], {})); + const [error] = await safe(shell.sudo([ CHECKVOLUME_CMD, targetDir, sourceDir ], {})); if (error && error.code === 2) throw new BoxError(BoxError.BAD_FIELD, `Target directory ${targetDir} is not empty`); if (error && error.code === 3) throw new BoxError(BoxError.BAD_FIELD, `Target directory ${targetDir} does not support chown`); diff --git a/src/apptask.js b/src/apptask.js index 882d797c2..d967f6eb5 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -167,14 +167,14 @@ async function addLogrotateConfig(app) { safe.fs.writeFileSync(tmpFilePath, logrotateConf); if (safe.error) throw new BoxError(BoxError.FS_ERROR, `Error writing logrotate config: ${safe.error.message}`); - const [error] = await safe(shell.promises.sudo([ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {})); + const [error] = await safe(shell.sudo([ CONFIGURE_LOGROTATE_CMD, 'add', app.id, tmpFilePath ], {})); if (error) throw new BoxError(BoxError.LOGROTATE_ERROR, `Error adding logrotate config: ${error.message}`); } async function removeLogrotateConfig(app) { assert.strictEqual(typeof app, 'object'); - const [error] = await safe(shell.promises.sudo([ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {})); + const [error] = await safe(shell.sudo([ CONFIGURE_LOGROTATE_CMD, 'remove', app.id ], {})); if (error) throw new BoxError(BoxError.LOGROTATE_ERROR, `Error removing logrotate config: ${error.message}`); } diff --git a/src/backuptask.js b/src/backuptask.js index d1442c04c..b1251867a 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -165,7 +165,7 @@ async function runBackupUpload(uploadConfig, progressCallback) { } // do not use debug for logging child output because it already has timestamps via it's own debug - const [error] = await safe(shell.promises.sudo([ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, onMessage, logger: process.stdout.write })); + const [error] = await safe(shell.sudo([ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, onMessage, logger: process.stdout.write })); if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed debug(`runBackupUpload: backuptask crashed`, error); throw new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed'); diff --git a/src/directoryserver.js b/src/directoryserver.js index 69e277254..0f2ffa5bb 100644 --- a/src/directoryserver.js +++ b/src/directoryserver.js @@ -74,7 +74,7 @@ async function applyConfig(config) { safe.fs.unlinkSync(paths.LDAP_ALLOWLIST_FILE); } - const [error] = await safe(shell.promises.sudo([ SET_LDAP_ALLOWLIST_CMD ], {})); + const [error] = await safe(shell.sudo([ SET_LDAP_ALLOWLIST_CMD ], {})); if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting ldap allowlist: ${error.message}`); if (!config.enabled) { diff --git a/src/logs.js b/src/logs.js index 1eee5f58a..817751e79 100644 --- a/src/logs.js +++ b/src/logs.js @@ -68,19 +68,17 @@ function tail(filePaths, options) { const args = [ `--lines=${lines}` ]; if (options.follow) args.push('--follow'); - if (options.sudo) { - const cp = child_process.spawn('/usr/bin/sudo', [ LOGTAIL_CMD, ...args, ...filePaths ]); - cp.terminate = () => { - child_process.execFile('/usr/bin/sudo', [ KILL_CHILD_CMD, cp.pid, process.pid ], { encoding: 'utf8' }, (error, stdout, stderr) => { - if (error) debug(`tail: failed to kill children`, stdout, stderr); - }); - }; - return cp; - } else { - const cp = child_process.spawn('/usr/bin/tail', args.concat(filePaths)); - cp.terminate = () => cp.kill('SIGKILL'); - return cp; - } + const cp = options.sudo + ? child_process.spawn('/usr/bin/sudo', [ LOGTAIL_CMD, ...args, ...filePaths ]) + : child_process.spawn('/usr/bin/tail', args.concat(filePaths)); + + cp.terminate = () => { + child_process.execFile('/usr/bin/sudo', [ KILL_CHILD_CMD, cp.pid, process.pid ], { encoding: 'utf8' }, (error, stdout, stderr) => { + if (error) debug(`tail: failed to kill children`, stdout, stderr); + }); + }; + + return cp; } function journalctl(unit, options) { diff --git a/src/mail.js b/src/mail.js index 5122c13f2..a31289268 100644 --- a/src/mail.js +++ b/src/mail.js @@ -986,7 +986,7 @@ async function delMailbox(name, domain, options, auditSource) { const mailbox =`${name}@${domain}`; if (options.deleteMails) { - const [error] = await safe(shell.promises.sudo([ REMOVE_MAILBOX_CMD, mailbox ], {})); + const [error] = await safe(shell.sudo([ REMOVE_MAILBOX_CMD, mailbox ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Error removing mailbox: ${error.message}`); } diff --git a/src/mounts.js b/src/mounts.js index c793ab135..4146f3f16 100644 --- a/src/mounts.js +++ b/src/mounts.js @@ -164,7 +164,7 @@ async function removeMount(mount) { if (constants.TEST) return; - await safe(shell.promises.sudo([ RM_MOUNT_CMD, hostPath ], {}), { debug }); // ignore any error + await safe(shell.sudo([ RM_MOUNT_CMD, hostPath ], {}), { debug }); // ignore any error if (mountType === exports.MOUNT_TYPE_SSHFS) { const keyFilePath = path.join(paths.SSHFS_KEYS_DIR, `id_rsa_${mountOptions.host}`); @@ -228,7 +228,7 @@ async function tryAddMount(mount, options) { if (constants.TEST) return; const mountFileContents = await renderMountFile(mount); - const [error] = await safe(shell.promises.sudo([ ADD_MOUNT_CMD, mountFileContents, options.timeout ], {})); + const [error] = await safe(shell.sudo([ ADD_MOUNT_CMD, mountFileContents, options.timeout ], {})); if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to unmount existing mount'); // at this point, the old mount config is still there if (options.skipCleanup) return; @@ -247,6 +247,6 @@ async function remount(mount) { if (constants.TEST) return; - const [error] = await safe(shell.promises.sudo([ REMOUNT_MOUNT_CMD, mount.hostPath ], {})); + const [error] = await safe(shell.sudo([ REMOUNT_MOUNT_CMD, mount.hostPath ], {})); if (error && error.code === 2) throw new BoxError(BoxError.MOUNT_ERROR, 'Failed to remount existing mount'); // at this point, the old mount config is still there } diff --git a/src/network.js b/src/network.js index 7f55c0c06..4a4033151 100644 --- a/src/network.js +++ b/src/network.js @@ -101,7 +101,7 @@ async function setBlocklist(blocklist, auditSource) { // this is done only because it's easier for the shell script and the firewall service to get the value if (!safe.fs.writeFileSync(paths.FIREWALL_BLOCKLIST_FILE, blocklist + '\n', 'utf8')) throw new BoxError(BoxError.FS_ERROR, safe.error.message); - const [error] = await safe(shell.promises.sudo([ SET_BLOCKLIST_CMD ], {})); + const [error] = await safe(shell.sudo([ SET_BLOCKLIST_CMD ], {})); if (error) throw new BoxError(BoxError.IPTABLES_ERROR, `Error setting blocklist: ${error.message}`); } diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 392b94561..2d9c4f104 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -174,7 +174,7 @@ async function validateCertificate(subdomain, domain, certificate) { async function notifyCertChange() { await mailServer.checkCertificate(); - await shell.promises.sudo([ RESTART_SERVICE_CMD, 'box' ], {}); // directory server + await shell.sudo([ RESTART_SERVICE_CMD, 'box' ], {}); // directory server const allApps = (await apps.list()).filter(app => app.runState !== apps.RSTATE_STOPPED); for (const app of allApps) { if (app.manifest.addons?.tls) await setupTlsAddon(app); @@ -184,7 +184,7 @@ async function notifyCertChange() { async function reload() { if (constants.TEST) return; - const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'nginx' ], {})); + const [error] = await safe(shell.sudo([ RESTART_SERVICE_CMD, 'nginx' ], {})); if (error) throw new BoxError(BoxError.NGINX_ERROR, `Error reloading nginx: ${error.message}`); } diff --git a/src/services.js b/src/services.js index 59436f296..c2b696a18 100644 --- a/src/services.js +++ b/src/services.js @@ -765,7 +765,7 @@ async function exportDatabase(addon) { if (safe.error) throw BoxError(BoxError.FS_ERROR, 'Error writing export checkpoint file'); // note: after this point, we are restart safe. it's ok if the box code crashes at this point await shell.spawn('docker', ['rm', '-f', addon], {}); // what if db writes something when quitting ... - await shell.promises.sudo([ RMADDONDIR_CMD, addon ], {}); // ready to start afresh + await shell.sudo([ RMADDONDIR_CMD, addon ], {}); // ready to start afresh } async function applyMemoryLimit(id) { @@ -888,7 +888,7 @@ async function setupLocalStorage(app, options) { const volumeDataDir = await apps.getStorageDir(app); - const [error] = await safe(shell.promises.sudo([ SETUPVOLUME_CMD, volumeDataDir ], {})); + const [error] = await safe(shell.sudo([ SETUPVOLUME_CMD, volumeDataDir ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Error creating app storage data dir: ${error.message}`); } @@ -899,7 +899,7 @@ async function clearLocalStorage(app, options) { debug('clearLocalStorage'); const volumeDataDir = await apps.getStorageDir(app); - const [error] = await safe(shell.promises.sudo([ CLEARVOLUME_CMD, volumeDataDir ], {})); + const [error] = await safe(shell.sudo([ CLEARVOLUME_CMD, volumeDataDir ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, error); } @@ -910,7 +910,7 @@ async function teardownLocalStorage(app, options) { debug('teardownLocalStorage'); const volumeDataDir = await apps.getStorageDir(app); - const [error] = await safe(shell.promises.sudo([ RMVOLUME_CMD, volumeDataDir ], {})); + const [error] = await safe(shell.sudo([ RMVOLUME_CMD, volumeDataDir ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, error); // sqlite files are automatically cleared @@ -1801,7 +1801,7 @@ async function startGraphite(existingInfra) { await docker.stopContainer('graphite'); await docker.deleteContainer('graphite'); - if (upgrading) await shell.promises.sudo([ RMADDONDIR_CMD, 'graphite' ], {}); + if (upgrading) await shell.sudo([ RMADDONDIR_CMD, 'graphite' ], {}); debug('startGraphite: starting graphite container'); await shell.bash(runCmd, { encoding: 'utf8' }); @@ -1809,7 +1809,7 @@ async function startGraphite(existingInfra) { if (existingInfra.version !== 'none' && existingInfra.images.graphite !== image) await docker.deleteImage(existingInfra.images.graphite); // restart collectd to get the disk stats after graphite starts. currently, there is no way to do graphite health check - setTimeout(async () => await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})), 60000); + setTimeout(async () => await safe(shell.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})), 60000); } async function setupProxyAuth(app, options) { @@ -1988,7 +1988,7 @@ async function teardownRedis(app, options) { await docker.deleteContainer(`redis-${app.id}`); - const [error] = await safe(shell.promises.sudo([ RMADDONDIR_CMD, 'redis', app.id ], {})); + const [error] = await safe(shell.sudo([ RMADDONDIR_CMD, 'redis', app.id ], {})); if (error) throw new BoxError(BoxError.FS_ERROR, `Error removing redis data: ${error.message}`); safe.fs.rmSync(path.join(paths.LOG_DIR, `redis-${app.id}`), { recursive: true, force: true }); @@ -2065,7 +2065,7 @@ async function statusDocker() { } async function restartDocker() { - const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'docker' ], {})); + const [error] = await safe(shell.sudo([ RESTART_SERVICE_CMD, 'docker' ], {})); if (error) debug(`restartDocker: error restarting docker. ${error.message}`); } @@ -2081,7 +2081,7 @@ async function statusUnbound() { } async function restartUnbound() { - const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'unbound' ], {})); + const [error] = await safe(shell.sudo([ RESTART_SERVICE_CMD, 'unbound' ], {})); if (error) debug(`restartDocker: error restarting unbound. ${error.message}`); } @@ -2091,7 +2091,7 @@ async function statusNginx() { } async function restartNginx() { - const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'nginx' ], {})); + const [error] = await safe(shell.sudo([ RESTART_SERVICE_CMD, 'nginx' ], {})); if (error) debug(`restartNginx: error restarting unbound. ${error.message}`); } @@ -2124,7 +2124,7 @@ async function restartGraphite() { await docker.restartContainer('graphite'); setTimeout(async () => { - const [error] = await safe(shell.promises.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})); + const [error] = await safe(shell.sudo([ RESTART_SERVICE_CMD, 'collectd' ], {})); if (error) debug(`restartGraphite: error restarting collected. ${error.message}`); }, 60000); } @@ -2226,7 +2226,7 @@ async function moveDataDir(app, targetVolumeId, targetVolumePrefix) { debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`); if (resolvedSourceDir !== resolvedTargetDir) { - const [error] = await safe(shell.promises.sudo([ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {})); + const [error] = await safe(shell.sudo([ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {})); if (error) throw new BoxError(BoxError.EXTERNAL_ERROR, `Error migrating data directory: ${error.message}`); } } diff --git a/src/shell.js b/src/shell.js index d77f56479..cc7cbbc45 100644 --- a/src/shell.js +++ b/src/shell.js @@ -5,7 +5,8 @@ const assert = require('assert'), child_process = require('child_process'), debug = require('debug')('box:shell'), once = require('./once.js'), - util = require('util'); + path = require('path'), + _ = require('./underscore.js'); exports = module.exports = shell; @@ -16,11 +17,12 @@ function shell(tag) { bash: bash.bind(null, tag), spawn: spawn.bind(null, tag), sudo: sudo.bind(null, tag), - promises: { sudo: util.promisify(sudo.bind(null, tag)) } + sudoCallback: sudoCallback.bind(null, tag) }; } const SUDO = '/usr/bin/sudo'; +const KILL_CHILD_CMD = path.join(__dirname, 'scripts/kill-child.sh'); function lineCount(buffer) { assert(Buffer.isBuffer(buffer)); @@ -44,18 +46,23 @@ function spawn(tag, file, args, options) { debug(`${tag}: ${file} ${args.join(' ').replace(/\n/g, '\\n')}`); const maxLines = options.maxLines || Number.MAX_SAFE_INTEGER; + const logger = options.logger || null; + const signal = options.signal || null; // note: we use our own handler and not the child_process one return new Promise((resolve, reject) => { - const cp = child_process.spawn(file, args, options); + const spawnOptions = _.omit(options, [ 'maxLines', 'logger', 'signal', 'onMessage', 'input', 'encoding' ]); + const cp = child_process.spawn(file, args, spawnOptions); const stdoutBuffers = [], stderrBuffers = []; let stdoutLineCount = 0, stderrLineCount = 0; cp.stdout.on('data', (data) => { + if (logger) return logger(data); stdoutBuffers.push(data); stdoutLineCount += lineCount(data); if (stdoutLineCount >= maxLines) return cp.kill('SIGKILL'); }); cp.stderr.on('data', (data) => { + if (logger) return logger(data); stderrBuffers.push(data); stderrLineCount += lineCount(data); if (stderrLineCount >= maxLines) return cp.kill('SIGKILL'); @@ -86,8 +93,16 @@ function spawn(tag, file, args, options) { debug(`${tag}: ${file} ${args.join(' ').replace(/\n/g, '\\n')} errored`, error); }); + signal?.addEventListener('abort', () => { + child_process.execFile('/usr/bin/sudo', [ KILL_CHILD_CMD, cp.pid, process.pid ], { encoding: 'utf8' }, (error, stdout, stderr) => { + if (error) debug(`${tag}: failed to kill children`, stdout, stderr); + }); + }, { once: true }); + + if (options.onMessage) cp.on('message', options.onMessage); // ipc mode messages + // https://github.com/nodejs/node/issues/25231 - if (options.input) { + if ('input' in options) { // when empty, just closes cp.stdin.write(options.input); cp.stdin.end(); } @@ -102,7 +117,25 @@ async function bash(tag, script, options) { return await spawn(tag, '/bin/bash', [ '-c', script ], options); } -function sudo(tag, args, options, callback) { +async function sudo(tag, args, options) { + assert.strictEqual(typeof tag, 'string'); + assert(Array.isArray(args)); + assert.strictEqual(typeof options, 'object'); + + const sudoArgs = []; + if (options.preserveEnv) sudoArgs.push('-E'); // -E preserves environment + + if (options.onMessage) { // enable ipc + sudoArgs.push('--close-from=4'); // keep the ipc open. requires closefrom_override in sudoers file + options.stdio = ['pipe', 'pipe', 'pipe', 'ipc']; + } + + const spawnArgs = [ ...sudoArgs, ...args ]; + + return await spawn(tag, SUDO, spawnArgs, options); +} + +function sudoCallback(tag, args, options, callback) { assert.strictEqual(typeof tag, 'string'); assert(Array.isArray(args)); assert.strictEqual(typeof options, 'object'); diff --git a/src/system.js b/src/system.js index 5f02006b8..3ded5d228 100644 --- a/src/system.js +++ b/src/system.js @@ -45,7 +45,7 @@ const REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'); async function du(file) { assert.strictEqual(typeof file, 'string'); - const [error, stdoutResult] = await safe(shell.promises.sudo([ DU_CMD, file ], { captureStdout: true })); + const [error, stdoutResult] = await safe(shell.sudo([ DU_CMD, file ], { encoding: 'utf8' })); if (error) throw new BoxError(BoxError.FS_ERROR, error); return parseInt(stdoutResult.trim(), 10); @@ -54,7 +54,7 @@ async function du(file) { async function hdparm(file) { assert.strictEqual(typeof file, 'string'); - const [error, stdoutResult] = await safe(shell.promises.sudo([ HDPARM_CMD, file ], { captureStdout: true })); + const [error, stdoutResult] = await safe(shell.sudo([ HDPARM_CMD, file ], { encoding: 'utf8' })); if (error) throw new BoxError(BoxError.FS_ERROR, error); const lines = stdoutResult.split('\n'); @@ -279,7 +279,7 @@ async function updateDiskUsage(progressCallback) { async function reboot() { await notifications.unpin(notifications.TYPE_REBOOT, {}); - const [error] = await safe(shell.promises.sudo([ REBOOT_CMD ], {})); + const [error] = await safe(shell.sudo([ REBOOT_CMD ], {})); if (error) debug('reboot: could not reboot. %o', error); } diff --git a/src/tasks.js b/src/tasks.js index bc3a31800..01795e6ed 100644 --- a/src/tasks.js +++ b/src/tasks.js @@ -172,7 +172,7 @@ async function startTask(id, options) { if (constants.TEST) sudoOptions.logStream = fs.createWriteStream('/dev/null'); // without this output is messed up, not sure why const p = new Promise((resolve, reject) => { - gTasks[id] = shell.sudo([ START_TASK_CMD, id, logFile, options.nice || 0, options.memoryLimit || 400, options.oomScoreAdjust || 0 ], sudoOptions, async function (sudoError) { + gTasks[id] = shell.sudoCallback([ START_TASK_CMD, id, logFile, options.nice || 0, options.memoryLimit || 400, options.oomScoreAdjust || 0 ], sudoOptions, async function (sudoError) { const code = sudoError ? sudoError.code : 0; debug(`startTask: ${id} completed with code ${code}. ignored: ${!gTasks[id]}`); @@ -233,7 +233,7 @@ async function stopTask(id) { debug(`stopTask: stopping task ${id}`); - await shell.promises.sudo([ STOP_TASK_CMD, id, ], {}); + await shell.sudo([ STOP_TASK_CMD, id, ], {}); } async function stopAllTasks() { @@ -241,7 +241,7 @@ async function stopAllTasks() { const cps = Object.values(gTasks); gTasks = {}; // this signals startTask() to not set completion status as "crashed" cps.forEach(cp => { debug(`stopAllTasks: terminating process group ${cp.pid}`); cp.terminate(); }); // cleanup all the sudos and systemd-run - const [error] = await safe(shell.promises.sudo([ STOP_TASK_CMD, 'all' ], { cwd: paths.baseDir() })); + const [error] = await safe(shell.sudo([ STOP_TASK_CMD, 'all' ], { cwd: paths.baseDir() })); if (error) debug(`stopAllTasks: error stopping stasks: ${error.message}`); } diff --git a/src/test/shell-test.js b/src/test/shell-test.js index 708659536..885291d3e 100644 --- a/src/test/shell-test.js +++ b/src/test/shell-test.js @@ -46,24 +46,15 @@ describe('shell', function () { }); describe('sudo', function () { - it('cannot sudo invalid program', function (done) { - shell.sudo([ 'randomprogram' ], {}, function (error) { - expect(error).to.be.ok(); - done(); - }); + it('cannot sudo invalid program', async function () { + const [error] = await safe(shell.sudo([ 'randomprogram' ], {})); + expect(error).to.be.ok(); }); - it('can sudo valid program', function (done) { + it('can sudo valid program', async function () { const RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/restartservice.sh'); - shell.sudo([ RELOAD_NGINX_CMD, 'nginx' ], {}, function (error) { - expect(error).to.be.ok(); - done(); - }); - }); - - it('can run valid program (promises)', async function () { - const RELOAD_NGINX_CMD = path.join(__dirname, '../src/scripts/restartservice.sh'); - await safe(shell.promises.sudo([ RELOAD_NGINX_CMD, 'nginx' ], {})); + const [error] = await safe(shell.sudo([ RELOAD_NGINX_CMD, 'nginx' ], {})); + expect(error).to.be.ok(); }); }); diff --git a/src/updater.js b/src/updater.js index 26b199221..51e737314 100644 --- a/src/updater.js +++ b/src/updater.js @@ -185,7 +185,7 @@ async function updateBox(boxUpdateInfo, options, progressCallback) { debug(`Updating box with ${boxUpdateInfo.sourceTarballUrl}`); progressCallback({ percent: 70, message: 'Installing update' }); - const [error] = await safe(shell.promises.sudo([ UPDATE_CMD, packageInfo.file, process.stdout.logFile ], {})); // run installer.sh from new box code as a separate service + const [error] = await safe(shell.sudo([ UPDATE_CMD, packageInfo.file, process.stdout.logFile ], {})); // run installer.sh from new box code as a separate service if (error) await locks.release(locks.TYPE_UPDATE); // Do not add any code here. The installer script will stop the box code any instant