| gniza version | } . GnizaWHM::
print qq{\n};
print qq{\n\n};
+# Inline JavaScript for live status polling
+print qq{
+};
+
print GnizaWHM::UI::page_footer();
Whostmgr::HTMLInterface::footer();
+
+# --- JSON status endpoint ---
+sub handle_status_json {
+ print "Content-Type: application/json\r\n\r\n";
+
+ my $lock_file = '/var/run/gniza.lock';
+ my $running = 0;
+ my $pid = '';
+
+ # Check lock file for running PID
+ if (-f $lock_file) {
+ if (open my $fh, '<', $lock_file) {
+ $pid = <$fh> // '';
+ chomp $pid;
+ close $fh;
+ if ($pid =~ /^\d+$/ && kill(0, $pid)) {
+ $running = 1;
+ } else {
+ $pid = '';
+ }
+ }
+ }
+
+ # Find LOG_DIR from main config
+ my $log_dir = '/var/log/gniza';
+ my $main_conf_file = '/etc/gniza/gniza.conf';
+ if (-f $main_conf_file) {
+ my $cfg = GnizaWHM::Config::parse($main_conf_file, 'main');
+ $log_dir = $cfg->{LOG_DIR} if $cfg->{LOG_DIR} && $cfg->{LOG_DIR} ne '';
+ }
+
+ # Find most recent gniza-*.log by mtime
+ my $log_file = '';
+ my @lines;
+ if (opendir my $dh, $log_dir) {
+ my @logs = sort {
+ (stat("$log_dir/$b"))[9] <=> (stat("$log_dir/$a"))[9]
+ } grep { /^gniza-\d{8}-\d{6}\.log$/ } readdir($dh);
+ closedir $dh;
+ $log_file = $logs[0] // '';
+ }
+
+ # Read last 15 lines if running and log exists
+ if ($log_file && $running) {
+ my $path = "$log_dir/$log_file";
+ if (open my $fh, '<', $path) {
+ my @all = <$fh>;
+ close $fh;
+ my $start = @all > 15 ? @all - 15 : 0;
+ @lines = @all[$start .. $#all];
+ chomp @lines;
+ }
+ }
+
+ # Build JSON manually (no JSON module dependency)
+ my $json = '{"running":' . ($running ? 'true' : 'false');
+ $json .= ',"pid":"' . _json_esc($pid) . '"';
+ $json .= ',"log_file":"' . _json_esc($log_file) . '"';
+ $json .= ',"lines":[';
+ my @json_lines;
+ for my $line (@lines) {
+ my $level = 'INFO';
+ if ($line =~ /\]\s+\[(\w+)\]/) {
+ $level = uc($1);
+ }
+ push @json_lines, '{"level":"' . _json_esc($level) . '","text":"' . _json_esc($line) . '"}';
+ }
+ $json .= join(',', @json_lines);
+ $json .= ']}';
+
+ print $json;
+}
+
+sub _json_esc {
+ my ($s) = @_;
+ $s //= '';
+ $s =~ s/\\/\\\\/g;
+ $s =~ s/"/\\"/g;
+ $s =~ s/\n/\\n/g;
+ $s =~ s/\r/\\r/g;
+ $s =~ s/\t/\\t/g;
+ # Escape control characters
+ $s =~ s/([\x00-\x1f])/sprintf("\\u%04x", ord($1))/ge;
+ return $s;
+}
diff --git a/whm/gniza-whm/lib/GnizaWHM/Cron.pm b/whm/gniza-whm/lib/GnizaWHM/Cron.pm
index 9f18197..f9885f9 100644
--- a/whm/gniza-whm/lib/GnizaWHM/Cron.pm
+++ b/whm/gniza-whm/lib/GnizaWHM/Cron.pm
@@ -87,7 +87,7 @@ sub install_schedule {
if (($conf->{SYSBACKUP} // '') eq 'yes') {
$extra_flags .= " --sysbackup";
}
- my $cmd_line = "$cron_expr $GNIZA_BIN backup${extra_flags} >> /var/log/gniza/cron-${name}.log 2>&1";
+ my $cmd_line = "$cron_expr $GNIZA_BIN backup${extra_flags} >/dev/null 2>&1";
# Read current crontab, strip existing entry for this schedule, append new
my $crontab = _read_crontab();
diff --git a/whm/gniza-whm/lib/GnizaWHM/Runner.pm b/whm/gniza-whm/lib/GnizaWHM/Runner.pm
index 180bdd1..023d0a0 100644
--- a/whm/gniza-whm/lib/GnizaWHM/Runner.pm
+++ b/whm/gniza-whm/lib/GnizaWHM/Runner.pm
@@ -6,6 +6,8 @@ use strict;
use warnings;
use IPC::Open3;
use Symbol 'gensym';
+use POSIX qw(setsid);
+use File::Temp qw(tempfile);
my $GNIZA_BIN = '/usr/local/bin/gniza';
@@ -47,9 +49,9 @@ my %OPT_PATTERNS = (
account => qr/^[a-z][a-z0-9_-]*$/,
);
-# run($cmd, $subcmd, \@args, \%opts)
-# Returns ($success, $stdout, $stderr).
-sub run {
+# _validate($cmd, $subcmd, \@args, \%opts)
+# Returns (1, undef) on success or (0, $error_msg) on failure.
+sub _validate {
my ($cmd, $subcmd, $args, $opts) = @_;
$args //= [];
$opts //= {};
@@ -81,21 +83,61 @@ sub run {
unless ($matched) {
my $desc = "gniza $cmd" . (defined $subcmd ? " $subcmd" : "") . " " . join(" ", @$args);
- return (0, '', "Command not allowed: $desc");
+ return (0, "Command not allowed: $desc");
}
# Validate options
for my $key (keys %$opts) {
my $pat = $OPT_PATTERNS{$key};
unless ($pat) {
- return (0, '', "Unknown option: --$key");
+ return (0, "Unknown option: --$key");
}
unless ($opts->{$key} =~ $pat) {
- return (0, '', "Invalid value for --$key: $opts->{$key}");
+ return (0, "Invalid value for --$key: $opts->{$key}");
}
}
- # Build command
+ return (1, undef);
+}
+
+# _shell_quote($str)
+# Single-quote a string for safe shell embedding.
+sub _shell_quote {
+ my ($str) = @_;
+ $str =~ s/'/'\\''/g;
+ return "'$str'";
+}
+
+# _build_cmd_line($cmd, $subcmd, \@args, \%opts)
+# Returns a shell command string.
+sub _build_cmd_line {
+ my ($cmd, $subcmd, $args, $opts) = @_;
+ $args //= [];
+ $opts //= {};
+
+ my @parts = (_shell_quote($GNIZA_BIN), _shell_quote($cmd));
+ push @parts, _shell_quote($subcmd) if defined $subcmd;
+ push @parts, _shell_quote($_) for @$args;
+ for my $key (sort keys %$opts) {
+ push @parts, _shell_quote("--$key=$opts->{$key}");
+ }
+
+ return join(' ', @parts);
+}
+
+# run($cmd, $subcmd, \@args, \%opts)
+# Returns ($success, $stdout, $stderr).
+sub run {
+ my ($cmd, $subcmd, $args, $opts) = @_;
+ $args //= [];
+ $opts //= {};
+
+ my ($valid, $err) = _validate($cmd, $subcmd, $args, $opts);
+ unless ($valid) {
+ return (0, '', $err);
+ }
+
+ # Build exec args
my @exec_args = ($cmd);
push @exec_args, $subcmd if defined $subcmd;
push @exec_args, @$args;
@@ -106,6 +148,57 @@ sub run {
return _exec(@exec_args);
}
+# run_async(\@commands)
+# Each command: [$cmd, $subcmd, \@args, \%opts]
+# Validates all commands upfront, then forks a detached child to run them.
+# Returns ($success, $error_msg).
+sub run_async {
+ my ($commands) = @_;
+
+ return (0, 'No commands provided') unless $commands && @$commands;
+
+ # Validate all commands upfront
+ my @cmd_lines;
+ for my $c (@$commands) {
+ my ($cmd, $subcmd, $args, $opts) = @$c;
+ my ($valid, $err) = _validate($cmd, $subcmd, $args, $opts);
+ unless ($valid) {
+ return (0, $err);
+ }
+ push @cmd_lines, _build_cmd_line($cmd, $subcmd, $args, $opts);
+ }
+
+ # Write bash wrapper script
+ my ($fh, $tmpfile) = tempfile('gniza-whm-job-XXXXXXXX', DIR => '/tmp', SUFFIX => '.sh');
+ print $fh "#!/bin/bash\n";
+ print $fh "set -uo pipefail\n";
+ for my $line (@cmd_lines) {
+ print $fh "$line\n";
+ }
+ print $fh "rm -f \"\$0\"\n";
+ close $fh;
+ chmod 0700, $tmpfile;
+
+ # Fork and detach
+ my $pid = fork();
+ if (!defined $pid) {
+ unlink $tmpfile;
+ return (0, "Fork failed: $!");
+ }
+
+ if ($pid == 0) {
+ # Child: detach from parent
+ setsid();
+ open STDIN, '<', '/dev/null';
+ open STDOUT, '>', '/dev/null';
+ open STDERR, '>', '/dev/null';
+ exec('/bin/bash', $tmpfile) or exit(1);
+ }
+
+ # Parent: don't wait for child
+ return (1, '');
+}
+
sub _exec {
my (@args) = @_;
diff --git a/whm/gniza-whm/logs.cgi b/whm/gniza-whm/logs.cgi
index 47305fd..5dd5198 100755
--- a/whm/gniza-whm/logs.cgi
+++ b/whm/gniza-whm/logs.cgi
@@ -125,6 +125,25 @@ sub show_list {
print qq{\n};
}
+ # Auto-refresh while gniza is running
+ print qq{\n};
+
print GnizaWHM::UI::page_footer();
}
@@ -263,6 +282,26 @@ sub show_file {
print qq{\n};
}
+ # Auto-refresh + auto-scroll while gniza is running
+ print qq{\n};
+
print GnizaWHM::UI::page_footer();
}
diff --git a/whm/gniza-whm/restore.cgi b/whm/gniza-whm/restore.cgi
index f092c78..f5aa662 100644
--- a/whm/gniza-whm/restore.cgi
+++ b/whm/gniza-whm/restore.cgi
@@ -890,19 +890,17 @@ sub handle_step4 {
exit;
}
- # Execute each type sequentially
- my @results;
+ # Build commands for async execution
+ my @commands;
for my $type (@selected_types) {
my %opts = (remote => $remote);
$opts{timestamp} = $timestamp if $timestamp ne '';
if ($SIMPLE_TYPES{$type}) {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', $type, [$account], \%opts);
- push @results, { type => $type, label => $TYPE_LABELS{$type} // $type, ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', $type, [$account], {%opts}];
} elsif ($type eq 'files') {
$opts{path} = $path if $path ne '';
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'files', [$account], \%opts);
- push @results, { type => $type, label => $TYPE_LABELS{$type} // $type, ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'files', [$account], {%opts}];
} elsif ($type eq 'database') {
my @dbs;
if ($dbnames ne '' && $dbnames ne '__ALL__') {
@@ -910,12 +908,10 @@ sub handle_step4 {
}
if (@dbs) {
for my $db (@dbs) {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'database', [$account, $db], \%opts);
- push @results, { type => $type, label => "Database: $db", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'database', [$account, $db], {%opts}];
}
} else {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'database', [$account], \%opts);
- push @results, { type => $type, label => 'Database: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'database', [$account], {%opts}];
}
} elsif ($type eq 'dbusers') {
my @dbus;
@@ -924,12 +920,10 @@ sub handle_step4 {
}
if (@dbus) {
for my $dbu (@dbus) {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'dbusers', [$account, $dbu], \%opts);
- push @results, { type => $type, label => "DB User: $dbu", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'dbusers', [$account, $dbu], {%opts}];
}
} else {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'dbusers', [$account], \%opts);
- push @results, { type => $type, label => 'Database Users: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'dbusers', [$account], {%opts}];
}
} elsif ($type eq 'mailbox') {
my @mbs;
@@ -938,12 +932,10 @@ sub handle_step4 {
}
if (@mbs) {
for my $mb (@mbs) {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'mailbox', [$account, $mb], \%opts);
- push @results, { type => $type, label => "Mailbox: $mb", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'mailbox', [$account, $mb], {%opts}];
}
} else {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'mailbox', [$account], \%opts);
- push @results, { type => $type, label => 'Mailbox: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'mailbox', [$account], {%opts}];
}
} elsif ($type eq 'domains') {
my @doms;
@@ -952,12 +944,10 @@ sub handle_step4 {
}
if (@doms) {
for my $dom (@doms) {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'domains', [$account, $dom], \%opts);
- push @results, { type => $type, label => "Domain: $dom", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'domains', [$account, $dom], {%opts}];
}
} else {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'domains', [$account], \%opts);
- push @results, { type => $type, label => 'Domains: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'domains', [$account], {%opts}];
}
} elsif ($type eq 'ssl') {
my @certs;
@@ -966,52 +956,22 @@ sub handle_step4 {
}
if (@certs) {
for my $cert (@certs) {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'ssl', [$account, $cert], \%opts);
- push @results, { type => $type, label => "SSL: $cert", ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'ssl', [$account, $cert], {%opts}];
}
} else {
- my ($ok, $stdout, $stderr) = GnizaWHM::Runner::run('restore', 'ssl', [$account], \%opts);
- push @results, { type => $type, label => 'SSL: All', ok => $ok, stdout => $stdout // '', stderr => $stderr // '' };
+ push @commands, ['restore', 'ssl', [$account], {%opts}];
}
- } else {
- push @results, { type => $type, label => $TYPE_LABELS{$type} // $type, ok => 0, stdout => '', stderr => 'Invalid restore type' };
}
}
- print "Content-Type: text/html\r\n\r\n";
- Whostmgr::HTMLInterface::defheader('GNIZA Backup Manager — Restore', '', '/cgi/gniza-whm/restore.cgi');
-
- print GnizaWHM::UI::page_header('Restore from Backup');
- print GnizaWHM::UI::render_nav('restore.cgi');
-
- # Overall status
- my $all_ok = !grep { !$_->{ok} } @results;
- my $any_ok = grep { $_->{ok} } @results;
- if ($all_ok) {
- print qq{ All restore operations completed successfully. \n};
- } elsif ($any_ok) {
- print qq{Some restore operations failed. See details below. \n};
+ my ($ok, $err) = GnizaWHM::Runner::run_async(\@commands);
+ if ($ok) {
+ GnizaWHM::UI::set_flash('success', 'Restore started in background. Watch progress below.');
+ print "Status: 302 Found\r\n";
+ print "Location: logs.cgi\r\n\r\n";
} else {
- print qq{All restore operations failed. \n};
+ GnizaWHM::UI::set_flash('error', "Failed to start restore: $err");
+ print "Status: 302 Found\r\n";
+ print "Location: restore.cgi\r\n\r\n";
}
-
- # Per-type output blocks
- for my $r (@results) {
- my $esc_label = GnizaWHM::UI::esc($r->{label});
- my $badge = $r->{ok}
- ? 'OK'
- : 'Failed';
-
- print qq{\n \n};
- print qq{ $esc_label $badge\n};
- my $output = $r->{stdout} . ($r->{stderr} =~ /\S/ ? "\n$r->{stderr}" : '');
- $output = '(no output)' unless $output =~ /\S/;
- print qq{ } . GnizaWHM::UI::esc($output) . qq{\n};
- print qq{ \n \n};
- }
-
- print qq{\n};
-
- print GnizaWHM::UI::page_footer();
- Whostmgr::HTMLInterface::footer();
}
|