diff --git a/README.md b/README.md index 514d27e..74eecaa 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ backend/ | 命令 | 说明 | | --- | --- | | `localapp server [start]` | 注册系统集成并启动当前用户 daemon | -| `localapp server run` | 以前台模式运行同一 Server,适合容器和服务管理器;Windows 上使用包内 native helper | +| `localapp server run` | 以前台模式运行同一 Server,默认监听 `127.0.0.1:50524`(`--port` 覆盖);适合容器、服务管理器和需要固定地址的机器 | | `localapp server stop/restart/status/logs/uninstall` | 管理当前用户 daemon | | `localapp init ` | 从 npm 包内置模板创建应用 | | `localapp build --package` | 构建并生成不含本地数据的 `.localapp` | diff --git a/docs/local-runtime.md b/docs/local-runtime.md index 65365eb..10ee821 100644 --- a/docs/local-runtime.md +++ b/docs/local-runtime.md @@ -6,11 +6,14 @@ LocalApp 只有一个 Server 实现。它可以作为开发机上的本地 Serve Windows 上的 daemon 是当前用户的计划任务:登录时启动,并以该用户自身的普通权限运行,不继承提权令牌。因此它需要有交互登录会话——只通过 SSH 使用、没有桌面会话的机器请改用 `localapp server run` 前台模式。首次注册计划任务可能需要管理员终端,之后 `status`、`stop`、`dev` 等命令在普通终端即可操作 daemon。 +前台模式同样是一种正式用法,适合需要**稳定监听地址**的机器(已保存的 profile、反向代理、容器端口映射,或本机无法注册计划任务):它监听固定的 **50524**,可用 `--port` 覆盖(`--port 0` 才表示临时端口),由启动它的终端显式维持进程,不涉及计划任务与 daemon。 + ```bash npm install --global @patodo/localapp localapp server # 等同于 localapp server start,注册并启动用户 daemon localapp server status -localapp server run # 容器/前台运行 +localapp server run # 容器/前台运行,默认监听 127.0.0.1:50524 +localapp server run --port 55441 # 固定到指定端口 ``` 不进行全局安装时,可使用 `npx --package @patodo/localapp localapp --version` 验证当前 diff --git a/packages/localapp/src/cli/help.ts b/packages/localapp/src/cli/help.ts index 0e6cefc..69bf4f7 100644 --- a/packages/localapp/src/cli/help.ts +++ b/packages/localapp/src/cli/help.ts @@ -123,11 +123,12 @@ Usage: Options: --data-dir Server database, files, and configuration directory --host
Listen address (default: 127.0.0.1) - --port Listen port, 0 selects an available port (default: 0) + --port Listen port, 0 selects an available port (default: 50524) -h, --help Show this help Examples: localapp server run + localapp server run --port 55441 localapp server run --data-dir ./localapp-data --port 3000 localapp server run --host 0.0.0.0 --port 3000 `], diff --git a/packages/localapp/src/commands/server.ts b/packages/localapp/src/commands/server.ts index 430165c..1176a7b 100644 --- a/packages/localapp/src/commands/server.ts +++ b/packages/localapp/src/commands/server.ts @@ -11,6 +11,14 @@ import { createNativeAdapter, type NativeAdapter, type NativeAdapterOptions } fr export type ServerCommandAction = "start" | "stop" | "restart" | "status" | "logs" | "uninstall"; export interface RunServerCommandOptions { action: ServerCommandAction; } + +/** + * A foreground Server keeps one address across restarts unless the operator + * asks for another one: a saved profile, a reverse proxy, or a container port + * mapping must not have to follow a port the Server picked for itself. Pass + * `--port 0` to ask for any available port instead. + */ +export const DEFAULT_FOREGROUND_PORT = 50524; export interface ServerCommandDependencies { layout?: RuntimeLayout; artifactDirectory?: string; @@ -79,7 +87,7 @@ export async function runServerForeground(options: { dataDir?: string; host?: st if (typeof manifest.serverEntrypoint !== "string") throw lifecycleError("canonical_server_unavailable", "The packed canonical LocalApp Server runtime is unavailable"); const entrypoint = path.join(artifact, ...manifest.serverEntrypoint.split("/")); const child = (dependencies.spawnOwnedProcess ?? spawnOwnedProcess)(process.execPath, [entrypoint, "start", "--data-dir", options.dataDir ?? layout.dataDir, - "--host", options.host ?? "127.0.0.1", "--port", String(options.port ?? 0)], { stdio: "inherit" }); + "--host", options.host ?? "127.0.0.1", "--port", String(options.port ?? DEFAULT_FOREGROUND_PORT)], { stdio: "inherit" }); return await new Promise((resolve, reject) => { let termination: Promise | undefined; let settled = false; diff --git a/packages/localapp/tests/server-foreground.test.ts b/packages/localapp/tests/server-foreground.test.ts index 88b8b03..2777eb6 100644 --- a/packages/localapp/tests/server-foreground.test.ts +++ b/packages/localapp/tests/server-foreground.test.ts @@ -5,7 +5,6 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest import { runServerForeground } from "../src/commands/server.js"; import { buildLocalAppPackage } from "../scripts/build-package.mjs"; import type { OwnedProcess } from "../src/process/process-tree.js"; - const root = path.resolve(process.cwd(), "../.."); const testRoot = path.join(root, "tmp/task-7b-foreground-tests"); const packageArtifact = path.join(testRoot, "package-artifact"); @@ -37,4 +36,31 @@ describe("server run foreground ownership", () => { releaseTerminate(); await expect(result).resolves.toBe(0); }); + + it("keeps one stable default port unless the operator asks for another", async () => { + // Break caught: the foreground Server picked an ephemeral port on every + // start, so a saved profile, a proxy target, or a published container port + // had to follow a port the Server chose for itself. + const invocations: string[][] = []; + const ownedStub = () => { + const child = new EventEmitter() as unknown as OwnedProcess["child"]; + return { + child, + pid: 42, + exited: Promise.resolve({ code: 0, signal: null }), + terminate: async () => undefined, + } as unknown as OwnedProcess; + }; + const run = (options: { port?: number }) => runServerForeground(options, { + artifactDirectory: packageArtifact, + spawnOwnedProcess: (_command, args) => { invocations.push([...args]); return ownedStub(); }, + }); + + await expect(run({})).resolves.toBe(0); + await expect(run({ port: 55441 })).resolves.toBe(0); + await expect(run({ port: 0 })).resolves.toBe(0); + + const portOf = (args: string[]) => args[args.indexOf("--port") + 1]; + expect(invocations.map(portOf)).toEqual(["50524", "55441", "0"]); + }); });