'use strict'; exports = module.exports = { reboot, getInfo, getMemory, getLogs, getLogStream, getMetrics, getMetricStream, getBlockDevices, getFilesystems, getFilesystemUsage, getCpus, }; const assert = require('node:assert'), BoxError = require('../boxerror.js'), HttpError = require('@cloudron/connect-lastmile').HttpError, HttpSuccess = require('@cloudron/connect-lastmile').HttpSuccess, metrics = require('../metrics.js'), safe = require('safetydance'), system = require('../system.js'); async function reboot(req, res, next) { // Finish the request, to let the appstore know we triggered the reboot next(new HttpSuccess(202, {})); await safe(system.reboot()); } async function getInfo(req, res, next) { const [error, info] = await safe(system.getInfo()); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { info })); } async function getMemory(req, res, next) { const [error, result] = await safe(system.getMemory()); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, result)); } async function getLogs(req, res, next) { assert.strictEqual(typeof req.params.unit, 'string'); const lines = typeof req.query.lines === 'string' ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id if (isNaN(lines)) return next(new HttpError(400, 'lines must be a number')); const options = { lines: lines, follow: false, format: typeof req.query.format === 'string' ? req.query.format : 'json' }; const [error, logStream] = await safe(system.getLogs(req.params.unit, options)); if (error) return next(BoxError.toHttpError(error)); res.writeHead(200, { 'Content-Type': 'application/x-logs', 'Content-Disposition': `attachment; filename="${req.params.unit}.log"`, 'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no' // disable nginx buffering }); res.on('close', () => logStream.destroy()); logStream.pipe(res); } async function getLogStream(req, res, next) { assert.strictEqual(typeof req.params.unit, 'string'); const lines = typeof req.query.lines === 'string' ? parseInt(req.query.lines, 10) : 10; // we ignore last-event-id if (isNaN(lines)) return next(new HttpError(400, 'lines must be a valid number')); function sse(id, data) { return 'id: ' + id + '\ndata: ' + data + '\n\n'; } if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream')); const options = { lines: lines, follow: true, format: typeof req.query.format === 'string' ? req.query.format : 'json' }; const [error, logStream] = await safe(system.getLogs(req.params.unit, options)); if (error) return next(BoxError.toHttpError(error)); res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // disable nginx buffering 'Access-Control-Allow-Origin': '*' }); res.write('retry: 3000\n'); res.on('close', () => logStream.destroy()); logStream.on('data', function (data) { const obj = JSON.parse(data); res.write(sse(obj.realtimeTimestamp, JSON.stringify(obj))); // send timestamp as id }); logStream.on('end', res.end.bind(res)); logStream.on('error', res.end.bind(res, null)); } async function getMetrics(req, res, next) { if (typeof req.query.fromSecs !== 'string' || !parseInt(req.query.fromSecs, 10)) return next(new HttpError(400, 'fromSecs must be a number')); if (typeof req.query.intervalSecs !== 'string' || !parseInt(req.query.intervalSecs, 10)) return next(new HttpError(400, 'intervalSecs must be a number')); const fromSecs = parseInt(req.query.fromSecs, 10); const intervalSecs = parseInt(req.query.intervalSecs, 10); const noNullPoints = typeof req.query.noNullPoints === 'string' ? (req.query.noNullPoints === '1' || req.query.noNullPoints === 'true') : false; const system = req.query.system === 'true'; const appIds = 'appId' in req.query ? (Array.isArray(req.query.appId) ? req.query.appId : [ req.query.appId ]) : []; const serviceIds = 'serviceId' in req.query ? (Array.isArray(req.query.serviceId) ? req.query.serviceId : [ req.query.serviceId ]) : []; const [error, result] = await safe(metrics.get({ fromSecs, intervalSecs, noNullPoints, system, appIds, serviceIds })); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(200, result)); } async function getMetricStream(req, res, next) { if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream')); const system = req.query.system === 'true'; const appIds = 'appId' in req.query ? (Array.isArray(req.query.appId) ? req.query.appId : [ req.query.appId ]) : []; const serviceIds = 'serviceId' in req.query ? (Array.isArray(req.query.serviceId) ? req.query.serviceId : [ req.query.serviceId ]) : []; const [error, metricStream] = await safe(metrics.getStream({ system, appIds, serviceIds })); if (error) return next(BoxError.toHttpError(error)); res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // disable nginx buffering 'Access-Control-Allow-Origin': '*' }); res.write('retry: 3000\n'); res.on('close', () => metricStream.destroy()); metricStream.on('data', function (obj) { // objectMode stream const sse = `data: ${JSON.stringify(obj)}\n\n`; res.write(sse); }); metricStream.on('end', res.end.bind(res)); metricStream.on('error', res.end.bind(res, null)); } async function getBlockDevices(req, res, next) { const [error, devices] = await safe(system.getBlockDevices()); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(200, { devices })); } async function getCpus(req, res, next) { const [error, cpus] = await safe(system.getCpus()); if (error) return next(BoxError.toHttpError(error)); next(new HttpSuccess(200, { cpus })); } async function getFilesystems(req, res, next) { const [error, filesystems] = await safe(system.getFilesystems()); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(200, { filesystems })); } async function getFilesystemUsage(req, res, next) { if (typeof req.query.filesystem !== 'string') return next(new HttpError(400, 'getFilesystemUsage')); if (req.headers.accept !== 'text/event-stream') return next(new HttpError(400, 'This API call requires EventStream')); const [error, task] = await safe(system.getFilesystemUsage(req.query.filesystem)); if (error) return next(BoxError.toHttpError(error)); res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no', // disable nginx buffering 'Access-Control-Allow-Origin': '*' }); res.write('retry: 30000\n'); // client should .close() to prevent reconnect within 30s res.on('close', () => task.stop()); task.on('data', function (type, data) { const obj = { type, ...data }; const sse = `data: ${JSON.stringify(obj)}\n\n`; res.write(sse); }); task.on('done', function (error) { const obj = { type: 'done', ...error }; const sse = `data: ${JSON.stringify(obj)}\n\n`; res.write(sse); res.end(); }); task.start(); // background }