geniejars 0.2.11 → 0.2.13
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 +6 -0
- package/geniejars.config.template.json +1 -1
- package/package.json +1 -1
- package/script/geniejars.mjs +3 -2
- package/src/agent.mjs +34 -1
- package/src/allocate.mjs +19 -17
- package/src/pool.mjs +6 -1
- package/src/run.mjs +80 -0
package/README.md
CHANGED
|
@@ -217,6 +217,12 @@ script/
|
|
|
217
217
|
|
|
218
218
|
See [COMMON-ISSUES.md](./COMMON-ISSUES.md) for solving common issues.
|
|
219
219
|
|
|
220
|
+
## Links and contact
|
|
221
|
+
|
|
222
|
+
[Github](https://github.com/SemanticTools/geniejars)
|
|
223
|
+
Contact me on my [Twitter/X account](https://x.com/willm_2022)
|
|
224
|
+
|
|
225
|
+
|
|
220
226
|
## License
|
|
221
227
|
|
|
222
228
|
MIT — see [LICENSE.md](./LICENSE.md)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"managerGroup": "geniejars-mgr",
|
|
8
8
|
"stateFile": "/tmp/geniejars-state.json",
|
|
9
9
|
"lockFile": "/tmp/geniejars-state.lock",
|
|
10
|
-
"backupDir": "
|
|
10
|
+
"backupDir": "~/geniejars-backups",
|
|
11
11
|
"socketDir": "/tmp/geniejars-sockets",
|
|
12
12
|
"nodePath": "/usr/bin/node",
|
|
13
13
|
"portBase": 10000,
|
package/package.json
CHANGED
package/script/geniejars.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { fileURLToPath } from 'url'
|
|
|
34
34
|
import { loadConfig, readState, allNames, homeDir, resolveName } from '../src/pool.mjs'
|
|
35
35
|
import { allocate, deallocate } from '../src/allocate.mjs'
|
|
36
36
|
import { prepare } from '../src/prepare.mjs'
|
|
37
|
-
import { runAs, startAs, stopProcess, agentStatus, ensureAgent } from '../src/run.mjs'
|
|
37
|
+
import { runAs, startAs, stopProcess, agentStatus, ensureAgent, killAgent } from '../src/run.mjs'
|
|
38
38
|
import { normalize } from '../src/normalize.mjs'
|
|
39
39
|
|
|
40
40
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -427,7 +427,7 @@ if (cmd === 'setup') {
|
|
|
427
427
|
if (!tag) die('--tag requires a value')
|
|
428
428
|
}
|
|
429
429
|
try {
|
|
430
|
-
const name = await allocate(config, { backup, clear, tag })
|
|
430
|
+
const name = await allocate(config, { backup, clear, tag, onBackup: p => console.log(`Backup: ${p}`) })
|
|
431
431
|
if (select) await setSelected(name)
|
|
432
432
|
console.log(name)
|
|
433
433
|
} catch (err) { die(err.message) }
|
|
@@ -437,6 +437,7 @@ if (cmd === 'setup') {
|
|
|
437
437
|
const state = await readState(config)
|
|
438
438
|
const name = requireName(args[0], state)
|
|
439
439
|
try {
|
|
440
|
+
await killAgent(config, name)
|
|
440
441
|
await deallocate(config, name)
|
|
441
442
|
if (getSelected() === name) await clearSelected()
|
|
442
443
|
console.log(`Released: ${name}`)
|
package/src/agent.mjs
CHANGED
|
@@ -78,7 +78,7 @@ function handleConnection(socket) {
|
|
|
78
78
|
socket.on('error', () => {})
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
function handleRequest(req, socket) {
|
|
81
|
+
async function handleRequest(req, socket) {
|
|
82
82
|
switch (req.type) {
|
|
83
83
|
|
|
84
84
|
case 'run': {
|
|
@@ -166,6 +166,39 @@ function handleRequest(req, socket) {
|
|
|
166
166
|
break
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
case 'killall': {
|
|
170
|
+
send(socket, { type: 'killed' })
|
|
171
|
+
socket.end()
|
|
172
|
+
setImmediate(() => process.kill(-1, 'SIGTERM'))
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
case 'clear': {
|
|
177
|
+
const home = process.env.HOME
|
|
178
|
+
try {
|
|
179
|
+
const entries = await fs.readdir(home).catch(() => [])
|
|
180
|
+
await Promise.all(entries.map(e => fs.rm(path.join(home, e), { recursive: true, force: true })))
|
|
181
|
+
send(socket, { type: 'cleared' })
|
|
182
|
+
} catch (err) {
|
|
183
|
+
send(socket, { type: 'error', message: err.message })
|
|
184
|
+
}
|
|
185
|
+
socket.end()
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'backup': {
|
|
190
|
+
const home = process.env.HOME
|
|
191
|
+
const child = spawn('tar', ['-czf', '-', '-C', path.dirname(home), path.basename(home)])
|
|
192
|
+
child.stdout.on('data', d => send(socket, { type: 'backup-data', data: d.toString('base64') }))
|
|
193
|
+
child.on('error', err => { send(socket, { type: 'error', message: err.message }); socket.end() })
|
|
194
|
+
child.on('close', code => {
|
|
195
|
+
if (code < 2) send(socket, { type: 'backup-done' })
|
|
196
|
+
else send(socket, { type: 'error', message: `tar exited with code ${code}` })
|
|
197
|
+
socket.end()
|
|
198
|
+
})
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
|
|
169
202
|
default: {
|
|
170
203
|
send(socket, { type: 'error', message: `Unknown request type: ${req.type}` })
|
|
171
204
|
socket.end()
|
package/src/allocate.mjs
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { acquireLock, readState, writeState, freeNames, homeDir } from './pool.mjs'
|
|
6
|
-
|
|
7
|
-
const execFileAsync = promisify(execFile)
|
|
3
|
+
import { acquireLock, readState, writeState, freeNames } from './pool.mjs'
|
|
4
|
+
import { ensureAgent, clearHomeAs, backupHomeAs } from './run.mjs'
|
|
8
5
|
|
|
9
6
|
// Allocate a free geniejars. Returns the geniejars name.
|
|
10
7
|
// Options:
|
|
@@ -12,7 +9,7 @@ const execFileAsync = promisify(execFile)
|
|
|
12
9
|
// clear: boolean (default true) - clear home dir contents
|
|
13
10
|
// tag: string (default null) - label stored in state
|
|
14
11
|
export async function allocate(config, options = {}) {
|
|
15
|
-
const { backup = false, clear = true, tag = null } = options
|
|
12
|
+
const { backup = false, clear = true, tag = null, onBackup = null } = options
|
|
16
13
|
|
|
17
14
|
const release = await acquireLock(config)
|
|
18
15
|
try {
|
|
@@ -25,7 +22,10 @@ export async function allocate(config, options = {}) {
|
|
|
25
22
|
|
|
26
23
|
const name = free[0]
|
|
27
24
|
|
|
28
|
-
if (backup)
|
|
25
|
+
if (backup) {
|
|
26
|
+
const archivePath = await backupHome(config, name)
|
|
27
|
+
if (onBackup) onBackup(archivePath)
|
|
28
|
+
}
|
|
29
29
|
if (clear) await clearHome(config, name)
|
|
30
30
|
|
|
31
31
|
// Allocate ports
|
|
@@ -77,20 +77,22 @@ export async function deallocate(config, name) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Remove all contents inside a geniejars's home dir (not the dir itself).
|
|
80
|
-
// Runs
|
|
80
|
+
// Runs via the gjar agent so the user can delete its own files regardless of permissions.
|
|
81
|
+
// Requires the manager server to be up (geniejars sys up).
|
|
81
82
|
export async function clearHome(config, name) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
await Promise.all(entries.map(e => fs.rm(path.join(home, e), { recursive: true, force: true })))
|
|
83
|
+
await ensureAgent(config, name)
|
|
84
|
+
await clearHomeAs(config, name)
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Create a tar.gz snapshot of the geniejars's home into backupDir.
|
|
88
|
-
//
|
|
88
|
+
// Agent tars its own home (as the gjar user), streams it over the socket,
|
|
89
|
+
// manager writes the archive. Returns the path to the archive.
|
|
89
90
|
export async function backupHome(config, name) {
|
|
90
|
-
|
|
91
|
+
await ensureAgent(config, name)
|
|
91
92
|
await fs.mkdir(config.backupDir, { recursive: true })
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
const now = new Date()
|
|
94
|
+
const date = now.toISOString().slice(0, 10).replace(/-/g, '')
|
|
95
|
+
const time = now.toISOString().slice(11, 19).replace(/:/g, '')
|
|
96
|
+
const basePath = path.join(config.backupDir, `${name}-${date}-${time}`)
|
|
97
|
+
return backupHomeAs(config, name, basePath)
|
|
96
98
|
}
|
package/src/pool.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises'
|
|
2
2
|
import { createWriteStream } from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
|
+
import os from 'os'
|
|
4
5
|
|
|
5
6
|
const LOCK_RETRIES = 20
|
|
6
7
|
const LOCK_DELAY_BASE_MS = 50
|
|
@@ -25,7 +26,11 @@ export async function loadConfig(configPath) {
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
const text = await fs.readFile(p, 'utf8')
|
|
28
|
-
const
|
|
29
|
+
const raw = JSON.parse(text)
|
|
30
|
+
const home = os.homedir()
|
|
31
|
+
const config = Object.fromEntries(
|
|
32
|
+
Object.entries(raw).map(([k, v]) => [k, typeof v === 'string' && v.startsWith('~/') ? path.join(home, v.slice(2)) : v])
|
|
33
|
+
)
|
|
29
34
|
if (!config.nodePath) config.nodePath = process.execPath
|
|
30
35
|
return config
|
|
31
36
|
}
|
package/src/run.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import net from 'net'
|
|
2
2
|
import fs from 'fs'
|
|
3
3
|
import path from 'path'
|
|
4
|
+
import { createHash } from 'crypto'
|
|
4
5
|
import { homeDir } from './pool.mjs'
|
|
5
6
|
|
|
6
7
|
function socketPath(config, name) {
|
|
@@ -62,6 +63,12 @@ function request(config, name, req, options = {}) {
|
|
|
62
63
|
case 'stopped':
|
|
63
64
|
settle(resolve, { stopped: true })
|
|
64
65
|
break
|
|
66
|
+
case 'cleared':
|
|
67
|
+
settle(resolve, { cleared: true })
|
|
68
|
+
break
|
|
69
|
+
case 'killed':
|
|
70
|
+
settle(resolve, { killed: true })
|
|
71
|
+
break
|
|
65
72
|
case 'status':
|
|
66
73
|
settle(resolve, { running: msg.running, pid: msg.pid })
|
|
67
74
|
break
|
|
@@ -132,11 +139,84 @@ export async function stopProcess(config, name) {
|
|
|
132
139
|
return request(config, name, { type: 'stop' })
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
// Kill all processes owned by the gjar user. Best-effort — ignores errors if agent is not running.
|
|
143
|
+
export async function killAgent(config, name) {
|
|
144
|
+
return request(config, name, { type: 'killall' }).catch(() => {})
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
// Get agent status.
|
|
136
148
|
export async function agentStatus(config, name) {
|
|
137
149
|
return request(config, name, { type: 'status' })
|
|
138
150
|
}
|
|
139
151
|
|
|
152
|
+
// Clear all contents of the geniejars's home dir, running as the gjar user.
|
|
153
|
+
export async function clearHomeAs(config, name) {
|
|
154
|
+
return request(config, name, { type: 'clear' })
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Back up the geniejars's home dir: agent tars and streams, manager writes and hashes.
|
|
158
|
+
// basePath is the archive path without extension (e.g. /backups/gjar002-20260324-1743).
|
|
159
|
+
// Writes to basePath.tmp while streaming, then renames to basePath-{hash8}.tar.gz.
|
|
160
|
+
// Returns the final archive path.
|
|
161
|
+
export function backupHomeAs(config, name, basePath) {
|
|
162
|
+
const tmpPath = basePath + '.tmp'
|
|
163
|
+
const writeStream = fs.createWriteStream(tmpPath)
|
|
164
|
+
const hash = createHash('sha256')
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const client = net.createConnection(socketPath(config, name))
|
|
167
|
+
let buf = ''
|
|
168
|
+
let settled = false
|
|
169
|
+
let receivedDone = false
|
|
170
|
+
|
|
171
|
+
function settle(fn, val) {
|
|
172
|
+
if (settled) return
|
|
173
|
+
settled = true
|
|
174
|
+
fn(val)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
client.on('connect', () => client.write(JSON.stringify({ type: 'backup' }) + '\n'))
|
|
178
|
+
client.on('data', chunk => {
|
|
179
|
+
buf += chunk.toString()
|
|
180
|
+
let nl
|
|
181
|
+
while ((nl = buf.indexOf('\n')) !== -1) {
|
|
182
|
+
const line = buf.slice(0, nl).trim()
|
|
183
|
+
buf = buf.slice(nl + 1)
|
|
184
|
+
if (!line) continue
|
|
185
|
+
let msg
|
|
186
|
+
try { msg = JSON.parse(line) } catch { continue }
|
|
187
|
+
switch (msg.type) {
|
|
188
|
+
case 'backup-data': {
|
|
189
|
+
const data = Buffer.from(msg.data, 'base64')
|
|
190
|
+
writeStream.write(data)
|
|
191
|
+
hash.update(data)
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
case 'backup-done': {
|
|
195
|
+
receivedDone = true
|
|
196
|
+
const digest = hash.digest('hex').slice(0, 8)
|
|
197
|
+
const finalPath = `${basePath}-${digest}.tar.gz`
|
|
198
|
+
writeStream.end(() => {
|
|
199
|
+
fs.rename(tmpPath, finalPath, err => {
|
|
200
|
+
if (err) settle(reject, err)
|
|
201
|
+
else settle(resolve, finalPath)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
case 'error':
|
|
207
|
+
writeStream.destroy()
|
|
208
|
+
fs.unlink(tmpPath, () => {})
|
|
209
|
+
settle(reject, new Error(msg.message))
|
|
210
|
+
client.destroy()
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
client.on('error', err => { writeStream.destroy(); fs.unlink(tmpPath, () => {}); settle(reject, new Error(`Agent connection failed (${name}): ${err.message}`)) })
|
|
216
|
+
client.on('close', () => { if (!settled && !receivedDone) { writeStream.destroy(); fs.unlink(tmpPath, () => {}); settle(reject, new Error('Connection closed unexpectedly')) } })
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
140
220
|
// Ensure a geniejars agent is up via the shell server.
|
|
141
221
|
// Requires the shell server to be running (geniejars sys up).
|
|
142
222
|
export function ensureAgent(config, name) {
|