hmgr 0.0.20 → 0.0.21
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/dist/gui/main.js +87 -4
- package/dist/gui/renderer/index.html +15 -0
- package/dist/gui/renderer/renderer.js +29 -1
- package/dist/gui/selbst-update.js +74 -0
- package/package.json +1 -1
package/dist/gui/main.js
CHANGED
|
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
13
13
|
import { bauen, umgebungLesen } from "../cli/main.js";
|
|
14
14
|
import { quelleAlsText, quelleAusText } from "../adapter/quelle-text.js";
|
|
15
15
|
import { paketVersion } from "../adapter/paket-version.js";
|
|
16
|
+
import { infoPlistPfad, neuereVersionAufPlatte } from "./selbst-update.js";
|
|
16
17
|
import { quelleName, quelleSchluessel } from "../kern/modell.js";
|
|
17
18
|
import { kannProfile } from "../ports/harness.js";
|
|
18
19
|
import { iconFuerQuelle } from "./icons.js";
|
|
@@ -331,10 +332,18 @@ function registriereIpc() {
|
|
|
331
332
|
// Neustart-Knopf an der GUI-Update-Zeile (E80): installiert das geladene Paket sofort, statt auf
|
|
332
333
|
// das nächste Beenden zu warten. Im Smoke-Test ein No-op — dort läuft `updatePruefen` ohnehin nie,
|
|
333
334
|
// der Knopf bleibt verborgen, aber ein Absturz durch ein fehlendes Update darf trotzdem nie sein.
|
|
335
|
+
// E90: mit einem geladenen Windows-Paket `quitAndInstall`, sonst (macOS: das Bundle liegt schon
|
|
336
|
+
// ersetzt auf der Platte) ein Neustart des Prozesses. Entscheidend ist das Merker-Flag, nicht die
|
|
337
|
+
// Plattform allein.
|
|
334
338
|
ipcMain.on('hmgr:neuStarten', () => {
|
|
335
339
|
if (smoketest)
|
|
336
340
|
return;
|
|
337
|
-
|
|
341
|
+
if (geladenesPaketVorhanden) {
|
|
342
|
+
autoUpdater.quitAndInstall();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
app.relaunch();
|
|
346
|
+
app.exit(0);
|
|
338
347
|
});
|
|
339
348
|
ipcMain.handle('hmgr:arbeitsverzeichnisHinzufuegen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisHinzufuegen(pfad)));
|
|
340
349
|
ipcMain.handle('hmgr:arbeitsverzeichnisVergessen', async (_event, pfad) => auffangen(async () => anwendungsfaelle.arbeitsverzeichnisVergessen(pfad)));
|
|
@@ -500,6 +509,7 @@ function fensterOeffnen() {
|
|
|
500
509
|
});
|
|
501
510
|
hauptfenster.on('closed', () => {
|
|
502
511
|
hauptfenster = null;
|
|
512
|
+
zuletztGemeldeteGuiVersion = null;
|
|
503
513
|
});
|
|
504
514
|
// Der <title> der index.html setzt den Fenstertitel beim Laden sonst zurück und überschriebe die
|
|
505
515
|
// Option oben (E72). Der DOM-Titel bleibt dabei, wie er ist, nur das Fenster folgt ihm nicht mehr.
|
|
@@ -1202,6 +1212,34 @@ function fensterOeffnen() {
|
|
|
1202
1212
|
// E72: der Fenstertitel steht nicht im DOM (dort steht weiter der <title> der
|
|
1203
1213
|
// index.html), er lässt sich nur hier im Main-Prozess ablesen.
|
|
1204
1214
|
.then(() => console.log(`[smoke] Fenster: ${JSON.stringify({ fenstertitel: fenster.getTitle() })}`))
|
|
1215
|
+
// E90: erst nach dem DOM-Auszug die Platten-Erkennung auslösen (Naht über
|
|
1216
|
+
// HMGR_GUI_SMOKE_PLATTENVERSION), damit die Leer-Prüfungen der Update-Zeile darin gelten.
|
|
1217
|
+
// Danach ein zweiter, kleiner Auszug nur zum Neustart-Modal.
|
|
1218
|
+
.then(async () => {
|
|
1219
|
+
if (process.env.HMGR_GUI_SMOKE_PLATTENVERSION === undefined)
|
|
1220
|
+
return;
|
|
1221
|
+
plattenVersionPruefen(true);
|
|
1222
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
1223
|
+
const auszug = await fenster.webContents.executeJavaScript(`(async () => {
|
|
1224
|
+
var dlg = document.getElementById('d-neustart');
|
|
1225
|
+
// Ein anderer Dialog steht noch offen (Rest des ersten Auszugs): das Modal wartet
|
|
1226
|
+
// solange, und öffnet erst, wenn der andere zu ist.
|
|
1227
|
+
var anderePlatzhalter = Array.from(document.querySelectorAll('dialog[open]')).filter(function (d) { return d !== dlg; });
|
|
1228
|
+
var wartetSolangeAndererOffen = anderePlatzhalter.length > 0 && dlg?.open !== true;
|
|
1229
|
+
anderePlatzhalter.forEach(function (d) { d.close(); });
|
|
1230
|
+
await new Promise(function (r) { setTimeout(r, 300); });
|
|
1231
|
+
var offen = dlg?.open === true;
|
|
1232
|
+
var titel = document.getElementById('dn-titel')?.textContent ?? null;
|
|
1233
|
+
var zeile = document.getElementById('selbstupdate-gui');
|
|
1234
|
+
var zeileText = zeile && !zeile.hidden ? zeile.textContent : null;
|
|
1235
|
+
var neustartKnopfSichtbar = document.getElementById('selbstupdate-neustart')?.hidden === false;
|
|
1236
|
+
var knopfTexte = Array.from(dlg?.querySelectorAll('.dfuss button') ?? []).map(function (b) { return b.textContent; }).join('|');
|
|
1237
|
+
dlg?.querySelector('button[value="spaeter"]')?.click();
|
|
1238
|
+
var nachSpaeterOffen = dlg?.open === true;
|
|
1239
|
+
return JSON.stringify({ wartetSolangeAndererOffen, offen, titel, zeileText, neustartKnopfSichtbar, knopfTexte, nachSpaeterOffen });
|
|
1240
|
+
})()`);
|
|
1241
|
+
console.log(`[smoke] Neustart-Modal: ${String(auszug)}`);
|
|
1242
|
+
})
|
|
1205
1243
|
.finally(() => app.quit());
|
|
1206
1244
|
}, 3000);
|
|
1207
1245
|
});
|
|
@@ -1215,7 +1253,10 @@ function fensterOeffnen() {
|
|
|
1215
1253
|
fenster.webContents.once('did-finish-load', () => {
|
|
1216
1254
|
fenster.webContents.send('hmgr:aktualisierenAnstossen', 'start');
|
|
1217
1255
|
});
|
|
1218
|
-
const intervall = setInterval(() =>
|
|
1256
|
+
const intervall = setInterval(() => {
|
|
1257
|
+
fenster.webContents.send('hmgr:aktualisierenAnstossen', 'intervall');
|
|
1258
|
+
plattenVersionPruefen();
|
|
1259
|
+
}, SECHS_STUNDEN_MS);
|
|
1219
1260
|
fenster.on('closed', () => clearInterval(intervall));
|
|
1220
1261
|
}
|
|
1221
1262
|
// Die GUI erfährt von Änderungen außerhalb ihrer selbst (E62): Fokus-Reload und Datei-Watcher.
|
|
@@ -1224,7 +1265,10 @@ function fensterOeffnen() {
|
|
|
1224
1265
|
// Aktualisieren (E44): ein Event dürfte sonst in den DOM-Auszug pfuschen.
|
|
1225
1266
|
if (!smoketest) {
|
|
1226
1267
|
const fenster = hauptfenster;
|
|
1227
|
-
fenster.on('focus', () =>
|
|
1268
|
+
fenster.on('focus', () => {
|
|
1269
|
+
neuLadenAnstossen();
|
|
1270
|
+
plattenVersionPruefen();
|
|
1271
|
+
});
|
|
1228
1272
|
void beobachtetePfade().then((pfade) => {
|
|
1229
1273
|
if (fenster.isDestroyed())
|
|
1230
1274
|
return; // Fenster schon zu, während die Pfade ermittelt wurden
|
|
@@ -1281,7 +1325,8 @@ function updatePruefen() {
|
|
|
1281
1325
|
.catch((fehler) => {
|
|
1282
1326
|
console.log(`[verlauf] ${fehler instanceof Error ? fehler.message : String(fehler)}`);
|
|
1283
1327
|
});
|
|
1284
|
-
|
|
1328
|
+
geladenesPaketVorhanden = true;
|
|
1329
|
+
selbstUpdateMelden({ art: 'gui', version: guiVersion, neuStartNoetig: 'geladen' });
|
|
1285
1330
|
});
|
|
1286
1331
|
}
|
|
1287
1332
|
void autoUpdater.checkForUpdatesAndNotify().catch((fehler) => {
|
|
@@ -1298,6 +1343,44 @@ function selbstUpdateMelden(nachricht) {
|
|
|
1298
1343
|
return;
|
|
1299
1344
|
hauptfenster.webContents.send('hmgr:selbstUpdate', nachricht);
|
|
1300
1345
|
}
|
|
1346
|
+
/** Windows: ein Paket ist geladen und wartet auf `quitAndInstall` (E90, Merker für `hmgr:neuStarten`). */
|
|
1347
|
+
let geladenesPaketVorhanden = false;
|
|
1348
|
+
/** Die beim Start festgehaltene Version des laufenden Prozesses. */
|
|
1349
|
+
const laufendeVersion = paketVersion();
|
|
1350
|
+
const PLATTEN_PRUEFUNG_MIN_ABSTAND_MS = 30_000;
|
|
1351
|
+
let letztePlattenPruefung = 0;
|
|
1352
|
+
let zuletztGemeldeteGuiVersion = null;
|
|
1353
|
+
/**
|
|
1354
|
+
* E90, macOS-Weg: das App-Bundle wird im Hintergrund vom Deploy-Skript (ditto/rsync) ersetzt, der
|
|
1355
|
+
* laufende Prozess merkt das nicht. Verglichen wird die Version in `Contents/Info.plist` mit der
|
|
1356
|
+
* beim Start festgehaltenen. Die Info.plist statt der package.json in `app.asar`, weil Electron den
|
|
1357
|
+
* asar-Header je Pfad zwischenspeichert und ein ersetztes Archiv nicht neu liest. Nur gepackt und
|
|
1358
|
+
* nur auf macOS (nur dort gibt es ein Bundle mit Info.plist); gedrosselt auf einen Lauf je 30 s.
|
|
1359
|
+
* Im Smoke-Test liefert `HMGR_GUI_SMOKE_PLATTENVERSION` eine künstliche Platten-Version, sonst
|
|
1360
|
+
* läuft dort nie eine echte Prüfung.
|
|
1361
|
+
*/
|
|
1362
|
+
function plattenVersionPruefen(erzwingen = false) {
|
|
1363
|
+
let platte;
|
|
1364
|
+
if (smoketest) {
|
|
1365
|
+
if (!erzwingen)
|
|
1366
|
+
return;
|
|
1367
|
+
const kuenstlich = process.env.HMGR_GUI_SMOKE_PLATTENVERSION;
|
|
1368
|
+
platte = kuenstlich !== undefined && kuenstlich !== '' ? kuenstlich : null;
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
if (!app.isPackaged || process.platform !== 'darwin')
|
|
1372
|
+
return;
|
|
1373
|
+
const jetzt = Date.now();
|
|
1374
|
+
if (jetzt - letztePlattenPruefung < PLATTEN_PRUEFUNG_MIN_ABSTAND_MS)
|
|
1375
|
+
return;
|
|
1376
|
+
letztePlattenPruefung = jetzt;
|
|
1377
|
+
platte = neuereVersionAufPlatte(infoPlistPfad(process.execPath), laufendeVersion);
|
|
1378
|
+
}
|
|
1379
|
+
if (platte === null || platte === zuletztGemeldeteGuiVersion)
|
|
1380
|
+
return;
|
|
1381
|
+
zuletztGemeldeteGuiVersion = platte;
|
|
1382
|
+
selbstUpdateMelden({ art: 'gui', version: platte, neuStartNoetig: 'installiert' });
|
|
1383
|
+
}
|
|
1301
1384
|
/**
|
|
1302
1385
|
* Die daneben installierte hmgr-CLI gegen npm (E77, F83=O1). Läuft auf allen Plattformen — anders
|
|
1303
1386
|
* als die GUI-Prüfung hängt sie an keinem Paket-Format und an keiner Signierung. Gemeldet wird nur,
|
|
@@ -255,6 +255,21 @@
|
|
|
255
255
|
</form>
|
|
256
256
|
</dialog>
|
|
257
257
|
|
|
258
|
+
<!-- Neustart-Angebot (E90): ein Update für hmgr selbst liegt bereit. -->
|
|
259
|
+
<dialog id="d-neustart">
|
|
260
|
+
<form method="dialog">
|
|
261
|
+
<div class="dkopf">
|
|
262
|
+
<span class="etikett">Update</span>
|
|
263
|
+
<h3 id="dn-titel"></h3>
|
|
264
|
+
<p>hmgr startet dafür neu. Offene Dialoge gehen dabei verloren.</p>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="dfuss">
|
|
267
|
+
<button class="knopf leise" value="spaeter">Später</button>
|
|
268
|
+
<button class="knopf signal" value="jetzt">Jetzt neu starten</button>
|
|
269
|
+
</div>
|
|
270
|
+
</form>
|
|
271
|
+
</dialog>
|
|
272
|
+
|
|
258
273
|
<dialog id="d-erfolg">
|
|
259
274
|
<form method="dialog">
|
|
260
275
|
<div class="dkopf">
|
|
@@ -2668,11 +2668,39 @@
|
|
|
2668
2668
|
die CLI installiert die GUI nicht selbst (F82). Die CLI-Zeile ist seit E82 eine Warnung, keine
|
|
2669
2669
|
bloße Ansage: eine alte CLI (vor 0.0.7, kennt --version nicht) schreibt dieselben Dateien
|
|
2670
2670
|
(state.json, verlauf.json) wie die neue GUI und kennt Bundles, Verlauf und WSL-Heime nicht. */
|
|
2671
|
+
/* Neustart-Modal (E90): je Version einmal. Nach „Später" kommt es für dieselbe Version nicht
|
|
2672
|
+
wieder (nur im Speicher, ein App-Neustart setzt es zurück). Steht gerade ein anderer Dialog
|
|
2673
|
+
offen, wartet es, bis der zu ist. */
|
|
2674
|
+
var neustartAngebotVersion = null;
|
|
2675
|
+
var neustartAbgelehnt = null;
|
|
2676
|
+
function neustartAnbieten(version) {
|
|
2677
|
+
if (version === neustartAbgelehnt) return;
|
|
2678
|
+
neustartAngebotVersion = version;
|
|
2679
|
+
neustartAngebotOeffnen();
|
|
2680
|
+
}
|
|
2681
|
+
function neustartAngebotOeffnen() {
|
|
2682
|
+
if (neustartAngebotVersion === null || neustartAngebotVersion === neustartAbgelehnt) return;
|
|
2683
|
+
if (document.querySelector('dialog[open]')) return;
|
|
2684
|
+
$('dn-titel').textContent = 'hmgr ' + neustartAngebotVersion + ' ist bereit';
|
|
2685
|
+
zeige($('d-neustart'));
|
|
2686
|
+
}
|
|
2687
|
+
$('d-neustart').addEventListener('close', function () {
|
|
2688
|
+
if ($('d-neustart').returnValue === 'jetzt') window.hmgr.neuStarten();
|
|
2689
|
+
else neustartAbgelehnt = neustartAngebotVersion;
|
|
2690
|
+
});
|
|
2691
|
+
// `close` bubbelt nicht, der Capture-Listener am Dokument sieht aber jeden geschlossenen Dialog.
|
|
2692
|
+
document.addEventListener('close', function (ev) {
|
|
2693
|
+
if (ev.target !== $('d-neustart')) setTimeout(neustartAngebotOeffnen, 0);
|
|
2694
|
+
}, true);
|
|
2695
|
+
|
|
2671
2696
|
function selbstUpdateZeigen(nachricht) {
|
|
2672
2697
|
if (nachricht.art === 'gui') {
|
|
2673
|
-
$('selbstupdate-gui').textContent =
|
|
2698
|
+
$('selbstupdate-gui').textContent = nachricht.neuStartNoetig === 'installiert'
|
|
2699
|
+
? 'hmgr ' + nachricht.version + ' ist installiert — Neustart lädt sie'
|
|
2700
|
+
: 'hmgr ' + nachricht.version + ' bereit — wird beim Beenden installiert';
|
|
2674
2701
|
$('selbstupdate-gui').hidden = false;
|
|
2675
2702
|
$('selbstupdate-neustart').hidden = false;
|
|
2703
|
+
neustartAnbieten(nachricht.version);
|
|
2676
2704
|
} else if (nachricht.art === 'cli') {
|
|
2677
2705
|
var p = $('selbstupdate-cli');
|
|
2678
2706
|
p.textContent = '';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Erkennung „das App-Bundle auf der Platte ist neuer als der laufende Prozess" (E90). Reine
|
|
2
|
+
// Funktionen ohne Electron-Import, damit sie ohne App testbar sind; das Verdrahten liegt in main.ts.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
function zerlegen(version) {
|
|
6
|
+
const treffer = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version.trim());
|
|
7
|
+
if (treffer === null)
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
kern: [Number(treffer[1]), Number(treffer[2]), Number(treffer[3])],
|
|
11
|
+
vorab: treffer[4] === undefined ? [] : treffer[4].split('.'),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/** Semver-Rangfolge: negativ, 0 oder positiv; Vorabversion ist kleiner als ihre Release. */
|
|
15
|
+
function vergleichen(a, b) {
|
|
16
|
+
for (let i = 0; i < 3; i++) {
|
|
17
|
+
const unterschied = a.kern[i] - b.kern[i];
|
|
18
|
+
if (unterschied !== 0)
|
|
19
|
+
return unterschied;
|
|
20
|
+
}
|
|
21
|
+
if (a.vorab.length === 0 && b.vorab.length === 0)
|
|
22
|
+
return 0;
|
|
23
|
+
if (a.vorab.length === 0)
|
|
24
|
+
return 1;
|
|
25
|
+
if (b.vorab.length === 0)
|
|
26
|
+
return -1;
|
|
27
|
+
for (let i = 0; i < Math.max(a.vorab.length, b.vorab.length); i++) {
|
|
28
|
+
const x = a.vorab[i];
|
|
29
|
+
const y = b.vorab[i];
|
|
30
|
+
if (x === undefined)
|
|
31
|
+
return -1;
|
|
32
|
+
if (y === undefined)
|
|
33
|
+
return 1;
|
|
34
|
+
const xZahl = /^\d+$/.test(x);
|
|
35
|
+
const yZahl = /^\d+$/.test(y);
|
|
36
|
+
if (xZahl && yZahl) {
|
|
37
|
+
if (Number(x) !== Number(y))
|
|
38
|
+
return Number(x) - Number(y);
|
|
39
|
+
}
|
|
40
|
+
else if (xZahl !== yZahl) {
|
|
41
|
+
return xZahl ? -1 : 1;
|
|
42
|
+
}
|
|
43
|
+
else if (x !== y) {
|
|
44
|
+
return x < y ? -1 : 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
/** `true`, wenn `platte` eine echt größere Version ist als `laufend`. Unlesbares gilt als „nein". */
|
|
50
|
+
export function istNeuer(platte, laufend) {
|
|
51
|
+
const p = zerlegen(platte);
|
|
52
|
+
const l = zerlegen(laufend);
|
|
53
|
+
return p !== null && l !== null && vergleichen(p, l) > 0;
|
|
54
|
+
}
|
|
55
|
+
/** `…/hmgr.app/Contents/MacOS/hmgr` → `…/hmgr.app/Contents/Info.plist`. */
|
|
56
|
+
export function infoPlistPfad(execPath) {
|
|
57
|
+
return path.join(path.dirname(execPath), '..', 'Info.plist');
|
|
58
|
+
}
|
|
59
|
+
/** Die CFBundleShortVersionString aus einer XML-Info.plist; `null` bei allem Unlesbaren. */
|
|
60
|
+
export function plistVersion(plistPfad) {
|
|
61
|
+
try {
|
|
62
|
+
const text = readFileSync(plistPfad, 'utf8');
|
|
63
|
+
const treffer = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]*)<\/string>/.exec(text);
|
|
64
|
+
return treffer?.[1]?.trim() || null;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Die Platten-Version, wenn sie neuer ist als die laufende, sonst `null`. Wirft nie. */
|
|
71
|
+
export function neuereVersionAufPlatte(plistPfad, laufend) {
|
|
72
|
+
const platte = plistVersion(plistPfad);
|
|
73
|
+
return platte !== null && istNeuer(platte, laufend) ? platte : null;
|
|
74
|
+
}
|