diff --git a/logviewer/config b/logviewer/config index 3bf7bf31f..13952b3a2 100644 --- a/logviewer/config +++ b/logviewer/config @@ -1,5 +1,5 @@ skip_index=1 -lines=1000 +lines=100 others=0 reverse=1 log_any=0 diff --git a/logviewer/lang/en b/logviewer/lang/en index 90938c829..66975e5cc 100644 --- a/logviewer/lang/en +++ b/logviewer/lang/en @@ -17,25 +17,34 @@ journal_journalctl_notice_warning=Notice and warning messages journal_journalctl_err_crit=Error and critical messages journal_journalctl_alert_emerg=Alert and emergency messages journal_journalctl_unit=Messages for specific unit -journal_since0=Current boot -journal_since1=7 days ago -journal_since2=24 hours ago -journal_since3=8 hours ago -journal_since4=1 hour ago -journal_since5=30 minutes ago -journal_since6=10 minutes ago -journal_since7=2 minute ago +journal_since0=Latest available +journal_since1=Real-time follow +journal_since2=Current boot +journal_since3=7 days ago +journal_since4=24 hours ago +journal_since5=8 hours ago +journal_since6=1 hour ago +journal_since7=30 minutes ago +journal_since8=10 minutes ago +journal_since9=3 minutes ago +journal_since10=1 minute ago +journal_sincefollow=in +journal_since=since view_title=View Logfile view_titlejournal=View Journal view_header=Last $1 lines of $2 view_header2=Last $1 lines +view_header3=Lines of $1 view_empty=Log file is empty view_refresh=Refresh view_filter=Filter lines with text $1 +view_filter2=Filter save_efile='$1' is not a valid filename : $2 save_ecannot2=You are not allowed to view this log +save_ecannot3=Error: You are not allowed to view this log +save_ecannot4=Error: Could not open '$1' save_ecannot6=You are not allowed to view arbitrary logs save_ecannot7=You are not allowed to view this extra log save_emissing=Missing log file to view diff --git a/logviewer/logviewer-lib.pl b/logviewer/logviewer-lib.pl index a6bb79045..4bd810c14 100755 --- a/logviewer/logviewer-lib.pl +++ b/logviewer/logviewer-lib.pl @@ -27,6 +27,21 @@ foreach $f (@files) { return 0; } +# get_journal_since +# Returns a list of journalctl commands to get logs since various times, +# which should correspond with language strings journal_since0, +# journal_since1, journal_since2, etc. +sub get_journal_since +{ +return + ("", "-f", + "-b", "-S '7 days ago'", + "-S '24 hours ago'", "-S '8 hours ago'", + "-S '1 hour ago'", "-S '30 minutes ago'", + "-S '10 minutes ago'", "-S '3 minutes ago'", + "-S '1 minute ago'"); +} + # get_systemctl_cmds([force-select]) # Returns logs for journalctl sub get_systemctl_cmds diff --git a/logviewer/view_log.cgi b/logviewer/view_log.cgi index 04aa1407f..7d586a2f6 100755 --- a/logviewer/view_log.cgi +++ b/logviewer/view_log.cgi @@ -7,7 +7,7 @@ require './logviewer-lib.pl'; &foreign_require("proc", "proc-lib.pl"); # Viewing a log file -@extras = &extra_log_files(); +my @extras = &extra_log_files(); if ($in{'idx'} =~ /^\//) { # The drop-down selector on this page has chosen a file if (&indexof($in{'idx'}, (map { $_->{'file'} } @extras)) >= 0) { @@ -21,25 +21,23 @@ if ($in{'idx'} =~ /^\//) { delete($in{'idx'}); delete($in{'oidx'}); } -my @journal_since = - ("-b", "-S '7 days ago'", - "-S '24 hours ago'", "-S '8 hours ago'", - "-S '1 hour ago'", "-S '30 minutes ago'", - "-S '10 minutes ago'", "-S '1 minute ago'"); +my @journal_since = &get_journal_since(); if ($in{'idx'} ne '') { # From systemctl commands if ($in{'idx'} =~ /^journal-/) { my @systemctl_cmds = &get_systemctl_cmds(1); my ($log); if ($in{'idx'} eq 'journal-u') { - ($log) = grep { $_->{'cmd'} =~ /-u\s+\w+/ } @systemctl_cmds; + ($log) = grep { $_->{'cmd'} =~ /-u\s+\w+/ } + @systemctl_cmds; $in{'idx'} = $log->{'id'}; } else { - ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds; - } + ($log) = grep { $_->{'id'} eq $in{'idx'} } + @systemctl_cmds; + } # If reverse is set, add it to the command - if ($config{'reverse'}) { + if ($reverse) { $log->{'cmd'} .= " -r"; } # If since is set and allowed, add it to the command @@ -114,6 +112,11 @@ else { } print "Refresh: $config{'refresh'}\r\n" if ($config{'refresh'}); +my $lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'}); +my $jfilter = $in{'filter'} ? $in{'filter'} : ""; +my $filter = $jfilter ? quotemeta($jfilter) : ""; +my $reverse = $config{'reverse'} ? 1 : 0; +my $follow = $in{'since'} eq '-f' ? 1 : 0; my $no_navlinks = $in{'nonavlinks'} == 1 ? 1 : undef; my $skip_index = $config{'skip_index'} == 1 ? 1 : undef; my $help_link = (!$no_navlinks && $skip_index) ? @@ -121,6 +124,9 @@ my $help_link = (!$no_navlinks && $skip_index) ? my $no_links = $no_navlinks || $skip_index; my $cmd_unpacked = $cmd; $cmd_unpacked =~ s/\\x([0-9A-Fa-f]{2})/pack('H2', $1)/eg; +$cmd_unpacked =~ s/\s+\-r// if ($follow); +$cmd_unpacked =~ s/\s+\-n\s+\d+// if ($follow); +$cmd_unpacked .= " -g \"@{[&html_escape($jfilter)]}\"" if ($filter); my $view_title = $in{'idx'} =~ /^journal/ ? $text{'view_titlejournal'} : $text{'view_title'}; &ui_print_header("".&html_escape($file || $cmd_unpacked)."", @@ -129,99 +135,144 @@ my $view_title = $in{'idx'} =~ /^journal/ ? ($no_navlinks || $skip_index) ? 1 : undef, 0, $help_link); -$lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'}); -$filter = $in{'filter'} ? quotemeta($in{'filter'}) : ""; - &filter_form(); -$| = 1; -print "
";
-local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
-$tailcmd =~ s/LINES/$lines/g;
-my $safe_proc_out;
-if ($filter ne "") {
-	# Are we supposed to filter anything? Then use grep.
-	local @cats;
-	if ($cmd) {
-		# Getting output from a command
-		push(@cats, $cmd);
-		}
-	elsif ($config{'compressed'}) {
-		# All compressed versions
-		foreach $l (&all_log_files($file)) {
-			$c = &catter_command($l);
-			push(@cats, $c) if ($c);
-			}
-		}
-	else {
-		# Just the one log
-		@cats = ( "cat ".quotemeta($file) );
-		}
-	$cat = "(".join(" ; ", @cats).")";
-	if ($config{'reverse'}) {
-		$tailcmd .= " | tac" if ($fullcmd !~ /journalctl/);
-		}
-	$eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
-	$dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
-	if (@cats) {
-		$got = &proc::safe_process_exec(
-			"$cat | grep -i -a $eflag $dashflag $filter ".
-			"| $tailcmd",
-			0, 0, STDOUT, undef, 1, 0, undef, 1);
-		}
-	else {
-		$got = undef;
-		}
-} else {
-	# Not filtering .. so cat the most recent non-empty file
-	if ($cmd) {
-		# Getting output from a command
-		$fullcmd = $cmd.($fullcmd !~ /journalctl/ ? "" : " | ".$tailcmd);
-		}
-	elsif ($config{'compressed'}) {
-		# Cat all compressed files
+# Standard output
+if (!$follow) {
+	$| = 1;
+	print "
";
+	local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
+	$tailcmd =~ s/LINES/$lines/g;
+	my $safe_proc_out;
+	if ($filter ne "") {
+		# Are we supposed to filter anything? Then use grep.
 		local @cats;
-		$total = 0;
-		foreach $l (reverse(&all_log_files($file))) {
-			next if (!-s $l);
-			$c = &catter_command($l);
-			if ($c) {
-				$len = int(&backquote_command(
-						"$c | wc -l"));
-				$total += $len;
-				push(@cats, $c);
-				last if ($total > $in{'lines'});
+		if ($cmd) {
+			# Getting output from a command
+			push(@cats, $cmd);
+			}
+		elsif ($config{'compressed'}) {
+			# All compressed versions
+			foreach $l (&all_log_files($file)) {
+				$c = &catter_command($l);
+				push(@cats, $c) if ($c);
 				}
 			}
+		else {
+			# Just the one log
+			@cats = ( "cat ".quotemeta($file) );
+			}
+		$cat = "(".join(" ; ", @cats).")";
+		if ($reverse) {
+			$tailcmd .= " | tac" if ($cmd !~ /journalctl/);
+			}
+		$eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
+		$dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
 		if (@cats) {
-			$cat = "(".join(" ; ", reverse(@cats)).")";
-			$fullcmd = $cat." | ".$tailcmd;
+			my $fcmd;
+			if ($cmd =~ /journalctl/) {
+				$fcmd = "$cmd -g $filter";
+				}
+			else {
+				$fcmd = "$cat | grep -i -a $eflag $dashflag $filter ".
+					"| $tailcmd";
+				}
+			$got = &proc::safe_process_exec($fcmd,
+				0, 0, STDOUT, undef, 1, 0, undef, 1);
 			}
 		else {
-			$fullcmd = undef;
+			$got = undef;
+			}
+	} else {
+		# Not filtering .. so cat the most recent non-empty file
+		if ($cmd) {
+			# Getting output from a command
+			$fullcmd = $cmd.($cmd =~ /journalctl/ ? "" : (" | ".$tailcmd));
+			}
+		elsif ($config{'compressed'}) {
+			# Cat all compressed files
+			local @cats;
+			$total = 0;
+			foreach $l (reverse(&all_log_files($file))) {
+				next if (!-s $l);
+				$c = &catter_command($l);
+				if ($c) {
+					$len = int(&backquote_command(
+							"$c | wc -l"));
+					$total += $len;
+					push(@cats, $c);
+					last if ($total > $in{'lines'});
+					}
+				}
+			if (@cats) {
+				$cat = "(".join(" ; ", reverse(@cats)).")";
+				$fullcmd = $cat." | ".$tailcmd;
+				}
+			else {
+				$fullcmd = undef;
+				}
+			}
+		else {
+			# Just run tail on the file
+			$fullcmd = $tailcmd." ".quotemeta($file);
+			}
+		if ($reverse && $fullcmd) {
+			$fullcmd .= " | tac" if ($fullcmd !~ /journalctl/);
+			}
+		if ($fullcmd) {
+			open(my $output_fh, '>', \$safe_proc_out);
+			$got = &proc::safe_process_exec(
+				$fullcmd, 0, 0, $output_fh, undef, 1, 0, undef, 1);
+			close($output_fh);
+			print $safe_proc_out if ($safe_proc_out !~ /-- No entries --/m);
+			}
+		else {
+			$got = undef;
 			}
 		}
-	else {
-		# Just run tail on the file
-		$fullcmd = $tailcmd." ".quotemeta($file);
-		}
-	if ($config{'reverse'} && $fullcmd) {
-		$fullcmd .= " | tac" if ($fullcmd !~ /journalctl/);
-		}
-	if ($fullcmd) {
-		open(my $output_fh, '>', \$safe_proc_out);
-		$got = &proc::safe_process_exec(
-			$fullcmd, 0, 0, $output_fh, undef, 1, 0, undef, 1);
-		close($output_fh);
-		print $safe_proc_out if ($safe_proc_out !~ /-- No entries --/m);
-		}
-	else {
-		$got = undef;
-		}
+	print "$text{'view_empty'}\n"
+		if (!$got || $safe_proc_out =~ /-- No entries --/m);
+	print "
\n"; + } +# Progressive output +else { + print "
";
+	print "
\n"; + print < + // Update log viewer with new data from the server + (async function () { + const logDataElement = document.getElementById("logdata"), + response = await fetch("view_log_progress.cgi?idx=$in{'idx'}&filter=$jfilter"), + reader = response.body.getReader(), + decoder = new TextDecoder("utf-8"), + processText = async function () { + let { done, value } = await reader.read(); + while (!done) { + const chunk = decoder.decode(value, { stream: true }).trim(), + dataReversed = logDataElement.getAttribute("data-reversed"); + let lines = chunk.split("\\n"); + if (dataReversed === "1") { + lines = lines.reverse(); + logDataElement.textContent = + lines.join("\\n") + "\\n" + logDataElement.textContent; + } + else { + logDataElement.textContent += lines.join("\\n") + "\\n"; + } + if (typeof logviewer_progress_update === 'function') { + logviewer_progress_update(chunk, dataReversed); + } + ({ done, value } = await reader.read()); + } + }; + processText().catch((error) => { + console.error("Failed to fetch log progress:", error); + }); + })(); + +EOF } -print "$text{'view_empty'}\n" - if (!$got || $safe_proc_out =~ /-- No entries --/m); -print "
\n"; &filter_form(); if ($no_links) { &ui_print_footer(); @@ -315,7 +366,9 @@ if (@logfiles && $found) { push(@$selots, [ $journal_since[$i], $text{'journal_since'.$i} ]); } - $sel .= "since  " . + my $since_label = $follow ? $text{'journal_sincefollow'} : + $text{'journal_since'}; + $sel .= "$since_label  " . &ui_select("since", $in{'since'}, $selots, undef, undef, undef, undef, "onChange='form.submit()'"); } @@ -324,12 +377,17 @@ else { $text_view_header = 'view_header2'; print &ui_hidden("idx", $in{'idx'}),"\n"; } - -print &text($text_view_header, " " . &ui_textbox("lines", $lines, 3), " $sel"),"\n"; +if ($follow) { + print &text('view_header3', " $sel"),"\n"; + } +else { + print &text($text_view_header, " " . &ui_textbox("lines", $lines, 3), " $sel"),"\n"; + } print "    \n"; print &text('view_filter', " " . &ui_textbox("filter", $in{'filter'}, 12)),"\n"; + print "  \n"; -print &ui_submit($text{'view_refresh'}); +print &ui_submit($follow ? $text{'view_filter2'} : $text{'view_refresh'}); print &ui_form_end(),"
\n"; } diff --git a/logviewer/view_log_progress.cgi b/logviewer/view_log_progress.cgi new file mode 100644 index 000000000..43b06d48e --- /dev/null +++ b/logviewer/view_log_progress.cgi @@ -0,0 +1,47 @@ +#!/usr/local/bin/perl +# view_log_progress.cgi +# Returns progressive output for some system log + +require './logviewer-lib.pl'; +&ReadParse(); +&foreign_require("proc", "proc-lib.pl"); + +# System log to follow +my @systemctl_cmds = &get_systemctl_cmds(1); +my ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds; +my $cmd = $log->{'cmd'}; + +# Disable output buffering +print "Content-Type: text/plain\n\n"; +$| = 1; + +# Access check +if (!$cmd || $cmd !~ /^journalctl/ || + !(&can_edit_log($log) && $access{'syslog'})) { + print $text{'save_ecannot3'}; + exit; + } + +# No lines for real time logs +$cmd =~ s/\s+\-n\s+\d+//; + +# Show real time logs +$cmd .= " -f"; + +# Add filter to the command if present +my $filter = $in{'filter'} ? quotemeta($in{'filter'}) : ""; +if ($filter) { + $cmd .= " -g $filter"; + } + +# Open a pipe to the journalctl command +my $pid = open(my $fh, '-|', "$cmd") || + print &text('save_ecannot4', $cmd).": $!"; + +# Read and output the log +while (my $line = <$fh>) { + print $line; + } + +# Clean up when done +close($fh);