fix(api): lazy-load node-pty to prevent API crash when native binary is missing
node-pty requires a compiled native addon (.node binary) that may not be available in all Docker environments. The eager import crashed the entire API at startup. Changed to dynamic import() in onModuleInit() so the service degrades gracefully — terminal sessions are disabled but all other API functionality works. Also added explicit node-gyp rebuild to Dockerfile deps stage since pnpm may skip postinstall scripts for native addons. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@ describe("TerminalService", () => {
|
||||
let service: TerminalService;
|
||||
let mockSocket: Socket;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// Reset mock implementations
|
||||
mockPtyProcess.onData.mockImplementation((_cb: (data: string) => void) => {});
|
||||
@@ -54,6 +54,8 @@ describe("TerminalService", () => {
|
||||
(_cb: (e: { exitCode: number; signal?: number }) => void) => {}
|
||||
);
|
||||
service = new TerminalService();
|
||||
// Trigger lazy import of node-pty (uses dynamic import(), intercepted by vi.mock)
|
||||
await service.onModuleInit();
|
||||
mockSocket = createMockSocket();
|
||||
});
|
||||
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
* - closeWorkspaceSessions: kill all sessions for a workspace (on disconnect)
|
||||
*/
|
||||
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import * as pty from "node-pty";
|
||||
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import type { Socket } from "socket.io";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
// Lazy-loaded in onModuleInit via dynamic import() to prevent crash
|
||||
// if the native binary is missing. node-pty requires a compiled .node
|
||||
// binary which may not be available in all Docker environments.
|
||||
type NodePtyModule = typeof import("node-pty");
|
||||
let pty: NodePtyModule | null = null;
|
||||
|
||||
/** Maximum concurrent PTY sessions per workspace */
|
||||
export const MAX_SESSIONS_PER_WORKSPACE = parseInt(
|
||||
process.env.TERMINAL_MAX_SESSIONS_PER_WORKSPACE ?? "10",
|
||||
@@ -31,7 +36,7 @@ const DEFAULT_ROWS = 24;
|
||||
export interface TerminalSession {
|
||||
sessionId: string;
|
||||
workspaceId: string;
|
||||
pty: pty.IPty;
|
||||
pty: import("node-pty").IPty;
|
||||
name?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -53,7 +58,7 @@ export interface SessionCreatedResult {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TerminalService {
|
||||
export class TerminalService implements OnModuleInit {
|
||||
private readonly logger = new Logger(TerminalService.name);
|
||||
|
||||
/**
|
||||
@@ -66,13 +71,30 @@ export class TerminalService {
|
||||
*/
|
||||
private readonly workspaceSessions = new Map<string, Set<string>>();
|
||||
|
||||
async onModuleInit(): Promise<void> {
|
||||
if (!pty) {
|
||||
try {
|
||||
pty = await import("node-pty");
|
||||
this.logger.log("node-pty loaded successfully — terminal sessions available");
|
||||
} catch {
|
||||
this.logger.warn(
|
||||
"node-pty native module not available — terminal sessions will be disabled. " +
|
||||
"Install build tools (python3, make, g++) and rebuild node-pty to enable."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new PTY session for the given workspace and socket.
|
||||
* Wires PTY onData -> emit terminal:output and onExit -> emit terminal:exit.
|
||||
*
|
||||
* @throws Error if workspace session limit is exceeded
|
||||
* @throws Error if workspace session limit is exceeded or node-pty is unavailable
|
||||
*/
|
||||
createSession(socket: Socket, options: CreateSessionOptions): SessionCreatedResult {
|
||||
if (!pty) {
|
||||
throw new Error("Terminal sessions are unavailable: node-pty native module failed to load");
|
||||
}
|
||||
const { workspaceId, name, cwd, socketId } = options;
|
||||
const cols = options.cols ?? DEFAULT_COLS;
|
||||
const rows = options.rows ?? DEFAULT_ROWS;
|
||||
|
||||
Reference in New Issue
Block a user