2021-05-14 10:51:37 +02:00
|
|
|
#!/usr/bin/env node
|
2020-08-06 14:36:25 -07:00
|
|
|
|
2018-12-09 03:20:00 -08:00
|
|
|
'use strict';
|
|
|
|
|
|
2021-05-18 13:28:48 -07:00
|
|
|
const apptask = require('./apptask.js'),
|
2021-07-14 11:07:19 -07:00
|
|
|
backupCleaner = require('./backupcleaner.js'),
|
|
|
|
|
backuptask = require('./backuptask.js'),
|
2023-08-13 10:06:01 +05:30
|
|
|
dashboard = require('./dashboard.js'),
|
2018-12-09 03:20:00 -08:00
|
|
|
database = require('./database.js'),
|
2021-08-13 17:22:28 -07:00
|
|
|
dns = require('./dns.js'),
|
2023-07-08 19:48:12 +05:30
|
|
|
dyndns = require('./dyndns.js'),
|
2019-10-25 15:58:11 -07:00
|
|
|
externalLdap = require('./externalldap.js'),
|
2020-08-04 22:16:38 -07:00
|
|
|
fs = require('fs'),
|
2023-08-04 20:54:16 +05:30
|
|
|
mailServer = require('./mailserver.js'),
|
network: fix premature connection closures with node 20 and above
the happy eyeballs implementation in node is buggy. ipv4 and ipv6 connections
are made in parallel and whichever responds first is chosen. when there is no
ipv6 (immediately errors with ENETUNREACH/EHOSTUNREACH) and when ipv4 is > 250ms,
the code erroneously times out.
see also https://github.com/nodejs/node/issues/54359
reproduction for those servers:
const options = {
hostname: 'www.cloudron.io', port: 80, path: '/', method: 'HEAD',
// family: 4, // uncomment to make it work
};
const req = require('http').request(options, (res) => {
console.log('statusCode:', res.statusCode);
res.on('data', () => {}); // drain
});
req.on('socket', (socket) => console.log('Socket assigned to request', socket););
req.on('error', (e) => console.error(e));
req.end();
2024-10-31 09:38:40 +01:00
|
|
|
net = require('net'),
|
2018-12-10 20:20:53 -08:00
|
|
|
reverseProxy = require('./reverseproxy.js'),
|
2021-07-12 23:35:30 -07:00
|
|
|
safe = require('safetydance'),
|
2022-10-12 10:26:21 +02:00
|
|
|
system = require('./system.js'),
|
2018-12-09 03:20:00 -08:00
|
|
|
tasks = require('./tasks.js'),
|
2022-04-15 17:40:46 -05:00
|
|
|
updater = require('./updater.js');
|
2018-12-09 03:20:00 -08:00
|
|
|
|
|
|
|
|
const TASKS = { // indexed by task type
|
2019-08-26 15:55:57 -07:00
|
|
|
app: apptask.run,
|
2021-09-26 18:37:04 -07:00
|
|
|
backup: backuptask.fullBackup,
|
2018-12-09 12:04:51 -08:00
|
|
|
update: updater.update,
|
2021-05-18 13:28:48 -07:00
|
|
|
checkCerts: reverseProxy.checkCerts,
|
2023-08-14 09:40:31 +05:30
|
|
|
prepareDashboardLocation: dashboard.prepareLocation,
|
2021-07-14 11:07:19 -07:00
|
|
|
cleanBackups: backupCleaner.run,
|
2019-10-25 15:58:11 -07:00
|
|
|
syncExternalLdap: externalLdap.sync,
|
2023-08-04 20:54:16 +05:30
|
|
|
changeMailLocation: mailServer.changeLocation,
|
2021-08-13 17:22:28 -07:00
|
|
|
syncDnsRecords: dns.syncDnsRecords,
|
2023-07-08 19:48:12 +05:30
|
|
|
syncDyndns: dyndns.sync,
|
2022-10-12 10:26:21 +02:00
|
|
|
updateDiskUsage: system.updateDiskUsage,
|
2018-12-10 21:05:46 -08:00
|
|
|
|
2024-06-03 19:18:36 +02:00
|
|
|
_identity: async (arg, progressCallback) => { progressCallback({}); return arg; },
|
|
|
|
|
_error: async (arg, progressCallback) => { progressCallback({}); throw new Error(`Failed for arg: ${arg}`); },
|
2022-04-28 18:03:36 -07:00
|
|
|
_crash: (arg) => { throw new Error(`Crashing for arg: ${arg}`); }, // the test looks for this debug string in the log file
|
2022-04-15 17:40:46 -05:00
|
|
|
_sleep: async (arg) => setTimeout(process.exit, arg)
|
2018-12-09 03:20:00 -08:00
|
|
|
};
|
|
|
|
|
|
2020-08-04 22:16:38 -07:00
|
|
|
if (process.argv.length !== 4) {
|
|
|
|
|
console.error('Pass the taskid and logfile as argument');
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
2018-12-09 03:20:00 -08:00
|
|
|
|
|
|
|
|
const taskId = process.argv[2];
|
2020-08-04 22:16:38 -07:00
|
|
|
const logFile = process.argv[3];
|
2021-08-30 22:01:34 -07:00
|
|
|
let logFd = null;
|
|
|
|
|
|
|
|
|
|
async function setupLogging() {
|
|
|
|
|
logFd = fs.openSync(logFile, 'a');
|
|
|
|
|
// we used to write using a stream before but it caches internally and there is no way to flush it when things crash
|
|
|
|
|
process.stdout.write = process.stderr.write = function (...args) {
|
|
|
|
|
const callback = typeof args[args.length-1] === 'function' ? args.pop() : function () {}; // callback is required for fs.write
|
|
|
|
|
fs.write.apply(fs, [logFd, ...args, callback]);
|
|
|
|
|
};
|
2023-05-15 19:09:40 +02:00
|
|
|
process.stdout.logFile = logFile; // used by update task
|
2021-08-30 22:01:34 -07:00
|
|
|
}
|
2020-08-04 22:16:38 -07:00
|
|
|
|
network: fix premature connection closures with node 20 and above
the happy eyeballs implementation in node is buggy. ipv4 and ipv6 connections
are made in parallel and whichever responds first is chosen. when there is no
ipv6 (immediately errors with ENETUNREACH/EHOSTUNREACH) and when ipv4 is > 250ms,
the code erroneously times out.
see also https://github.com/nodejs/node/issues/54359
reproduction for those servers:
const options = {
hostname: 'www.cloudron.io', port: 80, path: '/', method: 'HEAD',
// family: 4, // uncomment to make it work
};
const req = require('http').request(options, (res) => {
console.log('statusCode:', res.statusCode);
res.on('data', () => {}); // drain
});
req.on('socket', (socket) => console.log('Socket assigned to request', socket););
req.on('error', (e) => console.error(e));
req.end();
2024-10-31 09:38:40 +01:00
|
|
|
// happy eyeballs workaround. see box.js for detailed note
|
|
|
|
|
async function setupNetworking() {
|
2024-10-31 10:07:11 +01:00
|
|
|
net.setDefaultAutoSelectFamilyAttemptTimeout(2500);
|
network: fix premature connection closures with node 20 and above
the happy eyeballs implementation in node is buggy. ipv4 and ipv6 connections
are made in parallel and whichever responds first is chosen. when there is no
ipv6 (immediately errors with ENETUNREACH/EHOSTUNREACH) and when ipv4 is > 250ms,
the code erroneously times out.
see also https://github.com/nodejs/node/issues/54359
reproduction for those servers:
const options = {
hostname: 'www.cloudron.io', port: 80, path: '/', method: 'HEAD',
// family: 4, // uncomment to make it work
};
const req = require('http').request(options, (res) => {
console.log('statusCode:', res.statusCode);
res.on('data', () => {}); // drain
});
req.on('socket', (socket) => console.log('Socket assigned to request', socket););
req.on('error', (e) => console.error(e));
req.end();
2024-10-31 09:38:40 +01:00
|
|
|
}
|
|
|
|
|
|
2021-08-30 22:01:34 -07:00
|
|
|
// this is also used as the 'uncaughtException' handler which can only have synchronous functions
|
|
|
|
|
function exitSync(status) {
|
|
|
|
|
if (status.error) fs.write(logFd, status.error.stack + '\n', function () {});
|
|
|
|
|
fs.fsyncSync(logFd);
|
|
|
|
|
fs.closeSync(logFd);
|
|
|
|
|
process.exit(status.code);
|
2019-07-26 17:11:33 -07:00
|
|
|
}
|
|
|
|
|
|
2018-12-09 03:20:00 -08:00
|
|
|
// Main process starts here
|
2020-07-31 12:59:15 -07:00
|
|
|
const startTime = new Date();
|
2018-12-09 03:20:00 -08:00
|
|
|
|
2024-10-31 09:46:36 +01:00
|
|
|
async function main() {
|
2022-04-15 17:40:46 -05:00
|
|
|
try {
|
2024-10-31 09:46:36 +01:00
|
|
|
await setupLogging();
|
|
|
|
|
await setupNetworking();
|
|
|
|
|
await database.initialize();
|
|
|
|
|
} catch (initError) {
|
2024-11-01 16:15:28 +01:00
|
|
|
console.error(initError);
|
|
|
|
|
return process.exit(50);
|
|
|
|
|
}
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
const debug = require('debug')('box:taskworker'); // require this here so that logging handler is already setup
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
process.on('SIGTERM', () => exitSync({ code: 0 })); // sent as timeout notification
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
// ensure we log task crashes with the task logs. neither console.log nor debug are sync for some reason
|
|
|
|
|
process.on('uncaughtException', (error) => exitSync({ error, code: 1 }));
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
debug(`Starting task ${taskId}. Logs are at ${logFile}`);
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
const [getError, task] = await safe(tasks.get(taskId));
|
|
|
|
|
if (getError) return exitSync({ error: getError, code: 50 });
|
|
|
|
|
if (!task) return exitSync({ error: new Error(`Task ${taskId} not found`), code: 50 });
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
async function progressCallback(progress) {
|
|
|
|
|
await safe(tasks.update(taskId, progress), { debug });
|
|
|
|
|
}
|
2024-10-31 09:46:36 +01:00
|
|
|
|
2024-11-01 16:15:28 +01:00
|
|
|
try {
|
|
|
|
|
const [runError, result] = await safe(TASKS[task.type].apply(null, task.args.concat(progressCallback)));
|
|
|
|
|
const progress = {
|
|
|
|
|
result: result || null,
|
|
|
|
|
error: runError ? JSON.parse(JSON.stringify(runError, Object.getOwnPropertyNames(runError))) : null
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
debug(`Task took ${(new Date() - startTime)/1000} seconds`);
|
|
|
|
|
|
|
|
|
|
await safe(tasks.setCompleted(taskId, progress));
|
|
|
|
|
exitSync({ error: runError, code: runError ? 50 : 0 });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
exitSync({ error, code: 1 }); // do not call setCompleted() intentionally. the task code must be resilient enough to handle it
|
2021-07-12 23:35:30 -07:00
|
|
|
}
|
2024-10-31 09:46:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main();
|