lemonade-interactive-loader 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/lemonade-interactive-loader-1.0.4.tgz +0 -0
- package/package.json +2 -1
- package/src/README.md +1 -1
- package/src/cli/menu.js +20 -3
- package/src/cli/prompts.js +1 -0
- package/src/cli/setup-wizard.js +35 -8
- package/src/config/constants.js +17 -4
- package/src/services/download.js +1 -1
- package/src/services/server.js +113 -33
package/README.md
CHANGED
|
@@ -83,16 +83,17 @@ The main menu adapts based on whether you have a configuration saved:
|
|
|
83
83
|
|
|
84
84
|
### The Setup Wizard
|
|
85
85
|
|
|
86
|
-
Just answer
|
|
86
|
+
Just answer 9 simple questions:
|
|
87
87
|
|
|
88
88
|
1. **Network access?** Should the server be accessible from other devices?
|
|
89
89
|
2. **Port number?** Which port should it run on? (default: 8080)
|
|
90
90
|
3. **Logging level?** Choose from info, debug, warning, or error
|
|
91
|
-
4. **
|
|
92
|
-
5. **
|
|
93
|
-
6. **
|
|
94
|
-
7. **Custom
|
|
95
|
-
8. **
|
|
91
|
+
4. **Context window size?** Choose from 4K, 8K, 16K, 32K, 64K, 128K, or 256K tokens (default: 4K)
|
|
92
|
+
5. **Model directory?** Point to existing models (like LM Studio) if needed
|
|
93
|
+
6. **Interface type?** System tray or headless mode
|
|
94
|
+
7. **Custom arguments?** Any additional llama.cpp parameters?
|
|
95
|
+
8. **Custom build?** Use a specific llama.cpp build from GitHub?
|
|
96
|
+
9. **Backend?** Choose auto, vulkan, rocm, or cpu
|
|
96
97
|
|
|
97
98
|
## ✨ Key Features
|
|
98
99
|
|
|
@@ -169,7 +170,7 @@ Point Lemonade Server to your existing model directory (like LM Studio's):
|
|
|
169
170
|
|
|
170
171
|
Configuration is automatically saved and loaded:
|
|
171
172
|
|
|
172
|
-
- **Location**: `~/.lemonade-launcher/config.json` (Linux/macOS) or `%USERPROFILE%\.lemonade-launcher\config.json` (Windows)
|
|
173
|
+
- **Location**: `~/.lemonade-interactive-launcher/config.json` (Linux/macOS) or `%USERPROFILE%\.lemonade-interactive-launcher\config.json` (Windows)
|
|
173
174
|
- **Format**: JSON
|
|
174
175
|
- **Auto-saved**: After every setup or edit
|
|
175
176
|
|
|
@@ -250,7 +251,7 @@ Run Command Prompt or PowerShell as Administrator.
|
|
|
250
251
|
|
|
251
252
|
#### Build download fails
|
|
252
253
|
- Check your internet connection
|
|
253
|
-
- Ensure you have write permissions to `~/.lemonade-launcher/`
|
|
254
|
+
- Ensure you have write permissions to `~/.lemonade-interactive-launcher/`
|
|
254
255
|
- Try downloading the asset manually from GitHub
|
|
255
256
|
|
|
256
257
|
#### Server won't start
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemonade-interactive-loader",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Interactive CLI tool to launch Lemonade Server with custom arguments and download llama.cpp releases - Cross-platform (Windows/Linux)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"adm-zip": "^0.5.16",
|
|
36
36
|
"inquirer": "^8.2.6",
|
|
37
37
|
"tar": "^7.5.9",
|
|
38
|
+
"tree-kill": "^1.2.2",
|
|
38
39
|
"unzipper": "^0.12.3"
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/README.md
CHANGED
|
@@ -43,7 +43,7 @@ src/
|
|
|
43
43
|
### CLI (`cli/`)
|
|
44
44
|
- **menu.js**: Main menu system and command routing
|
|
45
45
|
- **prompts.js**: Interactive prompts for release/asset selection
|
|
46
|
-
- **setup-wizard.js**:
|
|
46
|
+
- **setup-wizard.js**: 9-question setup wizard
|
|
47
47
|
|
|
48
48
|
## Architecture Principles
|
|
49
49
|
|
package/src/cli/menu.js
CHANGED
|
@@ -213,6 +213,7 @@ async function viewConfiguration() {
|
|
|
213
213
|
console.log(`Host: ${config.host}`);
|
|
214
214
|
console.log(`Port: ${config.port}`);
|
|
215
215
|
console.log(`Log Level: ${config.logLevel}`);
|
|
216
|
+
console.log(`Context Size: ${config.contextSize}`);
|
|
216
217
|
console.log(`Backend: ${config.backend}`);
|
|
217
218
|
console.log(`Model Directory: ${config.modelDir}`);
|
|
218
219
|
console.log(`Run Mode: ${config.runMode}`);
|
|
@@ -263,15 +264,20 @@ async function resetConfiguration() {
|
|
|
263
264
|
/**
|
|
264
265
|
* Handle main menu command
|
|
265
266
|
* @param {string} command - Selected command
|
|
267
|
+
* @returns {Promise<boolean>} True if should exit the app
|
|
266
268
|
*/
|
|
267
269
|
async function handleCommand(command) {
|
|
270
|
+
let shouldExit = false;
|
|
271
|
+
|
|
268
272
|
switch (command) {
|
|
269
273
|
case 'setup':
|
|
270
274
|
await runSetupWizard(false);
|
|
271
275
|
if (await askLaunchServer()) {
|
|
272
276
|
const config = loadConfig();
|
|
273
277
|
if (Object.keys(config).length > 0) {
|
|
278
|
+
setupShutdownHandlers();
|
|
274
279
|
await launchLemonadeServer(config);
|
|
280
|
+
shouldExit = true;
|
|
275
281
|
}
|
|
276
282
|
}
|
|
277
283
|
break;
|
|
@@ -281,7 +287,9 @@ async function handleCommand(command) {
|
|
|
281
287
|
if (await askLaunchServer()) {
|
|
282
288
|
const config = loadConfig();
|
|
283
289
|
if (Object.keys(config).length > 0) {
|
|
290
|
+
setupShutdownHandlers();
|
|
284
291
|
await launchLemonadeServer(config);
|
|
292
|
+
shouldExit = true;
|
|
285
293
|
}
|
|
286
294
|
}
|
|
287
295
|
break;
|
|
@@ -294,7 +302,7 @@ async function handleCommand(command) {
|
|
|
294
302
|
await resetConfiguration();
|
|
295
303
|
break;
|
|
296
304
|
|
|
297
|
-
|
|
305
|
+
|
|
298
306
|
|
|
299
307
|
case 'manage':
|
|
300
308
|
let manageAction;
|
|
@@ -319,13 +327,17 @@ async function handleCommand(command) {
|
|
|
319
327
|
const config = loadConfig();
|
|
320
328
|
if (Object.keys(config).length === 0) {
|
|
321
329
|
console.log('No configuration found. Please run "setup" first.');
|
|
322
|
-
return;
|
|
330
|
+
return false;
|
|
323
331
|
}
|
|
324
332
|
// Set up shutdown handlers to kill server on exit
|
|
325
333
|
setupShutdownHandlers();
|
|
326
334
|
await launchLemonadeServer(config);
|
|
335
|
+
// After server exits, exit the app completely
|
|
336
|
+
shouldExit = true;
|
|
327
337
|
break;
|
|
328
338
|
}
|
|
339
|
+
|
|
340
|
+
return shouldExit;
|
|
329
341
|
}
|
|
330
342
|
|
|
331
343
|
/**
|
|
@@ -336,7 +348,12 @@ async function runCLI() {
|
|
|
336
348
|
|
|
337
349
|
while (continueRunning) {
|
|
338
350
|
const command = await showMainMenu();
|
|
339
|
-
await handleCommand(command);
|
|
351
|
+
const shouldExit = await handleCommand(command);
|
|
352
|
+
|
|
353
|
+
// Exit the app if we just exited from the server
|
|
354
|
+
if (shouldExit) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
340
357
|
|
|
341
358
|
const { continueRunning: shouldContinue } = await inquirer.prompt([
|
|
342
359
|
{
|
package/src/cli/prompts.js
CHANGED
|
@@ -223,6 +223,7 @@ function displayConfigSummary(config) {
|
|
|
223
223
|
console.log(`Host: ${config.host}`);
|
|
224
224
|
console.log(`Port: ${config.port}`);
|
|
225
225
|
console.log(`Log Level: ${config.logLevel}`);
|
|
226
|
+
console.log(`Context Size: ${config.contextSize}`);
|
|
226
227
|
console.log(`Backend: ${config.backend}`);
|
|
227
228
|
console.log(`Model Directory: ${config.modelDir}`);
|
|
228
229
|
console.log(`Run Mode: ${config.runMode}`);
|
package/src/cli/setup-wizard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const inquirer = require('inquirer');
|
|
2
|
-
const { DEFAULTS, BACKEND_TYPES, LOG_LEVELS, RUN_MODES, HOSTS } = require('../config/constants');
|
|
2
|
+
const { DEFAULTS, BACKEND_TYPES, LOG_LEVELS, RUN_MODES, HOSTS, CONTEXT_SIZES } = require('../config/constants');
|
|
3
3
|
const { loadConfig, saveConfig } = require('../config');
|
|
4
4
|
const { selectLlamaCppRelease, selectAsset, selectInstalledAssetPrompt, displayConfigSummary } = require('./prompts');
|
|
5
5
|
const { downloadAndExtractLlamaCpp } = require('../services/asset-manager');
|
|
@@ -54,16 +54,42 @@ async function runSetupWizard(isEdit = false) {
|
|
|
54
54
|
}
|
|
55
55
|
]);
|
|
56
56
|
|
|
57
|
-
// Q4:
|
|
57
|
+
// Q4: Context window size
|
|
58
|
+
const savedContextSize = existingConfig.contextSize || DEFAULTS.CONTEXT_SIZE;
|
|
59
|
+
const contextSizeChoices = [
|
|
60
|
+
{ name: '4K (4096 tokens) - Default', value: CONTEXT_SIZES['4K'] },
|
|
61
|
+
{ name: '8K (8192 tokens)', value: CONTEXT_SIZES['8K'] },
|
|
62
|
+
{ name: '16K (16384 tokens)', value: CONTEXT_SIZES['16K'] },
|
|
63
|
+
{ name: '32K (32768 tokens)', value: CONTEXT_SIZES['32K'] },
|
|
64
|
+
{ name: '64K (65536 tokens)', value: CONTEXT_SIZES['64K'] },
|
|
65
|
+
{ name: '128K (131072 tokens)', value: CONTEXT_SIZES['128K'] },
|
|
66
|
+
{ name: '256K (262144 tokens)', value: CONTEXT_SIZES['256K'] }
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Find the index of the saved/default context size
|
|
70
|
+
const defaultContextSizeIndex = contextSizeChoices.findIndex(choice => choice.value === savedContextSize);
|
|
71
|
+
const defaultContextSize = defaultContextSizeIndex >= 0 ? defaultContextSizeIndex : 0;
|
|
72
|
+
|
|
73
|
+
const { contextSize } = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'contextSize',
|
|
77
|
+
message: 'How big should the context window be?',
|
|
78
|
+
choices: contextSizeChoices,
|
|
79
|
+
default: defaultContextSize
|
|
80
|
+
}
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
// Q5: Custom model directory
|
|
58
84
|
const existingModelDir = existingConfig.modelDir;
|
|
59
|
-
const hasExistingModelDir = existingModelDir
|
|
85
|
+
const hasExistingModelDir = existingModelDir !== undefined;
|
|
60
86
|
|
|
61
87
|
const { useCustomModelDir } = await inquirer.prompt([
|
|
62
88
|
{
|
|
63
89
|
type: 'confirm',
|
|
64
90
|
name: 'useCustomModelDir',
|
|
65
91
|
message: 'Is there another model directory to use? (example: LM Studio)',
|
|
66
|
-
default:
|
|
92
|
+
default: hasExistingModelDir
|
|
67
93
|
}
|
|
68
94
|
]);
|
|
69
95
|
|
|
@@ -83,7 +109,7 @@ async function runSetupWizard(isEdit = false) {
|
|
|
83
109
|
finalModelDir = DEFAULTS.MODEL_DIR;
|
|
84
110
|
}
|
|
85
111
|
|
|
86
|
-
//
|
|
112
|
+
// Q6: System tray vs headless
|
|
87
113
|
const { runMode } = await inquirer.prompt([
|
|
88
114
|
{
|
|
89
115
|
type: 'list',
|
|
@@ -97,7 +123,7 @@ async function runSetupWizard(isEdit = false) {
|
|
|
97
123
|
}
|
|
98
124
|
]);
|
|
99
125
|
|
|
100
|
-
//
|
|
126
|
+
// Q7: Custom llama.cpp args
|
|
101
127
|
const existingLlamacppArgs = existingConfig.llamacppArgs || '';
|
|
102
128
|
const hasExistingArgs = existingLlamacppArgs.length > 0;
|
|
103
129
|
|
|
@@ -117,7 +143,7 @@ async function runSetupWizard(isEdit = false) {
|
|
|
117
143
|
{
|
|
118
144
|
type: 'input',
|
|
119
145
|
name: 'llamacppArgs',
|
|
120
|
-
message: 'Enter llama.cpp arguments (comma-separated, e.g., --
|
|
146
|
+
message: 'Enter llama.cpp arguments (comma-separated, e.g., --no-mmap,--batch-size 512):',
|
|
121
147
|
default: existingLlamacppArgs
|
|
122
148
|
}
|
|
123
149
|
]);
|
|
@@ -126,7 +152,7 @@ async function runSetupWizard(isEdit = false) {
|
|
|
126
152
|
finalLlamacppArgs = '';
|
|
127
153
|
}
|
|
128
154
|
|
|
129
|
-
//
|
|
155
|
+
// Q8: Custom llama.cpp build
|
|
130
156
|
const existingCustomPath = existingConfig.customLlamacppPath || '';
|
|
131
157
|
const hasExistingBuild = existingCustomPath.length > 0;
|
|
132
158
|
|
|
@@ -209,6 +235,7 @@ async function runSetupWizard(isEdit = false) {
|
|
|
209
235
|
host,
|
|
210
236
|
port: parseInt(port),
|
|
211
237
|
logLevel,
|
|
238
|
+
contextSize,
|
|
212
239
|
backend,
|
|
213
240
|
modelDir: finalModelDir,
|
|
214
241
|
runMode,
|
package/src/config/constants.js
CHANGED
|
@@ -2,14 +2,14 @@ const path = require('path');
|
|
|
2
2
|
const os = require('os');
|
|
3
3
|
|
|
4
4
|
// Configuration directories
|
|
5
|
-
const USER_CONFIG_DIR = path.join(os.homedir(), '.lemonade-launcher');
|
|
5
|
+
const USER_CONFIG_DIR = path.join(os.homedir(), '.lemonade-interactive-launcher');
|
|
6
6
|
const USER_CONFIG_FILE = path.join(USER_CONFIG_DIR, 'config.json');
|
|
7
|
-
const DEFAULT_LLAMACPP_INSTALL_DIR = path.join(os.homedir(), '.lemonade-launcher', 'llama-cpp');
|
|
7
|
+
const DEFAULT_LLAMACPP_INSTALL_DIR = path.join(os.homedir(), '.lemonade-interactive-launcher', 'llama-cpp');
|
|
8
8
|
|
|
9
9
|
// GitHub API
|
|
10
10
|
const GITHUB_RELEASES_URL = 'https://api.github.com/repos/ggml-org/llama.cpp/releases';
|
|
11
11
|
const GITHUB_API_HEADERS = {
|
|
12
|
-
'User-Agent': 'lemonade-launcher',
|
|
12
|
+
'User-Agent': 'lemonade-interactive-launcher',
|
|
13
13
|
'Accept': 'application/vnd.github.v3+json'
|
|
14
14
|
};
|
|
15
15
|
|
|
@@ -57,6 +57,17 @@ const HOSTS = {
|
|
|
57
57
|
ALL_INTERFACES: '0.0.0.0'
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
// Context window sizes (in tokens)
|
|
61
|
+
const CONTEXT_SIZES = {
|
|
62
|
+
'4K': 4096,
|
|
63
|
+
'8K': 8192,
|
|
64
|
+
'16K': 16384,
|
|
65
|
+
'32K': 32768,
|
|
66
|
+
'64K': 65536,
|
|
67
|
+
'128K': 131072,
|
|
68
|
+
'256K': 262144
|
|
69
|
+
};
|
|
70
|
+
|
|
60
71
|
// Default values
|
|
61
72
|
const DEFAULTS = {
|
|
62
73
|
PORT: 8080,
|
|
@@ -65,7 +76,8 @@ const DEFAULTS = {
|
|
|
65
76
|
EXPOSE_TO_NETWORK: false,
|
|
66
77
|
RUN_MODE: RUN_MODES.SYSTEM_TRAY,
|
|
67
78
|
MODEL_DIR: 'None',
|
|
68
|
-
BACKEND: BACKEND_TYPES.AUTO
|
|
79
|
+
BACKEND: BACKEND_TYPES.AUTO,
|
|
80
|
+
CONTEXT_SIZE: CONTEXT_SIZES['4K']
|
|
69
81
|
};
|
|
70
82
|
|
|
71
83
|
module.exports = {
|
|
@@ -79,5 +91,6 @@ module.exports = {
|
|
|
79
91
|
LOG_LEVELS,
|
|
80
92
|
RUN_MODES,
|
|
81
93
|
HOSTS,
|
|
94
|
+
CONTEXT_SIZES,
|
|
82
95
|
DEFAULTS
|
|
83
96
|
};
|
package/src/services/download.js
CHANGED
|
@@ -20,7 +20,7 @@ function downloadFile(url, outputPath) {
|
|
|
20
20
|
|
|
21
21
|
const file = fs.createWriteStream(outputPath);
|
|
22
22
|
|
|
23
|
-
const req = protocol.get(url, { headers: { 'User-Agent': 'lemonade-launcher' } }, (res) => {
|
|
23
|
+
const req = protocol.get(url, { headers: { 'User-Agent': 'lemonade-interactive-launcher' } }, (res) => {
|
|
24
24
|
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
25
25
|
downloadFile(res.headers.location, outputPath)
|
|
26
26
|
.then(resolve)
|
package/src/services/server.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const { execSync, spawn } = require('child_process');
|
|
3
|
+
const kill = require('tree-kill');
|
|
3
4
|
const { LEMONADE_SERVER_DEFAULT_PATH } = require('../config/constants');
|
|
4
5
|
const { findLlamaServer } = require('../utils/system');
|
|
5
6
|
const { getLlamaServerPath } = require('./asset-manager');
|
|
6
7
|
|
|
7
8
|
// Track the server process for graceful shutdown
|
|
8
9
|
let serverProcess = null;
|
|
10
|
+
let isShuttingDown = false;
|
|
11
|
+
let hasExited = false;
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Build server command arguments
|
|
@@ -13,7 +16,7 @@ let serverProcess = null;
|
|
|
13
16
|
* @returns {Array} Array of command arguments
|
|
14
17
|
*/
|
|
15
18
|
function buildServerArgs(config) {
|
|
16
|
-
const { host, port, logLevel, modelDir, llamacppArgs } = config;
|
|
19
|
+
const { host, port, logLevel, modelDir, llamacppArgs, contextSize } = config;
|
|
17
20
|
const args = [
|
|
18
21
|
'serve',
|
|
19
22
|
'--log-level', logLevel || 'info',
|
|
@@ -21,6 +24,10 @@ function buildServerArgs(config) {
|
|
|
21
24
|
'--port', port.toString()
|
|
22
25
|
];
|
|
23
26
|
|
|
27
|
+
if (contextSize) {
|
|
28
|
+
args.push('--ctx-size', contextSize.toString());
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
if (modelDir && modelDir !== 'None') {
|
|
25
32
|
args.push('--extra-models-dir', modelDir);
|
|
26
33
|
}
|
|
@@ -76,6 +83,7 @@ async function launchLemonadeServer(config) {
|
|
|
76
83
|
host,
|
|
77
84
|
port,
|
|
78
85
|
logLevel,
|
|
86
|
+
contextSize,
|
|
79
87
|
modelDir,
|
|
80
88
|
llamacppArgs,
|
|
81
89
|
runMode,
|
|
@@ -89,6 +97,7 @@ async function launchLemonadeServer(config) {
|
|
|
89
97
|
console.log(`Host: ${host}`);
|
|
90
98
|
console.log(`Port: ${port}`);
|
|
91
99
|
console.log(`Log Level: ${logLevel}`);
|
|
100
|
+
console.log(`Context Size: ${contextSize || 'default'}`);
|
|
92
101
|
console.log(`Backend: ${backend || 'auto'}`);
|
|
93
102
|
console.log(`Model Directory: ${modelDir || 'default'}`);
|
|
94
103
|
console.log(`Run Mode: ${runMode || 'headless'}`);
|
|
@@ -157,25 +166,38 @@ async function launchLemonadeServer(config) {
|
|
|
157
166
|
console.log('\nStarting server...\n');
|
|
158
167
|
|
|
159
168
|
try {
|
|
169
|
+
// Build the command for spawning
|
|
170
|
+
const commandStr = formatCommand(serverPath, args, {});
|
|
171
|
+
|
|
160
172
|
// Parse the command into executable and arguments
|
|
161
|
-
const parts =
|
|
173
|
+
const parts = commandStr.trim().split(/\s+/);
|
|
162
174
|
const executable = parts[0];
|
|
163
175
|
const execArgs = parts.slice(1);
|
|
164
176
|
|
|
177
|
+
console.log(`Spawning: ${executable} ${execArgs.join(' ')}`);
|
|
178
|
+
|
|
165
179
|
// Spawn the server process
|
|
166
180
|
serverProcess = spawn(executable, execArgs, {
|
|
167
181
|
stdio: 'inherit',
|
|
168
182
|
env: process.env
|
|
169
183
|
});
|
|
170
184
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
// Wait for the process to exit (blocking)
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
serverProcess.on('exit', (code, signal) => {
|
|
188
|
+
// Only log if shutdown wasn't initiated by us
|
|
189
|
+
if (!hasExited) {
|
|
190
|
+
console.log(`\nServer exited with status ${code || 'None'} and signal ${signal || 'None'}`);
|
|
191
|
+
}
|
|
192
|
+
serverProcess = null;
|
|
193
|
+
resolve(code);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
serverProcess.on('error', (err) => {
|
|
197
|
+
console.error(`Server process error: ${err.message}`);
|
|
198
|
+
serverProcess = null;
|
|
199
|
+
reject(err);
|
|
200
|
+
});
|
|
179
201
|
});
|
|
180
202
|
|
|
181
203
|
} catch (error) {
|
|
@@ -187,37 +209,95 @@ async function launchLemonadeServer(config) {
|
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
/**
|
|
190
|
-
* Gracefully shutdown the lemonade server
|
|
212
|
+
* Gracefully shutdown the lemonade server and all child processes
|
|
191
213
|
*/
|
|
192
214
|
function shutdownLemonadeServer() {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
console.error(`Error shutting down server: ${err.message}`);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
// Send SIGTERM signal
|
|
206
|
-
serverProcess.kill('SIGTERM');
|
|
207
|
-
|
|
208
|
-
// Force kill after 5 seconds if still running
|
|
209
|
-
setTimeout(() => {
|
|
210
|
-
if (serverProcess && !serverProcess.killed) {
|
|
211
|
-
console.log('Force killing server...');
|
|
212
|
-
serverProcess.kill('SIGKILL');
|
|
213
|
-
}
|
|
214
|
-
}, 5000);
|
|
215
|
+
// Prevent duplicate shutdown calls
|
|
216
|
+
if (isShuttingDown) {
|
|
217
|
+
console.log('Shutdown already in progress...');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (!serverProcess || !serverProcess.pid) {
|
|
222
|
+
console.log('No server process to shut down.');
|
|
223
|
+
return;
|
|
215
224
|
}
|
|
225
|
+
|
|
226
|
+
isShuttingDown = true;
|
|
227
|
+
hasExited = false;
|
|
228
|
+
console.log('\n\nShutting down Lemonade Server and child processes...');
|
|
229
|
+
|
|
230
|
+
// Check if process is still running
|
|
231
|
+
try {
|
|
232
|
+
// Sending signal 0 checks if process exists without actually sending a signal
|
|
233
|
+
process.kill(serverProcess.pid, 0);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
// Process is already dead
|
|
236
|
+
console.log('Server process is already terminated.');
|
|
237
|
+
serverProcess = null;
|
|
238
|
+
isShuttingDown = false;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Remove existing event listeners to prevent duplicates
|
|
243
|
+
serverProcess.removeAllListeners('exit');
|
|
244
|
+
serverProcess.removeAllListeners('error');
|
|
245
|
+
|
|
246
|
+
// Try graceful shutdown first (SIGINT)
|
|
247
|
+
serverProcess.on('exit', (code, signal) => {
|
|
248
|
+
hasExited = true;
|
|
249
|
+
console.log(`Server exited with status ${code || 'None'} and signal ${signal || 'None'}`);
|
|
250
|
+
console.log('Server shut down successfully.');
|
|
251
|
+
serverProcess = null;
|
|
252
|
+
isShuttingDown = false;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
serverProcess.on('error', (err) => {
|
|
256
|
+
console.error(`Error shutting down server: ${err.message}`);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Use tree-kill to terminate the process and all its children
|
|
260
|
+
kill(serverProcess.pid, 'SIGINT', (err) => {
|
|
261
|
+
if (err && !hasExited) {
|
|
262
|
+
// Only log error if process hasn't already exited naturally
|
|
263
|
+
if (err.code !== 'ESRCH') {
|
|
264
|
+
console.log(`Note: Could not kill process tree: ${err.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Force kill with SIGKILL after 3 seconds if still running
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
// Don't force kill if already exited
|
|
272
|
+
if (hasExited || !isShuttingDown) return;
|
|
273
|
+
|
|
274
|
+
// Check again if process is still running before force kill
|
|
275
|
+
try {
|
|
276
|
+
process.kill(serverProcess.pid, 0);
|
|
277
|
+
// Process still exists, force kill it
|
|
278
|
+
kill(serverProcess.pid, 'SIGKILL', (err) => {
|
|
279
|
+
if (err && !hasExited) {
|
|
280
|
+
// Only log error if process hasn't already exited naturally
|
|
281
|
+
if (err.code !== 'ESRCH') {
|
|
282
|
+
console.log(`Note: Could not force kill process: ${err.message}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
} catch (err) {
|
|
287
|
+
// Process has already exited
|
|
288
|
+
console.log('Process has already exited.');
|
|
289
|
+
}
|
|
290
|
+
}, 3000);
|
|
216
291
|
}
|
|
217
292
|
|
|
218
293
|
// Set up signal handlers for graceful shutdown
|
|
219
294
|
function setupShutdownHandlers() {
|
|
220
295
|
const shutdown = (signal) => {
|
|
296
|
+
if (isShuttingDown) {
|
|
297
|
+
console.log(`Received ${signal}, but shutdown already in progress...`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
221
301
|
console.log(`\n\nReceived ${signal}. Shutting down...`);
|
|
222
302
|
shutdownLemonadeServer();
|
|
223
303
|
setTimeout(() => process.exit(0), 2000);
|