Skip to content

Commit bf6cbe1

Browse files
author
Lasim
committed
feat(gateway): implement graceful and forceful server stop functionality
1 parent 55c38c0 commit bf6cbe1

File tree

5 files changed

+277
-125
lines changed

5 files changed

+277
-125
lines changed

services/gateway/src/commands/logout.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import ora from 'ora';
44
import { CredentialStorage } from '../core/auth/storage';
55
import { MCPConfigService } from '../core/mcp';
66
import { AuthenticationError } from '../types/auth';
7+
import { ServerStopService } from '../services/server-stop-service';
78

89
export function registerLogoutCommand(program: Command) {
910
program
@@ -53,7 +54,28 @@ export function registerLogoutCommand(program: Command) {
5354
console.log(chalk.green(`✅ Successfully logged out ${userEmail}`));
5455
}
5556

57+
// Stop the gateway server if it's running
58+
const stopService = new ServerStopService();
59+
if (stopService.isServerRunning()) {
60+
console.log(chalk.blue('🛑 Stopping gateway server...'));
61+
spinner = ora('Stopping gateway server and MCP processes...').start();
62+
63+
try {
64+
const stopResult = await stopService.stopGatewayServer({ timeout: 15 });
65+
if (stopResult.success && stopResult.wasRunning) {
66+
spinner.succeed('Gateway server stopped');
67+
console.log(chalk.gray('💡 All MCP servers have been stopped along with the gateway'));
68+
} else {
69+
spinner.succeed('Gateway server was not running');
70+
}
71+
} catch (error) {
72+
spinner.warn('Failed to stop gateway server gracefully');
73+
console.log(chalk.yellow(`⚠️ Gateway server may still be running: ${error instanceof Error ? error.message : String(error)}`));
74+
}
75+
}
76+
5677
console.log(chalk.gray(`💡 Use 'deploystack login' to authenticate again`));
78+
console.log(chalk.gray(`💡 Use 'deploystack start' to start the gateway server again`));
5779

5880
} catch (error) {
5981
if (spinner) {

services/gateway/src/commands/start.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,16 @@ export function registerStartCommand(program: Command) {
4949
}
5050

5151
} catch (error) {
52-
console.error(chalk.red('❌ Failed to start gateway:'), error instanceof Error ? error.message : String(error));
53-
process.exit(1);
52+
// In daemon child process, write clean error to stderr without formatting
53+
if (process.env.DEPLOYSTACK_DAEMON === 'true') {
54+
// Write raw error message to stderr for parent process
55+
process.stderr.write(error instanceof Error ? error.message : String(error));
56+
process.exit(1);
57+
} else {
58+
// Normal interactive mode - show formatted error
59+
console.error(chalk.red('❌ Failed to start gateway:'), error instanceof Error ? error.message : String(error));
60+
process.exit(1);
61+
}
5462
}
5563
});
5664
}
Lines changed: 26 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import { Command } from 'commander';
22
import chalk from 'chalk';
3-
import fs from 'fs';
4-
import path from 'path';
5-
import os from 'os';
6-
7-
// PID file location
8-
const PID_FILE = path.join(os.tmpdir(), 'deploystack-gateway.pid');
3+
import { ServerStopService } from '../services/server-stop-service';
94

105
export function registerStopCommand(program: Command) {
116
program
@@ -14,134 +9,47 @@ export function registerStopCommand(program: Command) {
149
.option('-f, --force', 'Force stop the server (SIGKILL)')
1510
.option('--timeout <seconds>', 'Timeout for graceful shutdown in seconds', '30')
1611
.action(async (options) => {
12+
const stopService = new ServerStopService();
13+
1714
try {
1815
const timeoutSeconds = parseInt(options.timeout, 10) || 30;
1916
console.log(chalk.blue('🛑 Stopping DeployStack Gateway...'));
2017

21-
// Check if PID file exists
22-
if (!fs.existsSync(PID_FILE)) {
23-
console.log(chalk.yellow('⚠️ Gateway server is not running (no PID file found)'));
24-
return;
25-
}
26-
27-
// Read PID from file
28-
const pidStr = fs.readFileSync(PID_FILE, 'utf8').trim();
29-
const pid = parseInt(pidStr, 10);
30-
31-
if (isNaN(pid)) {
32-
console.log(chalk.red('❌ Invalid PID in PID file'));
33-
removePidFile();
34-
return;
35-
}
36-
37-
// Check if process is running
38-
if (!isProcessRunning(pid)) {
39-
console.log(chalk.yellow('⚠️ Gateway server is not running (process not found)'));
40-
removePidFile();
41-
return;
42-
}
43-
44-
// NEW: The gateway now handles MCP server shutdown internally
45-
// When we send SIGTERM to the gateway, it will:
46-
// 1. Stop all MCP servers gracefully (following MCP spec)
47-
// 2. Stop the HTTP server
48-
// 3. Clean up and exit
49-
50-
if (options.force) {
51-
// Force stop - send SIGKILL immediately
18+
// Get current PID for progress messages
19+
const pid = stopService.getRunningPid();
20+
21+
if (options.force && pid) {
5222
console.log(chalk.yellow('⚠️ Force stopping gateway (MCP servers may not shutdown gracefully)'));
5323
console.log(chalk.gray(` Sending SIGKILL to process ${pid}...`));
54-
55-
try {
56-
process.kill(pid, 'SIGKILL');
57-
console.log(chalk.green('✅ Gateway server force stopped'));
58-
removePidFile();
59-
} catch {
60-
console.log(chalk.yellow('⚠️ Process was already stopped'));
61-
removePidFile();
62-
}
63-
} else {
64-
// Graceful stop - send SIGTERM and wait
24+
} else if (pid) {
6525
console.log(chalk.gray(` Sending SIGTERM to process ${pid}...`));
6626
console.log(chalk.gray(' Gateway will stop MCP servers first, then HTTP server...'));
27+
console.log(chalk.gray(` Waiting for graceful shutdown (timeout: ${timeoutSeconds}s)...`));
28+
}
6729

68-
try {
69-
process.kill(pid, 'SIGTERM');
70-
71-
// Wait for graceful shutdown with longer timeout for MCP server cleanup
72-
console.log(chalk.gray(` Waiting for graceful shutdown (timeout: ${timeoutSeconds}s)...`));
73-
74-
let attempts = 0;
75-
const maxAttempts = timeoutSeconds * 2; // Check every 500ms
76-
77-
while (attempts < maxAttempts && isProcessRunning(pid)) {
78-
await new Promise(resolve => setTimeout(resolve, 500));
79-
attempts++;
80-
81-
// Show progress every 5 seconds
82-
if (attempts % 10 === 0) {
83-
const elapsed = Math.floor(attempts / 2);
84-
console.log(chalk.gray(` Still shutting down... (${elapsed}s elapsed)`));
85-
}
86-
}
87-
88-
if (isProcessRunning(pid)) {
89-
console.log(chalk.yellow(`⚠️ Process did not stop within ${timeoutSeconds}s, forcing shutdown...`));
90-
process.kill(pid, 'SIGKILL');
91-
await new Promise(resolve => setTimeout(resolve, 2000));
92-
93-
if (isProcessRunning(pid)) {
94-
console.log(chalk.red('❌ Failed to stop process even with SIGKILL'));
95-
console.log(chalk.gray(` Process ${pid} may be stuck - manual intervention required`));
96-
process.exit(1);
97-
} else {
98-
console.log(chalk.yellow('⚠️ Gateway force stopped after timeout'));
99-
}
100-
} else {
101-
console.log(chalk.green('✅ Gateway server stopped gracefully'));
102-
}
103-
104-
removePidFile();
105-
} catch (error) {
106-
if ((error as NodeJS.ErrnoException).code === 'ESRCH') {
107-
console.log(chalk.yellow('⚠️ Process was already stopped'));
108-
removePidFile();
109-
} else {
110-
throw error;
111-
}
30+
// Use the service to stop the server
31+
const result = await stopService.stopGatewayServer({
32+
force: options.force,
33+
timeout: timeoutSeconds
34+
});
35+
36+
if (result.success) {
37+
if (result.wasRunning) {
38+
console.log(chalk.green(`✅ ${result.message}`));
39+
console.log(chalk.gray('💡 All MCP servers have been stopped along with the gateway'));
40+
} else {
41+
console.log(chalk.yellow(`⚠️ ${result.message}`));
11242
}
43+
} else {
44+
console.log(chalk.red(`❌ ${result.message}`));
45+
process.exit(1);
11346
}
11447

115-
console.log(chalk.gray('💡 All MCP servers have been stopped along with the gateway'));
116-
11748
} catch (error) {
118-
console.error(chalk.red('❌ Failed to stop gateway:'), error);
49+
console.error(chalk.red('❌ Failed to stop gateway:'), error instanceof Error ? error.message : String(error));
11950
process.exit(1);
12051
}
12152
});
12253
}
12354

124-
/**
125-
* Check if process is running
126-
*/
127-
function isProcessRunning(pid: number): boolean {
128-
try {
129-
process.kill(pid, 0); // Signal 0 checks if process exists
130-
return true;
131-
} catch {
132-
return false;
133-
}
134-
}
13555

136-
/**
137-
* Remove PID file
138-
*/
139-
function removePidFile(): void {
140-
try {
141-
if (fs.existsSync(PID_FILE)) {
142-
fs.unlinkSync(PID_FILE);
143-
}
144-
} catch {
145-
console.warn(chalk.yellow('⚠️ Could not remove PID file:'));
146-
}
147-
}

services/gateway/src/services/server-start-service.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,13 @@ export class ServerStartService {
6464
}
6565

6666
} catch (error) {
67-
throw new Error(`Failed to start gateway: ${error instanceof Error ? error.message : String(error)}`);
67+
// Don't double-prefix the error message if it already contains "Failed to start gateway"
68+
const errorMessage = error instanceof Error ? error.message : String(error);
69+
if (errorMessage.includes('Failed to start gateway')) {
70+
throw new Error(errorMessage);
71+
} else {
72+
throw new Error(`Failed to start gateway: ${errorMessage}`);
73+
}
6874
}
6975
}
7076

@@ -86,9 +92,9 @@ export class ServerStartService {
8692
const stdoutLog = path.join(logDir, 'stdout.log');
8793
const stderrLog = path.join(logDir, 'stderr.log');
8894

89-
// Open log files
90-
const stdout = fs.openSync(stdoutLog, 'a');
91-
const stderr = fs.openSync(stderrLog, 'a');
95+
// Clear and open log files (don't append to old logs)
96+
const stdout = fs.openSync(stdoutLog, 'w');
97+
const stderr = fs.openSync(stderrLog, 'w');
9298

9399
// Spawn daemon process with logging
94100
const child = spawn(process.execPath, [scriptPath, ...args], {
@@ -149,7 +155,9 @@ export class ServerStartService {
149155
if (fs.existsSync(stderrLog)) {
150156
const stderrContent = fs.readFileSync(stderrLog, 'utf8').trim();
151157
if (stderrContent) {
152-
errorDetails += `\nStderr: ${stderrContent.split('\n').slice(-5).join('\n')}`;
158+
// The child process writes clean error messages to stderr
159+
// Just use the error directly without "Stderr:" prefix
160+
errorDetails = stderrContent;
153161
}
154162
}
155163
if (startupError) {

0 commit comments

Comments
 (0)