gestion_table_tournante_libre/electron/main.js
Mathieu Benoit f2b66f2f92 [ADD] electron: confined file system, atomic writes, lock, bridge
The shell carries the file-system boundary (§ 13.1): the page names a
root the main process defines — data/ beside the published executable,
the product folder in Documents, or one the operator picks in a dialog —
and a relative path, refused if absolute, with .., a NUL, or a real
path outside the root. Writes go to a temporary file, flushed, renamed
over the target with retries; a failed rename never truncates it. The
lock records session, process and host. Chromium's profile lives in a
temporary folder removed at exit, not in AppData nor in the events.

Checked: node and real-folder long tests; real Electron start under Ozone.

--- FR ---

[ADD] electron : système de fichiers confiné, écriture atomique, verrou

La coquille porte la frontière du système de fichiers (§ 13.1) : la page
nomme une racine que le processus principal définit — data/ à côté de
l'exécutable publié, le dossier du produit dans Documents, ou un dossier
choisi par dialogue — et un chemin relatif, refusé s'il est absolu, porte
.., un NUL, ou un chemin réel hors de la racine. Une écriture passe par
un temporaire vidé sur le disque puis renommé par-dessus, réessayé ; un
renommage manqué ne tronque jamais la cible. Le profil de Chromium vit
dans un dossier temporaire effacé à la sortie, hors d'AppData.

Vérifié : épreuves node et longues sur dossier réel ; Electron réel lancé.

Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 17:34:31 -04:00

105 lines
4.6 KiB
JavaScript

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Processus principal de la coquille Electron (§ 13.1) : une fenêtre qui
// charge la page construite par Vite dans www/, et le système de fichiers de
// la plateforme (electron/fichiers.js), offert à la page par un canal IPC
// par primitive, que electron/preload.cjs expose sous window.gtt.fichiers.
//
// La page n'a ni Node ni accès au disque : isolation de contexte active,
// intégration de Node coupée, rendu en bac à sable. Elle n'atteint le
// processus principal que par ces canaux, et n'y désigne un fichier que par
// une racine que ce processus connaît et un chemin relatif à elle. Un nom
// importé interprété comme du balisage reste ainsi une faute d'affichage, et
// non un accès au disque.
import { app, BrowserWindow, dialog, ipcMain, Menu, shell } from 'electron';
import { mkdtempSync, rmSync } from 'node:fs';
import { readdir, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { creerFichiers, decrireSysteme, processusVivant, servir } from './fichiers.js';
// Chemins relatifs à ce module : ils valent dans le dépôt comme dans
// l'archive app.asar de l'exécutable, qui reproduit electron/ et www/.
const PRECHARGEMENT = fileURLToPath(new URL('./preload.cjs', import.meta.url));
const PAGE = fileURLToPath(new URL('../www/index.html', import.meta.url));
// Profil de Chromium (§ 8.6) : un dossier neuf sous le dossier temporaire du
// système, nommé gtt-profil-<pid>-…, posé avant que l'application soit
// prête — Chromium l'ouvre alors —, et retiré à la sortie. L'application ne
// garde rien dans ce profil, et un exécutable portable ne laisse ainsi rien
// dans AppData, ni dans le dossier de travail.
//
// Le retrait de la sortie n'est qu'un premier temps : Chromium écrit encore
// dans le profil après l'évènement quit — son état local, ses préférences,
// le stockage de session —, et le dossier renaît, réduit à quelques fichiers.
// Une séance qui s'arrête sans sortir n'en retire rien. Chaque démarrage
// retire donc, après l'ouverture de la fenêtre, les profils des séances
// terminées : le pid de leur nom ne désigne plus aucun processus de ce
// poste. Le profil d'une séance qui tourne encore reste.
const TEMPORAIRE = app.getPath('temp');
const PROFIL_DE_SEANCE = /^gtt-profil-(\d+)-/;
const PROFIL = mkdtempSync(join(TEMPORAIRE, `gtt-profil-${process.pid}-`));
app.setPath('userData', PROFIL);
app.on('quit', () => {
try {
rmSync(PROFIL, { recursive: true, force: true });
} catch {
// Au mieux : le démarrage suivant le retire.
}
});
async function retirerProfilsTermines() {
for (const nom of await readdir(TEMPORAIRE)) {
const pid = Number(PROFIL_DE_SEANCE.exec(nom)?.[1]);
if (!(pid > 0) || pid === process.pid || processusVivant(pid)) continue;
await rm(join(TEMPORAIRE, nom), { recursive: true, force: true }).catch(() => {});
}
}
// Le système de fichiers : emplacements et racines lus une fois, au
// démarrage (§ 8.6) ; les dialogues s'ouvrent sur la fenêtre de la page. Les
// gestionnaires sont posés avant le chargement de la page, qui peut les
// appeler dès son premier script.
let fenetre = null;
const fichiers = creerFichiers({
...decrireSysteme({
env: process.env,
plateforme: process.platform,
cheminSysteme: (nom) => app.getPath(nom),
nomProduit: app.getName(),
}),
dialog,
shell,
fenetre: () => fenetre,
});
servir(fichiers, (canal, gestionnaire) => ipcMain.handle(canal, gestionnaire));
function creerFenetre() {
fenetre = new BrowserWindow({
width: 1366,
height: 768,
webPreferences: {
preload: PRECHARGEMENT,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
});
// La page ne quitte jamais www/ : toute nouvelle fenêtre est refusée, toute
// navigation annulée, quelle que soit sa cible.
fenetre.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
fenetre.webContents.on('will-navigate', (evenement) => evenement.preventDefault());
fenetre.loadFile(PAGE);
}
// Sans menu posé par l'application, Electron installe le sien : en anglais,
// alors que l'interface est en français (§ 2), et porteur de « Reload » et
// des outils de développement. null le retire, et retire avec lui la barre
// de menu des fenêtres sous Windows et Linux ; il est posé avant la première
// fenêtre, qui naîtrait sinon avec la barre.
app.whenReady().then(() => {
Menu.setApplicationMenu(null);
creerFenetre();
retirerProfilsTermines().catch(() => {});
});