@metric-im/administrate 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.
- package/README.md +26 -0
- package/config.mjs +1 -1
- package/multisite.mjs +66 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,2 +1,28 @@
|
|
|
1
1
|
# administrate
|
|
2
2
|
Tools for site administration
|
|
3
|
+
|
|
4
|
+
## Certify
|
|
5
|
+
Certify will detect the hostname of incoming ssl requests and present the certs. If the domain name does not have a valid cert it requests one from letsencrypt and saves the keys to $HOME/.metric-im/
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import https from "https";
|
|
9
|
+
import { Certify } from '@metric-im/administrate';
|
|
10
|
+
const app = express();
|
|
11
|
+
const certify = await Certify.attach(app,{contactEmail:'me@there.com'});
|
|
12
|
+
// certify.SNI return {key: xxx, cert: ...} for the current domain and/or triggers a request for a cert
|
|
13
|
+
const https_server = https.createServer({...certify.SNI},app);
|
|
14
|
+
```
|
|
15
|
+
>NOTE: Set the environment variable PROFILE=DEV to instruct acme to fetch test circuits. Use this when testing or you will be throttled.
|
|
16
|
+
|
|
17
|
+
## Multisite
|
|
18
|
+
Multisite acts as a proxy service. It listens on port 80 (4080), 443 (4443) for all web traffic and routes to the designated service. A service is identified by domain name. multisite will look for a service matching the incoming domain name in the *sites* folder. It spawns the app found with npm start and assigns it an http port. All subsquent traffic for that domain are routed to this process.
|
|
19
|
+
|
|
20
|
+
Multisite can also launch a new instance of the current app with the sanitized domain name as the first argument. In this way the same code can run multiple named apps in separate silos.
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
const multiSite = MultiSite.attach(app);
|
|
24
|
+
```
|
|
25
|
+
See the Roots project [Harness](https;//github.com/rootz-global/harness). This is a simple host for multisite. It expects symlinks to all the apps the server responds to by domain name.
|
|
26
|
+
|
|
27
|
+
## Syncrhonize
|
|
28
|
+
Provides a web hook to github for manaing synchronization with a code branch.
|
package/config.mjs
CHANGED
|
@@ -56,7 +56,7 @@ export class DomainConfig extends Config {
|
|
|
56
56
|
this.domainConfigFile = join(this.domainConfigDir, 'config.ini');
|
|
57
57
|
if (!fs.existsSync(this.domainConfigDir)) {
|
|
58
58
|
fs.mkdirSync(this.domainConfigDir);
|
|
59
|
-
fs.writeFileSync(this.domainConfigFile
|
|
59
|
+
fs.writeFileSync(this.domainConfigFile,`name=${domain}`);
|
|
60
60
|
}
|
|
61
61
|
this.load();
|
|
62
62
|
}
|
package/multisite.mjs
CHANGED
|
@@ -18,6 +18,7 @@ export class MultiSite {
|
|
|
18
18
|
this._spawnPort = (parseInt(this.options.spawnPort||0) || 53874);
|
|
19
19
|
this.usedPorts = new Set();
|
|
20
20
|
this.healthCheckIntervals = new Map();
|
|
21
|
+
this.isShuttingDown = false;
|
|
21
22
|
this.setupCleanup();
|
|
22
23
|
}
|
|
23
24
|
get spawnPort() {
|
|
@@ -27,13 +28,58 @@ export class MultiSite {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
setupCleanup() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
// Set up centralized signal handlers that host applications can use
|
|
32
|
+
this.gracefulShutdown = this.gracefulShutdown.bind(this);
|
|
33
|
+
|
|
34
|
+
// Only handle the 'exit' event for emergency cleanup
|
|
35
|
+
process.on('exit', () => {
|
|
36
|
+
// Synchronous cleanup only - no async operations allowed in 'exit'
|
|
37
|
+
console.log('Emergency cleanup on exit...');
|
|
38
|
+
Object.values(this.sites).forEach((site) => {
|
|
39
|
+
if (site.proc && !site.proc.killed) {
|
|
40
|
+
site.proc.kill('SIGKILL');
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async gracefulShutdown(signal) {
|
|
47
|
+
if (this.isShuttingDown) {
|
|
48
|
+
console.log(`Received ${signal} again, forcing exit...`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.isShuttingDown = true;
|
|
53
|
+
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
console.log("Cleaning up child processes...");
|
|
57
|
+
await this.cleanup();
|
|
58
|
+
|
|
59
|
+
console.log("Graceful shutdown complete");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("Error during shutdown:", error);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setupSignalHandlers() {
|
|
68
|
+
// Method host applications can call to set up proper signal handling
|
|
69
|
+
process.on('SIGINT', () => this.gracefulShutdown('SIGINT'));
|
|
70
|
+
process.on('SIGTERM', () => this.gracefulShutdown('SIGTERM'));
|
|
71
|
+
console.log('MultiSite signal handlers installed');
|
|
33
72
|
}
|
|
34
73
|
|
|
35
74
|
async cleanup() {
|
|
36
75
|
console.log('Cleaning up spawned processes...');
|
|
76
|
+
|
|
77
|
+
// Set a global timeout for the entire cleanup process
|
|
78
|
+
const cleanupTimeout = setTimeout(() => {
|
|
79
|
+
console.log('Cleanup taking too long, forcing exit...');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}, 15000); // 15 second total timeout
|
|
82
|
+
|
|
37
83
|
const promises = Object.values(this.sites).map(async (site) => {
|
|
38
84
|
if (site.proc && !site.proc.killed) {
|
|
39
85
|
return new Promise((resolve) => {
|
|
@@ -43,7 +89,7 @@ export class MultiSite {
|
|
|
43
89
|
site.proc.kill('SIGKILL');
|
|
44
90
|
}
|
|
45
91
|
resolve();
|
|
46
|
-
},
|
|
92
|
+
}, 5000); // Reduced to 5 second timeout per process
|
|
47
93
|
|
|
48
94
|
site.proc.kill('SIGTERM');
|
|
49
95
|
|
|
@@ -62,6 +108,7 @@ export class MultiSite {
|
|
|
62
108
|
this.healthCheckIntervals.forEach(interval => clearInterval(interval));
|
|
63
109
|
this.healthCheckIntervals.clear();
|
|
64
110
|
|
|
111
|
+
clearTimeout(cleanupTimeout);
|
|
65
112
|
console.log('All spawned processes cleaned up');
|
|
66
113
|
}
|
|
67
114
|
|
|
@@ -135,7 +182,7 @@ export class MultiSite {
|
|
|
135
182
|
// spawn declared sites
|
|
136
183
|
if (fs.existsSync(resolve('./sites'))) {
|
|
137
184
|
instance.sites = (fs.readdirSync(resolve('./sites'))).reduce((result,hostName)=>{
|
|
138
|
-
const domainName = Site.
|
|
185
|
+
const domainName = Site.WashName(hostName);
|
|
139
186
|
const options = {
|
|
140
187
|
cwd: resolve(`./sites/${hostName}`),
|
|
141
188
|
env: {PORT:instance.spawnPort,meta:instance.options}
|
|
@@ -151,7 +198,7 @@ export class MultiSite {
|
|
|
151
198
|
const router = express.Router();
|
|
152
199
|
|
|
153
200
|
router.all(/.*/, async (req, res) => {
|
|
154
|
-
const domain = Site.
|
|
201
|
+
const domain = Site.WashName(req.hostname);
|
|
155
202
|
const site = this.sites[domain];
|
|
156
203
|
if (site) {
|
|
157
204
|
let target = `http://127.0.0.1:${site.options.env.PORT}${req.url}`;
|
|
@@ -188,14 +235,14 @@ export class MultiSite {
|
|
|
188
235
|
timeout: 30000,
|
|
189
236
|
responseType: 'stream'
|
|
190
237
|
});
|
|
191
|
-
|
|
238
|
+
|
|
192
239
|
console.log(`${response.status} ${target} (binary stream)`);
|
|
193
|
-
|
|
240
|
+
|
|
194
241
|
// Set headers without the problematic ones
|
|
195
242
|
const cleanHeaders = {...response.headers};
|
|
196
243
|
delete cleanHeaders['transfer-encoding'];
|
|
197
244
|
delete cleanHeaders['content-encoding'];
|
|
198
|
-
|
|
245
|
+
|
|
199
246
|
res.status(response.status).set(cleanHeaders);
|
|
200
247
|
response.data.pipe(res);
|
|
201
248
|
} else {
|
|
@@ -210,13 +257,13 @@ export class MultiSite {
|
|
|
210
257
|
timeout: 30000,
|
|
211
258
|
responseType: 'text'
|
|
212
259
|
});
|
|
213
|
-
|
|
260
|
+
|
|
214
261
|
console.log(`${response.status} ${target}`);
|
|
215
|
-
|
|
262
|
+
|
|
216
263
|
const cleanHeaders = {...response.headers};
|
|
217
264
|
delete cleanHeaders['transfer-encoding'];
|
|
218
265
|
delete cleanHeaders['content-encoding'];
|
|
219
|
-
|
|
266
|
+
|
|
220
267
|
res.status(response.status).set(cleanHeaders).send(response.data);
|
|
221
268
|
}
|
|
222
269
|
} catch (error) {
|
|
@@ -319,9 +366,12 @@ export class Site {
|
|
|
319
366
|
console.error(`${this.name}: Exception while spawning:`, err);
|
|
320
367
|
this.proc = null;
|
|
321
368
|
}
|
|
322
|
-
|
|
323
|
-
static
|
|
324
|
-
|
|
325
|
-
|
|
369
|
+
}
|
|
370
|
+
static WashName(hostName="") {
|
|
371
|
+
if (!hostName || hostName.match(/^[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}$/)) return "";
|
|
372
|
+
else return hostName.toLowerCase();
|
|
373
|
+
}
|
|
374
|
+
static SafeName(hostName) {
|
|
375
|
+
return Site.WashName(hostName).replace(/[^a-z0-9-]+/g,'_');
|
|
326
376
|
}
|
|
327
377
|
}
|