@chenpu17/serve-here 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/bin/serve-here.js +255 -2
  2. package/package.json +1 -1
package/bin/serve-here.js CHANGED
@@ -4,12 +4,212 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const process = require('process');
7
+ const { spawn } = require('child_process');
7
8
 
8
9
  const { createStaticServer } = require('../src/server');
9
10
  const pkg = require('../package.json');
10
11
 
11
12
  const DEFAULT_PORT = 8080;
12
13
  const DEFAULT_HOST = '0.0.0.0';
14
+ const PID_DIR = path.join(os.homedir(), '.serve-here');
15
+ const LOG_DIR = path.join(os.homedir(), '.serve-here', 'logs');
16
+
17
+ function ensureDirectories() {
18
+ if (!fs.existsSync(PID_DIR)) {
19
+ fs.mkdirSync(PID_DIR, { recursive: true });
20
+ }
21
+ if (!fs.existsSync(LOG_DIR)) {
22
+ fs.mkdirSync(LOG_DIR, { recursive: true });
23
+ }
24
+ }
25
+
26
+ function getPidFile(port) {
27
+ return path.join(PID_DIR, `serve-here-${port}.pid`);
28
+ }
29
+
30
+ function getLogFile(port) {
31
+ return path.join(LOG_DIR, `serve-here-${port}.log`);
32
+ }
33
+
34
+ function readPidFile(port) {
35
+ const pidFile = getPidFile(port);
36
+ if (fs.existsSync(pidFile)) {
37
+ try {
38
+ const content = fs.readFileSync(pidFile, 'utf8').trim();
39
+ const lines = content.split('\n');
40
+ const pid = parseInt(lines[0], 10);
41
+ const rootDir = lines[1] || '';
42
+ return { pid, rootDir };
43
+ } catch (error) {
44
+ return null;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function writePidFile(port, pid, rootDir) {
51
+ const pidFile = getPidFile(port);
52
+ fs.writeFileSync(pidFile, `${pid}\n${rootDir}`);
53
+ }
54
+
55
+ function removePidFile(port) {
56
+ const pidFile = getPidFile(port);
57
+ if (fs.existsSync(pidFile)) {
58
+ fs.unlinkSync(pidFile);
59
+ }
60
+ }
61
+
62
+ function isProcessRunning(pid) {
63
+ try {
64
+ process.kill(pid, 0);
65
+ return true;
66
+ } catch (error) {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ function startDaemon(options) {
72
+ ensureDirectories();
73
+
74
+ const rootDir = path.resolve(options.directory || process.cwd());
75
+ const port = options.port || DEFAULT_PORT;
76
+ const host = options.host || DEFAULT_HOST;
77
+
78
+ // Check if already running
79
+ const pidInfo = readPidFile(port);
80
+ if (pidInfo && isProcessRunning(pidInfo.pid)) {
81
+ console.error(`Error: Server already running on port ${port} (PID: ${pidInfo.pid})`);
82
+ console.error(`Serving: ${pidInfo.rootDir}`);
83
+ process.exit(1);
84
+ }
85
+
86
+ // Validate directory
87
+ let stats;
88
+ try {
89
+ stats = fs.statSync(rootDir);
90
+ } catch (error) {
91
+ console.error(`Error: directory "${rootDir}" does not exist or is not accessible.`);
92
+ process.exit(1);
93
+ }
94
+ if (!stats.isDirectory()) {
95
+ console.error(`Error: path "${rootDir}" is not a directory.`);
96
+ process.exit(1);
97
+ }
98
+
99
+ const logFile = getLogFile(port);
100
+ const out = fs.openSync(logFile, 'a');
101
+ const err = fs.openSync(logFile, 'a');
102
+
103
+ const child = spawn(process.execPath, [__filename, '--daemon-child', '-d', rootDir, '-p', String(port), '-H', host], {
104
+ detached: true,
105
+ stdio: ['ignore', out, err],
106
+ env: process.env
107
+ });
108
+
109
+ child.unref();
110
+
111
+ writePidFile(port, child.pid, rootDir);
112
+
113
+ console.log(`Server started in background (PID: ${child.pid})`);
114
+ console.log(`Serving: ${rootDir}`);
115
+ console.log(`Listening on port: ${port}`);
116
+ console.log(`Log file: ${logFile}`);
117
+ console.log(`\nTo stop: serve-here --stop -p ${port}`);
118
+ }
119
+
120
+ function stopDaemon(port) {
121
+ const pidInfo = readPidFile(port);
122
+ if (!pidInfo) {
123
+ console.error(`No server running on port ${port}`);
124
+ process.exit(1);
125
+ }
126
+
127
+ if (!isProcessRunning(pidInfo.pid)) {
128
+ console.log(`Server (PID: ${pidInfo.pid}) is not running, cleaning up...`);
129
+ removePidFile(port);
130
+ process.exit(0);
131
+ }
132
+
133
+ try {
134
+ process.kill(pidInfo.pid, 'SIGTERM');
135
+ console.log(`Stopping server (PID: ${pidInfo.pid})...`);
136
+
137
+ // Wait for process to stop
138
+ let attempts = 0;
139
+ const checkInterval = setInterval(() => {
140
+ attempts++;
141
+ if (!isProcessRunning(pidInfo.pid)) {
142
+ clearInterval(checkInterval);
143
+ removePidFile(port);
144
+ console.log('Server stopped.');
145
+ process.exit(0);
146
+ } else if (attempts >= 10) {
147
+ clearInterval(checkInterval);
148
+ console.error('Server did not stop gracefully, force killing...');
149
+ try {
150
+ process.kill(pidInfo.pid, 'SIGKILL');
151
+ } catch (e) {}
152
+ removePidFile(port);
153
+ process.exit(0);
154
+ }
155
+ }, 500);
156
+ } catch (error) {
157
+ console.error(`Failed to stop server: ${error.message}`);
158
+ removePidFile(port);
159
+ process.exit(1);
160
+ }
161
+ }
162
+
163
+ function showStatus(port) {
164
+ if (port) {
165
+ const pidInfo = readPidFile(port);
166
+ if (!pidInfo) {
167
+ console.log(`No server running on port ${port}`);
168
+ return;
169
+ }
170
+
171
+ const running = isProcessRunning(pidInfo.pid);
172
+ console.log(`Port ${port}:`);
173
+ console.log(` PID: ${pidInfo.pid}`);
174
+ console.log(` Status: ${running ? 'running' : 'stopped'}`);
175
+ console.log(` Directory: ${pidInfo.rootDir}`);
176
+ console.log(` Log: ${getLogFile(port)}`);
177
+
178
+ if (!running) {
179
+ removePidFile(port);
180
+ }
181
+ } else {
182
+ // Show all
183
+ ensureDirectories();
184
+ const files = fs.readdirSync(PID_DIR).filter(f => f.endsWith('.pid'));
185
+
186
+ if (files.length === 0) {
187
+ console.log('No servers running.');
188
+ return;
189
+ }
190
+
191
+ console.log('Running servers:\n');
192
+ for (const file of files) {
193
+ const match = file.match(/serve-here-(\d+)\.pid/);
194
+ if (match) {
195
+ const p = parseInt(match[1], 10);
196
+ const pidInfo = readPidFile(p);
197
+ if (pidInfo) {
198
+ const running = isProcessRunning(pidInfo.pid);
199
+ console.log(`Port ${p}:`);
200
+ console.log(` PID: ${pidInfo.pid}`);
201
+ console.log(` Status: ${running ? 'running' : 'stopped'}`);
202
+ console.log(` Directory: ${pidInfo.rootDir}`);
203
+ console.log('');
204
+
205
+ if (!running) {
206
+ removePidFile(p);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
13
213
 
14
214
  function main() {
15
215
  try {
@@ -25,6 +225,22 @@ function main() {
25
225
  process.exit(0);
26
226
  }
27
227
 
228
+ // Handle daemon commands
229
+ if (options.stop) {
230
+ stopDaemon(options.port || DEFAULT_PORT);
231
+ return;
232
+ }
233
+
234
+ if (options.status) {
235
+ showStatus(options.port);
236
+ return;
237
+ }
238
+
239
+ if (options.daemon && !options.daemonChild) {
240
+ startDaemon(options);
241
+ return;
242
+ }
243
+
28
244
  const rootDir = path.resolve(options.directory || process.cwd());
29
245
 
30
246
  let stats;
@@ -56,10 +272,19 @@ function main() {
56
272
  ...formatListeningAddresses(host, port)
57
273
  ];
58
274
  console.log(messageLines.join('\n '));
275
+
276
+ // Write PID file when running as daemon child
277
+ if (options.daemonChild) {
278
+ ensureDirectories();
279
+ writePidFile(port, process.pid, rootDir);
280
+ }
59
281
  });
60
282
 
61
283
  const shutdown = () => {
62
284
  console.log('\nShutting down...');
285
+ if (options.daemonChild) {
286
+ removePidFile(port);
287
+ }
63
288
  server.close(() => process.exit(0));
64
289
  };
65
290
 
@@ -77,7 +302,11 @@ function parseArguments(args) {
77
302
  port: undefined,
78
303
  host: undefined,
79
304
  help: false,
80
- version: false
305
+ version: false,
306
+ daemon: false,
307
+ daemonChild: false,
308
+ stop: false,
309
+ status: false
81
310
  };
82
311
 
83
312
  for (let i = 0; i < args.length; i += 1) {
@@ -105,6 +334,19 @@ function parseArguments(args) {
105
334
  case '--host':
106
335
  options.host = requireValue(args, ++i, '--host');
107
336
  break;
337
+ case '-D':
338
+ case '--daemon':
339
+ options.daemon = true;
340
+ break;
341
+ case '--daemon-child':
342
+ options.daemonChild = true;
343
+ break;
344
+ case '--stop':
345
+ options.stop = true;
346
+ break;
347
+ case '--status':
348
+ options.status = true;
349
+ break;
108
350
  default:
109
351
  if (arg.startsWith('-')) {
110
352
  throw new Error(`Unknown option: ${arg}`);
@@ -167,8 +409,19 @@ Options:
167
409
  -d, --dir <path> Directory to serve (defaults to current working directory)
168
410
  -p, --port <number> Port to listen on (default: ${DEFAULT_PORT})
169
411
  -H, --host <address> Hostname or IP to bind (default: ${DEFAULT_HOST})
412
+ -D, --daemon Run as a background daemon (does not occupy terminal)
413
+ --stop Stop a running daemon (use with -p to specify port)
414
+ --status Show status of running daemon(s)
170
415
  -h, --help Show this help message
171
- -V, --version Show version`);
416
+ -V, --version Show version
417
+
418
+ Examples:
419
+ serve-here Start server in foreground on port 8080
420
+ serve-here -D Start server as daemon on port 8080
421
+ serve-here -D -p 3000 Start daemon on port 3000
422
+ serve-here --stop Stop daemon on port 8080
423
+ serve-here --stop -p 3000 Stop daemon on port 3000
424
+ serve-here --status Show all running daemons`);
172
425
  }
173
426
 
174
427
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chenpu17/serve-here",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A minimal CLI to serve static files from a directory over HTTP.",
5
5
  "bin": {
6
6
  "serve-here": "bin/serve-here.js"