bs9 1.0.0 ā 1.3.0
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 +105 -9
- package/bin/bs9 +91 -7
- package/dist/bs9-064xs9r9. +148 -0
- package/dist/bs9-0gqcrp5t. +144 -0
- package/dist/bs9-33vcpmb9. +181 -0
- package/dist/bs9-nv7nseny. +197 -0
- package/dist/bs9-r6b9zpw0. +156 -0
- package/dist/bs9.js +2 -0
- package/package.json +3 -4
- package/src/commands/deps.ts +295 -0
- package/src/commands/profile.ts +338 -0
- package/src/commands/restart.ts +28 -3
- package/src/commands/start.ts +201 -21
- package/src/commands/status.ts +154 -109
- package/src/commands/stop.ts +31 -3
- package/src/commands/update.ts +322 -0
- package/src/commands/web.ts +29 -3
- package/src/database/pool.ts +335 -0
- package/src/discovery/consul.ts +285 -0
- package/src/loadbalancer/manager.ts +481 -0
- package/src/macos/launchd.ts +402 -0
- package/src/monitoring/advanced.ts +341 -0
- package/src/platform/detect.ts +137 -0
- package/src/windows/service.ts +391 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
interface DepOptions {
|
|
7
|
+
format?: string;
|
|
8
|
+
output?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ServiceDependency {
|
|
12
|
+
name: string;
|
|
13
|
+
dependsOn: string[];
|
|
14
|
+
status: 'running' | 'stopped' | 'failed' | 'unknown';
|
|
15
|
+
health: 'healthy' | 'unhealthy' | 'unknown';
|
|
16
|
+
port?: number;
|
|
17
|
+
endpoints: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface DependencyGraph {
|
|
21
|
+
services: ServiceDependency[];
|
|
22
|
+
edges: Array<{
|
|
23
|
+
from: string;
|
|
24
|
+
to: string;
|
|
25
|
+
type: 'http' | 'database' | 'message' | 'custom';
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function depsCommand(options: DepOptions): Promise<void> {
|
|
30
|
+
console.log('š BS9 Service Dependency Visualization');
|
|
31
|
+
console.log('='.repeat(80));
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const graph = await buildDependencyGraph();
|
|
35
|
+
|
|
36
|
+
if (options.format === 'dot') {
|
|
37
|
+
const dotOutput = generateDotGraph(graph);
|
|
38
|
+
if (options.output) {
|
|
39
|
+
await Bun.write(options.output, dotOutput);
|
|
40
|
+
console.log(`ā
Dependency graph saved to: ${options.output}`);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(dotOutput);
|
|
43
|
+
}
|
|
44
|
+
} else if (options.format === 'json') {
|
|
45
|
+
const jsonOutput = JSON.stringify(graph, null, 2);
|
|
46
|
+
if (options.output) {
|
|
47
|
+
await Bun.write(options.output, jsonOutput);
|
|
48
|
+
console.log(`ā
Dependency graph saved to: ${options.output}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.log(jsonOutput);
|
|
51
|
+
}
|
|
52
|
+
} else {
|
|
53
|
+
displayDependencyGraph(graph);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`ā Failed to analyze dependencies: ${error}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function buildDependencyGraph(): Promise<DependencyGraph> {
|
|
63
|
+
const services: ServiceDependency[] = [];
|
|
64
|
+
const edges: Array<{from: string; to: string; type: 'http' | 'database' | 'message' | 'custom'}> = [];
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// Get all BS9 services
|
|
68
|
+
const listOutput = execSync("systemctl --user list-units --type=service --no-pager --no-legend", { encoding: "utf-8" });
|
|
69
|
+
const lines = listOutput.split("\n").filter(line => line.includes(".service"));
|
|
70
|
+
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (!line.trim()) continue;
|
|
73
|
+
|
|
74
|
+
const match = line.match(/^(?:\s*([ā\sā]))?\s*([^\s]+)\.service\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+(.+)$/);
|
|
75
|
+
if (!match) continue;
|
|
76
|
+
|
|
77
|
+
const [, statusIndicator, name, loaded, active, sub, description] = match;
|
|
78
|
+
|
|
79
|
+
if (!description.includes("Bun Service:") && !description.includes("BS9 Service:")) continue;
|
|
80
|
+
|
|
81
|
+
const service: ServiceDependency = {
|
|
82
|
+
name,
|
|
83
|
+
dependsOn: [],
|
|
84
|
+
status: getServiceStatus(active, sub),
|
|
85
|
+
health: 'unknown',
|
|
86
|
+
endpoints: [],
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Extract port from description
|
|
90
|
+
const portMatch = description.match(/port[=:]?\s*(\d+)/i);
|
|
91
|
+
if (portMatch) {
|
|
92
|
+
service.port = Number(portMatch[1]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Analyze service for dependencies
|
|
96
|
+
const deps = await analyzeServiceDependencies(name, service.port);
|
|
97
|
+
service.dependsOn = deps.dependsOn;
|
|
98
|
+
service.endpoints = deps.endpoints;
|
|
99
|
+
|
|
100
|
+
services.push(service);
|
|
101
|
+
|
|
102
|
+
// Add edges to graph
|
|
103
|
+
for (const dep of deps.dependsOn) {
|
|
104
|
+
edges.push({
|
|
105
|
+
from: name,
|
|
106
|
+
to: dep,
|
|
107
|
+
type: deps.type,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Error fetching services:', error);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { services, edges };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function analyzeServiceDependencies(serviceName: string, port?: number): Promise<{
|
|
120
|
+
dependsOn: string[];
|
|
121
|
+
endpoints: string[];
|
|
122
|
+
type: 'http' | 'database' | 'message' | 'custom';
|
|
123
|
+
}> {
|
|
124
|
+
const dependsOn: string[] = [];
|
|
125
|
+
const endpoints: string[] = [];
|
|
126
|
+
let type: 'http' | 'database' | 'message' | 'custom' = 'custom';
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Check if service has health endpoints
|
|
130
|
+
if (port) {
|
|
131
|
+
endpoints.push(`http://localhost:${port}/healthz`);
|
|
132
|
+
endpoints.push(`http://localhost:${port}/readyz`);
|
|
133
|
+
endpoints.push(`http://localhost:${port}/metrics`);
|
|
134
|
+
|
|
135
|
+
// Check for common dependency patterns
|
|
136
|
+
try {
|
|
137
|
+
const response = Bun.fetch(`http://localhost:${port}/dependencies`, { timeout: 1000 });
|
|
138
|
+
if (response.ok) {
|
|
139
|
+
const deps = await response.json();
|
|
140
|
+
if (Array.isArray(deps)) {
|
|
141
|
+
dependsOn.push(...deps);
|
|
142
|
+
type = 'http';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Service doesn't expose dependencies endpoint
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Analyze service files for dependency patterns
|
|
151
|
+
try {
|
|
152
|
+
const servicePath = `/home/xarhang/.config/systemd/user/${serviceName}.service`;
|
|
153
|
+
const serviceContent = Bun.file(servicePath).text();
|
|
154
|
+
|
|
155
|
+
// Look for environment variables that suggest dependencies
|
|
156
|
+
const envMatches = serviceContent.match(/Environment=([^\n]+)/g) || [];
|
|
157
|
+
for (const env of envMatches) {
|
|
158
|
+
if (env.includes('DATABASE_URL') || env.includes('DB_HOST')) {
|
|
159
|
+
dependsOn.push('database');
|
|
160
|
+
type = 'database';
|
|
161
|
+
}
|
|
162
|
+
if (env.includes('REDIS_URL') || env.includes('REDIS_HOST')) {
|
|
163
|
+
dependsOn.push('redis');
|
|
164
|
+
type = 'message';
|
|
165
|
+
}
|
|
166
|
+
if (env.includes('API_URL') || env.includes('SERVICE_URL')) {
|
|
167
|
+
const urlMatch = env.match(/https?:\/\/([^:\/]+)/);
|
|
168
|
+
if (urlMatch) {
|
|
169
|
+
dependsOn.push(urlMatch[1]);
|
|
170
|
+
type = 'http';
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Service file not accessible
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error(`Error analyzing dependencies for ${serviceName}:`, error);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { dependsOn, endpoints, type };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function getServiceStatus(active: string, sub: string): 'running' | 'stopped' | 'failed' | 'unknown' {
|
|
186
|
+
if (active === 'active' && sub === 'running') return 'running';
|
|
187
|
+
if (active === 'inactive') return 'stopped';
|
|
188
|
+
if (active === 'failed' || sub === 'failed') return 'failed';
|
|
189
|
+
return 'unknown';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function displayDependencyGraph(graph: DependencyGraph): void {
|
|
193
|
+
console.log('\nš Service Dependencies:');
|
|
194
|
+
console.log('-'.repeat(60));
|
|
195
|
+
|
|
196
|
+
for (const service of graph.services) {
|
|
197
|
+
const statusIcon = getStatusIcon(service.status);
|
|
198
|
+
const healthIcon = getHealthIcon(service.health);
|
|
199
|
+
|
|
200
|
+
console.log(`${statusIcon} ${service.name} ${healthIcon}`);
|
|
201
|
+
|
|
202
|
+
if (service.dependsOn.length > 0) {
|
|
203
|
+
console.log(` āā Depends on: ${service.dependsOn.join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (service.endpoints.length > 0) {
|
|
207
|
+
console.log(` āā Endpoints: ${service.endpoints.slice(0, 2).join(', ')}${service.endpoints.length > 2 ? '...' : ''}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
console.log('');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('\nš Dependency Relationships:');
|
|
214
|
+
console.log('-'.repeat(60));
|
|
215
|
+
|
|
216
|
+
if (graph.edges.length === 0) {
|
|
217
|
+
console.log('No dependencies found between services.');
|
|
218
|
+
} else {
|
|
219
|
+
for (const edge of graph.edges) {
|
|
220
|
+
const fromService = graph.services.find(s => s.name === edge.from);
|
|
221
|
+
const toService = graph.services.find(s => s.name === edge.to);
|
|
222
|
+
|
|
223
|
+
const fromIcon = fromService ? getStatusIcon(fromService.status) : 'ā';
|
|
224
|
+
const toIcon = toService ? getStatusIcon(toService.status) : 'ā';
|
|
225
|
+
|
|
226
|
+
console.log(`${fromIcon} ${edge.from} ā ${edge.to} ${toIcon} (${edge.type})`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log('\nš Summary:');
|
|
231
|
+
console.log(` Total Services: ${graph.services.length}`);
|
|
232
|
+
console.log(` Dependencies: ${graph.edges.length}`);
|
|
233
|
+
console.log(` Running: ${graph.services.filter(s => s.status === 'running').length}`);
|
|
234
|
+
console.log(` Failed: ${graph.services.filter(s => s.status === 'failed').length}`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function generateDotGraph(graph: DependencyGraph): string {
|
|
238
|
+
let dot = 'digraph BS9_Dependencies {\n';
|
|
239
|
+
dot += ' rankdir=LR;\n';
|
|
240
|
+
dot += ' node [shape=box, style=filled];\n\n';
|
|
241
|
+
|
|
242
|
+
// Add nodes
|
|
243
|
+
for (const service of graph.services) {
|
|
244
|
+
const color = getNodeColor(service.status);
|
|
245
|
+
const label = `${service.name}\\n${service.status}`;
|
|
246
|
+
dot += ` "${service.name}" [label="${label}", fillcolor="${color}"];\n`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
dot += '\n';
|
|
250
|
+
|
|
251
|
+
// Add edges
|
|
252
|
+
for (const edge of graph.edges) {
|
|
253
|
+
const color = getEdgeColor(edge.type);
|
|
254
|
+
dot += ` "${edge.from}" -> "${edge.to}" [color="${color}", label="${edge.type}"];\n`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
dot += '}\n';
|
|
258
|
+
|
|
259
|
+
return dot;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getStatusIcon(status: string): string {
|
|
263
|
+
switch (status) {
|
|
264
|
+
case 'running': return 'ā
';
|
|
265
|
+
case 'stopped': return 'āøļø';
|
|
266
|
+
case 'failed': return 'ā';
|
|
267
|
+
default: return 'ā';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getHealthIcon(health: string): string {
|
|
272
|
+
switch (health) {
|
|
273
|
+
case 'healthy': return 'š¢';
|
|
274
|
+
case 'unhealthy': return 'š“';
|
|
275
|
+
default: return 'āŖ';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getNodeColor(status: string): string {
|
|
280
|
+
switch (status) {
|
|
281
|
+
case 'running': return 'lightgreen';
|
|
282
|
+
case 'stopped': return 'lightgray';
|
|
283
|
+
case 'failed': return 'lightcoral';
|
|
284
|
+
default: return 'lightyellow';
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function getEdgeColor(type: string): string {
|
|
289
|
+
switch (type) {
|
|
290
|
+
case 'http': return 'blue';
|
|
291
|
+
case 'database': return 'green';
|
|
292
|
+
case 'message': return 'orange';
|
|
293
|
+
default: return 'gray';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
|
|
6
|
+
interface ProfileOptions {
|
|
7
|
+
duration?: string;
|
|
8
|
+
output?: string;
|
|
9
|
+
service?: string;
|
|
10
|
+
interval?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ProfileData {
|
|
14
|
+
timestamp: number;
|
|
15
|
+
cpu: number;
|
|
16
|
+
memory: number;
|
|
17
|
+
heapUsed: number;
|
|
18
|
+
heapTotal: number;
|
|
19
|
+
external: number;
|
|
20
|
+
eventLoopLag: number;
|
|
21
|
+
activeHandles: number;
|
|
22
|
+
activeRequests: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface PerformanceProfile {
|
|
26
|
+
serviceName: string;
|
|
27
|
+
duration: number;
|
|
28
|
+
interval: number;
|
|
29
|
+
samples: ProfileData[];
|
|
30
|
+
summary: {
|
|
31
|
+
avgCpu: number;
|
|
32
|
+
maxCpu: number;
|
|
33
|
+
avgMemory: number;
|
|
34
|
+
maxMemory: number;
|
|
35
|
+
avgHeapUsed: number;
|
|
36
|
+
maxHeapUsed: number;
|
|
37
|
+
avgEventLoopLag: number;
|
|
38
|
+
maxEventLoopLag: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function profileCommand(options: ProfileOptions): Promise<void> {
|
|
43
|
+
const duration = Number(options.duration) || 60; // Default 60 seconds
|
|
44
|
+
const interval = Number(options.interval) || 1000; // Default 1 second
|
|
45
|
+
const serviceName = options.service;
|
|
46
|
+
|
|
47
|
+
if (!serviceName) {
|
|
48
|
+
console.error('ā Service name is required. Use --service <name>');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(`š Performance Profiling for Service: ${serviceName}`);
|
|
53
|
+
console.log(`ā±ļø Duration: ${duration}s | Interval: ${interval}ms`);
|
|
54
|
+
console.log('='.repeat(80));
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Check if service is running
|
|
58
|
+
const serviceStatus = checkServiceStatus(serviceName);
|
|
59
|
+
if (serviceStatus !== 'active') {
|
|
60
|
+
console.error(`ā Service '${serviceName}' is not running (status: ${serviceStatus})`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Get service port for metrics
|
|
65
|
+
const port = getServicePort(serviceName);
|
|
66
|
+
if (!port) {
|
|
67
|
+
console.error(`ā Could not determine port for service '${serviceName}'`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`š Collecting metrics from http://localhost:${port}/metrics`);
|
|
72
|
+
console.log('Press Ctrl+C to stop profiling early\n');
|
|
73
|
+
|
|
74
|
+
const profile = await collectPerformanceData(serviceName, port, duration, interval);
|
|
75
|
+
|
|
76
|
+
// Display results
|
|
77
|
+
displayProfileResults(profile);
|
|
78
|
+
|
|
79
|
+
// Save to file if requested
|
|
80
|
+
if (options.output) {
|
|
81
|
+
await saveProfileData(profile, options.output);
|
|
82
|
+
console.log(`š¾ Profile data saved to: ${options.output}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error(`ā Failed to profile service: ${error}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function checkServiceStatus(serviceName: string): string {
|
|
92
|
+
try {
|
|
93
|
+
const output = execSync(`systemctl --user is-active ${serviceName}`, { encoding: "utf-8" }).trim();
|
|
94
|
+
return output;
|
|
95
|
+
} catch {
|
|
96
|
+
return 'unknown';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getServicePort(serviceName: string): number | null {
|
|
101
|
+
try {
|
|
102
|
+
const servicePath = `/home/xarhang/.config/systemd/user/${serviceName}.service`;
|
|
103
|
+
const serviceContent = execSync(`cat ${servicePath}`, { encoding: "utf-8" });
|
|
104
|
+
|
|
105
|
+
// Extract port from environment variables
|
|
106
|
+
const portMatch = serviceContent.match(/Environment=.*PORT=(\d+)/);
|
|
107
|
+
if (portMatch) {
|
|
108
|
+
return Number(portMatch[1]);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extract port from description
|
|
112
|
+
const descMatch = serviceContent.match(/port[=:]?\s*(\d+)/i);
|
|
113
|
+
if (descMatch) {
|
|
114
|
+
return Number(descMatch[1]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function collectPerformanceData(
|
|
124
|
+
serviceName: string,
|
|
125
|
+
port: number,
|
|
126
|
+
duration: number,
|
|
127
|
+
interval: number
|
|
128
|
+
): Promise<PerformanceProfile> {
|
|
129
|
+
const samples: ProfileData[] = [];
|
|
130
|
+
const startTime = Date.now();
|
|
131
|
+
const endTime = startTime + (duration * 1000);
|
|
132
|
+
|
|
133
|
+
while (Date.now() < endTime) {
|
|
134
|
+
try {
|
|
135
|
+
const sample = await collectSample(port);
|
|
136
|
+
samples.push(sample);
|
|
137
|
+
|
|
138
|
+
// Show progress
|
|
139
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
140
|
+
const progress = Math.round((elapsed / duration) * 100);
|
|
141
|
+
process.stdout.write(`\rā³ Progress: ${elapsed}s/${duration}s (${progress}%) | Samples: ${samples.length}`);
|
|
142
|
+
|
|
143
|
+
await setTimeout(interval);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error(`\nā ļø Failed to collect sample: ${error}`);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
console.log('\n'); // New line after progress
|
|
151
|
+
|
|
152
|
+
// Calculate summary
|
|
153
|
+
const summary = calculateSummary(samples);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
serviceName,
|
|
157
|
+
duration,
|
|
158
|
+
interval,
|
|
159
|
+
samples,
|
|
160
|
+
summary,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function collectSample(port: number): Promise<ProfileData> {
|
|
165
|
+
const response = await fetch(`http://localhost:${port}/metrics`);
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const metricsText = await response.text();
|
|
171
|
+
const metrics = parsePrometheusMetrics(metricsText);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
cpu: metrics.cpu || 0,
|
|
176
|
+
memory: metrics.memory || 0,
|
|
177
|
+
heapUsed: metrics.heapUsed || 0,
|
|
178
|
+
heapTotal: metrics.heapTotal || 0,
|
|
179
|
+
external: metrics.external || 0,
|
|
180
|
+
eventLoopLag: metrics.eventLoopLag || 0,
|
|
181
|
+
activeHandles: metrics.activeHandles || 0,
|
|
182
|
+
activeRequests: metrics.activeRequests || 0,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parsePrometheusMetrics(metricsText: string): Record<string, number> {
|
|
187
|
+
const metrics: Record<string, number> = {};
|
|
188
|
+
const lines = metricsText.split('\n');
|
|
189
|
+
|
|
190
|
+
for (const line of lines) {
|
|
191
|
+
if (line.startsWith('#') || !line.trim()) continue;
|
|
192
|
+
|
|
193
|
+
const match = line.match(/^(.+)\s+([\d.]+)$/);
|
|
194
|
+
if (!match) continue;
|
|
195
|
+
|
|
196
|
+
const [, name, value] = match;
|
|
197
|
+
|
|
198
|
+
// Map Prometheus metrics to our internal names
|
|
199
|
+
switch (name) {
|
|
200
|
+
case 'process_cpu_seconds_total':
|
|
201
|
+
metrics.cpu = parseFloat(value);
|
|
202
|
+
break;
|
|
203
|
+
case 'process_resident_memory_bytes':
|
|
204
|
+
metrics.memory = parseFloat(value);
|
|
205
|
+
break;
|
|
206
|
+
case 'nodejs_heap_size_used_bytes':
|
|
207
|
+
metrics.heapUsed = parseFloat(value);
|
|
208
|
+
break;
|
|
209
|
+
case 'nodejs_heap_size_total_bytes':
|
|
210
|
+
metrics.heapTotal = parseFloat(value);
|
|
211
|
+
break;
|
|
212
|
+
case 'nodejs_external_memory_bytes':
|
|
213
|
+
metrics.external = parseFloat(value);
|
|
214
|
+
break;
|
|
215
|
+
case 'nodejs_eventloop_lag_seconds':
|
|
216
|
+
metrics.eventLoopLag = parseFloat(value) * 1000; // Convert to ms
|
|
217
|
+
break;
|
|
218
|
+
case 'nodejs_active_handles_total':
|
|
219
|
+
metrics.activeHandles = parseFloat(value);
|
|
220
|
+
break;
|
|
221
|
+
case 'nodejs_active_requests_total':
|
|
222
|
+
metrics.activeRequests = parseFloat(value);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return metrics;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function calculateSummary(samples: ProfileData[]): PerformanceProfile['summary'] {
|
|
231
|
+
if (samples.length === 0) {
|
|
232
|
+
return {
|
|
233
|
+
avgCpu: 0, maxCpu: 0,
|
|
234
|
+
avgMemory: 0, maxMemory: 0,
|
|
235
|
+
avgHeapUsed: 0, maxHeapUsed: 0,
|
|
236
|
+
avgEventLoopLag: 0, maxEventLoopLag: 0,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const sum = samples.reduce((acc, sample) => ({
|
|
241
|
+
cpu: acc.cpu + sample.cpu,
|
|
242
|
+
memory: acc.memory + sample.memory,
|
|
243
|
+
heapUsed: acc.heapUsed + sample.heapUsed,
|
|
244
|
+
eventLoopLag: acc.eventLoopLag + sample.eventLoopLag,
|
|
245
|
+
}), { cpu: 0, memory: 0, heapUsed: 0, eventLoopLag: 0 });
|
|
246
|
+
|
|
247
|
+
const count = samples.length;
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
avgCpu: sum.cpu / count,
|
|
251
|
+
maxCpu: Math.max(...samples.map(s => s.cpu)),
|
|
252
|
+
avgMemory: sum.memory / count,
|
|
253
|
+
maxMemory: Math.max(...samples.map(s => s.memory)),
|
|
254
|
+
avgHeapUsed: sum.heapUsed / count,
|
|
255
|
+
maxHeapUsed: Math.max(...samples.map(s => s.heapUsed)),
|
|
256
|
+
avgEventLoopLag: sum.eventLoopLag / count,
|
|
257
|
+
maxEventLoopLag: Math.max(...samples.map(s => s.eventLoopLag)),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function displayProfileResults(profile: PerformanceProfile): void {
|
|
262
|
+
console.log('\nš Performance Profile Results');
|
|
263
|
+
console.log('='.repeat(80));
|
|
264
|
+
|
|
265
|
+
console.log(`\nš§ Service: ${profile.serviceName}`);
|
|
266
|
+
console.log(`ā±ļø Duration: ${profile.duration}s | Samples: ${profile.samples.length}`);
|
|
267
|
+
console.log(`š Interval: ${profile.interval}ms`);
|
|
268
|
+
|
|
269
|
+
console.log('\nš¾ Memory Usage:');
|
|
270
|
+
console.log(` Average: ${formatBytes(profile.summary.avgMemory)} | Peak: ${formatBytes(profile.summary.maxMemory)}`);
|
|
271
|
+
console.log(` Heap Avg: ${formatBytes(profile.summary.avgHeapUsed)} | Heap Peak: ${formatBytes(profile.summary.maxHeapUsed)}`);
|
|
272
|
+
console.log(` External: ${formatBytes(profile.samples[0]?.external || 0)}`);
|
|
273
|
+
|
|
274
|
+
console.log('\nā” Performance:');
|
|
275
|
+
console.log(` CPU Avg: ${profile.summary.avgCpu.toFixed(2)}s | Peak: ${profile.summary.maxCpu.toFixed(2)}s`);
|
|
276
|
+
console.log(` Event Loop Avg: ${profile.summary.avgEventLoopLag.toFixed(2)}ms | Peak: ${profile.summary.maxEventLoopLag.toFixed(2)}ms`);
|
|
277
|
+
|
|
278
|
+
console.log('\nš Resources:');
|
|
279
|
+
const lastSample = profile.samples[profile.samples.length - 1];
|
|
280
|
+
if (lastSample) {
|
|
281
|
+
console.log(` Active Handles: ${lastSample.activeHandles} | Active Requests: ${lastSample.activeRequests}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Performance recommendations
|
|
285
|
+
console.log('\nš” Performance Insights:');
|
|
286
|
+
|
|
287
|
+
if (profile.summary.maxMemory > 500 * 1024 * 1024) { // > 500MB
|
|
288
|
+
console.log(' ā ļø High memory usage detected - consider memory optimization');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (profile.summary.maxEventLoopLag > 100) { // > 100ms
|
|
292
|
+
console.log(' ā ļø High event loop lag detected - consider optimizing async operations');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (profile.samples.length > 0) {
|
|
296
|
+
const firstSample = profile.samples[0];
|
|
297
|
+
if (firstSample.heapTotal > 0 && profile.summary.avgHeapUsed / firstSample.heapTotal > 0.8) { // > 80%
|
|
298
|
+
console.log(' ā ļø High heap usage ratio - potential memory leak');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (profile.samples.length < profile.duration * 1000 / profile.interval * 0.9) {
|
|
303
|
+
console.log(' ā ļø Some samples failed to collect - check service health');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
console.log('\nš Sample Timeline (last 10 samples):');
|
|
307
|
+
const recentSamples = profile.samples.slice(-10);
|
|
308
|
+
for (let i = 0; i < recentSamples.length; i++) {
|
|
309
|
+
const sample = recentSamples[i];
|
|
310
|
+
const time = new Date(sample.timestamp).toLocaleTimeString();
|
|
311
|
+
console.log(` ${time} | CPU: ${sample.cpu.toFixed(2)}s | Memory: ${formatBytes(sample.memory)} | Heap: ${formatBytes(sample.heapUsed)}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function formatBytes(bytes: number): string {
|
|
316
|
+
if (bytes === 0) return '0B';
|
|
317
|
+
const k = 1024;
|
|
318
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
319
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
320
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function saveProfileData(profile: PerformanceProfile, filename: string): Promise<void> {
|
|
324
|
+
const data = {
|
|
325
|
+
metadata: {
|
|
326
|
+
serviceName: profile.serviceName,
|
|
327
|
+
duration: profile.duration,
|
|
328
|
+
interval: profile.interval,
|
|
329
|
+
sampleCount: profile.samples.length,
|
|
330
|
+
timestamp: new Date().toISOString(),
|
|
331
|
+
},
|
|
332
|
+
summary: profile.summary,
|
|
333
|
+
samples: profile.samples,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const json = JSON.stringify(data, null, 2);
|
|
337
|
+
await Bun.write(filename, json);
|
|
338
|
+
}
|
package/src/commands/restart.ts
CHANGED
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { execSync } from "node:child_process";
|
|
4
|
+
import { getPlatformInfo } from "../platform/detect.js";
|
|
5
|
+
|
|
6
|
+
// Security: Service name validation
|
|
7
|
+
function isValidServiceName(name: string): boolean {
|
|
8
|
+
const validPattern = /^[a-zA-Z0-9._-]+$/;
|
|
9
|
+
return validPattern.test(name) && name.length <= 64 && !name.includes('..') && !name.includes('/');
|
|
10
|
+
}
|
|
4
11
|
|
|
5
12
|
export async function restartCommand(name: string): Promise<void> {
|
|
13
|
+
// Security: Validate service name
|
|
14
|
+
if (!isValidServiceName(name)) {
|
|
15
|
+
console.error(`ā Security: Invalid service name: ${name}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const platformInfo = getPlatformInfo();
|
|
20
|
+
|
|
6
21
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
22
|
+
if (platformInfo.isLinux) {
|
|
23
|
+
// Security: Use shell escaping to prevent injection
|
|
24
|
+
const escapedName = name.replace(/[^a-zA-Z0-9._-]/g, '');
|
|
25
|
+
execSync(`systemctl --user restart "${escapedName}"`, { stdio: "inherit" });
|
|
26
|
+
console.log(`š User service '${name}' restarted`);
|
|
27
|
+
} else if (platformInfo.isMacOS) {
|
|
28
|
+
const { launchdCommand } = await import("../macos/launchd.js");
|
|
29
|
+
await launchdCommand('restart', { name: `bs9.${name}` });
|
|
30
|
+
} else if (platformInfo.isWindows) {
|
|
31
|
+
const { windowsCommand } = await import("../windows/service.js");
|
|
32
|
+
await windowsCommand('restart', { name: `BS9_${name}` });
|
|
33
|
+
}
|
|
9
34
|
} catch (err) {
|
|
10
|
-
console.error(`ā Failed to restart
|
|
35
|
+
console.error(`ā Failed to restart service '${name}': ${err}`);
|
|
11
36
|
process.exit(1);
|
|
12
37
|
}
|
|
13
38
|
}
|