mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 11:02:33 -05:00
One executable per platform: that platform's published bundle repackaged as a Node.js single executable application whose ZIP Node 26.11 mounts as a virtual file system (useVfs + vfsArchive). Only FerretDB is copied out, once and verified, to wekan-files/bin; native addons load from the VFS. Everything WeKan writes is in wekan-files beside the executable, which WRITABLE_PATH points at: attachments, avatars, db, bin, backup, logs, versions. WeKan now treats a WRITABLE_PATH named wekan-files as the files root (models/lib/filesRoot.js, shared by every place that derived it). Started in a terminal it shows a menu: start, stop, status, version info as Admin Panel / Version shows it, update to the newest release or change to any other (checksum verified, backed up first, the old executable kept), backup to wekan-files/backup/YYYY-MM-DD_HH-MM-SS, run at boot (systemd, launchd, Task Scheduler, cron, Termux:Boot), log, settings. Every entry is also a command. start runs the start-wekan.sh loop: recovery, requested restore, FerretDB, recovery page, WeKan, restart. Meteor's main.js chdir()s into the bundle, which a VFS cannot be, so the server role keeps a virtual working directory. Node 26.11.1 breaks fs.writev/readv while any VFS is mounted; they are rebuilt on fs.write and fs.read. single-file.yml builds every target in releases/single-file/targets.json from its verified bundle (Macs on a Mac, to re-sign), starts it where a runner exists with browser tests on amd64, and attaches what started. Release All and Release All Missing call it. Thanks to xet7 !
105 lines
3.8 KiB
JavaScript
105 lines
3.8 KiB
JavaScript
// ============================================================================
|
|
// Runtime self-checks (docs/Security/Remediation/WeKan.md §9b)
|
|
// ----------------------------------------------------------------------------
|
|
// Cheap invariants — the kind an existing WeKan UNIT test asserts — that can be
|
|
// checked at runtime WITHOUT a browser and WITHOUT Playwright. Any failure is
|
|
// recorded to the Tests stream (Admin Panel → Problems → Tests) via testLog.
|
|
//
|
|
// WeKan does NOT require Playwright to be installed: browser-only end-to-end
|
|
// (Playwright) failures are deliberately NOT detected here — only issues
|
|
// detectable without it. Passing checks are not logged (only failures).
|
|
//
|
|
// Best-effort and never throws into startup. Server-only.
|
|
// ============================================================================
|
|
|
|
import { Meteor } from 'meteor/meteor';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import v8 from 'v8';
|
|
const { isFilesRoot } = require('/models/lib/filesRoot');
|
|
const { heapHealth, diskHealth, DEFAULT_DISK_BYTES } = require('/models/lib/runtimeHealth');
|
|
import { recordFailure } from '/server/lib/testLog';
|
|
|
|
function filesRoot() {
|
|
const base = process.env.WRITABLE_PATH || process.cwd();
|
|
return isFilesRoot(base) ? base : path.join(base, 'files');
|
|
}
|
|
|
|
// A trivial round-trip proves the database (FerretDB or MongoDB) is reachable
|
|
// and queryable — a failure here would break every WeKan test that touches data.
|
|
async function checkDatabaseRoundtrip() {
|
|
await Meteor.users.find({}, { limit: 1 }).countAsync();
|
|
}
|
|
|
|
// WRITABLE_PATH/files must be writable (attachments, avatars, backups depend on
|
|
// it) — a WeKan startup/upload test asserts this.
|
|
async function checkWritablePath() {
|
|
const dir = filesRoot();
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const probe = path.join(dir, '.wekan-selfcheck-' + process.pid);
|
|
fs.writeFileSync(probe, 'ok');
|
|
fs.unlinkSync(probe);
|
|
}
|
|
|
|
async function checkHeapHeadroom() {
|
|
const stats = v8.getHeapStatistics();
|
|
const result = heapHealth(stats.used_heap_size, stats.heap_size_limit);
|
|
if (!result.ok) throw new Error(result.detail);
|
|
}
|
|
|
|
async function checkFreeDiskSpace() {
|
|
const stats = fs.statfsSync(filesRoot());
|
|
const configured = Number(process.env.WEKAN_MIN_FREE_DISK_MB);
|
|
const minimum = Number.isFinite(configured) && configured >= 0
|
|
? configured * 1024 * 1024 : DEFAULT_DISK_BYTES;
|
|
const result = diskHealth(stats.bavail, stats.bsize, minimum);
|
|
if (!result.ok) throw new Error(result.detail);
|
|
}
|
|
|
|
const CHECKS = [
|
|
{ name: 'database-roundtrip', fn: checkDatabaseRoundtrip },
|
|
{ name: 'writable-path', fn: checkWritablePath },
|
|
{ name: 'heap-headroom', fn: checkHeapHeadroom },
|
|
{ name: 'free-disk-space', fn: checkFreeDiskSpace },
|
|
];
|
|
|
|
// Run all self-checks; record each failure to the Tests stream. Returns a small
|
|
// summary. Never throws.
|
|
export async function runSelfChecks() {
|
|
let failures = 0;
|
|
for (const c of CHECKS) {
|
|
try {
|
|
await c.fn();
|
|
} catch (e) {
|
|
failures += 1;
|
|
try {
|
|
recordFailure({
|
|
suite: 'unit',
|
|
test: 'self-check:' + c.name,
|
|
message: (e && e.message) || String(e),
|
|
});
|
|
} catch (_) { /* logging must never break a self-check */ }
|
|
}
|
|
}
|
|
return { checks: CHECKS.length, failures };
|
|
}
|
|
|
|
Meteor.startup(() => {
|
|
// Give the DB/connection a moment, then run once. Best-effort.
|
|
Meteor.setTimeout(() => {
|
|
runSelfChecks().catch(() => {});
|
|
}, 15000);
|
|
});
|
|
|
|
Meteor.methods({
|
|
// Admin-only: re-run the runtime self-checks on demand (no Playwright needed).
|
|
async runSelfChecks() {
|
|
const uid = this.userId;
|
|
const user = uid && (await Meteor.users.findOneAsync(uid));
|
|
if (!user || !user.isAdmin) {
|
|
throw new Meteor.Error('not-authorized', 'Admin only');
|
|
}
|
|
return runSelfChecks();
|
|
},
|
|
});
|