import express, { Request, Response } from "express";
import cors from "cors";
import { TerminalManager } from "./terminal-manager.js";
import {
CreateTerminalInput,
WriteTerminalInput,
ReadTerminalInput,
KillTerminalInput,
} from "./types.js";
/**
* REST API 服务器
* 提供 HTTP 接口来管理终端会话
*/
export class RestApiServer {
private app: express.Application;
private terminalManager: TerminalManager;
private server: any;
constructor(terminalManager: TerminalManager) {
this.app = express();
this.terminalManager = terminalManager;
this.setupMiddleware();
this.setupRoutes();
}
/**
* 设置中间件
*/
private setupMiddleware(): void {
// CORS 支持
this.app.use(
cors({
origin: "*",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
}),
);
// JSON 解析
this.app.use(express.json({ limit: "10mb" }));
this.app.use(express.urlencoded({ extended: true }));
// 请求日志
this.app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
next();
});
// 错误处理
this.app.use((error: any, req: Request, res: Response, next: any) => {
console.error("API Error:", error);
res.status(500).json({
error: "Internal Server Error",
message: error.message,
});
});
}
/**
* 设置路由
*/
private setupRoutes(): void {
// 健康检查
this.app.get("/health", (req: Request, res: Response) => {
const stats = this.terminalManager.getStats();
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
stats,
});
});
// 创建终端
this.app.post(
"/terminals",
async (req: Request, res: Response): Promise<void> => {
try {
const input: CreateTerminalInput = req.body;
const terminalId = await this.terminalManager.createTerminal(input);
const session = this.terminalManager.getTerminalInfo(terminalId);
if (!session) {
res.status(500).json({
error: "Failed to retrieve session info",
});
return;
}
res.status(201).json({
terminalId,
status: session.status,
pid: session.pid,
shell: session.shell,
cwd: session.cwd,
created: session.created.toISOString(),
});
} catch (error) {
res.status(400).json({
error: "Failed to create terminal",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 列出所有终端
this.app.get("/terminals", async (req: Request, res: Response) => {
try {
const result = await this.terminalManager.listTerminals();
res.json(result);
} catch (error) {
res.status(500).json({
error: "Failed to list terminals",
message: error instanceof Error ? error.message : String(error),
});
}
});
// 获取特定终端信息
this.app.get(
"/terminals/:terminalId",
async (req: Request, res: Response): Promise<void> => {
try {
const terminalId = req.params.terminalId;
if (!terminalId) {
res.status(400).json({ error: "Terminal ID is required" });
return;
}
const session = this.terminalManager.getTerminalInfo(terminalId);
if (!session) {
res.status(404).json({
error: "Terminal not found",
});
return;
}
res.json({
id: session.id,
pid: session.pid,
shell: session.shell,
cwd: session.cwd,
created: session.created.toISOString(),
lastActivity: session.lastActivity.toISOString(),
status: session.status,
});
} catch (error) {
res.status(500).json({
error: "Failed to get terminal info",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 向终端写入数据
this.app.post(
"/terminals/:terminalId/input",
async (req: Request, res: Response): Promise<void> => {
try {
const terminalId = req.params.terminalId;
const { input, appendNewline } = req.body as WriteTerminalInput;
if (!terminalId) {
res.status(400).json({ error: "Terminal ID is required" });
return;
}
if (!input) {
res.status(400).json({
error: "Input is required",
});
return;
}
const writeOptions: any = {
terminalId,
input,
};
if (appendNewline !== undefined) {
writeOptions.appendNewline = appendNewline;
}
await this.terminalManager.writeToTerminal(writeOptions);
res.json({
success: true,
message: "Input sent successfully",
});
} catch (error) {
res.status(400).json({
error: "Failed to write to terminal",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 读取终端输出(增强版)
this.app.get(
"/terminals/:terminalId/output",
async (req: Request, res: Response) => {
try {
const { terminalId } = req.params;
const since = req.query.since
? parseInt(req.query.since as string)
: undefined;
const maxLines = req.query.maxLines
? parseInt(req.query.maxLines as string)
: undefined;
const mode = (req.query.mode as string) || undefined;
const headLines = req.query.headLines
? parseInt(req.query.headLines as string)
: undefined;
const tailLines = req.query.tailLines
? parseInt(req.query.tailLines as string)
: undefined;
const result = await this.terminalManager.readFromTerminal({
terminalId: terminalId!,
since: since || undefined,
maxLines: maxLines || undefined,
mode: (mode as any) || undefined,
headLines: headLines || undefined,
tailLines: tailLines || undefined,
});
res.json(result);
} catch (error) {
res.status(400).json({
error: "Failed to read terminal output",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 获取终端统计信息
this.app.get(
"/terminals/:terminalId/stats",
async (req: Request, res: Response) => {
try {
const { terminalId } = req.params;
if (!terminalId) {
res.status(400).json({ error: "Terminal ID is required" });
return;
}
const result =
await this.terminalManager.getTerminalStats(terminalId);
res.json(result);
} catch (error) {
res.status(400).json({
error: "Failed to get terminal stats",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 终止终端
this.app.delete(
"/terminals/:terminalId",
async (req: Request, res: Response) => {
try {
const { terminalId } = req.params;
const signal = (req.query.signal as string) || "SIGTERM";
await this.terminalManager.killTerminal(terminalId!, signal);
res.json({
success: true,
message: "Terminal terminated successfully",
});
} catch (error) {
res.status(400).json({
error: "Failed to terminate terminal",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 调整终端大小
this.app.put(
"/terminals/:terminalId/resize",
async (req: Request, res: Response): Promise<void> => {
try {
const { terminalId } = req.params;
const { cols, rows } = req.body;
if (!cols || !rows) {
res.status(400).json({
error: "Both cols and rows are required",
});
return;
}
await this.terminalManager.resizeTerminal(terminalId!, cols, rows);
res.json({
success: true,
message: "Terminal resized successfully",
});
} catch (error) {
res.status(400).json({
error: "Failed to resize terminal",
message: error instanceof Error ? error.message : String(error),
});
}
},
);
// 获取管理器统计信息
this.app.get("/stats", (req: Request, res: Response) => {
try {
const stats = this.terminalManager.getStats();
res.json(stats);
} catch (error) {
res.status(500).json({
error: "Failed to get stats",
message: error instanceof Error ? error.message : String(error),
});
}
});
// API 文档
this.app.get("/", (req: Request, res: Response) => {
res.json({
name: "Persistent Terminal REST API",
version: "1.0.0",
description: "REST API for managing persistent terminal sessions",
endpoints: {
"GET /health": "Health check and stats",
"POST /terminals": "Create a new terminal session",
"GET /terminals": "List all terminal sessions",
"GET /terminals/:id": "Get terminal session info",
"POST /terminals/:id/input": "Send input to terminal",
"GET /terminals/:id/output": "Read terminal output",
"DELETE /terminals/:id": "Terminate terminal session",
"PUT /terminals/:id/resize": "Resize terminal",
"GET /stats": "Get manager statistics",
},
documentation: "See README.md for detailed usage instructions",
});
});
}
/**
* 启动服务器
*/
start(port = 3001, host = "localhost"): Promise<void> {
return new Promise((resolve, reject) => {
this.server = this.app.listen(port, host, (error?: any) => {
if (error) {
reject(error);
} else {
console.log(`REST API server listening on ${host}:${port}`);
const displayHost = host === "0.0.0.0" ? "localhost" : host;
console.log(
`API documentation available at http://${displayHost}:${port}/`,
);
resolve();
}
});
});
}
/**
* 停止服务器
*/
stop(): Promise<void> {
return new Promise((resolve) => {
if (this.server) {
this.server.close(() => {
console.log("REST API server stopped");
resolve();
});
} else {
resolve();
}
});
}
/**
* 获取 Express 应用实例
*/
getApp(): express.Application {
return this.app;
}
}