pmtiles-swarm 0.4.1 → 0.4.2
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/CHANGELOG.md +25 -0
- package/docs/running-as-a-service.md +68 -0
- package/package.json +1 -1
- package/src/engines/composite.js +26 -0
- package/src/hooks.js +59 -27
- package/src/web/index.html +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.4.2
|
|
11
|
+
### 🐞 Bug fixes
|
|
12
|
+
- **Resume data is saved on a node running more than one engine.** The periodic save is only
|
|
13
|
+
scheduled if the engine offers `saveResume`, and the composite engine — the one in use
|
|
14
|
+
whenever `secondaryEngines` is set — did not, so it was never scheduled at all. The only
|
|
15
|
+
writes left were at shutdown, and those hit the second half of this: the sidecar asked
|
|
16
|
+
`need_save_resume_data()` first, which answers "has anything changed since the last save"
|
|
17
|
+
rather than "does a resume file exist". An archive that had been seeding since it was added
|
|
18
|
+
answers no, so nothing was written for it and it re-hashed its whole store on every start —
|
|
19
|
+
half an hour of disk, for 800 GB, before it serves anything. Both halves are fixed; the
|
|
20
|
+
sidecar half ships in `pmtiles-torrent`.
|
|
21
|
+
- **A hook is no longer killed for being talkative.** Its output was collected whole into a
|
|
22
|
+
buffer, and past that buffer's size the child is killed — so a hook that generates a planet
|
|
23
|
+
could die hours in for the offence of saying too much, and the output that would have
|
|
24
|
+
explained it was the thing that overflowed. Output is streamed now and only the last twenty
|
|
25
|
+
lines are kept, so how much a hook says cannot decide whether it survives.
|
|
26
|
+
|
|
27
|
+
### 📚 Documentation
|
|
28
|
+
- **Sharing a folder with another service**, in the service guide: group membership is only the
|
|
29
|
+
first of three steps, and a folder at 0755 gives that group `r-x` — enough to hash and seed an
|
|
30
|
+
archive and not enough for `latestLink`, retention or a hook, so it looks like it worked until
|
|
31
|
+
the first thing that writes.
|
|
32
|
+
- **The read-only hooks panel says to restart.** Setting `allowHooksFromApi` in the config file
|
|
33
|
+
unlocks nothing until the node reads it, which it does once, at startup.
|
|
34
|
+
|
|
10
35
|
## 0.4.1
|
|
11
36
|
### 🐞 Bug fixes
|
|
12
37
|
- **A feed no longer walks backwards through its own history.** An item already taken was
|
|
@@ -214,6 +214,74 @@ anything that holds data:
|
|
|
214
214
|
`ReadWritePaths` names, so every one of those has to be listed. An archive
|
|
215
215
|
directory on another mount needs its own entry.
|
|
216
216
|
|
|
217
|
+
## Sharing a folder with another service
|
|
218
|
+
|
|
219
|
+
A folder produced by something else — a generation script, or a directory a
|
|
220
|
+
torrent client already owns — needs three things, and group membership is only
|
|
221
|
+
the first of them.
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
# 1. Put the service account in the owning group.
|
|
225
|
+
sudo usermod -aG qbittorrent-nox pmtiles-swarm
|
|
226
|
+
|
|
227
|
+
# 2. Give that group write, and setgid so new entries inherit it.
|
|
228
|
+
sudo find /mnt/hd-16TB/store/generated -type d -exec chmod 2775 {} +
|
|
229
|
+
sudo find /mnt/hd-16TB/store/generated -type f -exec chmod 664 {} +
|
|
230
|
+
|
|
231
|
+
# 3. Make what the service creates group-writable too.
|
|
232
|
+
sudo systemctl edit pmtiles-swarm
|
|
233
|
+
sudo systemctl restart pmtiles-swarm
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Step 3 opens an override; the two lines to put in it are:
|
|
237
|
+
|
|
238
|
+
```ini
|
|
239
|
+
[Service]
|
|
240
|
+
UMask=0002
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**A folder at 0755 gives the group `r-x`.** Membership alone buys read access
|
|
244
|
+
and nothing else, which is enough to hash and seed an archive and not enough to
|
|
245
|
+
do anything else with the folder — so this looks like it worked until the first
|
|
246
|
+
thing that writes.
|
|
247
|
+
|
|
248
|
+
Three features want write, and it is worth knowing which, because a read-only
|
|
249
|
+
folder is a perfectly reasonable way to run:
|
|
250
|
+
|
|
251
|
+
| | |
|
|
252
|
+
| --- | --- |
|
|
253
|
+
| `latestLink` | Creates and replaces a name in the folder |
|
|
254
|
+
| `keep`, `keepDays` | Deletes retired builds |
|
|
255
|
+
| `onComplete` | Whatever the script does, since it runs as this account |
|
|
256
|
+
|
|
257
|
+
Renaming and deleting need write on the **directory**, not on the file, which is
|
|
258
|
+
why the directory bits are the ones that matter. `UMask=0002` matters for the
|
|
259
|
+
other direction: without it a file the service creates is `0644`, and the other
|
|
260
|
+
service can delete it but not modify it.
|
|
261
|
+
|
|
262
|
+
**Group membership is read when a process starts**, so the restart is not
|
|
263
|
+
optional. Neither is `ReadWritePaths`: `ProtectSystem=strict` presents the rest
|
|
264
|
+
of the filesystem as read-only inside the unit's namespace, and the write is
|
|
265
|
+
refused there before the permission bits are consulted. Every folder outside
|
|
266
|
+
`/var/lib/pmtiles-swarm` has to be named, in a drop-in from
|
|
267
|
+
`systemctl edit pmtiles-swarm`:
|
|
268
|
+
|
|
269
|
+
```ini
|
|
270
|
+
[Service]
|
|
271
|
+
ReadWritePaths=/mnt/store/generated /mnt/work/planetiler
|
|
272
|
+
UMask=0002
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`ReadWritePaths=` accumulates, so a drop-in adds to what the unit already lists
|
|
276
|
+
rather than replacing it.
|
|
277
|
+
|
|
278
|
+
Two things that look like checks and are not. `id pmtiles-swarm` reads
|
|
279
|
+
`/etc/group` and shows the new group the instant `usermod` returns, whether or
|
|
280
|
+
not the running process has it — read `/proc/$(systemctl show -p MainPID --value
|
|
281
|
+
pmtiles-swarm)/status` instead. And `sudo -u pmtiles-swarm touch …` runs outside
|
|
282
|
+
the unit's namespace, so it succeeds on permissions alone while the service is
|
|
283
|
+
still being refused.
|
|
284
|
+
|
|
217
285
|
## The sidecar
|
|
218
286
|
|
|
219
287
|
The libtorrent engine runs Python as a child process, so the service user needs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "BitTorrent distribution for PMTiles map archives: create torrents, watch folders, publish and subscribe to RSS feeds, and seed through qBittorrent or an embedded client",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
package/src/engines/composite.js
CHANGED
|
@@ -340,6 +340,32 @@ export class CompositeEngine {
|
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Persists resume data on every engine that keeps any.
|
|
345
|
+
*
|
|
346
|
+
* Missing entirely until now, and the caller checks for it before setting
|
|
347
|
+
* its timer — so on any node with a secondary engine the periodic save was
|
|
348
|
+
* never scheduled, and the only resume data ever written was whatever the
|
|
349
|
+
* shutdown path managed. An archive that had been seeding since it was added
|
|
350
|
+
* therefore re-hashed its whole store on every start, which for 800 GB is
|
|
351
|
+
* half an hour of disk before it serves anything.
|
|
352
|
+
* @param {string} [infoHash] - One archive, or all of them when omitted.
|
|
353
|
+
* @returns {Promise<void>} - Resolves once every engine has been asked.
|
|
354
|
+
*/
|
|
355
|
+
async saveResume(infoHash) {
|
|
356
|
+
for (const engine of [this.#primary, ...this.#secondaries]) {
|
|
357
|
+
// WebTorrent keeps none, and says so by not offering the method.
|
|
358
|
+
if (!engine.saveResume) continue;
|
|
359
|
+
try {
|
|
360
|
+
await engine.saveResume(infoHash);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn(
|
|
363
|
+
`[composite] ${engine.name} could not save resume data: ${error.message}`,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
343
369
|
/**
|
|
344
370
|
* Peers from every engine, labelled with which one found them.
|
|
345
371
|
* @param {string} infoHash - The archive.
|
package/src/hooks.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
+
/** How much of a hook's output to repeat into this node's own log. */
|
|
5
|
+
const TAIL_LINES = 20;
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
8
|
* Running something when an archive arrives, and when it finishes.
|
|
6
9
|
*
|
|
@@ -200,32 +203,61 @@ export class ProgramHooks {
|
|
|
200
203
|
console.log(`[${label}] ${entry.name}: ${command} ${filled.join(' ')}`);
|
|
201
204
|
|
|
202
205
|
return new Promise((resolve) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
206
|
+
const timeout = (timeoutSeconds ?? 0) * 1000;
|
|
207
|
+
const child = spawn(command, filled, {
|
|
208
|
+
// A tile build runs for hours. Nothing here should assume otherwise,
|
|
209
|
+
// so the default is no timeout at all.
|
|
210
|
+
...(timeout > 0 ? { timeout } : {}),
|
|
211
|
+
cwd: hook.cwd,
|
|
212
|
+
env: { ...process.env, ...(hook.env ?? {}) },
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Streamed, and only the tail is kept.
|
|
216
|
+
//
|
|
217
|
+
// This used to collect the whole of stdout and stderr into a buffer, and
|
|
218
|
+
// a buffer has a size: past it, execFile kills the child. A hook that
|
|
219
|
+
// generates a planet says far more than any buffer worth holding, so a
|
|
220
|
+
// build could be killed hours in for the offence of being talkative —
|
|
221
|
+
// and the output that would have explained it was the thing that
|
|
222
|
+
// overflowed. Nothing is held now but the last few lines, so how much a
|
|
223
|
+
// hook says cannot decide whether it survives.
|
|
224
|
+
const tail = [];
|
|
225
|
+
let partial = '';
|
|
226
|
+
const collect = (chunk) => {
|
|
227
|
+
partial += chunk;
|
|
228
|
+
const lines = partial.split('\n');
|
|
229
|
+
partial = lines.pop() ?? '';
|
|
230
|
+
for (const line of lines) {
|
|
231
|
+
tail.push(line);
|
|
232
|
+
if (tail.length > TAIL_LINES) tail.shift();
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
236
|
+
stream?.setEncoding('utf8');
|
|
237
|
+
stream?.on('data', collect);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const report = (problem) => {
|
|
241
|
+
if (problem) {
|
|
242
|
+
console.error(`[${label}] ${entry.name}: ${problem}`);
|
|
243
|
+
} else {
|
|
244
|
+
console.log(`[${label}] ${entry.name}: finished`);
|
|
245
|
+
}
|
|
246
|
+
// A last line with no newline after it is still a line.
|
|
247
|
+
if (partial) collect('\n');
|
|
248
|
+
for (const line of tail) {
|
|
249
|
+
if (line.trim()) console.log(`[${label}] ${line}`);
|
|
250
|
+
}
|
|
251
|
+
resolve();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// A command that could not be started at all — no such file, not
|
|
255
|
+
// executable — never reaches 'close'.
|
|
256
|
+
child.on('error', (error) => report(error.message));
|
|
257
|
+
child.on('close', (code, signal) => {
|
|
258
|
+
if (signal) return report(`killed by ${signal}`);
|
|
259
|
+
report(code === 0 ? undefined : `exited with code ${code}`);
|
|
260
|
+
});
|
|
229
261
|
});
|
|
230
262
|
}
|
|
231
263
|
}
|
package/src/web/index.html
CHANGED
|
@@ -2602,7 +2602,11 @@
|
|
|
2602
2602
|
token that manages maps — it would be one that runs code.
|
|
2603
2603
|
Edit them in the config file, or set
|
|
2604
2604
|
<code>"allowHooksFromApi": true</code> there to take that
|
|
2605
|
-
trade deliberately and unlock this panel
|
|
2605
|
+
trade deliberately and unlock this panel —
|
|
2606
|
+
<b>then restart</b>, since the config file is read once at
|
|
2607
|
+
startup and nothing re-reads it. This panel is still
|
|
2608
|
+
read-only because the running node has not seen that
|
|
2609
|
+
setting, whatever the file now says.
|
|
2606
2610
|
</p>`
|
|
2607
2611
|
}
|
|
2608
2612
|
</div>`;
|