From patchwork Thu Dec 2 15:39:53 2021 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Leo-Andres Hofmann X-Patchwork-Id: 4891 Return-Path: Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail01.haj.ipfire.org", Issuer "R3" (verified OK)) by web04.haj.ipfire.org (Postfix) with ESMTPS id 4J4gBs1C12z3wg9 for ; Thu, 2 Dec 2021 15:40:21 +0000 (UTC) Received: from mail02.haj.ipfire.org (mail02.haj.ipfire.org [172.28.1.201]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail02.haj.ipfire.org", Issuer "R3" (verified OK)) by mail01.ipfire.org (Postfix) with ESMTPS id 4J4gBn46vQz3R2; Thu, 2 Dec 2021 15:40:17 +0000 (UTC) Received: from mail02.haj.ipfire.org (localhost [127.0.0.1]) by mail02.haj.ipfire.org (Postfix) with ESMTP id 4J4gBn2kjhz30HL; Thu, 2 Dec 2021 15:40:17 +0000 (UTC) Received: from mail01.ipfire.org (mail01.haj.ipfire.org [172.28.1.202]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (P-384) client-signature ECDSA (P-384)) (Client CN "mail01.haj.ipfire.org", Issuer "R3" (verified OK)) by mail02.haj.ipfire.org (Postfix) with ESMTPS id 4J4gBm3bjgz2yfp for ; Thu, 2 Dec 2021 15:40:16 +0000 (UTC) Received: from arche.uberspace.de (arche.uberspace.de [185.26.156.147]) (using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by mail01.ipfire.org (Postfix) with ESMTPS id 4J4gBg1LWxz2f6 for ; Thu, 2 Dec 2021 15:40:11 +0000 (UTC) Received: (qmail 22068 invoked from network); 2 Dec 2021 15:40:06 -0000 Received: from localhost (HELO localhost) (127.0.0.1) by arche.uberspace.de with SMTP; 2 Dec 2021 15:40:06 -0000 From: Leo-Andres Hofmann To: development@lists.ipfire.org Subject: [PATCH 2/4] pakfire.cgi: Implement JavaScript log message display Date: Thu, 2 Dec 2021 16:39:53 +0100 Message-Id: <20211202153955.1126-2-hofmann@leo-andres.de> X-Mailer: git-send-email 2.27.0.windows.1 In-Reply-To: <20211202153955.1126-1-hofmann@leo-andres.de> References: <20211202153955.1126-1-hofmann@leo-andres.de> MIME-Version: 1.0 ARC-Seal: i=1; s=202003rsa; d=lists.ipfire.org; t=1638459611; a=rsa-sha256; cv=none; b=jqil0/upNfXczagIb+gQOp3Rbsu3fG7ONlDWzCTqvguqi3+9xskSAaktf3dAd9baxxKXUs Cjm7VBTTc5hSdCr/r7aX+/Iqsn47/n24cpZaTkHLdWiPN+6URm9ADMc22y1kT65Uhko/8u u2ystx4/jbDkoyb+Xb85fO2J/dux/OwL521ad5q8aCJ3zaSWd8xCwT7KGdtmI62NT8eeai 5gAuM/xbRYFv4VjRxpRrpzD1XWXVIK8btLhXEbBRFJRfy5kCnRQDJBFYCk6DlgVNiGJpOW 4yxA/gbpLBkGYI9RAlAK/ldPlBEAt9NGr90E3DINGnkzfOodor1BP0LFCSGBGg== ARC-Authentication-Results: i=1; mail01.ipfire.org; dkim=none; spf=pass (mail01.ipfire.org: domain of hofmann@leo-andres.de designates 185.26.156.147 as permitted sender) smtp.mailfrom=hofmann@leo-andres.de; dmarc=pass (policy=none) header.from=leo-andres.de ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=lists.ipfire.org; s=202003rsa; t=1638459611; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=wm2Ef+dbhjZ4XQ/SUWB8l/fAgrfH8PkoT4WECJhupyI=; b=v7iA165fiVcxD8QHTSOzF0d/J61kyTJRuOIpnbeoRksnZ3Tnoifdwp292tlPzL2q13KOT8 XOzT86vUySY7IVi/v/U13MXQBGvib/MfwiphkjYN5yNvjW0P4vKqkmeqykMCQZnNbbCe/Q 1MvG8JUTgN7Bugx1ZxfiRy9k7U7QVCO1e9rVU+xYgNvvtUT1W/8an3LhoT5VG2n7kfjL9h gfkhj0DCityeJZPnmFk6AtBOF3/C/cBZjlfHSabvJFPapNE8dAZXjEPP9hLk6ya/Y6lYKM /e0d/kdvVbSe9I9+CQVaD5O6cOr7ac3neGbQzWyaoMMjQEhwX5N3iKR/ibOaAw== X-Rspamd-Queue-Id: 4J4gBg1LWxz2f6 Authentication-Results: mail01.ipfire.org; dkim=none; spf=pass (mail01.ipfire.org: domain of hofmann@leo-andres.de designates 185.26.156.147 as permitted sender) smtp.mailfrom=hofmann@leo-andres.de; dmarc=pass (policy=none) header.from=leo-andres.de X-Spamd-Result: default: False [-1.45 / 11.00]; BAYES_HAM(-3.00)[99.99%]; MID_CONTAINS_FROM(1.00)[]; NEURAL_SPAM(0.94)[0.942]; SPF_REPUTATION_HAM(-0.88)[-0.88338842932253]; MX_INVALID(0.50)[]; DMARC_POLICY_ALLOW(-0.50)[leo-andres.de,none]; R_MISSING_CHARSET(0.50)[]; R_SPF_ALLOW(-0.20)[+mx:c]; MIME_HTML_ONLY(0.20)[]; IP_REPUTATION_HAM(-0.01)[asn: 205766(0.00), country: DE(-0.01), ip: 185.26.156.147(0.00)]; ASN(0.00)[asn:205766, ipnet:185.26.156.0/24, country:DE]; R_DKIM_NA(0.00)[]; FROM_EQ_ENVFROM(0.00)[]; MIME_TRACE(0.00)[0:~]; RCVD_TLS_LAST(0.00)[]; RCVD_COUNT_TWO(0.00)[2]; FROM_HAS_DN(0.00)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; TO_DN_NONE(0.00)[]; RCPT_COUNT_ONE(0.00)[1]; ARC_SIGNED(0.00)[lists.ipfire.org:s=202003rsa:i=1]; ARC_NA(0.00)[] X-Rspamd-Server: mail01.haj.ipfire.org X-BeenThere: development@lists.ipfire.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: IPFire development talk List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: development-bounces@lists.ipfire.org Sender: "Development" Currently the page becomes unresponsive while Pakfire is busy. This patch implements a AJAX/JSON driven log output, to provide continuous information to the user while Pakfire is running. The output is updated 1x per second, if the load should be too high, the interval can be change by writing to "pakfire.refreshInterval". Signed-off-by: Leo-Andres Hofmann --- html/cgi-bin/pakfire.cgi | 153 ++++++++++++++++++---- html/html/include/pakfire.js | 241 +++++++++++++++++++++++++++++++++++ 2 files changed, 368 insertions(+), 26 deletions(-) create mode 100644 html/html/include/pakfire.js diff --git a/html/cgi-bin/pakfire.cgi b/html/cgi-bin/pakfire.cgi index 7957bc154..e5f5f7d6a 100644 --- a/html/cgi-bin/pakfire.cgi +++ b/html/cgi-bin/pakfire.cgi @@ -36,8 +36,11 @@ my %color = (); my %pakfiresettings = (); my %mainsettings = (); -&Header::showhttpheaders(); +# Load general settings +&General::readhash("${General::swroot}/main/settings", \%mainsettings); +&General::readhash("/srv/web/ipfire/html/themes/ipfire/include/colors.txt", \%color); +# Get CGI request data $cgiparams{'ACTION'} = ''; $cgiparams{'VALID'} = ''; @@ -46,12 +49,102 @@ $cgiparams{'DELPAKS'} = ''; &Header::getcgihash(\%cgiparams); -&General::readhash("${General::swroot}/main/settings", \%mainsettings); -&General::readhash("/srv/web/ipfire/html/themes/ipfire/include/colors.txt", \%color); +### Process AJAX/JSON request ### +if($cgiparams{'ACTION'} eq 'json-getstatus') { + # Send HTTP headers + _start_json_output(); + + # Collect Pakfire status and log messages + my %status = ( + 'running' => &_is_pakfire_busy() || "0", + 'running_since' => &General::age("$Pakfire::lockfile") || "0s", + 'reboot' => (-e "/var/run/need_reboot") || "0" + ); + my @messages = `tac /var/log/messages | sed -n '/pakfire:/{p;/Pakfire.*started/q}'`; + + # Start JSON file + print "{\n"; + + foreach my $key (keys %status) { + my $value = $status{$key}; + print qq{\t"$key": "$value",\n}; + } + + # Print sanitized messages in reverse order to undo previous "tac" + print qq{\t"messages": [\n}; + for my $index (reverse (0 .. $#messages)) { + my $line = $messages[$index]; + $line =~ s/[[:cntrl:]<>&\\]+//g; + + print qq{\t\t"$line"}; + print ",\n" unless $index < 1; + } + print "\n\t]\n"; + + # Finalize JSON file & stop + print "}"; + exit; +} + +### Start pakfire page ### +&Header::showhttpheaders(); + +###--- HTML HEAD ---### +my $extraHead = < + /* Pakfire log viewer */ + section#pflog-header { + width: 100%; + display: flex; + text-align: left; + align-items: center; + column-gap: 20px; + } + #pflog-header > div:last-child { + margin-left: auto; + margin-right: 20px; + } + #pflog-header span { + line-height: 1.3em; + } + #pflog-header span:empty::before { + content: "\\200b"; /* zero width space */ + } + + pre#pflog-messages { + margin-top: 0.7em; + padding-top: 0.7em; + border-top: 0.5px solid $Header::bordercolour; -&Header::openpage($Lang::tr{'pakfire configuration'}, 1); + text-align: left; + min-height: 15em; + overflow-x: auto; + } + + + + +END +; +###--- END HTML HEAD ---### + +&Header::openpage($Lang::tr{'pakfire configuration'}, 1, $extraHead); &Header::openbigbox('100%', 'left', '', $errormessage); +# Process Pakfire commands if (($cgiparams{'ACTION'} eq 'install') && (! &_is_pakfire_busy())) { my @pkgs = split(/\|/, $cgiparams{'INSPAKS'}); if ("$cgiparams{'FORCE'}" eq "on") { @@ -170,29 +263,30 @@ if ($errormessage) { &Header::closebox(); } -# Check if pakfire is already running. -if (&_is_pakfire_busy()) { - &Header::openbox( 'Waiting', 1, "" ); - print < - - $Lang::tr{  - - $Lang::tr{'pakfire working'} - -
- -
- -END - my @output = `grep pakfire /var/log/messages | tail -20`; - foreach (@output) { - print "$_
"; - } - print < - +# Show log output while Pakfire is running +if(&_is_pakfire_busy()) { + &Header::openbox("100%", "center", "Pakfire"); + + print < +
$Lang::tr{'active'}
+
+ $Lang::tr{'pakfire working'}
+
+ +
+
$Lang::tr{'refresh'}
+ + + +

+
+
 END
+;
+
 	&Header::closebox();
 	&Header::closebigbox();
 	&Header::closepage();
@@ -320,3 +414,10 @@ sub _is_pakfire_busy {
 	# Test presence of PID or lockfile
 	return (($pakfire_pid) || (-e "$Pakfire::lockfile"));
 }
+
+# Send HTTP headers
+sub _start_json_output {
+	print "Cache-Control: no-cache, no-store\n";
+	print "Content-Type: application/json\n";
+	print "\n"; # End of HTTP headers
+}
diff --git a/html/html/include/pakfire.js b/html/html/include/pakfire.js
new file mode 100644
index 000000000..0950870e0
--- /dev/null
+++ b/html/html/include/pakfire.js
@@ -0,0 +1,241 @@
+/*#############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2007-2021  IPFire Team                       #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see .       #
+#                                                                             #
+#############################################################################*/
+
+"use strict";
+
+// Pakfire Javascript functions (requires jQuery)
+class PakfireJS {
+	constructor() {
+		//--- Public properties ---
+		// Translation strings
+		this.i18n = new PakfireI18N();
+
+		//--- Private properties ---
+		// Status flags (access outside constructor only with setter/getter)
+		this._states = Object.create(null);
+		this._states.running = false;
+		this._states.reboot = false;
+
+		// Status refresh helper
+		this._autoRefresh = {
+			delay: 1000, //Delay between requests (default: 1s)
+			jsonAction: 'getstatus', //CGI POST action parameter
+			timeout: 5000, //XHR timeout (5s)
+
+			delayTimer: null, //setTimeout reference
+			jqXHR: undefined, //jQuery.ajax promise reference
+			get runningDelay() { //Waiting for end of delay
+				return (this.delayTimer !== null);
+			},
+			get runningXHR() { //Waiting for CGI response
+				return (this.jqXHR && (this.jqXHR.state() === 'pending'));
+			},
+			get isRunning() {
+				return (this.runningDelay || this.runningXHR);
+			}
+		};
+	}
+
+	//### Public properties ###
+
+	// Pakfire is running (true/false)
+	set running(state) {
+		if(this._states.running !== state) {
+			this._states.running = state;
+			this._states_onChange('running');
+		}
+	}
+	get running() {
+		return this._states.running;
+	}
+
+	// Reboot needed (true/false)
+	set reboot(state) {
+		if(this._states.reboot !== state) {
+			this._states.reboot = state;
+			this._states_onChange('reboot');
+		}
+	}
+	get reboot() {
+		return this._states.reboot;
+	}
+
+	// Status refresh interval in ms
+	set refreshInterval(delay) {
+		if(delay < 500) {
+			delay = 500; //enforce reasonable minimum
+		}
+		this._autoRefresh.delay = delay;
+	}
+	get refreshInterval() {
+		return this._autoRefresh.delay;
+	}
+
+	// Document loaded (call once from jQuery.ready)
+	documentReady() {
+		// Status refresh late start
+		if(this.running && (! this._autoRefresh.isRunning)) {
+			this._autoRefresh_runNow();
+		}
+	}
+
+	//### Private properties ###
+
+	// Pakfire status change handler
+	// property: Affected status (running, reboot, ...)
+	_states_onChange(property) {
+		// Always update UI
+		if(this.running) {
+			$('#pflog-status').text(this.i18n.get('working'));
+			$('#pflog-action').empty();
+		} else {
+			$('#pflog-status').text(this.i18n.get('finished'));
+			if(this.reboot) { //Enable return or reboot links in UI
+				$('#pflog-action').html(this.i18n.get('link_reboot'));
+			} else {
+				$('#pflog-action').html(this.i18n.get('link_return'));
+			}
+		}
+
+		// Start/stop status refresh if Pakfire started/stopped
+		if(property === 'running') {
+			if(this.running) {
+				this._autoRefresh_runNow();
+			} else {
+				this._autoRefresh_clearSchedule();
+			}
+		}
+	}
+
+	//--- Status refresh scheduling functions ---
+
+	// Immediately perform AJAX status refresh request
+	_autoRefresh_runNow() {
+		if(this._autoRefresh.runningXHR) {
+			return; // Don't send multiple requests
+		}
+		this._autoRefresh_clearSchedule(); // Stop scheduled refresh, will send immediately
+
+		// Send AJAX request, attach listeners
+		this._autoRefresh.jqXHR = this._JSON_get(this._autoRefresh.jsonAction, this._autoRefresh.timeout);
+		this._autoRefresh.jqXHR.done(function() { // Request succeeded
+			if(this.running) { // Keep refreshing while Pakfire is running
+				this._autoRefresh_scheduleRun();
+			}
+		});
+		this._autoRefresh.jqXHR.fail(function() { // Request failed
+			this._autoRefresh_scheduleRun(); // Try refreshing until valid status is received
+		});
+	}
+
+	// Schedule next refresh
+	_autoRefresh_scheduleRun() {
+		if(this._autoRefresh.runningDelay || this._autoRefresh.runningXHR) {
+			return; // Refresh already scheduled or in progress
+		}
+		this._autoRefresh.delayTimer = window.setTimeout(function() {
+			this._autoRefresh.delayTimer = null;
+			this._autoRefresh_runNow();
+		}.bind(this), this._autoRefresh.delay);
+	}
+
+	// Stop scheduled refresh (can still be refreshed up to 1x if XHR is already sent)
+	_autoRefresh_clearSchedule() {
+		if(this._autoRefresh.runningDelay) {
+			window.clearTimeout(this._autoRefresh.delayTimer);
+			this._autoRefresh.delayTimer = null;
+		}
+	}
+
+	//--- JSON request & data handling ---
+
+	// Load JSON data from Pakfire CGI, using a POST request
+	// action: POST paramter "json-[action]"
+	// maxTime: XHR timeout, 0 = no timeout
+	_JSON_get(action, maxTime = 0) {
+		return $.ajax({
+			url: '/cgi-bin/pakfire.cgi',
+			method: 'POST',
+			timeout: maxTime,
+			context: this,
+			data: {'ACTION': `json-${action}`},
+			dataType: 'json' //automatically check and convert result
+		})
+			.done(function(response) {
+				this._JSON_process(action, response);
+			});
+	}
+
+	// Process successful response from Pakfire CGI
+	// action: POST paramter "json-[action]" used to send request
+	// data: JSON data object
+	_JSON_process(action, data) {
+		// Pakfire status refresh
+		if(action === this._autoRefresh.jsonAction) {
+			// Update status flags
+			this.running = (data['running'] != '0');
+			this.reboot = (data['reboot'] != '0');
+
+			// Update timer display
+			if(this.running && data['running_since']) {
+				$('#pflog-time').text(this.i18n.get('since') + data['running_since']);
+			} else {
+				$('#pflog-time').empty();
+			}
+
+			// Print log messages
+			let messages = "";
+			data['messages'].forEach(function(line) {
+				messages += `${line}\n`;
+			});
+			$('#pflog-messages').text(messages);
+		}
+	}
+}
+
+// Simple translation strings helper
+// Format: {key: "translation"}
+class PakfireI18N {
+	constructor() {
+		this._strings = Object.create(null); //Object without prototypes
+	}
+
+	// Get translation
+	get(key) {
+		if(Object.prototype.hasOwnProperty.call(this._strings, key)) {
+			return this._strings[key];
+		}
+		return `(undefined string '${key}')`;
+	}
+
+	// Load key/translation object
+	load(translations) {
+		if(translations instanceof Object) {
+			Object.assign(this._strings, translations);
+		}
+	}
+}
+
+//### Initialize Pakfire ###
+const pakfire = new PakfireJS();
+
+$(function() {
+	pakfire.documentReady();
+});