pmtiles-swarm 0.40.0 → 0.41.1
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 +35 -0
- package/docs/running-as-a-service.md +24 -6
- package/package.json +1 -1
- package/src/library.js +25 -16
- package/src/web/index.html +59 -4
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@
|
|
|
7
7
|
### 🐞 Bug fixes
|
|
8
8
|
- _...Add new stuff here..._
|
|
9
9
|
|
|
10
|
+
## 0.41.1
|
|
11
|
+
### ✨ Features and improvements
|
|
12
|
+
|
|
13
|
+
### 🐞 Bug fixes
|
|
14
|
+
- **`KillMode=mixed` in the documented unit, and it should have been there from the start.** systemd's
|
|
15
|
+
default is `control-group`, which sends `SIGTERM` to every process in the unit at the same instant —
|
|
16
|
+
the node and the Python sidecar together. The node's shutdown then asks a sidecar that is already
|
|
17
|
+
dying to write its resume data, into a pipe that is closing, and the answer never comes.
|
|
18
|
+
|
|
19
|
+
Resume data is how an archive comes back knowing what it holds. Without it, archives return at 0%
|
|
20
|
+
after a restart and only a recheck discovers they were complete all along. `mixed` signals the main
|
|
21
|
+
process only; the node stops the sidecar itself and waits for the write. Nothing is left running —
|
|
22
|
+
anything still alive at `TimeoutStopSec` is killed, sidecar included. **Existing installs need this
|
|
23
|
+
added by hand**, followed by `systemctl daemon-reload`.
|
|
24
|
+
- **The check added in 0.40.0 asked its first question only of archives recorded as complete**, and so
|
|
25
|
+
missed the case that prompted it. An archive interrupted mid-download comes back recorded as
|
|
26
|
+
incomplete, so a restore that failed to hand it over left it absent from the engine and unreported
|
|
27
|
+
by the very check meant to notice — which is what a row reading 0% with no state at all is. Whether
|
|
28
|
+
the engine is holding an archive is now asked of everything restore handed over; whether the
|
|
29
|
+
`seedOnly` claim held is still asked only of the archives that make one.
|
|
30
|
+
|
|
31
|
+
## 0.41.0
|
|
32
|
+
### ✨ Features and improvements
|
|
33
|
+
- **An archive's details now open directly under the row you clicked**, instead of below the whole
|
|
34
|
+
table. On a node with a page of archives the panel appeared off-screen, so clicking a row looked
|
|
35
|
+
like it had done nothing but highlight it.
|
|
36
|
+
|
|
37
|
+
The panel sits in a `<tbody>` of its own, between the rows above it and the rows below — a table
|
|
38
|
+
may have several, and that is what keeps the three-second refresh from disturbing it. Rebuilding
|
|
39
|
+
the rows around the panel leaves it alone; holding it among them and re-inserting it each render
|
|
40
|
+
would blur whatever was focused inside, which would have made the seeding limit fields impossible
|
|
41
|
+
to type into for longer than one poll.
|
|
42
|
+
|
|
43
|
+
### 🐞 Bug fixes
|
|
44
|
+
|
|
10
45
|
## 0.40.0
|
|
11
46
|
### ✨ Features and improvements
|
|
12
47
|
- **Restore now checks that what it handed over is actually being seeded.** "20 of 20 archives handed
|
|
@@ -180,6 +180,11 @@ RestartSec=5
|
|
|
180
180
|
# lock and cancels downloads in flight. Worst case is about 20 seconds.
|
|
181
181
|
TimeoutStopSec=45
|
|
182
182
|
|
|
183
|
+
# The node stops the sidecar itself, and needs it alive to do so. The default
|
|
184
|
+
# signals both at once, which kills the sidecar before it can write its resume
|
|
185
|
+
# data — see "The two lines that matter".
|
|
186
|
+
KillMode=mixed
|
|
187
|
+
|
|
183
188
|
# A seeding node holds a socket per peer, and the tile reader holds file
|
|
184
189
|
# descriptors of its own.
|
|
185
190
|
LimitNOFILE=65535
|
|
@@ -215,12 +220,23 @@ down and **exits 0**, expecting to be brought back.
|
|
|
215
220
|
`Restart=on-failure` ignores an exit 0, so the first use of that button would
|
|
216
221
|
stop the node and leave the unit reporting success.
|
|
217
222
|
|
|
223
|
+
**`KillMode=mixed`, not the default.** The default is `control-group`, which
|
|
224
|
+
sends `SIGTERM` to _every_ process in the unit at the same instant — the node
|
|
225
|
+
and the Python sidecar together. The node's shutdown then asks a sidecar that is
|
|
226
|
+
already dying to write its resume data, into a pipe that is closing, and the
|
|
227
|
+
answer never comes. Resume data is how an archive comes back knowing what it
|
|
228
|
+
holds; without it, archives return at 0% after a restart and have to be
|
|
229
|
+
rechecked to find out they were complete all along.
|
|
230
|
+
|
|
231
|
+
`mixed` sends the signal to the main process only. The node stops the sidecar
|
|
232
|
+
itself, in order, and waits for the resume data to be written. Nothing is left
|
|
233
|
+
running: anything still alive when `TimeoutStopSec` expires is killed, sidecar
|
|
234
|
+
included.
|
|
235
|
+
|
|
218
236
|
**No `ExecStop=`.** systemd already sends `SIGTERM`, and the node handles it
|
|
219
237
|
from the moment it starts. `ExecStop=/bin/kill -15 $MAINPID` is redundant, and
|
|
220
|
-
becomes wrong
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
Leave `KillMode` at its default, so the sidecar goes with its parent.
|
|
238
|
+
becomes wrong under `KillMode=process` — that one leaves the rest of the unit
|
|
239
|
+
running indefinitely, so the sidecar would keep the data directory locked.
|
|
224
240
|
|
|
225
241
|
## Where it writes
|
|
226
242
|
|
|
@@ -587,8 +603,10 @@ and lives as long as it does, so a new sidecar sits on disk doing nothing until
|
|
|
587
603
|
the service is restarted. Most of what changes between releases is in there.
|
|
588
604
|
|
|
589
605
|
Nothing under `/etc/pmtiles-swarm` is touched, and restarting does not re-check
|
|
590
|
-
the archives: a clean stop writes resume data, and `TimeoutStopSec` above
|
|
591
|
-
|
|
606
|
+
the archives: a clean stop writes resume data, and `TimeoutStopSec` above leaves
|
|
607
|
+
room for it. That depends on `KillMode=mixed` — without it the sidecar is
|
|
608
|
+
signalled at the same moment as the node and dies before it can write anything,
|
|
609
|
+
which is what a library that comes back at 0% after every restart looks like.
|
|
592
610
|
|
|
593
611
|
Confirm both halves moved, since the sidecar has its own version:
|
|
594
612
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmtiles-swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.1",
|
|
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/library.js
CHANGED
|
@@ -1840,10 +1840,7 @@ export class Library {
|
|
|
1840
1840
|
* @returns {Promise<void>} - Resolves once every claim has been checked.
|
|
1841
1841
|
*/
|
|
1842
1842
|
async #verifySeeding(entries) {
|
|
1843
|
-
|
|
1844
|
-
(entry) => entry.complete !== false && entry.mode !== 'cache',
|
|
1845
|
-
);
|
|
1846
|
-
if (claimed.length === 0) return;
|
|
1843
|
+
if (entries.length === 0) return;
|
|
1847
1844
|
|
|
1848
1845
|
// One listing rather than a status call each: this runs over the whole
|
|
1849
1846
|
// library on every start, and a round trip per archive is a cost paid by
|
|
@@ -1854,25 +1851,37 @@ export class Library {
|
|
|
1854
1851
|
}
|
|
1855
1852
|
|
|
1856
1853
|
let wrong = 0;
|
|
1857
|
-
for (const entry of
|
|
1854
|
+
for (const entry of entries) {
|
|
1858
1855
|
const status = held.get(entry.infoHash);
|
|
1859
|
-
|
|
1860
|
-
// Checking is the engine doing the right thing already, and progress
|
|
1861
|
-
// during it is the fraction hashed rather than the fraction held.
|
|
1862
|
-
if (status && (status.progress >= 1 || status.state === 'checking')) {
|
|
1863
|
-
continue;
|
|
1864
|
-
}
|
|
1865
|
-
wrong += 1;
|
|
1866
1856
|
const label = `[seeding] ${entry.name}`;
|
|
1867
1857
|
|
|
1858
|
+
// Held at all is a different question from held whole, and it is asked
|
|
1859
|
+
// of everything restore handed over. Asking it only of complete archives
|
|
1860
|
+
// was the first version of this check, and it missed the case that
|
|
1861
|
+
// prompted it: an archive interrupted mid-download comes back recorded
|
|
1862
|
+
// as incomplete, so a restore that silently failed to hand it over left
|
|
1863
|
+
// it absent from the engine and unreported by the very check meant to
|
|
1864
|
+
// notice. Absent is absent — it is neither seeding nor downloading.
|
|
1868
1865
|
if (!status) {
|
|
1866
|
+
wrong += 1;
|
|
1869
1867
|
console.error(
|
|
1870
|
-
`${label}:
|
|
1871
|
-
'
|
|
1868
|
+
`${label}: restore handed this to the engine and the engine is not ` +
|
|
1869
|
+
'holding it. It is neither seeding nor downloading, and nothing ' +
|
|
1870
|
+
'will start it before the next restart.',
|
|
1872
1871
|
);
|
|
1873
1872
|
continue;
|
|
1874
1873
|
}
|
|
1875
1874
|
|
|
1875
|
+
// Everything below is about the `seedOnly` claim, and only a complete
|
|
1876
|
+
// archive makes one. An incomplete one is supposed to read as a partial
|
|
1877
|
+
// download, so its progress says nothing about whether anything is wrong.
|
|
1878
|
+
if (entry.complete === false || entry.mode === 'cache') continue;
|
|
1879
|
+
|
|
1880
|
+
// Checking is the engine doing the right thing already, and progress
|
|
1881
|
+
// during it is the fraction hashed rather than the fraction held.
|
|
1882
|
+
if (status.progress >= 1 || status.state === 'checking') continue;
|
|
1883
|
+
wrong += 1;
|
|
1884
|
+
|
|
1876
1885
|
const file = entry.savePath
|
|
1877
1886
|
? path.join(entry.savePath, entry.name)
|
|
1878
1887
|
: null;
|
|
@@ -1921,8 +1930,8 @@ export class Library {
|
|
|
1921
1930
|
|
|
1922
1931
|
if (wrong > 0) {
|
|
1923
1932
|
console.error(
|
|
1924
|
-
`[seeding] ${wrong} of ${
|
|
1925
|
-
'
|
|
1933
|
+
`[seeding] ${wrong} of ${entries.length} restored archives are not ` +
|
|
1934
|
+
'in the state the catalog describes. The lines above say which and why.',
|
|
1926
1935
|
);
|
|
1927
1936
|
}
|
|
1928
1937
|
}
|
package/src/web/index.html
CHANGED
|
@@ -89,6 +89,19 @@
|
|
|
89
89
|
tbody tr { cursor: pointer; }
|
|
90
90
|
tbody tr:hover { background: var(--panel); }
|
|
91
91
|
tbody tr.on { background: var(--panel); box-shadow: inset 3px 0 0 var(--accent); }
|
|
92
|
+
/* The open archive's details, sitting in the table under the row they
|
|
93
|
+
belong to. Not a row anybody clicks: it carries the panel rather than
|
|
94
|
+
describing an archive, so it takes none of the row affordances. */
|
|
95
|
+
tbody tr.detailrow { cursor: default; }
|
|
96
|
+
tbody tr.detailrow:hover { background: none; }
|
|
97
|
+
tbody tr.detailrow > td {
|
|
98
|
+
padding: 0;
|
|
99
|
+
white-space: normal;
|
|
100
|
+
border-bottom: 1px solid var(--line);
|
|
101
|
+
}
|
|
102
|
+
/* Flush with the row above it, where the margin that separated it from
|
|
103
|
+
the table would now be a gap in the middle of one. */
|
|
104
|
+
tbody tr.detailrow .panel { margin-top: 0; border-radius: 0; border-width: 0; }
|
|
92
105
|
td.name { white-space: normal; word-break: break-word; min-width: 16rem; }
|
|
93
106
|
.sub { color: var(--muted); font-size: 0.8rem; }
|
|
94
107
|
.track {
|
|
@@ -396,6 +409,21 @@
|
|
|
396
409
|
</tr>
|
|
397
410
|
</thead>
|
|
398
411
|
<tbody id="rows"></tbody>
|
|
412
|
+
<!-- The open archive's details, in a tbody of their own between the
|
|
413
|
+
rows above it and the rows below. A table may have several, and
|
|
414
|
+
that is what makes this work: the three-second refresh rebuilds
|
|
415
|
+
the rows on either side without touching the panel. Putting it
|
|
416
|
+
in with them and moving it each render would blur whatever was
|
|
417
|
+
focused inside it, every three seconds, while it was being
|
|
418
|
+
typed into. -->
|
|
419
|
+
<tbody id="detail-body" hidden>
|
|
420
|
+
<tr class="detailrow">
|
|
421
|
+
<td colspan="12">
|
|
422
|
+
<div class="panel" id="detail" hidden></div>
|
|
423
|
+
</td>
|
|
424
|
+
</tr>
|
|
425
|
+
</tbody>
|
|
426
|
+
<tbody id="rows-below"></tbody>
|
|
399
427
|
</table>
|
|
400
428
|
<div id="fetching"></div>
|
|
401
429
|
<div class="empty" id="empty" hidden>
|
|
@@ -403,7 +431,7 @@
|
|
|
403
431
|
pull in what your torrent client already seeds.
|
|
404
432
|
</div>
|
|
405
433
|
|
|
406
|
-
|
|
434
|
+
|
|
407
435
|
</section>
|
|
408
436
|
|
|
409
437
|
<section id="view-categories" hidden>
|
|
@@ -1235,9 +1263,28 @@
|
|
|
1235
1263
|
return `${minutes}m`;
|
|
1236
1264
|
}
|
|
1237
1265
|
|
|
1266
|
+
/**
|
|
1267
|
+
* Closes the details, panel and row together.
|
|
1268
|
+
*
|
|
1269
|
+
* Two elements have to agree now that the panel sits in the table: the
|
|
1270
|
+
* panel carries the content, and the tbody around it is what makes a row
|
|
1271
|
+
* of the list. Hiding only the panel leaves an empty bordered row in the
|
|
1272
|
+
* middle of the archives until the next refresh tidies it.
|
|
1273
|
+
* @returns {void}
|
|
1274
|
+
*/
|
|
1275
|
+
function closeDetail() {
|
|
1276
|
+
$('detail').hidden = true;
|
|
1277
|
+
$('detail-body').hidden = true;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1238
1280
|
function renderRows() {
|
|
1239
1281
|
const rows = $('rows');
|
|
1282
|
+
const below = $('rows-below');
|
|
1240
1283
|
rows.innerHTML = '';
|
|
1284
|
+
below.innerHTML = '';
|
|
1285
|
+
// Filled into the first until the open archive has been written, and
|
|
1286
|
+
// into the second afterwards, so its details sit between the two.
|
|
1287
|
+
let target = rows;
|
|
1241
1288
|
|
|
1242
1289
|
// Filtered and sorted here rather than where the data arrives, so the
|
|
1243
1290
|
// three-second poll keeps refreshing what is on screen without
|
|
@@ -1308,10 +1355,18 @@
|
|
|
1308
1355
|
selected = selected === entry.infoHash ? null : entry.infoHash;
|
|
1309
1356
|
renderRows();
|
|
1310
1357
|
if (selected) renderDetail(selected, false);
|
|
1311
|
-
else
|
|
1358
|
+
else closeDetail();
|
|
1312
1359
|
};
|
|
1313
|
-
|
|
1360
|
+
target.append(tr);
|
|
1361
|
+
|
|
1362
|
+
// Everything after the open archive goes below its details. The
|
|
1363
|
+
// panel itself is never appended here — see the tbody it lives in.
|
|
1364
|
+
if (entry.infoHash === selected) target = below;
|
|
1314
1365
|
}
|
|
1366
|
+
|
|
1367
|
+
$('detail-body').hidden = !shown.some(
|
|
1368
|
+
(entry) => entry.infoHash === selected,
|
|
1369
|
+
);
|
|
1315
1370
|
}
|
|
1316
1371
|
|
|
1317
1372
|
/**
|
|
@@ -2092,7 +2147,7 @@ Every piece is hashed against the ` +
|
|
|
2092
2147
|
{ method: 'DELETE' },
|
|
2093
2148
|
);
|
|
2094
2149
|
selected = null;
|
|
2095
|
-
|
|
2150
|
+
closeDetail();
|
|
2096
2151
|
refresh();
|
|
2097
2152
|
} catch (error) {
|
|
2098
2153
|
toast(error.message);
|