Skip to content
API Documentation
BastionGuard™
BackupPage.cpp
Go to the documentation of this file.
1/*
2 * BastionGuard™
3 * BackupPage.cpp
4 */
5
6#include "BackupPage.hpp"
7#include <glibmm/spawn.h>
8#include <glibmm/i18n.h>
9#include <glibmm/fileutils.h>
10#include <glibmm/main.h>
11#include <giomm.h>
12#include <sys/statvfs.h>
13#include <sys/wait.h>
14#include <sstream>
15#include <iomanip>
16#include <memory>
17#include <thread>
18
19// ---------------------------------------------------------------------------
20// Costanti
21// ---------------------------------------------------------------------------
22
23static const char* BG_BACKUP_LAUNCHER = "/usr/bin/bastionguard-backup-launcher";
24static const char* RECOVERY_SQUASHFS = "/boot/bastionguard-recovery/live/filesystem.squashfs";
25static const char* RECOVERY_KERNEL = "/boot/bastionguard-recovery/vmlinuz";
26static const char* RECOVERY_DIR = "/boot/bastionguard-recovery";
27static const char* GRUB_ENTRY = "/etc/grub.d/42_bastionguard-recovery";
28static const char* BUILD_SCRIPT = "/usr/share/bastionguard-backup/scripts/build-recovery-squashfs.sh";
29static const char* INSTALL_GRUB_SCRIPT = "/usr/share/bastionguard-backup/scripts/install-grub-entry.sh";
30
31static constexpr guint64 MIN_SPACE_BYTES = 2ULL * 1024 * 1024 * 1024; // 2 GB
32
33// ---------------------------------------------------------------------------
34// Helper: formatta bytes leggibili
35// ---------------------------------------------------------------------------
36
37static std::string fmtSize(guint64 bytes)
38{
39 std::ostringstream ss;
40
41 if (bytes >= 1024ULL * 1024 * 1024) {
42 ss << std::fixed << std::setprecision(1)
43 << (double)bytes / (1024.0 * 1024 * 1024);
44 return Glib::ustring::compose(_("%1 GB"), ss.str());
45 }
46
47 if (bytes >= 1024ULL * 1024) {
48 ss << std::fixed << std::setprecision(0)
49 << (double)bytes / (1024.0 * 1024);
50 return Glib::ustring::compose(_("%1 MB"), ss.str());
51 }
52
53 ss << (bytes / 1024);
54 return Glib::ustring::compose(_("%1 KB"), ss.str());
55}
56
57// ---------------------------------------------------------------------------
58// Costruttore
59// ---------------------------------------------------------------------------
60
62: Gtk::Box(Gtk::Orientation::VERTICAL, 0)
63{
64 logBuffer = Gtk::TextBuffer::create();
65
66 notebook.append_page(*buildBackupTab(), _("💾 Backup"));
67 notebook.append_page(*buildGrubTab(), _("🛡 Recovery GRUB"));
68
69 // Layout a due colonne: tab operative a sinistra e log a destra.
70 auto* columns = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 0);
71
72 auto* leftColumn = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
73 leftColumn->set_hexpand(true);
74 notebook.set_vexpand(true);
75 leftColumn->append(notebook);
76
77 auto* rightColumn = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 0);
78 rightColumn->set_size_request(340, -1); // colonna laterale ~340px
79 rightColumn->append(*buildLogSection());
80
81 columns->append(*leftColumn);
82 columns->append(*rightColumn);
83
84 set_vexpand(true);
85 append(*columns);
86 columns->set_vexpand(true);
87
88 // Aggiorna stato immagine all'avvio
90}
91
93{
94 if (alive_) alive_->store(false, std::memory_order_release);
95 if (pulseTimer.connected()) pulseTimer.disconnect();
96}
97
98// ---------------------------------------------------------------------------
99// Backup TAB
100// ---------------------------------------------------------------------------
101
103{
104 auto box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 16);
105 box->set_margin_top(20);
106 box->set_margin_bottom(20);
107 box->set_margin_start(20);
108 box->set_margin_end(20);
109
110 auto label = Gtk::make_managed<Gtk::Label>();
111 label->set_markup(
112 _("<b>Gestione snapshot BastionGuard</b>\n\n"
113 "Per creare, eliminare e ripristinare snapshot "
114 "aprire l'interfaccia grafica dedicata.\n"));
115 label->set_wrap(true);
116 label->set_halign(Gtk::Align::START);
117 box->append(*label);
118
119 auto launchButton = Gtk::make_managed<Gtk::Button>();
120 launchButton->set_label(_("🛡 Apri Gestione Backup"));
121 launchButton->get_style_context()->add_class("suggested-action");
122 launchButton->signal_clicked().connect([this]() {
123 try {
124 Glib::spawn_command_line_async(BG_BACKUP_LAUNCHER);
125 logSafe(Glib::ustring(_("Avvio gestore backup BastionGuard...\n")).raw());
126 } catch (...) {
127 logSafe(Glib::ustring(_("Errore durante l'avvio del gestore backup BastionGuard.\n")).raw());
128 }
129 });
130 box->append(*launchButton);
131
132 return box;
133}
134
135// ---------------------------------------------------------------------------
136// Recovery GRUB TAB
137// ---------------------------------------------------------------------------
138
140{
141 auto box = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 14);
142 box->set_margin_top(16);
143 box->set_margin_bottom(16);
144 box->set_margin_start(16);
145 box->set_margin_end(16);
146
147 // ── Descrizione ────────────────────────────────────────────────────
148 auto desc = Gtk::make_managed<Gtk::Label>();
149 desc->set_markup(
150 _("<b>BastionGuard Recovery Environment</b>\n\n"
151 "Installa una voce GRUB per avviare un sistema Linux "
152 "minimale isolato e ripristinare snapshot in sicurezza.\n\n"
153 "<b>⚠ Requisiti:</b>\n"
154 " • Almeno <b>2 GB</b> di spazio libero sulla partizione <tt>/boot</tt>\n"
155 " • Connessione internet per il download dei pacchetti Debian\n"
156 " • La build può richiedere 10–20 minuti"));
157 desc->set_wrap(true);
158 desc->set_halign(Gtk::Align::START);
159 box->append(*desc);
160
161 // ── Card stato immagine ─────────────────────────────────────────────
162 auto card = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL, 6);
163 card->get_style_context()->add_class("card");
164 card->set_margin_top(4);
165
166 recoveryStatusLabel.set_halign(Gtk::Align::START);
167 recoveryStatusLabel.set_use_markup(true);
168 card->append(recoveryStatusLabel);
169
170 recoverySpaceLabel.set_halign(Gtk::Align::START);
171 recoverySpaceLabel.set_use_markup(true);
172 card->append(recoverySpaceLabel);
173
174 box->append(*card);
175
176 // ── Risultato ultimo comando ────────────────────────────────────────
177 recoveryResultLabel.set_halign(Gtk::Align::START);
178 recoveryResultLabel.set_use_markup(true);
179 recoveryResultLabel.hide();
180 box->append(recoveryResultLabel);
181
182 // ── Progress bar ────────────────────────────────────────────────────
183 recoveryProgress.set_show_text(true);
184 recoveryProgress.set_text(_("In attesa..."));
185 recoveryProgress.hide();
186 box->append(recoveryProgress);
187
188 // ── Pulsanti ────────────────────────────────────────────────────────
189 auto btnBox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL, 8);
190
191 buildRecoveryButton.set_label(_("🔨 Build filesystem squashfs"));
192 buildRecoveryButton.signal_clicked().connect(
193 sigc::mem_fun(*this, &BackupPage::onBuildRecovery));
194 btnBox->append(buildRecoveryButton);
195
196 installGrubButton.set_label(_("⬆ Installa voce GRUB"));
197 installGrubButton.get_style_context()->add_class("suggested-action");
198 installGrubButton.signal_clicked().connect(
199 sigc::mem_fun(*this, &BackupPage::onInstallGrub));
200 btnBox->append(installGrubButton);
201
202 removeGrubButton.set_label(_("🗑 Rimuovi voce GRUB"));
203 removeGrubButton.get_style_context()->add_class("destructive-action");
204 removeGrubButton.signal_clicked().connect(
205 sigc::mem_fun(*this, &BackupPage::onRemoveGrub));
206 btnBox->append(removeGrubButton);
207
208 box->append(*btnBox);
209
210 return box;
211}
212
213// ---------------------------------------------------------------------------
214// Log section
215// ---------------------------------------------------------------------------
216
218{
219 auto frame = Gtk::make_managed<Gtk::Frame>(_("Log operazioni"));
220
221 logView.set_buffer(logBuffer);
222 logView.set_editable(false);
223 logView.set_monospace(true);
224 logView.set_wrap_mode(Gtk::WrapMode::WORD_CHAR);
225
226 auto scroll = Gtk::make_managed<Gtk::ScrolledWindow>();
227 scroll->set_child(logView);
228 // Pannello laterale: si espande in ALTEZZA per riempire lo spazio delle
229 // tab, e ha una larghezza contenuta così non ruba spazio al contenuto.
230 scroll->set_policy(Gtk::PolicyType::AUTOMATIC, Gtk::PolicyType::AUTOMATIC);
231 scroll->set_vexpand(true);
232
233 frame->set_child(*scroll);
234 frame->set_vexpand(true);
235 frame->set_size_request(340, -1); // larghezza ~340px, altezza flessibile
236 frame->set_margin_top(10);
237 frame->set_margin_bottom(10);
238 frame->set_margin_end(12);
239 frame->set_margin_start(6);
240 return frame;
241}
242
243// ---------------------------------------------------------------------------
244// Aggiorna stato immagine recovery
245// ---------------------------------------------------------------------------
246
248{
249 bool squashfs_ok = Glib::file_test(RECOVERY_SQUASHFS, Glib::FileTest::EXISTS);
250 bool kernel_ok = Glib::file_test(RECOVERY_KERNEL, Glib::FileTest::EXISTS);
251 bool grub_ok = Glib::file_test(GRUB_ENTRY, Glib::FileTest::EXISTS);
252
253 std::string status_markup;
254
255 if (squashfs_ok && kernel_ok) {
256 std::string size_str = "?";
257 try {
258 auto info = Gio::File::create_for_path(RECOVERY_SQUASHFS)
259 ->query_info("standard::size");
260 size_str = fmtSize((guint64)info->get_size());
261 } catch (...) {}
262
263 status_markup = Glib::ustring::compose(
264 _("<span foreground='#22c55e'>✅ <b>Immagine recovery presente</b></span>\n"
265 "<small><tt>%1</tt>\n"
266 "filesystem.squashfs: %2\n"
267 "Voce GRUB: %3</small>"),
269 size_str,
270 grub_ok ? _("✅ installata") : _("⚠ non installata")
271 );
272 } else {
273 status_markup =
274 _("<span foreground='#f59e0b'>⚠ <b>Immagine recovery non trovata</b></span>\n"
275 "<small>Usare \"Build filesystem squashfs\" per crearla.</small>");
276 }
277
278 recoveryStatusLabel.set_markup(status_markup);
279
280 // Spazio disponibile su /boot
281 struct statvfs vfs;
282 std::string space_markup;
283
284 const char* check_path = Glib::file_test("/boot", Glib::FileTest::IS_DIR)
285 ? "/boot" : "/";
286
287 if (statvfs(check_path, &vfs) == 0) {
288 guint64 free_bytes = (guint64)vfs.f_bavail * vfs.f_frsize;
289 std::string free_str = fmtSize(free_bytes);
290 bool enough = free_bytes >= MIN_SPACE_BYTES;
291
292 space_markup = Glib::ustring::compose(
293 _("Spazio libero su <tt>%1</tt>: <b>%2</b> %3"),
294 check_path,
295 free_str,
296 enough
297 ? _("<span foreground='#22c55e'>✅ sufficiente</span>")
298 : _("<span foreground='#ef4444'>❌ insufficiente (minimo 2 GB)</span>")
299 );
300 } else {
301 space_markup = _("Spazio disponibile: <i>non rilevato</i>");
302 }
303
304 recoverySpaceLabel.set_markup(space_markup);
305}
306
307// ---------------------------------------------------------------------------
308// setResult — mostra alert success/error
309// ---------------------------------------------------------------------------
310
311void BackupPage::setResult(bool success, const std::string& msg)
312{
313 if (success) {
314 recoveryResultLabel.set_markup(
315 Glib::ustring::compose(
316 "<span foreground='#ffffff' background='#22c55e'> ✅ %1 </span>",
317 msg
318 )
319 );
320 recoveryResultLabel.get_style_context()->add_class("alert-success");
321 recoveryResultLabel.get_style_context()->remove_class("alert-danger");
322 } else {
323 recoveryResultLabel.set_markup(
324 Glib::ustring::compose(
325 "<span foreground='#ffffff' background='#c0392b'> ❌ %1 </span>",
326 msg
327 )
328 );
329 recoveryResultLabel.get_style_context()->add_class("alert-danger");
330 recoveryResultLabel.get_style_context()->remove_class("alert-success");
331 }
332
333 recoveryResultLabel.show();
335}
336
337
338
339// ---------------------------------------------------------------------------
340// Handlers
341// ---------------------------------------------------------------------------
342
344{
345
346 std::string cmd = std::string("pkexec bash \"") + BUILD_SCRIPT + "\"";
347 logSafe(Glib::ustring(_("==> Avvio build Recovery environment...\n")).raw());
348 logSafe(Glib::ustring(_(" (può richiedere 10-20 minuti)\n")).raw());
349 runAsyncStreaming(cmd, true);
350}
351
353{
354
355 std::string cmd = std::string("pkexec bash \"") + INSTALL_GRUB_SCRIPT + "\"";
356 logSafe(Glib::ustring(_("==> Installazione voce GRUB...\n")).raw());
357 runAsyncStreaming(cmd, true);
358}
359
361{
362
363 std::string cmd =
364 "pkexec bash -c 'rm -f /etc/grub.d/42_bastionguard-recovery && update-grub'";
365 logSafe(Glib::ustring(_("==> Rimozione voce GRUB...\n")).raw());
366 runAsyncStreaming(cmd, false);
367}
368
369// ---------------------------------------------------------------------------
370// runAsyncStreaming — esegue comando con output live nel log
371// ---------------------------------------------------------------------------
372
373void BackupPage::runAsyncStreaming(const std::string& cmd, bool show_success)
374{
375 if (buildRunning) {
376 logSafe(Glib::ustring(_("⚠ Operazione già in corso.\n")).raw());
377 return;
378 }
379
380 buildRunning = true;
381 buildRecoveryButton.set_sensitive(false);
382 installGrubButton.set_sensitive(false);
383 removeGrubButton.set_sensitive(false);
384
385 recoveryProgress.set_text(_("Operazione in corso..."));
386 recoveryProgress.show();
387 recoveryResultLabel.hide();
388
389 pulseTimer = Glib::signal_timeout().connect([this]() -> bool {
390 if (!buildRunning)
391 return false;
392 recoveryProgress.pulse();
393 return true;
394 }, 200);
395
396 std::vector<std::string> argv = { "/bin/bash", "-c", cmd };
397 int stdout_fd = -1;
398 int stderr_fd = -1;
399 Glib::Pid pid = 0;
400
401 try {
402 Glib::spawn_async_with_pipes(
403 "",
404 argv,
405 Glib::SpawnFlags::SEARCH_PATH | Glib::SpawnFlags::DO_NOT_REAP_CHILD,
406 sigc::slot<void()>(),
407 &pid,
408 nullptr,
409 &stdout_fd,
410 &stderr_fd
411 );
412 } catch (const Glib::Error& e) {
413 logSafe(
414 Glib::ustring::compose(_("Errore avvio processo: %1\n"), e.what()).raw()
415 );
416 buildRunning = false;
417 pulseTimer.disconnect();
418 recoveryProgress.hide();
419 buildRecoveryButton.set_sensitive(true);
420 installGrubButton.set_sensitive(true);
421 removeGrubButton.set_sensitive(true);
422 setResult(false, _("Errore avvio processo"));
423 return;
424 }
425
426 auto stdout_ch = Glib::IOChannel::create_from_fd(stdout_fd);
427 auto stderr_ch = Glib::IOChannel::create_from_fd(stderr_fd);
428 stdout_ch->set_encoding("");
429 stderr_ch->set_encoding("");
430 stdout_ch->set_flags(Glib::IOFlags::NONBLOCK);
431 stderr_ch->set_flags(Glib::IOFlags::NONBLOCK);
432
433 // ANTI-BOOTLOOP: condividiamo le sigc::connection per le due signal_io
434 // dentro shared_ptr, così sia le callback I/O (in caso di HUP/ERR) sia
435 // il child_watch possono disconnetterle. Senza questa disconnessione
436 // esplicita, quando il processo termina i fd vanno in IO_HUP e GLib
437 // continua a richiamare la callback all'infinito (busy loop sul main
438 // loop → BackupPage va in "bootloop" e la UI si blocca, specie durante
439 // build pesanti come `build-recovery-squashfs.sh` che dura 10-20 min).
440 auto stdout_conn = std::make_shared<sigc::connection>();
441 auto stderr_conn = std::make_shared<sigc::connection>();
442 auto alive = alive_;
443
444 // Helper che drena ciò che resta sul channel in modo non bloccante,
445 // ignorando errori. Chiamato sia su HUP sia alla terminazione del child.
446 auto drain_channel = [this, alive](Glib::RefPtr<Glib::IOChannel> ch, bool is_stderr) {
447 if (!ch) return;
448 try {
449 for (;;) {
450 Glib::ustring line;
451 auto st = ch->read_line(line);
452 if (st != Glib::IOStatus::NORMAL || line.empty()) break;
453 BastionGuard::if_alive(alive, [this, &line, is_stderr]() {
454 if (is_stderr) {
455 logSafe(Glib::ustring::compose(_("⚠ %1"), line.raw()).raw());
456 } else {
457 logSafe(line.raw());
458 }
459 });
460 }
461 } catch (...) {}
462 };
463
464 // Leggi stdout
465 *stdout_conn = Glib::signal_io().connect(
466 [this, alive, stdout_ch, stdout_conn, drain_channel](Glib::IOCondition cond) -> bool {
467 // Drena tutto ciò che è disponibile.
468 if ((cond & Glib::IOCondition::IO_IN) != Glib::IOCondition()) {
469 try {
470 Glib::ustring line;
471 auto status = stdout_ch->read_line(line);
472 if (status == Glib::IOStatus::NORMAL && !line.empty()) {
473 BastionGuard::if_alive(alive, [this, &line]() {
474 logSafe(line.raw());
475 });
476 }
477 } catch (...) {}
478 }
479 // FIX BOOTLOOP: HUP/ERR → drena residuo, poi stacca la sorgente
480 // ritornando false. Senza questo, GLib richiama la callback
481 // all'infinito perché HUP è level-triggered.
482 if ((cond & (Glib::IOCondition::IO_HUP | Glib::IOCondition::IO_ERR))
483 != Glib::IOCondition()) {
484 drain_channel(stdout_ch, false);
485 return false;
486 }
487 return true;
488 },
489 stdout_fd, Glib::IOCondition::IO_IN | Glib::IOCondition::IO_HUP | Glib::IOCondition::IO_ERR
490 );
491
492 // Leggi stderr
493 *stderr_conn = Glib::signal_io().connect(
494 [this, alive, stderr_ch, stderr_conn, drain_channel](Glib::IOCondition cond) -> bool {
495 if ((cond & Glib::IOCondition::IO_IN) != Glib::IOCondition()) {
496 try {
497 Glib::ustring line;
498 auto status = stderr_ch->read_line(line);
499 if (status == Glib::IOStatus::NORMAL && !line.empty()) {
500 BastionGuard::if_alive(alive, [this, &line]() {
501 logSafe(Glib::ustring::compose(_("⚠ %1"), line.raw()).raw());
502 });
503 }
504 } catch (...) {}
505 }
506 if ((cond & (Glib::IOCondition::IO_HUP | Glib::IOCondition::IO_ERR))
507 != Glib::IOCondition()) {
508 drain_channel(stderr_ch, true);
509 return false;
510 }
511 return true;
512 },
513 stderr_fd, Glib::IOCondition::IO_IN | Glib::IOCondition::IO_HUP | Glib::IOCondition::IO_ERR
514 );
515
516 // Attendi completamento processo
517 Glib::signal_child_watch().connect(
518 [this, alive, show_success, stdout_ch, stderr_ch, stdout_conn, stderr_conn, drain_channel]
519 (Glib::Pid p, int status) {
520 // FIX BOOTLOOP: stacca esplicitamente le due signal_io PRIMA
521 // di chiudere i channel, altrimenti se il fd viene riciclato
522 // dal kernel le callback si riarmano su un altro fd.
523 if (stdout_conn && stdout_conn->connected()) stdout_conn->disconnect();
524 if (stderr_conn && stderr_conn->connected()) stderr_conn->disconnect();
525
526 // Drena eventuale residuo prima di chiudere.
527 drain_channel(stdout_ch, false);
528 drain_channel(stderr_ch, true);
529
530 try { stdout_ch->close(); } catch (...) {}
531 try { stderr_ch->close(); } catch (...) {}
532 Glib::spawn_close_pid(p);
533
534 BastionGuard::if_alive(alive, [this, show_success, status]() {
535 pulseTimer.disconnect();
536 buildRunning = false;
537 recoveryProgress.hide();
538 buildRecoveryButton.set_sensitive(true);
539 installGrubButton.set_sensitive(true);
540 removeGrubButton.set_sensitive(true);
541
542 bool ok = (WIFEXITED(status) && WEXITSTATUS(status) == 0);
543
544 if (ok) {
545 logSafe(Glib::ustring(_("==> Operazione completata con successo.\n")).raw());
546 if (show_success)
547 setResult(true, _("Completato — disponibile al prossimo avvio"));
548 } else {
549 int code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
550 logSafe(
551 Glib::ustring::compose(
552 _("==> Operazione terminata con errore (codice: %1)\n"),
553 code
554 ).raw()
555 );
556 setResult(
557 false,
558 Glib::ustring::compose(
559 _("Operazione fallita (codice %1)"),
560 code
561 ).raw()
562 );
563 }
564 });
565 },
566 pid
567 );
568}
569
570// ---------------------------------------------------------------------------
571// logSafe — aggiunge testo al log sul thread principale
572// ---------------------------------------------------------------------------
573
574void BackupPage::logSafe(const std::string& msg)
575{
576 auto alive = alive_;
577 Glib::signal_idle().connect_once([this, alive, msg]() {
578 BastionGuard::if_alive(alive, [this, &msg]() {
579 logBuffer->insert(logBuffer->end(), msg);
580 auto mark = logBuffer->get_insert();
581 logView.move_mark_onscreen(mark);
582 });
583 });
584}
static std::string fmtSize(guint64 bytes)
static const char * GRUB_ENTRY
static const char * RECOVERY_KERNEL
static const char * RECOVERY_SQUASHFS
static constexpr guint64 MIN_SPACE_BYTES
static const char * RECOVERY_DIR
static const char * INSTALL_GRUB_SCRIPT
static const char * BUILD_SCRIPT
static const char * BG_BACKUP_LAUNCHER
#define _(s)
sigc::connection pulseTimer
void logSafe(const std::string &msg)
void setResult(bool success, const std::string &msg)
Glib::RefPtr< Gtk::TextBuffer > logBuffer
Gtk::Label recoveryResultLabel
void onRemoveGrub()
void updateRecoveryStatus()
Gtk::Button removeGrubButton
void onInstallGrub()
Gtk::Widget * buildGrubTab()
Gtk::Button installGrubButton
Gtk::Widget * buildBackupTab()
~BackupPage() override
Gtk::Notebook notebook
BastionGuard::AliveFlag alive_
Gtk::Label recoveryStatusLabel
void runAsyncStreaming(const std::string &cmd, bool show_success=false)
Gtk::Label recoverySpaceLabel
Gtk::Button buildRecoveryButton
bool buildRunning
Gtk::ProgressBar recoveryProgress
void onBuildRecovery()
Gtk::TextView logView
Gtk::Widget * buildLogSection()
void if_alive(const AliveFlag &alive, Fn &&fn)