# Directory Structure
```
├── .gitignore
├── Dockerfile
├── package.json
├── PROJ_INFO_4AI.md
├── README_CN.md
├── README.md
├── smithery.yaml
├── src
│ ├── executor.ts
│ ├── index.ts
│ └── ssh.ts
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Terminal MCP Server
[](https://smithery.ai/server/@weidwonder/terminal-mcp-server)
## Notice 注意事项
Current Project not in maintance anymore. I recommend you guys to use more advanced command tool —— [Desktop Commander](https://desktopcommander.app/)
当前项目已经不在维护。我建议大家用更先进的终端MCP工具 [Desktop Commander](https://desktopcommander.app/)
*[中文文档](README_CN.md)*
Terminal MCP Server is a Model Context Protocol (MCP) server that allows executing commands on local or remote hosts. It provides a simple yet powerful interface for AI models and other applications to execute system commands, either on the local machine or on remote hosts via SSH.
## Features
- **Local Command Execution**: Execute commands directly on the local machine
- **Remote Command Execution**: Execute commands on remote hosts via SSH
- **Session Persistence**: Support for persistent sessions that reuse the same terminal environment for a specified time (default 20 minutes)
- **Environment Variables**: Set custom environment variables for commands
- **Multiple Connection Methods**: Connect via stdio or SSE (Server-Sent Events)
## Installation
### Installing via Smithery
To install terminal-mcp-server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@weidwonder/terminal-mcp-server):
```bash
npx -y @smithery/cli install @weidwonder/terminal-mcp-server --client claude
```
### Manual Installation
```bash
# Clone the repository
git clone https://github.com/weidwonder/terminal-mcp-server.git
cd terminal-mcp-server
# Install dependencies
npm install
# Build the project
npm run build
```
## Usage
### Starting the Server
```bash
# Start the server using stdio (default mode)
npm start
# Or run the built file directly
node build/index.js
```
### Starting the Server in SSE Mode
The SSE (Server-Sent Events) mode allows you to connect to the server remotely via HTTP.
```bash
# Start the server in SSE mode
npm run start:sse
# Or run the built file directly with SSE flag
node build/index.js --sse
```
You can customize the SSE server with the following command-line options:
| Option | Description | Default |
|--------|-------------|---------|
| `--port` or `-p` | The port to listen on | 8080 |
| `--endpoint` or `-e` | The endpoint path | /sse |
| `--host` or `-h` | The host to bind to | localhost |
Example with custom options:
```bash
# Start SSE server on port 3000, endpoint /mcp, and bind to all interfaces
node build/index.js --sse --port 3000 --endpoint /mcp --host 0.0.0.0
```
This will start the server and listen for SSE connections at `http://0.0.0.0:3000/mcp`.
### Testing with MCP Inspector
```bash
# Start the MCP Inspector tool
npm run inspector
```
## The execute_command Tool
The execute_command tool is the core functionality provided by Terminal MCP Server, used to execute commands on local or remote hosts.
### Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| command | string | Yes | The command to execute |
| host | string | No | The remote host to connect to. If not provided, the command will be executed locally |
| username | string | Required when host is specified | The username for SSH connection |
| session | string | No | Session name, defaults to "default". The same session name will reuse the same terminal environment for 20 minutes |
| env | object | No | Environment variables, defaults to an empty object |
### Examples
#### Executing a Command Locally
```json
{
"command": "ls -la",
"session": "my-local-session",
"env": {
"NODE_ENV": "development"
}
}
```
#### Executing a Command on a Remote Host
```json
{
"host": "example.com",
"username": "user",
"command": "ls -la",
"session": "my-remote-session",
"env": {
"NODE_ENV": "production"
}
}
```
## Configuring with AI Assistants
### Configuring with Roo Code
1. Open VSCode and install the Roo Code extension
2. Open the Roo Code settings file: `~/Library/Application Support/Code/User/globalStorage/rooveterinaryinc.roo-cline/settings/cline_mcp_settings.json`
3. Add the following configuration:
#### For stdio mode (local connection)
```json
{
"mcpServers": {
"terminal-mcp": {
"command": "node",
"args": ["/path/to/terminal-mcp-server/build/index.js"],
"env": {}
}
}
}
```
#### For SSE mode (remote connection)
```json
{
"mcpServers": {
"terminal-mcp-sse": {
"url": "http://localhost:8080/sse",
"headers": {}
}
}
}
```
Replace `localhost:8080/sse` with your actual server address, port, and endpoint if you've customized them.
### Configuring with Cline
1. Open the Cline settings file: `~/.cline/config.json`
2. Add the following configuration:
#### For stdio mode (local connection)
```json
{
"mcpServers": {
"terminal-mcp": {
"command": "node",
"args": ["/path/to/terminal-mcp-server/build/index.js"],
"env": {}
}
}
}
```
#### For SSE mode (remote connection)
```json
{
"mcpServers": {
"terminal-mcp-sse": {
"url": "http://localhost:8080/sse",
"headers": {}
}
}
}
```
### Configuring with Claude Desktop
1. Open the Claude Desktop settings file: `~/Library/Application Support/Claude/claude_desktop_config.json`
2. Add the following configuration:
#### For stdio mode (local connection)
```json
{
"mcpServers": {
"terminal-mcp": {
"command": "node",
"args": ["/path/to/terminal-mcp-server/build/index.js"],
"env": {}
}
}
}
```
#### For SSE mode (remote connection)
```json
{
"mcpServers": {
"terminal-mcp-sse": {
"url": "http://localhost:8080/sse",
"headers": {}
}
}
}
```
## Best Practices
### Command Execution
- Before running commands, it's best to determine the system type (Mac, Linux, etc.)
- Use full paths to avoid path-related issues
- For command sequences that need to maintain environment, use `&&` to connect multiple commands
- For long-running commands, consider using `nohup` or `screen`/`tmux`
### SSH Connection
- Ensure SSH key-based authentication is set up
- If connection fails, check if the key file exists (default path: `~/.ssh/id_rsa`)
- Make sure the SSH service is running on the remote host
### Session Management
- Use the session parameter to maintain environment between related commands
- For operations requiring specific environments, use the same session name
- Note that sessions will automatically close after 20 minutes of inactivity
### Error Handling
- Command execution results include both stdout and stderr
- Check stderr to determine if the command executed successfully
- For complex operations, add verification steps to ensure success
## Important Notes
- For remote command execution, SSH key-based authentication must be set up in advance
- For local command execution, commands will run in the context of the user who started the server
- Session timeout is 20 minutes, after which the connection will be automatically closed
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
FROM node:lts-alpine
# Create app directory
WORKDIR /app
# Install app dependencies
# A wildcard is used to ensure both package.json and package-lock.json are copied
COPY package*.json ./
# Install dependencies (skip prepare script to avoid build issues, we'll explicitly run build step)
RUN npm install --ignore-scripts
# Copy app source code
COPY . .
# Build the application
RUN npm run build
# Expose port 8080 for SSE mode (if used)
EXPOSE 8080
# Command to run the application
CMD ["node", "build/index.js"]
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "terminal-mcp-server",
"version": "0.1.0",
"description": "操作X主机",
"private": true,
"type": "module",
"bin": {
"terminal-mcp-server": "./build/index.js"
},
"files": [
"build"
],
"scripts": {
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
"prepare": "npm run build",
"watch": "tsc --watch",
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
"start": "node build/index.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.6.0",
"ssh2": "^1.16.0"
},
"devDependencies": {
"@types/node": "^20.11.24",
"@types/ssh2": "^1.15.4",
"typescript": "^5.3.3"
}
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
properties:
sse:
type: boolean
default: false
description: If true, run the server in SSE mode instead of default stdio mode.
port:
type: number
default: 8080
description: Port to listen on when running in SSE mode.
endpoint:
type: string
default: /sse
description: Endpoint path for SSE connections.
host:
type: string
default: localhost
description: Host to bind to when running in SSE mode.
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => { const args = []; if(config.sse){ args.push('--sse'); if(config.port) { args.push('--port', config.port.toString()); } if(config.endpoint){ args.push('--endpoint', config.endpoint); } if(config.host){ args.push('--host', config.host); } } return { command: 'node', args: ['build/index.js', ...args] }; }
exampleConfig:
sse: true
port: 3000
endpoint: /mcp
host: 0.0.0.0
```
--------------------------------------------------------------------------------
/src/ssh.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from 'ssh2';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
export class SSHManager {
private client: Client | null = null;
private connection: Promise<void> | null = null;
private timeout: NodeJS.Timeout | null = null;
private sessionTimeout: number = 20 * 60 * 1000; // 20 minutes
constructor() {
this.client = new Client();
}
async connect(host: string): Promise<void> {
if (this.connection) {
return this.connection;
}
const privateKey = fs.readFileSync(path.join(os.homedir(), '.ssh', 'id_rsa'));
this.connection = new Promise((resolve, reject) => {
this.client = new Client();
this.client
.on('ready', () => {
this.resetTimeout();
resolve();
})
.on('error', (err) => {
reject(err);
})
.connect({
host: host,
username: 'weidwonder',
privateKey: privateKey,
});
});
return this.connection;
}
private resetTimeout(): void {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(async () => {
console.log('SSH session timeout, disconnecting');
await this.disconnect();
}, this.sessionTimeout);
}
async executeCommand(host: string, command: string, env: Record<string, string> | {} = {}): Promise<{stdout: string; stderr: string}> {
if (!this.client) {
await this.connect(host);
}
this.resetTimeout();
return new Promise((resolve, reject) => {
// 使用完整的shell路径,以交互式登录模式执行命令
// 这更接近于用户直接SSH登录的体验
// 构建环境变量设置命令
const envSetup = Object.entries(env as Record<string, string>)
.map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`)
.join(' && ');
// 如果有环境变量,先设置环境变量,再执行命令
const fullCommand = envSetup ? `${envSetup} && ${command}` : command;
this.client?.exec(`/bin/bash --login -c "${fullCommand}"`, (err, stream) => {
if (err) {
reject(err);
return
}
let stdout = ""
let stderr = '';
stream
.on("data", (data: Buffer) => {
this.resetTimeout();
stdout += data.toString();
})
.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
})
.on('close', () => {
resolve({ stdout, stderr });
})
.on('error', (err) => {
reject(err);
});
});
});
}
async disconnect(): Promise<void> {
if (this.client) {
this.client.end();
this.client = null;
this.connection = null;
}
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ErrorCode,
McpError,
} from "@modelcontextprotocol/sdk/types.js";
import { CommandExecutor } from "./executor.js";
// 全局日志函数,确保所有日志都通过stderr输出
export const log = {
debug: (message: string, ...args: any[]) => {
if (process.env.DEBUG === 'true') {
console.error(`[DEBUG] ${message}`, ...args);
}
},
info: (message: string, ...args: any[]) => {
console.error(`[INFO] ${message}`, ...args);
},
warn: (message: string, ...args: any[]) => {
console.error(`[WARN] ${message}`, ...args);
},
error: (message: string, ...args: any[]) => {
console.error(`[ERROR] ${message}`, ...args);
}
};
const commandExecutor = new CommandExecutor();
// 创建服务器
function createServer() {
const server = new Server(
{
name: "remote-ops-server",
version: "0.1.0",
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "execute_command",
description: "Execute commands on remote hosts or locally (This tool can be used for both remote hosts and the current machine)",
inputSchema: {
type: "object",
properties: {
host: {
type: "string",
description: "Host to connect to (optional, if not provided the command will be executed locally)"
},
username: {
type: "string",
description: "Username for SSH connection (required when host is specified)"
},
session: {
type: "string",
description: "Session name, defaults to 'default'. The same session name will reuse the same terminal environment for 20 minutes, which is useful for operations requiring specific environments like conda.",
default: "default"
},
command: {
type: "string",
description: "Command to execute. Before running commands, it's best to determine the system type (Mac, Linux, etc.)"
},
env: {
type: "object",
description: "Environment variables",
default: {}
}
},
required: ["command"]
}
}
]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (request.params.name !== "execute_command") {
throw new McpError(ErrorCode.MethodNotFound, "Unknown tool");
}
const host = request.params.arguments?.host ? String(request.params.arguments.host) : undefined;
const username = request.params.arguments?.username ? String(request.params.arguments.username) : undefined;
const session = String(request.params.arguments?.session || "default");
const command = String(request.params.arguments?.command);
if (!command) {
throw new McpError(ErrorCode.InvalidParams, "Command is required");
}
const env = request.params.arguments?.env || {};
// 如果指定了host但没有指定username
if (host && !username) {
throw new McpError(ErrorCode.InvalidParams, "Username is required when host is specified");
}
try {
const result = await commandExecutor.executeCommand(command, {
host,
username,
session,
env: env as Record<string, string>
});
return {
content: [{
type: "text",
text: `Command Output:\nstdout: ${result.stdout}\nstderr: ${result.stderr}`
}]
};
} catch (error) {
if (error instanceof Error && error.message.includes('SSH')) {
throw new McpError(
ErrorCode.InternalError,
`SSH connection error: ${error.message}. Please ensure SSH key-based authentication is set up.`
);
}
throw error;
}
} catch (error) {
if (error instanceof McpError) {
throw error;
}
throw new McpError(
ErrorCode.InternalError,
error instanceof Error ? error.message : String(error)
);
}
});
return server;
}
async function main() {
try {
// 使用标准输入输出
const server = createServer();
// 设置MCP错误处理程序
server.onerror = (error) => {
log.error(`MCP Error: ${error.message}`);
};
const transport = new StdioServerTransport();
await server.connect(transport);
log.info("Remote Ops MCP server running on stdio");
// 处理进程退出
process.on('SIGINT', async () => {
log.info("Shutting down server...");
await commandExecutor.disconnect();
process.exit(0);
});
} catch (error) {
log.error("Server error:", error);
process.exit(1);
}
}
main().catch((error) => {
log.error("Server error:", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/src/executor.ts:
--------------------------------------------------------------------------------
```typescript
import { Client } from 'ssh2';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { exec } from 'child_process';
import { log } from './index.js';
export class CommandExecutor {
private sessions: Map<string, {
client: Client | null;
connection: Promise<void> | null;
timeout: NodeJS.Timeout | null;
host?: string;
env?: Record<string, string>; // 添加环境变量存储
shell?: any; // 添加shell会话
shellReady?: boolean; // shell是否准备好
}> = new Map();
private sessionTimeout: number = 20 * 60 * 1000; // 20 minutes
constructor() {}
private getSessionKey(host: string | undefined, sessionName: string): string {
return `${host || 'local'}-${sessionName}`;
}
async connect(host: string, username: string, sessionName: string = 'default'): Promise<void> {
const sessionKey = this.getSessionKey(host, sessionName);
const session = this.sessions.get(sessionKey);
// 如果会话存在且连接有效,直接返回现有连接
if (session?.connection && session?.client) {
// 检查客户端是否仍然连接
if (session.client.listenerCount('ready') > 0 || session.client.listenerCount('data') > 0) {
log.info(`Reusing existing session: ${sessionKey}`);
return session.connection;
}
// 如果客户端已断开连接,清理旧会话
log.info(`Session ${sessionKey} disconnected, creating new session`);
this.sessions.delete(sessionKey);
}
try {
const privateKey = fs.readFileSync(path.join(os.homedir(), '.ssh', 'id_rsa'));
const client = new Client();
const connection = new Promise<void>((resolve, reject) => {
client
.on('ready', () => {
log.info(`Session ${sessionKey} connected`);
this.resetTimeout(sessionKey);
// 创建一个交互式shell
client.shell((err, stream) => {
if (err) {
log.error(`Failed to create interactive shell: ${err.message}`);
reject(err);
return;
}
log.info(`Creating interactive shell for session ${sessionKey}`);
// 获取会话对象
const sessionData = this.sessions.get(sessionKey);
if (sessionData) {
// 设置shell和shellReady标志
sessionData.shell = stream;
sessionData.shellReady = true;
// 更新会话
this.sessions.set(sessionKey, sessionData);
}
// 处理shell关闭事件
stream.on('close', () => {
log.info(`Interactive shell for session ${sessionKey} closed`);
const sessionData = this.sessions.get(sessionKey);
if (sessionData) {
sessionData.shellReady = false;
this.sessions.set(sessionKey, sessionData);
}
});
// 等待shell准备好
stream.write('echo "Shell ready"\n');
// 解析promise
resolve();
});
})
.on('error', (err) => {
log.error(`会话 ${sessionKey} 连接错误:`, err.message);
reject(err);
})
.connect({
host: host,
username: username,
privateKey: privateKey,
keepaliveInterval: 60000, // 每分钟发送一次keepalive包
});
});
log.info(`Creating new session: ${sessionKey}`);
this.sessions.set(sessionKey, {
client,
connection,
timeout: null,
host,
shell: null,
shellReady: false
});
return connection;
} catch (error) {
if (error instanceof Error && error.message.includes('ENOENT')) {
throw new Error('SSH key file does not exist, please ensure SSH key-based authentication is set up');
}
throw error;
}
}
private resetTimeout(sessionKey: string): void {
const session = this.sessions.get(sessionKey);
if (!session) return;
if (session.timeout) {
clearTimeout(session.timeout);
}
session.timeout = setTimeout(async () => {
log.info(`Session ${sessionKey} timeout, disconnecting`);
await this.disconnectSession(sessionKey);
}, this.sessionTimeout);
this.sessions.set(sessionKey, session);
}
async executeCommand(
command: string,
options: {
host?: string;
username?: string;
session?: string;
env?: Record<string, string>;
} = {}
): Promise<{stdout: string; stderr: string}> {
const { host, username, session = 'default', env = {} } = options;
const sessionKey = this.getSessionKey(host, session);
// 如果指定了host,则使用SSH执行命令
if (host) {
if (!username) {
throw new Error('Username is required when using SSH');
}
let sessionData = this.sessions.get(sessionKey);
// 检查会话是否存在且有效
let needNewConnection = false;
if (!sessionData || sessionData.host !== host) {
needNewConnection = true;
} else if (sessionData.client) {
// 检查客户端是否仍然连接
if (sessionData.client.listenerCount('ready') === 0 && sessionData.client.listenerCount('data') === 0) {
log.info(`Session ${sessionKey} disconnected, reconnecting`);
needNewConnection = true;
}
} else {
needNewConnection = true;
}
// 如果需要新连接,则创建
if (needNewConnection) {
log.info(`Creating new connection for command execution: ${sessionKey}`);
await this.connect(host, username, session);
sessionData = this.sessions.get(sessionKey);
} else {
log.info(`Reusing existing session for command execution: ${sessionKey}`);
}
if (!sessionData || !sessionData.client) {
throw new Error(`无法创建到 ${host} 的SSH会话`);
}
this.resetTimeout(sessionKey);
// 检查是否有交互式shell可用
if (sessionData.shellReady && sessionData.shell) {
log.info(`Executing command using interactive shell: ${command}`);
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
let commandFinished = false;
const uniqueMarker = `CMD_END_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
// 构建环境变量设置命令
const envSetup = Object.entries(env)
.map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`)
.join(' && ');
// 如果有环境变量,先设置环境变量,再执行命令
const fullCommand = envSetup ? `${envSetup} && ${command}` : command;
// 添加数据处理器
const dataHandler = (data: Buffer) => {
const str = data.toString();
log.debug(`Shell数据: ${str}`);
if (str.includes(uniqueMarker)) {
// 命令执行完成
commandFinished = true;
// 提取命令输出(从命令开始到标记之前的内容)
const lines = stdout.split('\n');
let commandOutput = '';
let foundCommand = false;
for (const line of lines) {
if (foundCommand) {
if (line.includes(uniqueMarker)) {
break;
}
commandOutput += line + '\n';
} else if (line.includes(fullCommand)) {
foundCommand = true;
}
}
// 解析输出
resolve({ stdout: commandOutput.trim(), stderr });
// 移除处理器
sessionData.shell.removeListener('data', dataHandler);
clearTimeout(timeout);
} else if (!commandFinished) {
stdout += str;
}
};
// 添加错误处理器
const errorHandler = (err: Error) => {
stderr += err.message;
reject(err);
sessionData.shell.removeListener('data', dataHandler);
sessionData.shell.removeListener('error', errorHandler);
};
// 监听数据和错误
sessionData.shell.on('data', dataHandler);
sessionData.shell.on('error', errorHandler);
// 执行命令并添加唯一标记
// 使用一个更明确的方式来执行命令和捕获输出
sessionData.shell.write(`echo "Starting command execution: ${fullCommand}"\n`);
sessionData.shell.write(`${fullCommand}\n`);
sessionData.shell.write(`echo "${uniqueMarker}"\n`);
// 设置超时
const timeout = setTimeout(() => {
if (!commandFinished) {
stderr += "Command execution timed out";
resolve({ stdout, stderr });
sessionData.shell.removeListener('data', dataHandler);
sessionData.shell.removeListener('error', errorHandler);
}
}, 30000); // 30秒超时
});
} else {
log.info(`Executing command using exec: ${command}`);
return new Promise((resolve, reject) => {
// 构建环境变量设置命令
const envSetup = Object.entries(env)
.map(([key, value]) => `export ${key}="${String(value).replace(/"/g, '\\"')}"`)
.join(' && ');
// 如果有环境变量,先设置环境变量,再执行命令
const fullCommand = envSetup ? `${envSetup} && ${command}` : command;
sessionData?.client?.exec(`/bin/bash --login -c "${fullCommand.replace(/"/g, '\\"')}"`, (err, stream) => {
if (err) {
reject(err);
return;
}
let stdout = "";
let stderr = '';
stream
.on("data", (data: Buffer) => {
this.resetTimeout(sessionKey);
stdout += data.toString();
})
.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
})
.on('close', () => {
resolve({ stdout, stderr });
})
.on('error', (err) => {
reject(err);
});
});
});
}
}
// 否则在本地执行命令
else {
// 在本地执行命令时,也使用会话机制来保持环境变量
log.info(`Executing command using local session: ${sessionKey}`);
// 检查是否已有本地会话
let sessionData = this.sessions.get(sessionKey);
let sessionEnv = {};
if (!sessionData) {
// 为本地会话创建一个空条目,以便跟踪超时
sessionData = {
client: null,
connection: null,
timeout: null,
host: undefined,
env: { ...env } // 保存初始环境变量
};
this.sessions.set(sessionKey, sessionData);
log.info(`Creating new local session: ${sessionKey}`);
sessionEnv = env;
} else {
log.info(`Reusing existing local session: ${sessionKey}`);
// 合并现有会话环境变量和新的环境变量
if (!sessionData.env) {
sessionData.env = {};
}
sessionData.env = { ...sessionData.env, ...env };
sessionEnv = sessionData.env;
// 更新会话
this.sessions.set(sessionKey, sessionData);
}
this.resetTimeout(sessionKey);
return new Promise((resolve, reject) => {
// 构建环境变量,优先级:系统环境变量 < 会话环境变量 < 当前命令环境变量
const envVars = { ...process.env, ...sessionEnv };
// 执行命令
log.info(`Executing local command: ${command}`);
exec(command, { env: envVars }, (error, stdout, stderr) => {
if (error && error.code !== 0) {
// 我们不直接拒绝,而是返回错误信息作为stderr
resolve({ stdout, stderr: stderr || error.message });
} else {
resolve({ stdout, stderr });
}
});
});
}
}
private async disconnectSession(sessionKey: string): Promise<void> {
const session = this.sessions.get(sessionKey);
if (session) {
if (session.shell) {
log.info(`Closing interactive shell for session ${sessionKey}`);
session.shell.end();
session.shellReady = false;
}
if (session.client) {
log.info(`Disconnecting SSH connection for session ${sessionKey}`);
session.client.end();
}
if (session.timeout) {
clearTimeout(session.timeout);
}
log.info(`Disconnecting session: ${sessionKey}`);
this.sessions.delete(sessionKey);
}
}
async disconnect(): Promise<void> {
const disconnectPromises = Array.from(this.sessions.keys()).map(
sessionKey => this.disconnectSession(sessionKey)
);
await Promise.all(disconnectPromises);
this.sessions.clear();
}
}
```