lobstakit-cloud 1.3.2 → 1.3.3
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/lib/gateway.js +165 -108
- package/package.json +1 -1
package/lib/gateway.js
CHANGED
|
@@ -1,44 +1,148 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LobstaKit Cloud — Gateway Manager
|
|
3
3
|
*
|
|
4
|
-
* Controls the openclaw-gateway systemd service.
|
|
4
|
+
* Controls the openclaw-gateway systemd user service.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
8
|
const fs = require('fs');
|
|
9
|
+
const http = require('http');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
9
12
|
|
|
10
13
|
const SERVICE_NAME = 'openclaw-gateway';
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
// Paths
|
|
16
|
+
const OPENCLAW_CONFIG = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
17
|
+
const LOBSTAKIT_CONFIG = path.join(os.homedir(), '.lobstakit', 'config.json');
|
|
18
|
+
|
|
19
|
+
// Env vars needed for systemctl --user to work from a headless root session
|
|
20
|
+
function userEnv() {
|
|
21
|
+
const uid = (() => { try { return execSync('id -u', { encoding: 'utf8', timeout: 3000 }).trim(); } catch { return '0'; } })();
|
|
22
|
+
return {
|
|
23
|
+
...process.env,
|
|
24
|
+
XDG_RUNTIME_DIR: `/run/user/${uid}`,
|
|
25
|
+
DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${uid}/bus`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Token helpers ────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the gateway auth token from ~/.openclaw/openclaw.json.
|
|
33
|
+
* Returns null if not found.
|
|
34
|
+
*/
|
|
35
|
+
function readGatewayToken() {
|
|
36
|
+
try {
|
|
37
|
+
const raw = fs.readFileSync(OPENCLAW_CONFIG, 'utf8');
|
|
38
|
+
const cfg = JSON.parse(raw);
|
|
39
|
+
return cfg?.gateway?.auth?.token || null;
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Persist the gateway auth token into ~/.lobstakit/config.json
|
|
47
|
+
* so health checks can authenticate without re-reading openclaw.json every time.
|
|
48
|
+
*/
|
|
49
|
+
function persistGatewayToken(token) {
|
|
50
|
+
try {
|
|
51
|
+
let cfg = {};
|
|
52
|
+
try { cfg = JSON.parse(fs.readFileSync(LOBSTAKIT_CONFIG, 'utf8')); } catch { /* new file */ }
|
|
53
|
+
cfg.gatewayToken = token;
|
|
54
|
+
fs.writeFileSync(LOBSTAKIT_CONFIG, JSON.stringify(cfg, null, 2));
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('[gateway] Failed to persist token:', err.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Read token from openclaw.json and persist into lobstakit config.
|
|
62
|
+
* Called after every gateway install/restart so both sides stay in sync.
|
|
63
|
+
*/
|
|
64
|
+
function syncGatewayToken() {
|
|
65
|
+
const token = readGatewayToken();
|
|
66
|
+
if (token) {
|
|
67
|
+
persistGatewayToken(token);
|
|
68
|
+
console.log('[gateway] Auth token synced to lobstakit config');
|
|
69
|
+
} else {
|
|
70
|
+
console.warn('[gateway] Could not read gateway auth token from openclaw.json');
|
|
71
|
+
}
|
|
72
|
+
return token;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the stored gateway token from lobstakit config (fast path).
|
|
77
|
+
* Falls back to reading directly from openclaw.json.
|
|
78
|
+
*/
|
|
79
|
+
function getStoredToken() {
|
|
80
|
+
try {
|
|
81
|
+
const cfg = JSON.parse(fs.readFileSync(LOBSTAKIT_CONFIG, 'utf8'));
|
|
82
|
+
if (cfg?.gatewayToken) return cfg.gatewayToken;
|
|
83
|
+
} catch { /* fall through */ }
|
|
84
|
+
return readGatewayToken();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── Health check via HTTP ────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Ping the gateway HTTP API. Returns a promise resolving to true if healthy.
|
|
91
|
+
*/
|
|
92
|
+
function pingGateway(token) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const opts = {
|
|
95
|
+
hostname: '127.0.0.1',
|
|
96
|
+
port: 3001,
|
|
97
|
+
path: '/api/status',
|
|
98
|
+
method: 'GET',
|
|
99
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
100
|
+
timeout: 3000,
|
|
101
|
+
};
|
|
102
|
+
const req = http.request(opts, (res) => {
|
|
103
|
+
resolve(res.statusCode === 200 || res.statusCode === 401); // 401 = running but token stale
|
|
104
|
+
});
|
|
105
|
+
req.on('error', () => resolve(false));
|
|
106
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Service control ──────────────────────────────────────────────────────────
|
|
13
112
|
|
|
14
113
|
/**
|
|
15
114
|
* Check if the gateway service is currently running.
|
|
115
|
+
* Tries systemctl --user first, then falls back to HTTP ping.
|
|
16
116
|
*/
|
|
17
117
|
function isGatewayRunning() {
|
|
118
|
+
// Primary: systemctl --user (accurate for user services)
|
|
18
119
|
try {
|
|
19
|
-
const result = execSync(`systemctl is-active ${SERVICE_NAME}`, {
|
|
120
|
+
const result = execSync(`systemctl --user is-active ${SERVICE_NAME}`, {
|
|
20
121
|
encoding: 'utf8',
|
|
21
|
-
timeout: 5000
|
|
122
|
+
timeout: 5000,
|
|
123
|
+
env: userEnv(),
|
|
22
124
|
}).trim();
|
|
23
|
-
|
|
125
|
+
if (result === 'active') return true;
|
|
126
|
+
} catch { /* fall through to HTTP check */ }
|
|
127
|
+
|
|
128
|
+
// Fallback: HTTP ping (works even if systemctl --user is unavailable)
|
|
129
|
+
// Run synchronously via a child process trick
|
|
130
|
+
try {
|
|
131
|
+
const token = getStoredToken();
|
|
132
|
+
const authHeader = token ? `-H "Authorization: Bearer ${token}"` : '';
|
|
133
|
+
const result = execSync(
|
|
134
|
+
`curl -s -o /dev/null -w '%{http_code}' ${authHeader} http://127.0.0.1:3001/api/status`,
|
|
135
|
+
{ encoding: 'utf8', timeout: 4000 }
|
|
136
|
+
).trim();
|
|
137
|
+
return result === '200' || result === '401';
|
|
24
138
|
} catch {
|
|
25
139
|
return false;
|
|
26
140
|
}
|
|
27
141
|
}
|
|
28
142
|
|
|
29
143
|
/**
|
|
30
|
-
* Ensure D-Bus user session
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* Root cause of "systemctl --user unavailable: Failed to connect to bus:
|
|
34
|
-
* No medium found": OpenClaw calls `systemctl --user` internally on startup.
|
|
35
|
-
* On headless VPS, no D-Bus user session exists. Even if LobstaKit sets up
|
|
36
|
-
* D-Bus in its own process, the gateway runs as a *separate* system service
|
|
37
|
-
* and never inherits those env vars.
|
|
38
|
-
*
|
|
39
|
-
* Fix: write a systemd drop-in that bakes XDG_RUNTIME_DIR and
|
|
40
|
-
* DBUS_SESSION_BUS_ADDRESS into the service unit so every restart picks
|
|
41
|
-
* them up automatically — no manual intervention required.
|
|
144
|
+
* Ensure D-Bus user session and systemd drop-in are set up.
|
|
145
|
+
* Needed so the gateway can call `systemctl --user` internally.
|
|
42
146
|
*/
|
|
43
147
|
function ensureUserSession() {
|
|
44
148
|
try {
|
|
@@ -48,86 +152,48 @@ function ensureUserSession() {
|
|
|
48
152
|
const busSocket = `${runtimeDir}/bus`;
|
|
49
153
|
const busAddress = `unix:path=${busSocket}`;
|
|
50
154
|
|
|
51
|
-
|
|
52
|
-
execSync(`
|
|
53
|
-
encoding: 'utf8',
|
|
54
|
-
timeout: 5000
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// 2. Ensure runtime dir exists with correct permissions
|
|
58
|
-
execSync(`mkdir -p ${runtimeDir} && chmod 700 ${runtimeDir} && chown ${uid}:${uid} ${runtimeDir} 2>/dev/null || true`, {
|
|
59
|
-
encoding: 'utf8',
|
|
60
|
-
timeout: 3000
|
|
61
|
-
});
|
|
155
|
+
execSync(`loginctl enable-linger ${username} 2>/dev/null || true`, { encoding: 'utf8', timeout: 5000 });
|
|
156
|
+
execSync(`mkdir -p ${runtimeDir} && chmod 700 ${runtimeDir} 2>/dev/null || true`, { encoding: 'utf8', timeout: 3000 });
|
|
62
157
|
|
|
63
|
-
// 3. Start dbus-daemon if socket not already present
|
|
64
158
|
let dbusRunning = false;
|
|
65
|
-
try {
|
|
66
|
-
execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 2000 });
|
|
67
|
-
dbusRunning = true;
|
|
68
|
-
} catch { /* socket not there yet */ }
|
|
159
|
+
try { execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 2000 }); dbusRunning = true; } catch { /* not running */ }
|
|
69
160
|
|
|
70
161
|
if (!dbusRunning) {
|
|
71
162
|
try {
|
|
72
|
-
execSync(
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
timeout: 5000,
|
|
77
|
-
env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir }
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
// Wait up to 2s for socket to appear
|
|
163
|
+
execSync(`dbus-daemon --session --address=${busAddress} --fork --nopidfile`, {
|
|
164
|
+
encoding: 'utf8', timeout: 5000,
|
|
165
|
+
env: { ...process.env, XDG_RUNTIME_DIR: runtimeDir },
|
|
166
|
+
});
|
|
81
167
|
for (let i = 0; i < 10; i++) {
|
|
82
|
-
try {
|
|
83
|
-
|
|
84
|
-
dbusRunning = true;
|
|
85
|
-
break;
|
|
86
|
-
} catch {
|
|
87
|
-
execSync('sleep 0.2', { timeout: 500 });
|
|
88
|
-
}
|
|
168
|
+
try { execSync(`test -S ${busSocket}`, { encoding: 'utf8', timeout: 500 }); dbusRunning = true; break; } catch { /* wait */ }
|
|
169
|
+
execSync('sleep 0.2', { encoding: 'utf8', timeout: 1000 });
|
|
89
170
|
}
|
|
90
|
-
} catch (e) {
|
|
91
|
-
console.warn('[gateway] dbus-daemon unavailable:', e.message);
|
|
92
|
-
// dbus-daemon may not be installed — XDG_RUNTIME_DIR alone may be enough
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 4. Write systemd drop-in so the service unit inherits these env vars
|
|
97
|
-
// on every start/restart, not just when LobstaKit happens to call this.
|
|
98
|
-
const lines = [
|
|
99
|
-
'# Auto-generated by LobstaKit — do not edit manually',
|
|
100
|
-
'[Service]',
|
|
101
|
-
`Environment="XDG_RUNTIME_DIR=${runtimeDir}"`,
|
|
102
|
-
];
|
|
103
|
-
if (dbusRunning) {
|
|
104
|
-
lines.push(`Environment="DBUS_SESSION_BUS_ADDRESS=${busAddress}"`);
|
|
171
|
+
} catch (e) { console.warn('[gateway] dbus-daemon start failed:', e.message); }
|
|
105
172
|
}
|
|
106
|
-
const dropInContent = lines.join('\n') + '\n';
|
|
107
|
-
|
|
108
|
-
execSync(`mkdir -p ${DROP_IN_DIR}`, { encoding: 'utf8', timeout: 3000 });
|
|
109
|
-
fs.writeFileSync(DROP_IN_FILE, dropInContent, 'utf8');
|
|
110
173
|
|
|
111
|
-
//
|
|
112
|
-
|
|
174
|
+
// Write systemd drop-in so the gateway service inherits D-Bus env
|
|
175
|
+
const dropInDir = `/root/.config/systemd/user`;
|
|
176
|
+
const dropInFile = `${dropInDir}/${SERVICE_NAME}.service.d/dbus-env.conf`;
|
|
177
|
+
const dropInContent = `[Service]\nEnvironment=XDG_RUNTIME_DIR=${runtimeDir}\nEnvironment=DBUS_SESSION_BUS_ADDRESS=${busAddress}\n`;
|
|
178
|
+
try {
|
|
179
|
+
execSync(`mkdir -p ${path.dirname(dropInFile)}`, { encoding: 'utf8', timeout: 3000 });
|
|
180
|
+
fs.writeFileSync(dropInFile, dropInContent);
|
|
181
|
+
execSync(`systemctl --user daemon-reload`, { encoding: 'utf8', timeout: 10000, env: userEnv() });
|
|
182
|
+
} catch (e) { console.warn('[gateway] drop-in write failed:', e.message); }
|
|
113
183
|
|
|
114
|
-
console.log(`[gateway] Session ready — runtimeDir=${runtimeDir} dbus=${dbusRunning}
|
|
184
|
+
console.log(`[gateway] Session ready — runtimeDir=${runtimeDir} dbus=${dbusRunning}`);
|
|
115
185
|
} catch (err) {
|
|
116
|
-
console.
|
|
186
|
+
console.error('[gateway] ensureUserSession failed:', err.message);
|
|
117
187
|
}
|
|
118
188
|
}
|
|
119
189
|
|
|
120
|
-
/**
|
|
121
|
-
* Start the gateway service.
|
|
122
|
-
*/
|
|
123
190
|
function startGateway() {
|
|
124
191
|
ensureUserSession();
|
|
125
192
|
try {
|
|
126
|
-
execSync(`systemctl
|
|
127
|
-
|
|
128
|
-
timeout: 15000
|
|
129
|
-
});
|
|
193
|
+
execSync(`systemctl --user enable ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 10000, env: userEnv() });
|
|
194
|
+
execSync(`systemctl --user start ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 15000, env: userEnv() });
|
|
130
195
|
console.log('[gateway] Service started');
|
|
196
|
+
setTimeout(() => syncGatewayToken(), 3000); // token available after ~2s startup
|
|
131
197
|
return { success: true };
|
|
132
198
|
} catch (err) {
|
|
133
199
|
console.error('[gateway] Failed to start:', err.message);
|
|
@@ -135,15 +201,9 @@ function startGateway() {
|
|
|
135
201
|
}
|
|
136
202
|
}
|
|
137
203
|
|
|
138
|
-
/**
|
|
139
|
-
* Stop the gateway service.
|
|
140
|
-
*/
|
|
141
204
|
function stopGateway() {
|
|
142
205
|
try {
|
|
143
|
-
execSync(`systemctl stop ${SERVICE_NAME}`, {
|
|
144
|
-
encoding: 'utf8',
|
|
145
|
-
timeout: 15000
|
|
146
|
-
});
|
|
206
|
+
execSync(`systemctl --user stop ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 15000, env: userEnv() });
|
|
147
207
|
console.log('[gateway] Service stopped');
|
|
148
208
|
return { success: true };
|
|
149
209
|
} catch (err) {
|
|
@@ -152,21 +212,14 @@ function stopGateway() {
|
|
|
152
212
|
}
|
|
153
213
|
}
|
|
154
214
|
|
|
155
|
-
/**
|
|
156
|
-
* Restart the gateway service (enables it first if not already enabled).
|
|
157
|
-
*/
|
|
158
215
|
function restartGateway() {
|
|
159
216
|
ensureUserSession();
|
|
160
217
|
try {
|
|
161
|
-
execSync(`systemctl enable ${SERVICE_NAME}`, {
|
|
162
|
-
|
|
163
|
-
timeout: 10000
|
|
164
|
-
});
|
|
165
|
-
execSync(`systemctl restart ${SERVICE_NAME}`, {
|
|
166
|
-
encoding: 'utf8',
|
|
167
|
-
timeout: 15000
|
|
168
|
-
});
|
|
218
|
+
execSync(`systemctl --user enable ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 10000, env: userEnv() });
|
|
219
|
+
execSync(`systemctl --user restart ${SERVICE_NAME}`, { encoding: 'utf8', timeout: 15000, env: userEnv() });
|
|
169
220
|
console.log('[gateway] Service enabled and restarted');
|
|
221
|
+
// Sync token after restart — gateway writes its token on startup (~2s)
|
|
222
|
+
setTimeout(() => syncGatewayToken(), 3000);
|
|
170
223
|
return { success: true };
|
|
171
224
|
} catch (err) {
|
|
172
225
|
console.error('[gateway] Failed to restart:', err.message);
|
|
@@ -174,18 +227,19 @@ function restartGateway() {
|
|
|
174
227
|
}
|
|
175
228
|
}
|
|
176
229
|
|
|
177
|
-
/**
|
|
178
|
-
* Get recent gateway logs from journalctl.
|
|
179
|
-
*/
|
|
180
230
|
function getGatewayLogs(lines = 50) {
|
|
181
231
|
try {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
232
|
+
// Try user journal first, fall back to system journal
|
|
233
|
+
try {
|
|
234
|
+
return execSync(`journalctl --user -u ${SERVICE_NAME} -n ${lines} --no-pager`, {
|
|
235
|
+
encoding: 'utf8', timeout: 5000, env: userEnv(),
|
|
236
|
+
});
|
|
237
|
+
} catch {
|
|
238
|
+
return execSync(`journalctl -u ${SERVICE_NAME} -n ${lines} --no-pager`, {
|
|
239
|
+
encoding: 'utf8', timeout: 5000,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
187
242
|
} catch (err) {
|
|
188
|
-
console.error('[gateway] Failed to get logs:', err.message);
|
|
189
243
|
return `Error retrieving logs: ${err.message}`;
|
|
190
244
|
}
|
|
191
245
|
}
|
|
@@ -195,5 +249,8 @@ module.exports = {
|
|
|
195
249
|
startGateway,
|
|
196
250
|
stopGateway,
|
|
197
251
|
restartGateway,
|
|
198
|
-
getGatewayLogs
|
|
252
|
+
getGatewayLogs,
|
|
253
|
+
syncGatewayToken,
|
|
254
|
+
readGatewayToken,
|
|
255
|
+
persistGatewayToken,
|
|
199
256
|
};
|