Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ Use `npx durindoor` for a one-time run without a global install.

Run `durindoor --help` for the installed version's authoritative option list.

## Memory limit

The server starts with a 6 GB V8 heap cap. Set `NINEROUTER_MAX_OLD_SPACE_SIZE`
to a positive integer in MB to override it, or `0` to let Node size the heap:

```bash
NINEROUTER_MAX_OLD_SPACE_SIZE=8192 durindoor
NINEROUTER_MAX_OLD_SPACE_SIZE=0 durindoor
```

An existing `--max-old-space-size` setting in `NODE_OPTIONS` also suppresses
the default CLI heap flag.

## Data

Native installations use these defaults when `DATA_DIR` is not set:
Expand Down
5 changes: 3 additions & 2 deletions cli/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ if (hasFlag("--version", "-v")) {
}

const { ensureSqliteRuntime, buildEnvWithRuntime } = require("./hooks/sqliteRuntime");
const { buildNodeArgs } = require("./hooks/nodeFlags");
const { ensureTrayRuntime } = require("./hooks/trayRuntime");
const { killByPidFile } = require("./hooks/killByPidFile");

Expand Down Expand Up @@ -520,7 +521,7 @@ async function recoverStaleMitmOwnershipBeforeStartup() {
if (stopMitmViaManagerSync(port, { preserveDesiredState: true })) return;

const nonce = crypto.randomBytes(24).toString("hex");
const child = spawn(RUNTIME, ["--max-old-space-size=6144", serverPath], {
const child = spawn(RUNTIME, buildNodeArgs(serverPath, process.env), {
cwd: standaloneDir,
// A recovery worker may intentionally outlive this CLI after a failed
// cleanup. Ignore inherited output so no referenced/fillable pipe can keep
Expand Down Expand Up @@ -639,7 +640,7 @@ function startServer(updatePromise) {
function spawnServer(extraEnv = {}) {
serverStartTime = Date.now();
crashLog = [];
const child = spawn(RUNTIME, ["--max-old-space-size=6144", serverPath], {
const child = spawn(RUNTIME, buildNodeArgs(serverPath, process.env), {
cwd: standaloneDir,
stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"],
detached: true,
Expand Down
40 changes: 40 additions & 0 deletions cli/hooks/nodeFlags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const DEFAULT_MAX_OLD_SPACE_MB = 6144;
const HEAP_FLAG_PATTERN = /(^|\s)--max[-_]old[-_]space[-_]size(?:=|\s|$)/;

/**
* Resolves operator-controlled heap flags without overriding NODE_OPTIONS.
* Dedicated values must contain decimal digits only; zero leaves heap sizing
* entirely to Node (upstream #3368).
* @param {NodeJS.ProcessEnv} env
* @returns {string[]}
*/
function resolveHeapFlags(env = process.env) {
const explicit = String(env.NINEROUTER_MAX_OLD_SPACE_SIZE ?? "").trim();
if (explicit) {
if (explicit === "0") return [];
const megabytes = /^\d+$/.test(explicit) ? Number(explicit) : NaN;
if (Number.isInteger(megabytes) && megabytes > 0) {
return [`--max-old-space-size=${megabytes}`];
}
console.warn(
`[durindoor] ignoring NINEROUTER_MAX_OLD_SPACE_SIZE="${explicit}": expected a positive integer (MB) or 0`,
);
}

if (HEAP_FLAG_PATTERN.test(String(env.NODE_OPTIONS ?? ""))) return [];
return [`--max-old-space-size=${DEFAULT_MAX_OLD_SPACE_MB}`];
}

/**
* Builds server child argv in one seam so additional Node flags can precede
* heap flags without changing either CLI spawn path (upstream #3368).
*
* @param {string} serverPath
* @param {NodeJS.ProcessEnv} env
* @returns {string[]}
*/
function buildNodeArgs(serverPath, env = process.env) {
return [...resolveHeapFlags(env), serverPath];
}

module.exports = { buildNodeArgs, resolveHeapFlags };
1 change: 1 addition & 0 deletions docs/reference/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The native default remains `~/.9router` on macOS/Linux and `%APPDATA%\9router` o
| `PORT` | `20128` | HTTP port for the production gateway and dashboard. |
| `HOSTNAME` | runtime dependent | Bind address. Use `0.0.0.0` only in containers or deliberately exposed deployments. |
| `NODE_ENV` | development unless set | Use `production` for production starts. |
| `NINEROUTER_MAX_OLD_SPACE_SIZE` | `6144` MB for CLI starts | CLI child heap cap in MB. Use a positive decimal integer to override it, or `0` to let Node size the heap. An existing heap setting in `NODE_OPTIONS` suppresses the default when this variable is unset. |
| `BASE_URL` | local URL | Server-side origin for callbacks and selected routes. |
| `NEXT_PUBLIC_BASE_URL` | local URL | Browser-visible origin. Use the public HTTPS origin for remote deployments. |
| `TRUST_PROXY` | `false` | Trust forwarded IP headers only behind a trusted reverse proxy. |
Expand Down
70 changes: 70 additions & 0 deletions tests/unit/cli-heap-flags-3365.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createRequire } from "node:module";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it, vi } from "vitest";

const require = createRequire(import.meta.url);
const { buildNodeArgs, resolveHeapFlags } = require("../../cli/hooks/nodeFlags.js");

const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "..", "..");
const CLI_SOURCE = readFileSync(path.join(ROOT, "cli", "cli.js"), "utf8");

describe("CLI heap flags (upstream #3368)", () => {
it("keeps the existing 6144 MB default", () => {
expect(resolveHeapFlags({})).toEqual(["--max-old-space-size=6144"]);
});

it("routes both server spawns through buildNodeArgs", () => {
expect(CLI_SOURCE.match(
/spawn\(RUNTIME, buildNodeArgs\(serverPath, process\.env\), \{/g,
)).toHaveLength(2);
expect(CLI_SOURCE).not.toContain("--max-old-space-size=");
});

it("prefers NINEROUTER_MAX_OLD_SPACE_SIZE", () => {
expect(resolveHeapFlags({
NINEROUTER_MAX_OLD_SPACE_SIZE: "8192",
NODE_OPTIONS: "--max-old-space-size=4096",
})).toEqual(["--max-old-space-size=8192"]);
});

it("does not override a NODE_OPTIONS heap setting", () => {
expect(resolveHeapFlags({ NODE_OPTIONS: "--max-old-space-size=4096" })).toEqual([]);
});

it("emits no heap flag when explicitly disabled with 0", () => {
expect(resolveHeapFlags({ NINEROUTER_MAX_OLD_SPACE_SIZE: "0" })).toEqual([]);
});

it("falls back to the default for a non-numeric value", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
expect(resolveHeapFlags({ NINEROUTER_MAX_OLD_SPACE_SIZE: "large" }))
.toEqual(["--max-old-space-size=6144"]);
expect(warn).toHaveBeenCalledOnce();
} finally {
warn.mockRestore();
}
});

it("rejects non-decimal numeric forms", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
try {
expect(resolveHeapFlags({ NINEROUTER_MAX_OLD_SPACE_SIZE: "0x10" }))
.toEqual(["--max-old-space-size=6144"]);
expect(resolveHeapFlags({ NINEROUTER_MAX_OLD_SPACE_SIZE: "1e3" }))
.toEqual(["--max-old-space-size=6144"]);
expect(resolveHeapFlags({ NINEROUTER_MAX_OLD_SPACE_SIZE: "+5" }))
.toEqual(["--max-old-space-size=6144"]);
} finally {
warn.mockRestore();
}
});

it("builds server argv through one helper", () => {
expect(buildNodeArgs("/app/server.js", { NINEROUTER_MAX_OLD_SPACE_SIZE: "8192" }))
.toEqual(["--max-old-space-size=8192", "/app/server.js"]);
});
});
Loading