waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ app src main ipc.ts

20,7 KB Rohdatei
app/src/main/ipc.ts — 565 Zeilen
1 /**
2 * Registrierung der IPC-Handler – ausschließlich über den typisierten Vertrag
3 * aus `src/shared/ipc.ts`.
4 *
5 * Die Typisierung beschreibt, was der Renderer schicken SOLL. Was er
6 * tatsächlich schickt, ist an dieser Grenze unbekannt: die Nutzlasten werden
7 * deshalb in den Fachmodulen (`katalog.ts`, `lernstand.ts`) zur Laufzeit
8 * geprüft, bevor irgendetwas damit geschieht.
9 */
10
11 import { join } from 'node:path';
12
13 import { app, BrowserWindow, clipboard, dialog, ipcMain, type IpcMainInvokeEvent } from 'electron';
14
15 import {
16 IPC_KANAELE,
17 KANAL_HILFSMITTEL_GEAENDERT,
18 type AnwendungsInfo,
19 type IpcAnfrage,
20 type IpcAntwort,
21 type IpcKanal,
22 type Katalogwechsel,
23 } from '../shared/ipc';
24 import { anzeigegroesseSetzen } from './anzeige';
25 import { dateiZeigen } from './dateizugriff';
26 import { datenbankStatus } from './datenbank';
27 import { einstellungenLesen, einstellungenSchreiben } from './einstellungen';
28 import { erklaerungenLaden } from './erklaerungen';
29 import { glossarLaden } from './glossar';
30 import { normtexteLaden } from './normtexte';
31 import { themenLaden } from './themen';
32 import { katalogBild, katalogLaden } from './katalog';
33 import { fehlerprotokollExportieren, fragenlisteExportieren } from './fragendruck-export';
34 import { lernberichtExportieren } from './lernbericht-export';
35 import {
36 datenbankKonstruktor,
37 istBeschaedigt,
38 lernstandBeiseitelegen,
39 lernstandInstanz,
40 lernstandOffen,
41 lernstandSchliessen,
42 type Lernstand,
43 } from './lernstand';
44 import { protokollPfad } from './protokoll';
45 import { kopieVorDemVerwerfen, selbstsicherungAnlegen } from './selbstsicherung';
46 import { datenschutzLesen } from './datenschutz';
47 import { lizenzenLesen } from './lizenzen';
48 import { pruefungInstanz, pruefungZuruecksetzen, type Pruefung } from './pruefung';
49 import {
50 sicherungAnlegen,
51 sicherungEinspielen,
52 sicherungUebernehmen,
53 sicherungPruefen,
54 type Umgebung as Sicherungsumgebung,
55 } from './sicherung-dialoge';
56 import { laufMelden } from './laufwaechter';
57 import { istErlaubteNavigation, unterstuetzungOeffnen } from './sicherheit';
58
59 /** Dateiname des Lernstands im `userData`-Verzeichnis. */
60 const LERNSTAND_DATEI = 'lernstand.db';
61
62 /**
63 * Anwendungsversion, zur Bauzeit aus der package.json eingesetzt
64 * (siehe `electron.vite.config.ts`).
65 *
66 * `app.getVersion()` taugt dafür nicht durchgängig: Wird der gebaute
67 * Einstiegspunkt direkt gestartet – in der Entwicklung und in den
68 * Oberflächentests – findet Electron keine package.json und liefert
69 * ersatzweise seine eigene Version zurück.
70 */
71 declare const __ANWENDUNGS_VERSION__: string | undefined;
72
73 function anwendungsVersion(): string {
74 return typeof __ANWENDUNGS_VERSION__ === 'string' ? __ANWENDUNGS_VERSION__ : app.getVersion();
75 }
76
77 /**
78 * Baukennung und Commit-Datum, ebenfalls zur Bauzeit eingesetzt.
79 *
80 * Ohne Git beim Bauen bleiben beide leer; die Oberfläche lässt die Zeile
81 * dann weg, statt „unbekannt“ anzuzeigen.
82 */
83 declare const __BAUKENNUNG__: string | undefined;
84 declare const __BAUSTAND__: string | undefined;
85
86 function zeichenkette(wert: string | undefined): string {
87 return typeof wert === 'string' ? wert : '';
88 }
89
90 /**
91 * Stammt die Anfrage aus unserer eigenen Oberfläche?
92 *
93 * Zweite Verteidigungslinie hinter der Navigationssperre in `sicherheit.ts`.
94 * Käme eine fremde Seite doch einmal in den Renderer, hätte sie über das
95 * Preload-Skript die volle Bridge in der Hand – hier endet das trotzdem.
96 *
97 * Ein Rahmen ohne URL wird durchgelassen: Das kommt vor, wenn der Rahmen
98 * gerade abgebaut wird, und wäre kein Angriff, sondern ein Wettlauf.
99 */
100 function vonEigenerOberflaeche(ereignis: IpcMainInvokeEvent): boolean {
101 const herkunft = ereignis.senderFrame?.url;
102 return herkunft === undefined || herkunft === '' || istErlaubteNavigation(herkunft);
103 }
104
105 /**
106 * Typsichere Hülle um `ipcMain.handle`: Kanal, Anfrage- und Antworttyp werden
107 * aus {@link IpcVertrag} abgeleitet, sodass Tippfehler zu Compilerfehlern
108 * werden statt zu stillen `undefined`-Antworten.
109 */
110 function behandeln<K extends IpcKanal>(
111 kanal: K,
112 /* Auch ein Versprechen ist erlaubt: `ipcMain.handle` wartet es ab. Gebraucht
113 wird das vom Export, der einen Dialog zeigt und danach schreibt. */
114 behandler: (
115 ereignis: IpcMainInvokeEvent,
116 anfrage: IpcAnfrage<K>,
117 ) => IpcAntwort<K> | Promise<IpcAntwort<K>>,
118 ): void {
119 ipcMain.handle(kanal, (ereignis, anfrage: unknown) => {
120 if (!vonEigenerOberflaeche(ereignis)) {
121 console.warn(`[sicherheit] IPC-Anfrage fremder Herkunft abgewiesen: ${kanal}`);
122 throw new Error('Diese Anfrage stammt nicht aus der Oberfläche dieser Anwendung.');
123 }
124 return behandler(ereignis, anfrage as IpcAnfrage<K>);
125 });
126 }
127
128 /**
129 * Holt ein Feld aus einer IPC-Nutzlast. Trotz Typisierung wird hier nichts
130 * vorausgesetzt – die eigentliche Prüfung der Werte macht das Fachmodul.
131 */
132 function feld(anfrage: unknown, name: string): unknown {
133 if (typeof anfrage !== 'object' || anfrage === null || Array.isArray(anfrage)) {
134 throw new Error(`Ungültige Anfrage: Es wurde ein Objekt mit dem Feld „${name}“ erwartet.`);
135 }
136 return (anfrage as Record<string, unknown>)[name];
137 }
138
139 /**
140 * Befund des Katalogstand-Abgleichs. Der Abgleich selbst läuft beim Öffnen
141 * des Lernstands; hier wird er nur abgeholt. Scheitert das Öffnen – etwa bei
142 * einem Lernstand aus einer neueren Programmversion oder ohne Katalog –,
143 * antwortet die Systemauskunft trotzdem: Diese Fehler haben ihre eigenen
144 * Meldungen an anderer Stelle.
145 */
146 function katalogwechselLesen(): Katalogwechsel | null {
147 try {
148 return lernstand().katalogwechsel;
149 } catch {
150 return null;
151 }
152 }
153
154 function anwendungsInfo(): AnwendungsInfo {
155 return {
156 anwendungsVersion: anwendungsVersion(),
157 baukennung: zeichenkette(__BAUKENNUNG__),
158 baustand: zeichenkette(__BAUSTAND__),
159 electronVersion: process.versions.electron,
160 chromeVersion: process.versions.chrome,
161 nodeVersion: process.versions.node,
162 plattform: process.platform,
163 datenbank: datenbankStatus(),
164 katalogwechsel: katalogwechselLesen(),
165 protokollPfad: protokollPfad(),
166 };
167 }
168
169 /**
170 * Lernstand beim ersten Bedarf öffnen. Der Katalog wird dabei mitgeladen –
171 * ohne ihn lassen sich weder Fragen zusammenstellen noch Statistiken bilden.
172 *
173 * **Der eine Fall, in dem es hier etwas anzubieten gibt.** Ist die Datei
174 * beschädigt, endete das bis 0.22.0 in einer Sackgasse: Der Renderer meldete
175 * „Ihr Lernprofil konnte nicht geladen werden … Antworten werden dabei nicht
176 * gespeichert“, und dabei blieb es bei jedem Start aufs Neue. Wer nicht von
177 * sich aus auf die Idee kam, im `userData`-Verzeichnis nachzusehen, konnte
178 * die Anwendung nie wieder benutzen.
179 *
180 * Deshalb fragt der Hauptprozess hier nach. Der Dialog ist bewusst
181 * synchron und nativ: Er muss vorliegen, bevor dieser Aufruf zurückkehrt,
182 * und er wird auch dann noch vorgelesen, wenn die Oberfläche nichts anzeigen
183 * kann. Nichts wird von selbst getan – die beschädigte Datei bleibt liegen,
184 * bis jemand ausdrücklich zustimmt, und auch dann wird sie nur umbenannt.
185 */
186 function lernstand(): Lernstand {
187 const pfad = join(app.getPath('userData'), LERNSTAND_DATEI);
188 try {
189 return lernstandInstanz(pfad, katalogLaden());
190 } catch (fehler: unknown) {
191 if (!istBeschaedigt(fehler)) {
192 throw fehler;
193 }
194 return beschaedigtenLernstandKlaeren(pfad, fehler);
195 }
196 }
197
198 /**
199 * Bietet nach einer beschädigten Datei den Neuanfang an – einmal je Start.
200 *
201 * Der Merker verhindert eine Dialogschleife: Jeder IPC-Kanal, der einen
202 * Lernstand braucht, liefe sonst in dieselbe Rückfrage, und der Startbildschirm
203 * fragt mehrere davon gleich nacheinander ab.
204 */
205 let beschaedigungGeklaert = false;
206
207 function beschaedigtenLernstandKlaeren(pfad: string, fehler: unknown): Lernstand {
208 if (beschaedigungGeklaert) {
209 throw fehler;
210 }
211 beschaedigungGeklaert = true;
212
213 const wahl = dialog.showMessageBoxSync({
214 type: 'error',
215 buttons: ['Beiseitelegen und neu anfangen', 'Nichts tun'],
216 defaultId: 1,
217 cancelId: 1,
218 noLink: true,
219 title: 'Waffensachkunde – Lernsoftware',
220 message: 'Die Datei mit Ihrem Lernstand ist beschädigt.',
221 detail: [
222 'Sie lässt sich nicht mehr öffnen. Gelöscht wird nichts: Auf Wunsch wird die beschädigte Datei umbenannt und beiseitegelegt, und die Anwendung beginnt mit einem leeren Lernstand.',
223 'Haben Sie eine Sicherung, können Sie sie danach unter „Lernstand sichern und übertragen“ einspielen.',
224 `Die beschädigte Datei bleibt in jedem Fall erhalten unter:\n${pfad}`,
225 ].join('\n\n'),
226 });
227
228 if (wahl !== 0) {
229 throw fehler;
230 }
231
232 const beiseite = lernstandBeiseitelegen(pfad);
233 console.warn(`[lernstand] Beschädigte Datei beiseitegelegt: ${beiseite}`);
234 return lernstandInstanz(pfad, katalogLaden());
235 }
236
237 /**
238 * Die Prüfungssimulation arbeitet auf demselben Lernstand: ein Lauf soll in
239 * Statistik und Wiedervorlage einfließen, nicht daneben stehen.
240 */
241 function pruefung(): Pruefung {
242 return pruefungInstanz(lernstand(), katalogLaden());
243 }
244
245 export function ipcRegistrieren(): void {
246 behandeln('anwendung:info', () => anwendungsInfo());
247 behandeln('einstellungen:lesen', () => einstellungenLesen());
248 behandeln('einstellungen:schreiben', (_ereignis, einstellungen) =>
249 einstellungenSchreiben(einstellungen),
250 );
251
252 /* Electron meldet über `accessibilitySupportEnabled`, ob eine assistive
253 Technologie erkannt wurde. Die Oberfläche richtet danach die Voreinstellung
254 der Zeichenkürzel aus – sie kollidieren im Lesemodus von NVDA und JAWS mit
255 deren Schnellnavigation. */
256 behandeln('system:hilfsmittel', () => app.accessibilitySupportEnabled);
257
258 /*
259 Die Zwischenablage liegt im Hauptprozess, weil der Renderer sie nicht
260 bekommt: `sicherheit.ts` lehnt sämtliche Berechtigungen ab, und Blink
261 fragt für `navigator.clipboard.writeText()` eine ab. Nachgemessen im
262 gebauten Fenster – `isSecureContext` true, Aufruf trotzdem
263 `NotAllowedError: Write permission denied`.
264
265 Die Obergrenze ist kein Misstrauen gegen die eigene Oberfläche, sondern
266 dieselbe Sorgfalt wie bei jeder anderen Nutzlast: Was über die Brücke
267 kommt, wird geprüft, bevor es wirkt. Der Meldeblock ist rund 500 Zeichen
268 lang; 20 000 lassen jede vorstellbare Erweiterung zu und schließen aus,
269 dass ein Fehler anderswo die Zwischenablage des Nutzers mit Unrat füllt.
270 */
271 behandeln('system:kopieren', (_ereignis, anfrage) => {
272 const text = feld(anfrage, 'text');
273 if (typeof text !== 'string' || text.length === 0 || text.length > 20_000) {
274 return false;
275 }
276 clipboard.writeText(text);
277 return true;
278 });
279
280 /*
281 Der einzige Kanal, der nach außen führt. Er nimmt eine Adresse entgegen,
282 damit prüfbar bleibt, dass die Oberfläche genau die anfordert, die sie
283 anzeigt – geöffnet wird aber ausschließlich die eine eingetragene, siehe
284 `sicherheit.ts`. Ohne Eintrag öffnet er nichts, und die Oberfläche zeigt
285 das Angebot dann gar nicht erst an.
286 */
287 behandeln('system:unterstuetzung', (_ereignis, anfrage) =>
288 unterstuetzungOeffnen(feld(anfrage, 'url')),
289 );
290
291 /*
292 Öffnet ausschließlich, was dieser Prozess in dieser Sitzung selbst
293 geschrieben hat. Der Renderer schickt eine Kennung, keinen Pfad – wer
294 einen Pfad mitbringen darf, darf auch einen anderen mitbringen.
295 */
296 behandeln('system:datei-zeigen', async (_ereignis, anfrage) => {
297 const wunsch = feld(anfrage, 'wunsch');
298 return dateiZeigen(feld(anfrage, 'kennung'), wunsch === 'ordner' ? 'ordner' : 'oeffnen');
299 });
300
301 behandeln('ansicht:groesse', (_ereignis, anfrage) =>
302 anzeigegroesseSetzen(feld(anfrage, 'prozent')),
303 );
304
305 behandeln('katalog:laden', () => katalogLaden());
306 behandeln('katalog:bild', (_ereignis, anfrage) => katalogBild(feld(anfrage, 'bildId')));
307 behandeln('erklaerungen:laden', () => erklaerungenLaden());
308 behandeln('glossar:laden', () => glossarLaden());
309 behandeln('gesetz:normtexte', () => normtexteLaden());
310 behandeln('themen:laden', () => themenLaden());
311
312 behandeln('profil:liste', () => lernstand().profile());
313 behandeln('profil:anlegen', (_ereignis, anfrage) =>
314 lernstand().profilAnlegen(feld(anfrage, 'name')),
315 );
316 behandeln('profil:aktualisieren', (_ereignis, anfrage) =>
317 lernstand().profilAktualisieren(anfrage),
318 );
319 behandeln('profil:loeschen', (_ereignis, anfrage) => {
320 /* Erst die Kopie, dann das Löschen: Misslingt sie, wird nichts
321 gelöscht – der Fehler geht an die Oberfläche. */
322 kopieVorVerwerfen();
323 return lernstand().profilLoeschen(feld(anfrage, 'id'));
324 });
325
326 behandeln('lernen:sitzung', (_ereignis, anfrage) =>
327 lernstand().sitzung(feld(anfrage, 'profilId'), feld(anfrage, 'filter')),
328 );
329 behandeln('lernen:antworten', (_ereignis, anfrage) =>
330 lernstand().antworten(feld(anfrage, 'profilId'), feld(anfrage, 'protokoll')),
331 );
332 behandeln('lernen:merken', (_ereignis, anfrage) =>
333 lernstand().merken(
334 feld(anfrage, 'profilId'),
335 feld(anfrage, 'frageId'),
336 feld(anfrage, 'gemerkt'),
337 ),
338 );
339 behandeln('lernen:fragestand', (_ereignis, anfrage) =>
340 lernstand().frageStand(feld(anfrage, 'profilId'), feld(anfrage, 'frageId')),
341 );
342 behandeln('lernen:hartnaeckige', (_ereignis, anfrage) =>
343 lernstand().hartnaeckige(feld(anfrage, 'profilId')),
344 );
345 behandeln('lernen:uebersicht', (_ereignis, anfrage) =>
346 lernstand().uebersicht(feld(anfrage, 'profilId')),
347 );
348 behandeln('lernen:plan', (_ereignis, anfrage) => lernstand().lernplan(feld(anfrage, 'profilId')));
349 behandeln('lernen:zuruecksetzen', (_ereignis, anfrage) => {
350 kopieVorVerwerfen();
351 return lernstand().zuruecksetzen(feld(anfrage, 'profilId'), feld(anfrage, 'kapitel'));
352 });
353
354 /*
355 Sicherung des Lernstands. Drei Kanäle, weil zwischen „das steht in der
356 Datei" und „ja, ersetze meinen Lernstand" eine Entscheidung des Nutzers
357 liegt: Der Prüfschritt ist folgenlos, erst `einspielen` fasst etwas an.
358
359 Die Umgebung wird bei jedem Aufruf frisch gebaut – `lernstand()` muss beim
360 Einspielen NACH dem Ersetzen eine neue Instanz liefern, und ein
361 festgehaltener Verweis zeigte auf die geschlossene alte.
362 */
363 const sicherungsumgebung = (): Sicherungsumgebung => ({
364 lernstand,
365 katalog: katalogLaden,
366 Datenbank: datenbankKonstruktor(),
367 jetzt: () => new Date(),
368 });
369
370 behandeln('sicherung:anlegen', async (ereignis) =>
371 sicherungAnlegen(BrowserWindow.fromWebContents(ereignis.sender), sicherungsumgebung()),
372 );
373
374 behandeln('sicherung:pruefen', async (ereignis) =>
375 sicherungPruefen(BrowserWindow.fromWebContents(ereignis.sender), sicherungsumgebung()),
376 );
377
378 behandeln('sicherung:einspielen', (ereignis, anfrage) =>
379 sicherungEinspielen(
380 feld(anfrage, 'vorgang'),
381 BrowserWindow.fromWebContents(ereignis.sender),
382 sicherungsumgebung(),
383 ),
384 );
385
386 /* Ohne `async`, wie das Einspielen: Der Hauptprozess ist einfädig, und
387 solange nichts abgewartet wird, kann sich kein zweiter Kanal
388 dazwischenschieben und in derselben Datenbank schreiben. */
389 behandeln('sicherung:uebernehmen', (ereignis, anfrage) =>
390 sicherungUebernehmen(
391 feld(anfrage, 'vorgang'),
392 feld(anfrage, 'profilIndex'),
393 BrowserWindow.fromWebContents(ereignis.sender),
394 sicherungsumgebung(),
395 ),
396 );
397
398 /* Der Speicherdialog hängt am Fenster, aus dem der Auftrag kam – modal ist
399 für Screenreader die klarere Lage als ein freischwebender Dialog. */
400 behandeln('druck:lernbericht', async (ereignis, anfrage) =>
401 lernberichtExportieren(
402 {
403 lernstand: lernstand(),
404 pruefung: pruefung(),
405 katalog: katalogLaden(),
406 elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
407 jetzt: () => new Date(),
408 },
409 feld(anfrage, 'profilId'),
410 feld(anfrage, 'schriftgroesse'),
411 ),
412 );
413
414 behandeln('druck:fehlerprotokoll', async (ereignis, anfrage) =>
415 fehlerprotokollExportieren(
416 {
417 lernstand: lernstand(),
418 katalog: katalogLaden(),
419 elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
420 jetzt: () => new Date(),
421 },
422 feld(anfrage, 'profilId'),
423 feld(anfrage, 'schriftgroesse'),
424 feld(anfrage, 'tiefe'),
425 ),
426 );
427
428 behandeln('druck:fragenliste', async (ereignis, anfrage) =>
429 fragenlisteExportieren(
430 {
431 lernstand: lernstand(),
432 katalog: katalogLaden(),
433 elternfenster: BrowserWindow.fromWebContents(ereignis.sender),
434 jetzt: () => new Date(),
435 },
436 feld(anfrage, 'bereiche'),
437 feld(anfrage, 'schriftgroesse'),
438 feld(anfrage, 'mitLoesungen'),
439 ),
440 );
441
442 behandeln('pruefung:starten', (_ereignis, anfrage) =>
443 pruefung().starten(feld(anfrage, 'profilId'), feld(anfrage, 'auftrag')),
444 );
445 behandeln('pruefung:auswerten', (_ereignis, anfrage) =>
446 pruefung().auswerten(
447 feld(anfrage, 'profilId'),
448 feld(anfrage, 'auftrag'),
449 feld(anfrage, 'antworten'),
450 feld(anfrage, 'dauerMs'),
451 feld(anfrage, 'zeitAbgelaufen'),
452 ),
453 );
454 behandeln('pruefung:sichern', (_ereignis, anfrage) =>
455 pruefung().sichern(feld(anfrage, 'profilId'), feld(anfrage, 'stand')),
456 );
457 behandeln('pruefung:offen', (_ereignis, anfrage) =>
458 pruefung().offenerBogen(feld(anfrage, 'profilId')),
459 );
460 behandeln('pruefung:verwerfen', (_ereignis, anfrage) => {
461 pruefung().verwerfen(feld(anfrage, 'profilId'));
462 return undefined;
463 });
464 behandeln('pruefung:laeuft', (ereignis, anfrage) => {
465 /* An den sendenden `webContents` gebunden, nicht an ein Modul-Merkmal:
466 Sonst spräche ein zweites Fenster für das erste mit. */
467 const laeuft = feld(anfrage, 'laeuft');
468 laufMelden(ereignis.sender, laeuft === true);
469 return undefined;
470 });
471 behandeln('pruefung:verlauf', (_ereignis, anfrage) =>
472 pruefung().verlauf(feld(anfrage, 'profilId')),
473 );
474
475 behandeln('lizenzen:lesen', () => lizenzenLesen());
476 behandeln('datenschutz:lesen', () => datenschutzLesen());
477 }
478
479 /**
480 * Meldet Änderungen des Hilfsmittel-Zustands an alle Fenster.
481 *
482 * Ein Screenreader kann auch mitten in der Sitzung gestartet werden – gerade
483 * dann, wenn jemand merkt, dass er ihn braucht. Ohne diese Meldung bliebe die
484 * Oberfläche auf dem Stand des Programmstarts.
485 */
486 function hilfsmittelMelden(_ereignis: unknown, aktiv: boolean): void {
487 for (const fenster of BrowserWindow.getAllWindows()) {
488 if (!fenster.isDestroyed()) {
489 fenster.webContents.send(KANAL_HILFSMITTEL_GEAENDERT, aktiv);
490 }
491 }
492 }
493
494 export function hilfsmittelBeobachten(): void {
495 /* Benannt und nicht anonym, damit `ipcAbmelden` ihn wieder lösen kann.
496 Anonym ließ er sich nicht entfernen: `ipcAbmelden` heißt „Gegenstück für
497 Tests und sauberes Herunterfahren“ und hielt das bis Fassung 0.24.1 für
498 diesen einen Zuhörer nicht ein. In der Auslieferung folgenlos – der
499 Aufruf geschieht genau einmal –, aber ein Gegenstück, das nicht alles
500 zurücknimmt, ist beim nächsten Mal die Falle. */
501 app.on('accessibility-support-changed', hilfsmittelMelden);
502 }
503
504 /**
505 * Legt beim Beenden eine Sicherheitskopie an, wenn eine fällig ist.
506 *
507 * **Vor** dem Schließen: `VACUUM INTO` braucht die offene Verbindung. Und
508 * nur, wenn der Lernstand in dieser Sitzung überhaupt geöffnet wurde – wer
509 * das Programm startet und gleich wieder beendet, hat nichts zu sichern.
510 *
511 * Ein Fehlschlag hält das Beenden nicht auf. Eine Anwendung, die sich wegen
512 * einer misslungenen Nebensache nicht schließen lässt, wäre der schlechtere
513 * Handel; die Kopie ist eine Zugabe, kein Versprechen.
514 */
515 /**
516 * Sicherheitskopie vor einem Schritt, der Daten vernichtet.
517 *
518 * Misslingt sie, wirft sie – und `behandeln` reicht den Fehler an die
519 * Oberfläche durch, ohne dass der Schritt ausgeführt wird. Das ist der
520 * Unterschied zur Kopie beim Beenden: Dort ist sie eine Zugabe, hier ist sie
521 * die Bedingung.
522 */
523 function kopieVorVerwerfen(): void {
524 const offen = lernstandOffen();
525 if (offen === null) {
526 return;
527 }
528 const pfad = kopieVorDemVerwerfen(
529 offen.datenbank,
530 app.getPath('userData'),
531 datenbankKonstruktor(),
532 );
533 console.info(`[sicherung] Sicherheitskopie vor dem Verwerfen: ${pfad}`);
534 }
535
536 function selbstsicherungBeimBeenden(): void {
537 const offen = lernstandOffen();
538 if (offen === null) {
539 return;
540 }
541
542 try {
543 const pfad = selbstsicherungAnlegen(
544 offen.datenbank,
545 app.getPath('userData'),
546 datenbankKonstruktor(),
547 );
548 if (pfad !== null) {
549 console.info(`[sicherung] Selbsttätige Sicherheitskopie: ${pfad}`);
550 }
551 } catch (fehler: unknown) {
552 console.warn('[sicherung] Selbsttätige Sicherheitskopie misslang:', fehler);
553 }
554 }
555
556 /** Gegenstück für Tests und sauberes Herunterfahren. */
557 export function ipcAbmelden(): void {
558 for (const kanal of IPC_KANAELE) {
559 ipcMain.removeHandler(kanal);
560 }
561 app.off('accessibility-support-changed', hilfsmittelMelden);
562 pruefungZuruecksetzen();
563 selbstsicherungBeimBeenden();
564 lernstandSchliessen();
565 }