fix(api): lazy-load node-pty to prevent API crash on missing native binary (#525)
All checks were successful
ci/woodpecker/push/api Pipeline was successful
All checks were successful
ci/woodpecker/push/api Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #525.
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,19 @@
|
||||
* - 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 { IPty } from "node-pty";
|
||||
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.
|
||||
interface NodePtyModule {
|
||||
spawn: (file: string, args: string[], options: Record<string, unknown>) => IPty;
|
||||
}
|
||||
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 +39,7 @@ const DEFAULT_ROWS = 24;
|
||||
export interface TerminalSession {
|
||||
sessionId: string;
|
||||
workspaceId: string;
|
||||
pty: pty.IPty;
|
||||
pty: IPty;
|
||||
name?: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
@@ -53,7 +61,7 @@ export interface SessionCreatedResult {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TerminalService {
|
||||
export class TerminalService implements OnModuleInit {
|
||||
private readonly logger = new Logger(TerminalService.name);
|
||||
|
||||
/**
|
||||
@@ -66,13 +74,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