sql-kite 1.0.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/bin/sql-kite.js +2 -0
- package/package.json +49 -0
- package/src/commands/delete.js +43 -0
- package/src/commands/import-server.js +50 -0
- package/src/commands/import.js +280 -0
- package/src/commands/init.js +193 -0
- package/src/commands/list.js +33 -0
- package/src/commands/new.js +69 -0
- package/src/commands/open.js +23 -0
- package/src/commands/ports.js +50 -0
- package/src/commands/start.js +128 -0
- package/src/commands/stop.js +71 -0
- package/src/index.js +73 -0
- package/src/utils/db-init.js +20 -0
- package/src/utils/meta-migration.js +259 -0
- package/src/utils/paths.js +71 -0
- package/src/utils/port-finder.js +239 -0
- package/src/utils/port-registry.js +233 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { homedir } from 'os';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
export const SQL_KITE_HOME = join(homedir(), '.sql-kite');
|
|
6
|
+
export const RUNTIME_DIR = join(SQL_KITE_HOME, 'runtime');
|
|
7
|
+
export const STUDIO_DIR = join(SQL_KITE_HOME, 'studio');
|
|
8
|
+
export const LOGS_DIR = join(SQL_KITE_HOME, 'logs');
|
|
9
|
+
|
|
10
|
+
export function getProjectPath(name) {
|
|
11
|
+
return join(RUNTIME_DIR, name);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getProjectDbPath(name) {
|
|
15
|
+
return join(getProjectPath(name), 'db.sqlite');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getProjectMetaPath(name) {
|
|
19
|
+
return join(getProjectPath(name), '.studio', 'meta.db');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getProjectServerInfoPath(name) {
|
|
23
|
+
return join(getProjectPath(name), '.studio', 'server.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ensureSqlKiteDirs() {
|
|
27
|
+
[SQL_KITE_HOME, RUNTIME_DIR, STUDIO_DIR, LOGS_DIR].forEach(dir => {
|
|
28
|
+
if (!existsSync(dir)) {
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function projectExists(name) {
|
|
35
|
+
return existsSync(getProjectPath(name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate project name to prevent path traversal attacks
|
|
40
|
+
* Only allows alphanumeric, hyphens, underscores
|
|
41
|
+
* @param {string} name - Project name to validate
|
|
42
|
+
* @returns {{ valid: boolean, error?: string, sanitized?: string }}
|
|
43
|
+
*/
|
|
44
|
+
export function validateProjectName(name) {
|
|
45
|
+
if (!name || typeof name !== 'string') {
|
|
46
|
+
return { valid: false, error: 'Project name is required' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const trimmed = name.trim();
|
|
50
|
+
|
|
51
|
+
if (trimmed.length === 0) {
|
|
52
|
+
return { valid: false, error: 'Project name cannot be empty' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (trimmed.length > 50) {
|
|
56
|
+
return { valid: false, error: 'Project name too long (max 50 characters)' };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Block path traversal attempts
|
|
60
|
+
if (trimmed.includes('..') || trimmed.includes('/') || trimmed.includes('\\')) {
|
|
61
|
+
return { valid: false, error: 'Project name contains invalid characters (no paths allowed)' };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Only allow alphanumeric, hyphens, underscores
|
|
65
|
+
const validPattern = /^[a-zA-Z0-9_-]+$/;
|
|
66
|
+
if (!validPattern.test(trimmed)) {
|
|
67
|
+
return { valid: false, error: 'Project name can only contain letters, numbers, hyphens, and underscores' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { valid: true, sanitized: trimmed };
|
|
71
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { createServer } from 'net';
|
|
2
|
+
import portRegistry from './port-registry.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Port configuration
|
|
6
|
+
*/
|
|
7
|
+
const PORT_CONFIG = {
|
|
8
|
+
DEFAULT_START: 3000,
|
|
9
|
+
DEFAULT_END: 3999,
|
|
10
|
+
FALLBACK_START: 4000,
|
|
11
|
+
FALLBACK_END: 4999,
|
|
12
|
+
MAX_ATTEMPTS: 100,
|
|
13
|
+
SCAN_BATCH_SIZE: 10
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if a single port is available at OS level
|
|
18
|
+
*/
|
|
19
|
+
async function isPortAvailable(port) {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const server = createServer();
|
|
22
|
+
|
|
23
|
+
const cleanup = () => {
|
|
24
|
+
server.removeAllListeners();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
server.once('error', (err) => {
|
|
28
|
+
cleanup();
|
|
29
|
+
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
|
|
30
|
+
resolve(false);
|
|
31
|
+
} else {
|
|
32
|
+
resolve(false);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
server.once('listening', () => {
|
|
37
|
+
server.close(() => {
|
|
38
|
+
cleanup();
|
|
39
|
+
resolve(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Bind to all interfaces to ensure we check properly
|
|
44
|
+
server.listen(port, '0.0.0.0');
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find a free port in the given range
|
|
50
|
+
* Uses intelligent scanning with registry integration
|
|
51
|
+
*/
|
|
52
|
+
async function findFreePortInRange(startPort, endPort, projectName = null) {
|
|
53
|
+
// Auto-cleanup stale allocations
|
|
54
|
+
portRegistry.autoCleanup();
|
|
55
|
+
|
|
56
|
+
// Get currently allocated ports from registry
|
|
57
|
+
const allocatedPorts = portRegistry.getAllocatedPorts();
|
|
58
|
+
|
|
59
|
+
let attempts = 0;
|
|
60
|
+
let currentPort = startPort;
|
|
61
|
+
|
|
62
|
+
while (currentPort <= endPort && attempts < PORT_CONFIG.MAX_ATTEMPTS) {
|
|
63
|
+
attempts++;
|
|
64
|
+
|
|
65
|
+
// Skip ports already allocated in registry
|
|
66
|
+
if (allocatedPorts.includes(currentPort)) {
|
|
67
|
+
currentPort++;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if port is available at OS level
|
|
72
|
+
const available = await isPortAvailable(currentPort);
|
|
73
|
+
|
|
74
|
+
if (available) {
|
|
75
|
+
// Try to reserve in registry if project name provided
|
|
76
|
+
if (projectName) {
|
|
77
|
+
const reserved = await portRegistry.reservePort(projectName, currentPort);
|
|
78
|
+
if (reserved) {
|
|
79
|
+
return currentPort;
|
|
80
|
+
}
|
|
81
|
+
// Port was taken by another process between check and reserve
|
|
82
|
+
// Continue to next port
|
|
83
|
+
currentPort++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return currentPort;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
currentPort++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Scan multiple ports in parallel (batch scanning)
|
|
98
|
+
* Much faster for finding free ports in busy systems
|
|
99
|
+
*/
|
|
100
|
+
async function scanPortsBatch(ports) {
|
|
101
|
+
const results = await Promise.all(
|
|
102
|
+
ports.map(async (port) => ({
|
|
103
|
+
port,
|
|
104
|
+
available: await isPortAvailable(port)
|
|
105
|
+
}))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const availablePort = results.find(r => r.available);
|
|
109
|
+
return availablePort ? availablePort.port : null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Find a free port with intelligent strategies
|
|
114
|
+
*
|
|
115
|
+
* Strategy:
|
|
116
|
+
* 1. Check registry for existing project allocation
|
|
117
|
+
* 2. Try default range (3000-3999) with batch scanning
|
|
118
|
+
* 3. Fallback to higher range (4000-4999)
|
|
119
|
+
* 4. Sequential scan as last resort
|
|
120
|
+
*
|
|
121
|
+
* @param {number} startPort - Preferred starting port (default: 3000)
|
|
122
|
+
* @param {string} projectName - Project name for registry tracking (optional)
|
|
123
|
+
* @returns {Promise<number>} - Free port number
|
|
124
|
+
*/
|
|
125
|
+
export async function findFreePort(startPort = PORT_CONFIG.DEFAULT_START, projectName = null) {
|
|
126
|
+
try {
|
|
127
|
+
// Auto-cleanup stale allocations first
|
|
128
|
+
portRegistry.autoCleanup();
|
|
129
|
+
|
|
130
|
+
// If project name provided, check if already has allocated port
|
|
131
|
+
if (projectName) {
|
|
132
|
+
const existingPort = portRegistry.getProjectPort(projectName);
|
|
133
|
+
if (existingPort) {
|
|
134
|
+
// Verify it's still valid
|
|
135
|
+
const isValid = await isPortAvailable(existingPort);
|
|
136
|
+
if (isValid) {
|
|
137
|
+
return existingPort;
|
|
138
|
+
}
|
|
139
|
+
// Port no longer valid, release it
|
|
140
|
+
portRegistry.releasePort(projectName);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Strategy 1: Try the requested start port
|
|
145
|
+
if (startPort >= PORT_CONFIG.DEFAULT_START && startPort <= PORT_CONFIG.DEFAULT_END) {
|
|
146
|
+
const allocatedPorts = portRegistry.getAllocatedPorts();
|
|
147
|
+
if (!allocatedPorts.includes(startPort)) {
|
|
148
|
+
const available = await isPortAvailable(startPort);
|
|
149
|
+
if (available) {
|
|
150
|
+
if (projectName) {
|
|
151
|
+
const reserved = await portRegistry.reservePort(projectName, startPort);
|
|
152
|
+
if (reserved) return startPort;
|
|
153
|
+
} else {
|
|
154
|
+
return startPort;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Strategy 2: Quick batch scan in default range
|
|
161
|
+
const defaultRangeStart = Math.max(startPort, PORT_CONFIG.DEFAULT_START);
|
|
162
|
+
const batchPorts = [];
|
|
163
|
+
for (let i = 0; i < PORT_CONFIG.SCAN_BATCH_SIZE; i++) {
|
|
164
|
+
const port = defaultRangeStart + i;
|
|
165
|
+
if (port <= PORT_CONFIG.DEFAULT_END) {
|
|
166
|
+
batchPorts.push(port);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (batchPorts.length > 0) {
|
|
171
|
+
const allocatedPorts = portRegistry.getAllocatedPorts();
|
|
172
|
+
const freePorts = batchPorts.filter(p => !allocatedPorts.includes(p));
|
|
173
|
+
|
|
174
|
+
if (freePorts.length > 0) {
|
|
175
|
+
const batchResult = await scanPortsBatch(freePorts);
|
|
176
|
+
if (batchResult) {
|
|
177
|
+
if (projectName) {
|
|
178
|
+
const reserved = await portRegistry.reservePort(projectName, batchResult);
|
|
179
|
+
if (reserved) return batchResult;
|
|
180
|
+
} else {
|
|
181
|
+
return batchResult;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Strategy 3: Sequential scan in default range
|
|
188
|
+
let port = await findFreePortInRange(
|
|
189
|
+
defaultRangeStart,
|
|
190
|
+
PORT_CONFIG.DEFAULT_END,
|
|
191
|
+
projectName
|
|
192
|
+
);
|
|
193
|
+
if (port) return port;
|
|
194
|
+
|
|
195
|
+
// Strategy 4: Fallback range
|
|
196
|
+
port = await findFreePortInRange(
|
|
197
|
+
PORT_CONFIG.FALLBACK_START,
|
|
198
|
+
PORT_CONFIG.FALLBACK_END,
|
|
199
|
+
projectName
|
|
200
|
+
);
|
|
201
|
+
if (port) return port;
|
|
202
|
+
|
|
203
|
+
// Strategy 5: Extended range (5000-9999)
|
|
204
|
+
port = await findFreePortInRange(5000, 9999, projectName);
|
|
205
|
+
if (port) return port;
|
|
206
|
+
|
|
207
|
+
// No port found
|
|
208
|
+
throw new Error(
|
|
209
|
+
'Unable to find a free port. Please ensure you have available ports in range 3000-9999'
|
|
210
|
+
);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error.message.includes('Unable to find')) {
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
// Fallback to simple sequential search on unexpected errors
|
|
216
|
+
return findFreePortInRange(startPort, startPort + 1000, projectName);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Release a port for a project
|
|
222
|
+
*/
|
|
223
|
+
export function releasePort(projectName) {
|
|
224
|
+
return portRegistry.releasePort(projectName);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Get port registry status
|
|
229
|
+
*/
|
|
230
|
+
export function getPortStatus() {
|
|
231
|
+
return portRegistry.getStatus();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Manual cleanup of stale allocations
|
|
236
|
+
*/
|
|
237
|
+
export function cleanupStalePorts() {
|
|
238
|
+
return portRegistry.cleanup();
|
|
239
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { join } from 'path';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync } from 'fs';
|
|
3
|
+
import { RUNTIME_DIR } from './paths.js';
|
|
4
|
+
import { createServer } from 'net';
|
|
5
|
+
|
|
6
|
+
const REGISTRY_FILE = join(RUNTIME_DIR, '.port-registry.json');
|
|
7
|
+
const LOCK_TIMEOUT = 5000; // 5 seconds
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Port Registry - Manages port allocation across all SQL Kite projects
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Centralized port tracking
|
|
14
|
+
* - Atomic port allocation
|
|
15
|
+
* - Conflict detection
|
|
16
|
+
* - Automatic cleanup of stale allocations
|
|
17
|
+
* - Fast port validation
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
class PortRegistry {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.ensureRegistryFile();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
ensureRegistryFile() {
|
|
26
|
+
if (!existsSync(RUNTIME_DIR)) {
|
|
27
|
+
mkdirSync(RUNTIME_DIR, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
if (!existsSync(REGISTRY_FILE)) {
|
|
30
|
+
this.writeRegistry({ ports: {}, last_cleanup: Date.now() });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
readRegistry() {
|
|
35
|
+
try {
|
|
36
|
+
const data = readFileSync(REGISTRY_FILE, 'utf-8');
|
|
37
|
+
return JSON.parse(data);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
// Corrupted registry, recreate
|
|
40
|
+
this.writeRegistry({ ports: {}, last_cleanup: Date.now() });
|
|
41
|
+
return { ports: {}, last_cleanup: Date.now() };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
writeRegistry(data) {
|
|
46
|
+
writeFileSync(REGISTRY_FILE, JSON.stringify(data, null, 2));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all currently allocated ports
|
|
51
|
+
*/
|
|
52
|
+
getAllocatedPorts() {
|
|
53
|
+
const registry = this.readRegistry();
|
|
54
|
+
return Object.values(registry.ports).map(entry => entry.port);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a port is allocated to any project
|
|
59
|
+
*/
|
|
60
|
+
isPortAllocated(port) {
|
|
61
|
+
const allocatedPorts = this.getAllocatedPorts();
|
|
62
|
+
return allocatedPorts.includes(port);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get port for a specific project
|
|
67
|
+
*/
|
|
68
|
+
getProjectPort(projectName) {
|
|
69
|
+
const registry = this.readRegistry();
|
|
70
|
+
return registry.ports[projectName]?.port || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Reserve a port for a project atomically
|
|
75
|
+
*/
|
|
76
|
+
async reservePort(projectName, port) {
|
|
77
|
+
const registry = this.readRegistry();
|
|
78
|
+
|
|
79
|
+
// Check if project already has a port
|
|
80
|
+
if (registry.ports[projectName]) {
|
|
81
|
+
const existingPort = registry.ports[projectName].port;
|
|
82
|
+
// Verify the existing port is still valid
|
|
83
|
+
const isValid = await this.isPortActuallyFree(existingPort);
|
|
84
|
+
if (isValid) {
|
|
85
|
+
return existingPort;
|
|
86
|
+
}
|
|
87
|
+
// Stale allocation, remove it
|
|
88
|
+
delete registry.ports[projectName];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check if port is already allocated
|
|
92
|
+
const allocatedPorts = Object.values(registry.ports).map(e => e.port);
|
|
93
|
+
if (allocatedPorts.includes(port)) {
|
|
94
|
+
return null; // Port already taken
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify port is actually free at OS level
|
|
98
|
+
const isFree = await this.isPortActuallyFree(port);
|
|
99
|
+
if (!isFree) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Reserve the port
|
|
104
|
+
registry.ports[projectName] = {
|
|
105
|
+
port,
|
|
106
|
+
allocated_at: Date.now(),
|
|
107
|
+
pid: process.pid
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
this.writeRegistry(registry);
|
|
111
|
+
return port;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Release a port for a project
|
|
116
|
+
*/
|
|
117
|
+
releasePort(projectName) {
|
|
118
|
+
const registry = this.readRegistry();
|
|
119
|
+
if (registry.ports[projectName]) {
|
|
120
|
+
delete registry.ports[projectName];
|
|
121
|
+
this.writeRegistry(registry);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a port is actually free at OS level
|
|
129
|
+
*/
|
|
130
|
+
async isPortActuallyFree(port) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
const server = createServer();
|
|
133
|
+
|
|
134
|
+
server.once('error', (err) => {
|
|
135
|
+
if (err.code === 'EADDRINUSE') {
|
|
136
|
+
resolve(false);
|
|
137
|
+
} else {
|
|
138
|
+
resolve(false);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
server.once('listening', () => {
|
|
143
|
+
server.close(() => {
|
|
144
|
+
resolve(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.listen(port, '0.0.0.0');
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Cleanup stale port allocations
|
|
154
|
+
* Checks if server.json files exist for allocated projects
|
|
155
|
+
*/
|
|
156
|
+
cleanup() {
|
|
157
|
+
const registry = this.readRegistry();
|
|
158
|
+
const projects = Object.keys(registry.ports);
|
|
159
|
+
let cleanedCount = 0;
|
|
160
|
+
|
|
161
|
+
for (const projectName of projects) {
|
|
162
|
+
const serverInfoPath = join(RUNTIME_DIR, projectName, '.studio', 'server.json');
|
|
163
|
+
|
|
164
|
+
// If server.json doesn't exist, the port allocation is stale
|
|
165
|
+
if (!existsSync(serverInfoPath)) {
|
|
166
|
+
delete registry.ports[projectName];
|
|
167
|
+
cleanedCount++;
|
|
168
|
+
} else {
|
|
169
|
+
// Verify the server is actually running
|
|
170
|
+
try {
|
|
171
|
+
const serverInfo = JSON.parse(readFileSync(serverInfoPath, 'utf-8'));
|
|
172
|
+
try {
|
|
173
|
+
// Check if process exists (signal 0 doesn't kill, just checks)
|
|
174
|
+
process.kill(serverInfo.pid, 0);
|
|
175
|
+
} catch (e) {
|
|
176
|
+
// Process doesn't exist, cleanup
|
|
177
|
+
delete registry.ports[projectName];
|
|
178
|
+
unlinkSync(serverInfoPath);
|
|
179
|
+
cleanedCount++;
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
// Corrupted server.json
|
|
183
|
+
delete registry.ports[projectName];
|
|
184
|
+
cleanedCount++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
registry.last_cleanup = Date.now();
|
|
190
|
+
this.writeRegistry(registry);
|
|
191
|
+
|
|
192
|
+
return cleanedCount;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Auto cleanup if last cleanup was more than 1 hour ago
|
|
197
|
+
*/
|
|
198
|
+
autoCleanup() {
|
|
199
|
+
const registry = this.readRegistry();
|
|
200
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
201
|
+
|
|
202
|
+
if (!registry.last_cleanup || (Date.now() - registry.last_cleanup) > ONE_HOUR) {
|
|
203
|
+
return this.cleanup();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get registry status
|
|
211
|
+
*/
|
|
212
|
+
getStatus() {
|
|
213
|
+
const registry = this.readRegistry();
|
|
214
|
+
return {
|
|
215
|
+
total_allocations: Object.keys(registry.ports).length,
|
|
216
|
+
allocations: registry.ports,
|
|
217
|
+
last_cleanup: registry.last_cleanup
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Force clear all allocations (dangerous - for debugging only)
|
|
223
|
+
*/
|
|
224
|
+
clearAll() {
|
|
225
|
+
this.writeRegistry({ ports: {}, last_cleanup: Date.now() });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Singleton instance
|
|
230
|
+
const portRegistry = new PortRegistry();
|
|
231
|
+
|
|
232
|
+
export default portRegistry;
|
|
233
|
+
export { PortRegistry };
|