Files
Lauri Ojansivu 9a118c83b2 Meteor in .tools/.meteor no longer springboards forever.
A build from the menu printed uname -p and sysctl hw.cpu64bit_capable
over and over and never reached its first package. With
METEOR_WAREHOUSE_DIR set and no SANDBOX, Meteor takes the running tool
to be wherever the warehouse's "meteor" link points - 3.5.4, the release
it was installed with - so with .meteor/release at 3.6-rc.1 every start
springboarded to 3.6.0-rc.1, found itself the wrong tool again and
springboarded again: 28 times in 20 seconds of meteor --version. The
earlier 1.1 GB build log was the same loop, its module trace printed
anew on every exec.

tools-env.sh and tools-env.bat now set SANDBOX=true beside the
warehouse, as Meteor's own self-test does; that one check is all it
changes. The Nextcloud test scripts and the dev-source runner, which set
the warehouse themselves, do the same. meteor --version now springboards
once, from the linked 3.5.4 to 3.6.0-rc.1.

tests/meteorSpringboardLoop.test.cjs checks both env scripts, that
nothing sets the warehouse without SANDBOX, and runs the installed
Meteor to count its springboards.

Thanks to xet7 !
2026-10-31 13:23:24 +02:00

168 lines
6.5 KiB
JavaScript

'use strict';
// The installed Meteor distribution supplies Node, MongoDB and package code.
// Application JavaScript is loaded by runtime.cjs from the checkout itself.
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const net = require('node:net');
const {
spawn
} = require('node:child_process');
const root = path.resolve(__dirname, '../..');
async function waitForPortRelease(port, timeout = 15000) {
const deadline = Date.now() + timeout;
do {
const occupied = await new Promise(resolve => {
const socket = net.connect({ host: '127.0.0.1', port });
const finish = value => { socket.destroy(); resolve(value); };
socket.once('connect', () => finish(true));
socket.once('error', error => finish(error.code !== 'ECONNREFUSED'));
socket.setTimeout(500, () => finish(true));
});
if (!occupied) return true;
if (Date.now() >= deadline) return false;
await new Promise(resolve => setTimeout(resolve, 100));
} while (true);
}
function findMeteor() {
const release = fs.readFileSync(path.join(root, '.meteor/release'), 'utf8').trim().split('@')[1];
const version = release.replace(/^(\d+\.\d+)(-)/, '$1.0$2');
const warehouses = [process.env.METEOR_WAREHOUSE_DIR, path.join(os.homedir(), '.meteor'), process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, '.meteor'), path.join(root, '.tools/.meteor')].filter(Boolean);
const arch = `os.${process.platform === 'win32' ? 'windows' : process.platform === 'darwin' ? 'osx' : process.platform}.${process.arch === 'arm64' ? process.platform === 'darwin' ? 'arm64' : 'aarch64' : 'x86_64'}`;
for (const warehouse of warehouses) {
const tool = path.join(warehouse, 'packages/meteor-tool', version, `mt-${arch}`);
const node = path.join(tool, 'dev_bundle/bin', process.platform === 'win32' ? 'node.exe' : 'node');
if (fs.existsSync(node)) return {
warehouse,
tool,
node
};
}
throw new Error(`Install the checkout's Meteor ${release} with the build menu first; its Node toolchain was not found.`);
}
function options(argv) {
let port = 3000;
let visualize = false;
let serverTests = false;
for (let i = 0; i < argv.length; i++) {
if (argv[i] === '--port') port = Number(argv[++i]);else if (argv[i] === '--extra-packages' && argv[++i] === 'bundle-visualizer') visualize = true;else if (argv[i] === '--server-tests') serverTests = true;else if (argv[i] !== '--production') throw new Error(`Unknown source-server option: ${argv[i]}`);
}
if (!Number.isInteger(port) || port < 1 || port > 65534) throw new Error('Dev port must be between 1 and 65534 (MongoDB uses the next port).');
return {
port,
visualize,
serverTests
};
}
if (require.main === module) {
try {
const meteor = findMeteor();
const {
port,
visualize,
serverTests
} = options(process.argv.slice(2));
const temp = path.join(root, '.tools/tmp');
fs.mkdirSync(temp, {
recursive: true
});
const spawnOptions = {
cwd: root,
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
env: {
...process.env,
WRITABLE_PATH: process.env.WRITABLE_PATH || path.join(root, '.tools/dev-source/files'),
WITH_API: process.env.WITH_API || 'true',
DDP_TRANSPORT: process.env.DDP_TRANSPORT || 'sockjs',
DEFAULT_METEOR_REACTIVITY_ORDER: process.env.DEFAULT_METEOR_REACTIVITY_ORDER || 'changeStreams,oplog,polling',
TMPDIR: temp,
...(process.platform === 'win32' ? {
TEMP: temp,
TMP: temp
} : {}),
METEOR_WAREHOUSE_DIR: meteor.warehouse,
// As releases/tools-env.sh: the running tool, not the warehouse's launcher.
SANDBOX: 'true',
WEKAN_SOURCE_TOOL: meteor.tool,
WEKAN_SOURCE_TESTS: serverTests ? '1' : '0',
WEKAN_SOURCE_VISUALIZE: visualize ? '1' : '0',
PORT: String(port),
ROOT_URL: process.env.ROOT_URL || `http://localhost:${port}`
}
};
let child;
let restarting = false;
let stopping = false;
let debounce;
const watchers = [];
function closeWatchers() {
clearTimeout(debounce);
for (const watcher of watchers) watcher.close();
}
function launch() {
let ownedMongoPort;
child = spawn(meteor.node, [path.join(__dirname, 'runtime.cjs')], spawnOptions);
child.on('message', message => {
if (message?.type === 'source-mongo-started') ownedMongoPort = message.port;
});
child.on('error', error => {
console.error(error.message);
closeWatchers();
process.exitCode = 1;
});
child.on('exit', async (code, signal) => {
// App shutdown hooks may exit before Mongo finishes stopping. Wait in
// the parent too, but only for a database this child actually started.
if (ownedMongoPort && !await waitForPortRelease(ownedMongoPort)) {
console.error(`Development MongoDB port ${ownedMongoPort} did not close.`);
closeWatchers();
process.exitCode = 1;
return;
}
if (restarting && !stopping) {
restarting = false;
launch();
} else {
closeWatchers();
process.exitCode = code ?? (signal ? 1 : 0);
}
});
}
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => {
stopping = true;
closeWatchers();
child.kill(signal);
});
// Tests own their restart boundaries. Interactive development reloads
// source changes without writing any application build output.
if (!serverTests && process.env.WEKAN_SOURCE_WATCH !== '0') {
for (const directory of ['client', 'imports', 'models', 'server', 'packages', 'config', 'public']) {
const watched = path.join(root, directory);
if (!fs.existsSync(watched)) continue;
watchers.push(fs.watch(watched, {
recursive: true
}, (_event, filename) => {
if (!filename || String(filename).split(/[\\/]/).some(part => part.startsWith('.') || part === 'node_modules')) return;
clearTimeout(debounce);
debounce = setTimeout(() => {
if (stopping || restarting) return;
console.log(`Source changed: ${directory}/${filename}; restarting.`);
restarting = true;
child.kill('SIGTERM');
}, 300);
}));
}
}
launch();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}
module.exports = {
findMeteor,
options,
waitForPortRelease
};