[kdesrc-build/make_it_mojo] /: mojo: Add a backend-support class to allow headless ops.
Michael Pyne
null at kde.org
Mon Oct 8 18:57:43 BST 2018
Git commit ea5ed02ce5cd08b70af1db171279600d373d008b by Michael Pyne.
Committed on 08/10/2018 at 17:44.
Pushed by mpyne into branch 'make_it_mojo'.
mojo: Add a backend-support class to allow headless ops.
This adds a backend support class (BackendServer, no ksb:: prefix for
now since Mojolicious uses the basedir for this module to look for
supporting files, like the HTML/embedded Perl templates).
To use the backend, for now run kdesrc-build with only the option
--backend, which will start the server and output the URL to stdout.
This introduces another class as well (ksb::UserInterface::TTY) to serve
as the default user interface, and makes kdesrc-build the driver for
this interface (instead of the more generic ksb::Application). This uses
the backend interface directly as well, enforcing its normal use.
There's a lot more to do to refactor a clean separation between
model/controller and the user interface / "view" here, especially at the
TTY, but this code does at least work and I think it's a clean enough
foundation that all the heavy lifting should be done.
M +0 -11 doc/index.docbook
M +0 -23 doc/man-kdesrc-build.1.docbook
M +69 -42 kdesrc-build
A +251 -0 modules/BackendServer.pm
M +50 -579 modules/ksb/Application.pm
M +1 -10 modules/ksb/BuildContext.pm
A +227 -0 modules/ksb/UserInterface/TTY.pm
M +2 -1 modules/ksb/Util.pm
A +206 -0 modules/templates/event_viewer.html.ep
A +112 -0 modules/templates/index.html.ep
A +31 -0 modules/templates/layouts/default.html.ep
https://commits.kde.org/kdesrc-build/ea5ed02ce5cd08b70af1db171279600d373d008b
diff --git a/doc/index.docbook b/doc/index.docbook
index 3ccbe25..1a03236 100644
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -3265,17 +3265,6 @@ kdepim: master
</listitem>
</varlistentry>
-<varlistentry id="cmdline-launch-browser">
-<term><parameter>--launch-browser</parameter></term>
-<listitem><para>
- When &kdesrc-build; is already running (that is, in a separate terminal
- session), you can use this option to have &kdesrc-build; launch a web
- browser that will show a web page showing the status of the build process.
- This does not require Internet access, and can be more convenient than the
- command-line output.
-</para></listitem>
-</varlistentry>
-
<varlistentry id="cmdline-no-rebuild-on-fail">
<term><parameter>--no-rebuild-on-fail</parameter></term>
<listitem><para>
diff --git a/doc/man-kdesrc-build.1.docbook b/doc/man-kdesrc-build.1.docbook
index b04f6be..8abdca5 100644
--- a/doc/man-kdesrc-build.1.docbook
+++ b/doc/man-kdesrc-build.1.docbook
@@ -51,12 +51,6 @@
<arg rep="repeat"><replaceable>Module name</replaceable></arg>
</cmdsynopsis>
-<cmdsynopsis>
-<command>kdesrc-build</command>
-<arg choice='plain'>--launch-browser</arg>
-</cmdsynopsis>
-</refsynopsisdiv>
-
<refsect1>
<title>DESCRIPTION</title>
@@ -580,23 +574,6 @@ combining short options into one at this point. (E.g. running
</listitem>
</varlistentry>
-<varlistentry>
-<term>
-<option>--launch-browser</option>
-</term>
-
-<listitem>
-<para>
- When <command>kdesrc-build</command> is already running (in a separate
- terminal), you can run this command to run a Web browser to show a web page
- that will track the status of the running kdesrc-build session. The
- browser is opened using the <command>xdg-open</command> so this requires
- your environment to be configured to associate your preferred browser to
- web pages.
-</para>
-</listitem>
-</varlistentry>
-
<varlistentry>
<term>
<option>--run=<replaceable>foo</replaceable></option>
diff --git a/kdesrc-build b/kdesrc-build
index 299cd30..680a2e7 100755
--- a/kdesrc-build
+++ b/kdesrc-build
@@ -36,10 +36,6 @@ use FindBin qw($RealBin);
use lib "$RealBin/../share/kdesrc-build/modules";
use lib "$RealBin/modules";
-# Force all symbols to be in this package. We can tell if we're being called
-# through require/eval/etc. by using the "caller" function.
-package main;
-
use strict;
use warnings;
@@ -48,10 +44,15 @@ use Data::Dumper;
use File::Find; # For our lndir reimplementation.
use File::Path qw(remove_tree);
+use Mojo::IOLoop;
+use Mojo::Server::Daemon;
+
use ksb::Debug;
use ksb::Util;
use ksb::Version qw(scriptVersion);
use ksb::Application;
+use ksb::UserInterface::TTY;
+use BackendServer;
use 5.014; # Require Perl 5.14
@@ -229,6 +230,34 @@ sub findMissingModules
return @missingModules;
}
+# Rather than running an interactive build, launches a web server that can be
+# interacted with by and outside user interface, printing the URL to the server
+# on stdout and then remaining in the foreground until killed.
+sub launchBackend
+{
+ # Manually setup the daemon so that we can figure out what port it
+ # ends up on.
+ my $daemon = Mojo::Server::Daemon->new(
+ app => BackendServer->new,
+ listen => ['http://localhost'],
+ silent => 1,
+ );
+
+ $daemon->start; # Grabs the socket to listen on
+
+ my $port = $daemon->ports->[0] or do {
+ say STDERR "Can't autodetect which TCP port was assigned!";
+ exit 1;
+ };
+
+ say "http://localhost:$port";
+
+ Mojo::IOLoop->start
+ unless Mojo::IOLoop->is_running;
+
+ exit 0;
+}
+
# Script starts.
# Ensure some critical Perl modules are available so that the user isn't surprised
@@ -247,17 +276,10 @@ EOF
exit 1;
}
-# Adding in a way to load all the functions without running the program to
-# enable some kind of automated QA testing.
-if (defined caller && caller eq 'test')
-{
- my $scriptVersion = scriptVersion();
- say "kdesrc-build being run from testing framework, BRING IT.";
- say "kdesrc-build is version $scriptVersion";
- return 1;
-}
+# Drop here if we're in backend-only mode.
+launchBackend()
+ if (scalar @ARGV == 1 && $ARGV[0] eq '--backend');
-my $app;
our @atexit_subs;
END {
@@ -270,16 +292,8 @@ END {
# Use some exception handling to avoid ucky error messages
eval
{
- $app = ksb::Application->new;
-
- my @selectors = $app->establishContext(@ARGV);
- my @modules = $app->modulesFromSelectors(@selectors);
- if (!@modules) {
- say "Nothing to build";
- exit 0;
- }
-
- $app->setModulesToProcess(@modules);
+ my $app = BackendServer->new(@ARGV);
+ my $ui = ksb::UserInterface::TTY->new($app);
# Hack for debugging current state.
if (exists $ENV{KDESRC_BUILD_DUMP_CONTEXT}) {
@@ -288,37 +302,50 @@ eval
# This method call dumps the first list with the variables named by the
# second list.
- print Data::Dumper->Dump([$app->context()], [qw(ctx)]);
+ print Data::Dumper->Dump([$app->ksb->context()], [qw(ctx)]);
}
- push @atexit_subs, sub { $app->finish(99) };
- my $result = $app->runAllModulePhases();
+ push @atexit_subs, sub { $app->ksb->finish(99) };
+
+ # TODO: Reimplement --print-modules, --query modes, which wouldn't go through ->start
+ my $result = $ui->start();
@atexit_subs = (); # Clear exit handlers
- $app->finish($result);
+
+ # env driver is just the ~/.config/kde-env-*.sh, session driver is that + ~/.xsession
+ my $ctx = $app->context;
+ if ($ctx->getOption('install-environment-driver') ||
+ $ctx->getOption('install-session-driver'))
+ {
+ ksb::Application::_installCustomSessionDriver($ctx);
+ }
+
+ # Exits the script
+ my $logdir = $app->context()->getLogDir();
+ note ("Your logs are saved in y[$logdir]");
+ exit $result;
};
if (my $err = $@)
{
if (had_an_exception()) {
- print "kdesrc-build encountered an exceptional error condition:\n";
- print " ========\n";
- print " $err\n";
- print " ========\n";
- print "\tCan't continue, so stopping now.\n";
-
- if ($err->{'exception_type'} eq 'Internal') {
- print "\nPlease submit a bug against kdesrc-build on https://bugs.kde.org/\n"
- }
+ say "kdesrc-build encountered an exceptional error condition:";
+ say " ========";
+ say " $err";
+ say " ========";
+ say "\tCan't continue, so stopping now.";
+
+ say "\nPlease submit a bug against kdesrc-build on https://bugs.kde.org/"
+ if ($err->{exception_type} eq 'Internal');
}
else {
- # We encountered an error.
- print "Encountered an error in the execution of the script.\n";
- print "The error reported was $err\n";
- print "Please submit a bug against kdesrc-build on https://bugs.kde.org/\n";
+ # An exception was raised, but not one that kdesrc-build generated
+ say "Encountered an error in the execution of the script.";
+ say "The error reported was $err";
+ say "Please submit a bug against kdesrc-build on https://bugs.kde.org/";
}
exit 99;
}
-# vim: set et sw=4 ts=4 fdm=marker:
+# vim: set et sw=4 ts=4:
diff --git a/modules/BackendServer.pm b/modules/BackendServer.pm
new file mode 100644
index 0000000..6b689f2
--- /dev/null
+++ b/modules/BackendServer.pm
@@ -0,0 +1,251 @@
+package BackendServer;
+
+# Make this subclass a Mojolicious app
+use Mojo::Base 'Mojolicious';
+use Mojo::Util qw(trim);
+
+use ksb::Application;
+
+# This is written in a kind of domain-specific language for Mojolicious for
+# now, to setup a web server backend for clients / frontends to communicate
+# with.
+# See https://mojolicious.org/perldoc/Mojolicious/Guides/Tutorial
+
+has 'options';
+has 'selectors';
+
+sub new
+{
+ my ($class, @opts) = @_;
+ return $class->SUPER::new(options => [@opts]);
+}
+
+# Adds a helper method to each HTTP context object to return the
+# ksb::Application class in use
+sub make_new_ksb
+{
+ my $c = shift;
+ my $app = ksb::Application->new->setHeadless;
+
+ my @selectors = $app->establishContext(@{$c->app->{options}});
+ $c->app->selectors([@selectors]);
+ $c->app->log->info("Selectors are ", join(', ', @selectors));
+
+ return $app;
+}
+
+# Package-shared variables for helpers and closures
+my $LAST_RESULT;
+my $BUILD_PROMISE;
+my $IN_PROGRESS;
+my $KSB_APP;
+
+sub startup {
+ my $self = shift;
+
+ $self->helper(ksb => sub {
+ my ($c, $new_ksb) = @_;
+
+ $KSB_APP //= make_new_ksb($c);
+ $KSB_APP = $new_ksb if $new_ksb;
+
+ return $KSB_APP;
+ });
+
+ $self->helper(in_build => sub { $IN_PROGRESS });
+ $self->helper(context => sub { shift->ksb->context() });
+
+ my $r = $self->routes;
+ $self->_generateRoutes;
+
+ return;
+}
+
+sub _generateRoutes {
+ my $self = shift;
+ my $r = $self->routes;
+
+ $r->get('/' => sub {
+ my $c = shift;
+ my $app = $c->ksb();
+ my $isApp = $app->isa('ksb::Application') ? 'app' : 'not app';
+ $c->stash(app => "Application is a $isApp");
+ $c->render(template => 'index');
+ } => 'index');
+
+ $r->post('/reset' => sub {
+ my $c = shift;
+
+ if ($c->in_build || !defined $LAST_RESULT) {
+ $c->res->code(400);
+ return $c->render;
+ }
+
+ my $old_result = $LAST_RESULT;
+ $c->ksb(make_new_ksb($c));
+ undef $LAST_RESULT;
+
+ $c->render(json => { last_result => $old_result });
+ });
+
+ $r->get('/context/options' => sub {
+ my $c = shift;
+ $c->render(json => $c->ksb->context()->{options});
+ });
+
+ $r->get('/context/options/:option' => sub {
+ my $c = shift;
+ my $ctx = $c->ksb->context();
+
+ my $opt = $c->param('option') or do {
+ $c->res->code(400);
+ return $c->render;
+ };
+
+ if (defined $ctx->{options}->{$opt}) {
+ $c->render(json => { $opt => $ctx->{options}->{$opt} });
+ }
+ else {
+ $c->res->code(404);
+ $c->reply->not_found;
+ }
+ });
+
+ $r->get('/modules' => sub {
+ my $c = shift;
+ $c->render(json => $c->ksb->context()->moduleList());
+ } => 'module_lookup');
+
+ $r->get('/known_modules' => sub {
+ my $c = shift;
+ my $resolver = $c->ksb->{module_resolver};
+ my @setsAndModules = @{$resolver->{inputModulesAndOptions}};
+ my @output = map {
+ $_->isa('ksb::ModuleSet')
+ ? [ $_->name(), $_->moduleNamesToFind() ]
+ : $_->name() # should be a ksb::Module
+ } @setsAndModules;
+
+ $c->render(json => \@output);
+ });
+
+ $r->post('/modules' => sub {
+ my $c = shift;
+ my $selectorList = $c->req->json;
+ my $build_all = $c->req->headers->header('X-BuildAllModules');
+
+ # Remove empty selectors
+ my @modules = grep { !!$_ } map { trim($_ // '') } @{$selectorList};
+
+ # If not building all then ensure there's at least one module to build
+ if ($c->in_build || !$selectorList || (!@modules && !$build_all) || (@modules && $build_all)) {
+ $c->app->log->error("Something was wrong with modules to assign to build");
+ return $c->render(text => "Invalid request sent", status => 400);
+ }
+
+ eval {
+ @modules = $c->ksb->modulesFromSelectors(@modules);
+ $c->ksb->setModulesToProcess(@modules);
+ };
+
+ if ($@) {
+ return $c->render(text => $@->{message}, status => 400);
+ }
+
+ my $numSels = @modules; # count
+
+ $c->render(json => ["$numSels handled"]);
+ }, 'post_modules');
+
+ $r->get('/module/:modname' => sub {
+ my $c = shift;
+ my $name = $c->stash('modname');
+
+ my $module = $c->ksb->context()->lookupModule($name);
+ if (!$module) {
+ $c->render(template => 'does_not_exist');
+ return;
+ }
+
+ my $opts = {
+ options => $module->{options},
+ persistent => $c->ksb->context()->{persistent_options}->{$name},
+ };
+ $c->render(json => $opts);
+ });
+
+ $r->get('/module/:modname/logs/error' => sub {
+ my $c = shift;
+ my $name = $c->stash('modname');
+ $c->render(text => "TODO: Error logs for $name");
+ });
+
+ $r->get('/config' => sub {
+ my $c = shift;
+ $c->render(text => $c->ksb->context()->rcFile());
+ });
+
+ $r->post('/config' => sub {
+ # TODO If new filename can be loaded, load it and reset application object
+ die "Unimplemented";
+ });
+
+ $r->get('/build-metadata' => sub {
+ die "Unimplemented";
+ });
+
+ $r->websocket('/events' => sub {
+ my $c = shift;
+
+ $c->inactivity_timeout(0);
+
+ my $ctx = $c->ksb->context();
+ my $monitor = $ctx->statusMonitor();
+
+ # Send prior events the receiver wouldn't have received yet
+ my @curEvents = $monitor->events();
+ $c->send({json => \@curEvents});
+
+ # Hook up an event handler to send future events as they're generated
+ $monitor->on(newEvent => sub {
+ my ($monitor, $resultRef) = @_;
+ $c->on(drain => sub { $c->finish })
+ if ($resultRef->{event} eq 'build_done');
+ $c->send({json => [ $resultRef ]});
+ });
+ });
+
+ $r->get('/event_viewer' => sub {
+ my $c = shift;
+ $c->render(template => 'event_viewer');
+ });
+
+ $r->get('/building' => sub {
+ my $c = shift;
+ $c->render(text => $c->in_build ? 'True' : 'False');
+ });
+
+ $r->post('/build' => sub {
+ my $c = shift;
+ if ($c->in_build) {
+ $c->res->code(400);
+ $c->render(text => 'Build already in progress, cancel it first.');
+ return;
+ }
+
+ $c->app->log->debug('Starting build');
+
+ $IN_PROGRESS = 1;
+
+ $BUILD_PROMISE = $c->ksb->startHeadlessBuild->finally(sub {
+ my ($result) = @_;
+ $c->app->log->debug("Build done");
+ $IN_PROGRESS = 0;
+ return $LAST_RESULT = $result;
+ });
+
+ $c->render(text => $c->url_for('event_viewer')->to_abs->to_string);
+ });
+}
+
+1;
diff --git a/modules/ksb/Application.pm b/modules/ksb/Application.pm
index e444ceb..efb8587 100644
--- a/modules/ksb/Application.pm
+++ b/modules/ksb/Application.pm
@@ -73,6 +73,10 @@ sub new
return $self;
}
+# Call after establishContext (to read in config file and do one-time metadata
+# reading).
+#
+# Need to call this before you call startHeadlessBuild
sub setModulesToProcess
{
my ($self, @modules) = @_;
@@ -80,6 +84,18 @@ sub setModulesToProcess
$self->context()->addModule($_)
foreach @modules;
+
+ # i.e. niceness, ulimits, etc.
+ $self->context()->setupOperatingEnvironment();
+}
+
+# Sets the application to be non-interactive, intended to make this suitable as
+# a backend for a Mojolicious-based web server with a separate U/I.
+sub setHeadless
+{
+ my $self = shift;
+ $self->{run_mode} = 'headless';
+ return $self;
}
# Method: _readCommandLineOptionsAndSelectors
@@ -349,10 +365,6 @@ sub establishContext
my %ignoredSelectors =
map { $_, 1 } @{$cmdlineGlobalOptions->{'ignore-modules'}};
- if (exists $cmdlineGlobalOptions->{'launch-browser'}) {
- _launchStatusViewerBrowser(); # does not return
- }
-
my @startProgramAndArgs = @{$cmdlineGlobalOptions->{'start-program'}};
delete @{$cmdlineGlobalOptions}{qw/ignore-modules start-program/};
@@ -622,31 +634,26 @@ sub _resolveModuleDependencies
return @modules;
}
-# Runs all update, build, install, etc. phases. Basically this *is* the
-# script.
-# The metadata module must already have performed its update by this point.
-sub runAllModulePhases
+# Similar to the old interactive runAllModulePhases. Actually performs the
+# build for the modules selected by setModulesToProcess.
+#
+# Returns a Mojo::Promise that must be waited on. The promise resolves to
+# return a single success/failure result; use the event handler for now to get
+# more detail during a build.
+sub startHeadlessBuild
{
my $self = shift;
my $ctx = $self->context();
- my @modules = $self->modules();
+ $ctx->statusMonitor()->createBuildPlan($ctx);
- if ($ctx->getOption('print-modules')) {
- for my $m (@modules) {
- say ((" " x ($m->getOption('#dependency-level', 'module') // 0)), "$m");
- }
- return 0; # Abort execution early!
- }
+ my $promiseChain = ksb::PromiseChain->new;
+ my $startPromise = Mojo::Promise->new;
- $self->context()->setupOperatingEnvironment(); # i.e. niceness, ulimits, etc.
+ # These succeed or die outright
+ $startPromise = _handle_updates ($ctx, $promiseChain, $startPromise);
+ $startPromise = _handle_build ($ctx, $promiseChain, $startPromise);
- # After this call, we must run the finish() method
- # to cleanly complete process execution.
- if (!pretending() && !$self->context()->takeLock())
- {
- print "$0 is already running!\n";
- exit 1; # Don't finish(), it's not our lockfile!!
- }
+ die "Can't obtain build lock" unless $ctx->takeLock();
# Install signal handlers to ensure that the lockfile gets closed.
_installSignalHandlers(sub {
@@ -655,79 +662,32 @@ sub runAllModulePhases
$self->finish(5);
});
- my $runMode = $self->runMode();
+ $startPromise->resolve; # allow build to start
+ my $promise = $promiseChain->makePromiseChain($startPromise)->finally(sub {
+ my @results = @_;
+ my $result = 0; # success, non-zero is failure
- if ($runMode eq 'query') {
- my $queryMode = $ctx->getOption('query', 'module');
+ # Must use ! here to make '0 but true' hack work
+ $result = 1 if defined first { !($_->[0] // 1) } @results;
- # Default to ->getOption as query method.
- # $_[0] is short name for first param.
- my $query = sub { $_[0]->getOption($queryMode) };
- $query = sub { $_[0]->fullpath('source') } if $queryMode eq 'source-dir';
- $query = sub { $_[0]->fullpath('build') } if $queryMode eq 'build-dir';
- $query = sub { $_[0]->installationPath() } if $queryMode eq 'install-dir';
- $query = sub { $_[0]->fullProjectPath() } if $queryMode eq 'project-path';
- $query = sub { ($_[0]->scm()->_determinePreferredCheckoutSource())[0] // '' }
- if $queryMode eq 'branch';
+ $ctx->statusMonitor()->markBuildDone();
+ $ctx->closeLock();
- if (@modules == 1) {
- # No leading module name, just the value
- say $query->($modules[0]);
+ my $failedModules = join(',', map { "$_" } $ctx->listFailedModules());
+ if ($failedModules) {
+ # We don't clear the list of failed modules on success so that
+ # someone can build one or two modules and still use
+ # --rebuild-failures
+ $ctx->setPersistentOption('global', 'last-failed-module-list', $failedModules);
}
- else {
- for my $m (@modules) {
- say "$m: ", $query->($m);
- }
- }
-
- return 0;
- }
- my $result;
+ $ctx->storePersistentOptions();
+ _cleanup_log_directory($ctx);
- if ($runMode eq 'build')
- {
- # Build then install packages
- $result = _handle_async_build ($ctx);
- }
- elsif ($runMode eq 'install')
- {
- # Install directly
- # TODO: Merge with previous by splitting 'install' into a separate
- # phase
- $result = _handle_install ($ctx);
- }
- elsif ($runMode eq 'uninstall')
- {
- $result = _handle_uninstall ($ctx);
- }
-
- _cleanup_log_directory($ctx) if $ctx->getOption('purge-old-logs');
- _output_failed_module_lists($ctx);
-
- # Record all failed modules. Unlike the 'resume-list' option this doesn't
- # include any successfully-built modules in between failures.
- my $failedModules = join(',', map { "$_" } $ctx->listFailedModules());
- if ($failedModules) {
- # We don't clear the list of failed modules on success so that
- # someone can build one or two modules and still use
- # --rebuild-failures
- $ctx->setPersistentOption('global', 'last-failed-module-list', $failedModules);
- }
-
- # env driver is just the ~/.config/kde-env-*.sh, session driver is that + ~/.xsession
- if ($ctx->getOption('install-environment-driver') ||
- $ctx->getOption('install-session-driver'))
- {
- _installCustomSessionDriver($ctx);
- }
-
- my $color = 'g[b[';
- $color = 'r[b[' if $result;
-
- info ("${color}", $result ? ":-(" : ":-)") unless pretending();
+ return $result;
+ });
- return $result;
+ return $promise;
}
# Method: finish
@@ -743,11 +703,6 @@ sub finish
my $ctx = $self->context();
my $exitcode = shift // 0;
- # This is created even under --pretend, make sure it's removed
- my $run = $ENV{XDG_RUNTIME_DIR} // 'tmp';
- my $path = "$run/kdesrc-build-status-server";
- unlink $path if -e $path;
-
if (pretending() || $self->{_base_pid} != $$) {
# Abort early if pretending or if we're not the same process
# that was started by the user (for async mode)
@@ -1475,13 +1430,6 @@ sub _handle_build
# If there's an update phase we need to depend on it and show status
if (my $updatePromise = $promiseChain->promiseFor("$module/update")) {
$promiseChain->addDep("$module/build", "$module/update");
- $updatePromise->catch(sub {
- my $err = shift;
- # TODO: The error msg needs to be handled by status viewer.
- $ctx->statusViewer()->_clearLine();
- error ("\ty[b[$module] failed to update! $err");
- return $updatePromise; # Don't change the promise we're just whining
- });
}
};
@@ -1508,450 +1456,6 @@ sub _handle_build
sub { $ctx->unsetPersistentOption('global', 'resume-list') });
}
-# Finds a decent port for the monitoring server, creates a file at a known
-# location with the URL that will match the server, and returns the port and
-# path to the file (so that it may be unlinked once the server is shutdown)
-sub _find_open_monitor_port
-{
- # Ensure the file containing our listen URL is available.
- my $run = $ENV{XDG_RUNTIME_DIR};
- if (!$run) {
- note (" b[r[*] b[y[XDG_RUNTIME_DIR] is not set, using /tmp for now");
- $run = '/tmp';
- }
-
- my $path = "$run/kdesrc-build-status-server";
- error (" b[r[*] stale status server runtime socket file leftover, removing.")
- if (-e $path);
-
- # We set sticky bit (in the 01666) to indicate this file should not be
- # removed during long-running builds (e.g. by systemd).
- sysopen (my $fh, $path, O_CREAT | O_WRONLY, 01666) or do {
- error (" b[r[*] Unable to open status server runtime socket file, external viewers won't work.");
- return;
- };
-
- # With the file open we can generate a port and create a URL
- my $port = Mojo::IOLoop::Server->generate_port;
-
- say $fh "http://localhost:$port";
- close $fh or do {
- error (" b[y[*] Received an error closing runtime socket file: $!");
- unlink ($path);
- return;
- };
-
- return ($port, $path);
-}
-
-# Returns an HTML page suitable for display in a modern browser, that can read
-# status events over a WebSocket
-sub _generate_status_viewer_page
-{
- my $url = shift;
- my $templater = Mojo::Template->new;
-
- my $template = <<'EOF';
-% my $url = shift;
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8"/>
- <title>kdesrc-build status viewer</title>
-
- <style>
-td.pending {
- background-color: lightgray;
-}
-
-td.done {
- background-color: lightblue;
-}
-
-td.done.success {
- background-color: lightgreen;
-}
-
-td.done.error {
- background-color: pink;
-}
- </style>
-</head>
-
-<body>
- <h1>kdesrc-build status</h1>
- <div id="divStatus">
- Building...
- </div>
- <table id="tblResult">
- <tr><th>Module</th><th>Update</th><th>Build / Install</th></tr>
- </table>
- <div id="logEntries">
- </div>
-</body>
-
-<script>
- let addRow = (moduleName) => {
- let eventTable = document.getElementById('tblResult');
- let newRow = document.createElement('tr');
- let moduleNameCell = document.createElement('td');
- let updateDoneCell = document.createElement('td');
- let buildDoneCell = document.createElement('td');
-
- moduleNameCell.textContent = moduleName;
- updateDoneCell.id = 'updateCell_' + moduleName;
- updateDoneCell.className = 'pending';
- buildDoneCell.id = 'buildCell_' + moduleName;
- buildDoneCell.className = 'pending';
-
- newRow.appendChild(moduleNameCell);
- newRow.appendChild(updateDoneCell);
- newRow.appendChild(buildDoneCell);
- eventTable.appendChild(newRow);
- }
-
- let handleEvent = (ev) => {
- if (ev.event === "build_plan") {
- for (const module of ev.build_plan) {
- addRow(module.name);
- }
- }
- else if (ev.event === "build_done") {
- document.getElementById('divStatus').textContent = 'Build complete';
- }
- else if (ev.event === "phase_started") {
- const phase = ev.phase_started.phase;
- const module = ev.phase_started.module;
-
- let cell = document.getElementById(phase + "Cell_" + module);
- if (!cell) {
- return;
- }
-
- cell.className = 'working';
- cell.textContent = 'Working...';
- }
- else if (ev.event === "phase_progress") {
- const phase = ev.phase_progress.phase;
- const module = ev.phase_progress.module;
- const progressAry = ev.phase_progress.progress;
-
- let cell = document.getElementById(phase + "Cell_" + module);
- if (!cell) {
- return;
- }
-
- cell.textContent = `${progressAry[0]} / ${progressAry[1]}`;
- }
- else if (ev.event === "phase_completed") {
- const phase = ev.phase_completed.phase;
- const module = ev.phase_completed.module;
-
- let cell = document.getElementById(phase + "Cell_" + module);
- if (!cell) {
- return;
- }
-
- cell.className = 'done';
- if (['success', 'error'].
- includes(ev.phase_completed.result))
- {
- cell.classList.add(ev.phase_completed.result);
- }
-
- if (ev.phase_completed.error_log) {
- const logUrl = ev.phase_completed.error_log;
- cell.innerHTML = `<a target='_blank' href='${logUrl}'>${ev.phase_completed.result}</a>`;
- } else {
- cell.innerHTML = ev.phase_completed.result;
- }
- }
- else if (ev.event === "log_entries") {
- const phase = ev.log_entries.phase;
- const module = ev.log_entries.module;
- const entries = ev.log_entries.entries;
-
- console.dir(ev);
-
- let newText = '';
- for(const entry of entries) {
- newText += module + ": " + entry + "<br>";
- }
-
- let entriesDiv = document.getElementById('logEntries');
- entriesDiv.innerHTML = entriesDiv.innerHTML + newText;
- }
- else {
- console.log("Unhandled event ", ev.event);
- console.dir(ev);
- }
- }
-
- let ws = new WebSocket('<%= "$url/" %>');
-
- ws.onmessage = (msg_event) => {
- const events = JSON.parse(msg_event.data);
-
- if (!events) {
- console.log(`Received invalid JSON object in WebSocket handler ${msg_event}`);
- return;
- }
-
- // event should be an array of JSON objects
- for (const e of events) {
- handleEvent(e);
- }
- }
-</script>
-</html>
-EOF
-
- return $templater->render($template, $url);
-}
-
-# Launches a server to handle responding to status requests.
-#
-# - $ctx, the build context
-# - $done_promise should be a promise that, once resolved, should indicate that
-# it is time to shut the server down.
-#
-# returns a promise that can be waited on until the server is shut down
-sub _handle_monitoring
-{
- my ($ctx, $done_promise) = @_;
-
- my ($port, $server_url_path) = _find_open_monitor_port();
-
- # Clients which have open websocket subscriptions to event updates
- my %subscribers;
-
- # Clients who are current on events. Normally should be same as above.
- my %currentSubscribers;
-
- # If we can't find a port to listen on, don't hold up the rest of the run
- return Mojo::Promise->new->resolve if !$port;
-
- # Setup a simple server to respond to requests about kdesrc-build status
- my $daemon = Mojo::Server::Daemon->new(
- # IPv4 and IPv6 localhost-only
- listen => ["http://127.0.0.1:$port", "http://[::1]:$port"]
- );
- $daemon->silent(!ksb::Debug::debugging());
- $daemon->inactivity_timeout(0); # Disable timeouts to allow long polling
-
- # Remove existing default handler and install our own
- $daemon->unsubscribe('request')->on(request => sub {
- my ($daemon, $tx) = @_;
-
- my $method = $tx->req->method;
- my $path = $tx->req->url->path;
-
- if ($tx->is_websocket && !$tx->established) {
- # WebSocket request comes in, which must be manually accepted and
- # upgraded
-
- # Add to the list of subscribers. The 'newEvent' handler below
- # will make them current (so that we don't potentially miss events
- # already pending in the event loop).
- $subscribers{$tx->connection} = $tx;
-
- $tx->on(finish => sub {
- my $tx = shift;
- delete $subscribers{$tx->connection};
- delete $currentSubscribers{$tx->connection};
- });
-
- $tx->res->code(101); # Signal to Mojolicious to accept the upgrade
- }
- elsif ($method eq 'GET') {
- # HTTP or WS
- if ($path->contains('/list')) {
- my %seen;
- my @modules;
- my @events = $ctx->statusMonitor()->events();
-
- # unique items, preserve order
- foreach my $result (@events) {
- my $m = $result->{module};
- push @modules, $m unless exists $seen{$m};
- $seen{$m} = 1;
- }
-
- $tx->res->code(200);
- $tx->res->headers->content_type('application/json');
- $tx->res->body(Mojo::JSON::encode_json(\@modules));
- }
- elsif ($path->to_string eq '/') {
- my $response = _generate_status_viewer_page("ws://localhost:$port");
-
- $tx->res->code(200);
- $tx->res->headers->content_type('text/html');
- $tx->res->body($response);
- }
- elsif ($path->contains('/error_log')) {
- my $moduleName = $path->[1] // '';
- my $module = $ctx->lookupModule($moduleName);
- my $logfile;
-
- $logfile = $module->getOption('#error-log-file', 'module') if $module;
-
- if ($logfile && -f $logfile) {
- my $asset = Mojo::Asset::File->new(path => $logfile);
- $tx->res->content->asset($asset);
- $tx->res->headers->content_type('text/plain');
- $tx->res->code(200);
- }
- elsif ($module && !$logfile) {
- $tx->res->code(404);
- }
- else {
- $tx->res->code(400);
- }
- }
- else {
- $tx->res->code(404);
- }
- }
- else {
- $tx->res->code(500);
- }
-
- # Mojolicious will complete processing and send response
- $tx->resume;
- });
-
- $daemon->start;
-
- my $stop_sent = Mojo::Promise->new;
-
- # Announce changes as they happen to subscribers
- $ctx->statusMonitor()->on(newEvent => sub {
- my ($statusMonitor, $resultRef) = @_;
-
- if ($resultRef->{event} eq 'build_done' && !%subscribers) {
- # Resolve this early if no one is waiting on us, otherwise we'll
- # block forever waiting to let someone know we're done
- $stop_sent->resolve;
- }
-
- foreach my $tx (values %subscribers) {
- if ($resultRef->{event} eq 'build_done') {
- # Don't exit until we've sent the last event
- $tx->on(drain => sub { $stop_sent->resolve });
- }
-
- if (exists $currentSubscribers{$tx->connection}) {
- # Should match schema for send below
- $tx->send({ json => [ $resultRef ] });
- } else {
- # This includes the new event we just recv'd
- my @events = $ctx->statusMonitor()->events();
- $tx->send({ json => \@events });
- $currentSubscribers{$tx->connection} = 1;
- }
- }
- });
-
- my $time_promise = Mojo::Promise->new;
-
- # useful for debugging to ensure server is available for at least a few
- # seconds.
- # Mojo::IOLoop->timer(10, sub { $time_promise->resolve; });
- Mojo::IOLoop->timer(0, sub { $time_promise->resolve; });
-
- my $stop_promise = Mojo::Promise->all($stop_sent, $done_promise, $time_promise)->then(sub {
- $daemon->stop;
- unlink($server_url_path);
- });
-
- return $stop_promise;
-}
-
-sub getStatusServerURL
-{
- my $run = $ENV{XDG_RUNTIME_DIR} // '/tmp';
- open my $fh, '<', "$run/kdesrc-build-status-server"
- or croak_internal("Couldn't find status server");
- my $path = <$fh>;
- croak_internal("Error reading status server URL: $!")
- unless defined $path;
- close $fh
- or croak_internal("I/O error reading status server URL: $!");
-
- chomp($path);
- return $path;
-}
-
-sub _handle_ui
-{
- my ($ctx, $stop_promise) = @_;
- my $path = getStatusServerURL();
-
- # Note on object lifetimes: Perl is convenient like C++ in that it will
- # typically destroy 'lexical' objects (declared with 'my') when no scope
- # has a reference to that object.
- #
- # What this means for callback-heavy code is that the object creating the
- # events being fed to callbacks needs to outlive the callbacks somehow,
- # otherwise the death of the controller will close all the connections it
- # had created.
- #
- # Since the UserAgent we create is controlling the callbacks being fed to
- # our U/I handler, it needs to outlive this function in the chain of
- # callbacks that we return to the caller. This is handled in one of the
- # promise handlers below.
-
- my $ua = Mojo::UserAgent->new;
- my $ui = $ctx->statusViewer();
- my $url_ws = Mojo::URL->new($path)->clone->scheme('ws');
- $ua->connect_timeout(5);
- $ua->request_timeout(20);
- $ua->inactivity_timeout(0); # Allow long-poll
- $ua->max_redirects(0);
- $ua->max_connections(0); # disable keepalive to avoid server closing connection on us
- $ua->max_response_size(16384);
-
- return $ua->websocket_p($url_ws->clone->path("events"))
- ->then(sub {
- my $ws = shift;
-
- $ws->on(json => sub {
- my ($ws, $resultRef) = @_;
- foreach my $modRef (@{$resultRef}) {
- eval { $ui->notifyEvent($modRef); };
-
- if ($@) {
- error ("Failure encountered $@");
- $ws->finish;
- undef $ua;
- $stop_promise->reject($@);
- }
-
- if ($modRef->{event} eq 'build_done') {
- # We've reported the build is complete, activate the
- # promise holding things together
- $stop_promise->resolve;
- }
- }
- });
-
- $ws->on(finish => sub {
- # Shouldn't happen in a normal build but it's probably possible
- $stop_promise->resolve;
- });
-
- # The 'stop' promise is resolved when update/build done.
- $stop_promise->then(sub {
- # Keep UserAgent alive until we close the WebSocket.
- my $lifetime_extender = \$ua;
-
- $ws->finish;
- });
-
- return;
- });
-}
-
# Function: _handle_async_build
#
# This subroutine special-cases the handling of the update and build phases, by
@@ -1968,22 +1472,10 @@ sub _handle_ui
sub _handle_async_build
{
my ($ctx) = @_;
-
- my $kdesrc = $ctx->getSourceDir();
-
my $result = 0;
- my $update_done = 0;
- my $module_promises = { };
- my $stop_everything_p = Mojo::Promise->new;
$ctx->statusMonitor()->createBuildPlan($ctx);
- # The U/I will declare when we're done, which will cause monitor to halt
- my $monitor_p = _handle_monitoring ($ctx, $stop_everything_p);
- # Keep a reference to U/I promise since that's where the U/I code will actually
- # run, allowing the ref to be GC'd stops the U/I updates.
- my $ui_ready = _handle_ui($ctx, $stop_everything_p);
-
my $promiseChain = ksb::PromiseChain->new;
my $start_promise = Mojo::Promise->new;
@@ -2013,9 +1505,7 @@ sub _handle_async_build
$start_promise->resolve;
Mojo::IOLoop->stop; # Force the wait below to block
- Mojo::Promise->all($chain, $ui_ready, $monitor_p)->then(sub {
- Mojo::IOLoop->stop; # FIN
- })->wait;
+ $chain->wait;
return $result;
}
@@ -2746,25 +2236,6 @@ sub _reachableModuleLogs
return keys %tempHash;
}
-# Runs xdg-open to the URL at $XDG_RUNTIME_DIR/kdesrc-build-status-server, if
-# that file exists and is readable. Otherwise lets the user know there was an
-# error. Either way this function always exits the process immediately.
-sub _launchStatusViewerBrowser
-{
- my $run = $ENV{XDG_RUNTIME_DIR} // '/tmp';
- my $file = "$run/kdesrc-build-status-server";
- my $url = eval { Mojo::File->new($file)->slurp };
-
- if ($url) {
- exec { 'xdg-open' } 'xdg-open', $url or die
- "Failed to launch browser, couldn't run xdg-open: $!";
- }
- else {
- say "Unable to launch browser for the status server, couldn't find right URL";
- exit 1;
- }
-}
-
# Installs the given subroutine as a signal handler for a set of signals which
# could kill the program.
#
diff --git a/modules/ksb/BuildContext.pm b/modules/ksb/BuildContext.pm
index a75e12b..b6ecb28 100644
--- a/modules/ksb/BuildContext.pm
+++ b/modules/ksb/BuildContext.pm
@@ -1,4 +1,4 @@
-package ksb::BuildContext 0.35;
+package ksb::BuildContext 0.36;
# Class: BuildContext
#
@@ -154,13 +154,11 @@ sub new
rcFiles => [@rcfiles],
rcFile => undef,
env => { },
- # pending => { }, # exists only under a subprocess
persistent_options => { }, # These are kept across multiple script runs
ignore_list => [ ], # List of KDE project paths to ignore completely
kde_projects_metadata => undef, # Enumeration of kde-projects
kde_dependencies_metadata => undef, # Dependency resolution of kde-projects
logical_module_resolver => undef, # For branch-group option
- status_view => ksb::StatusView->new(),
status_monitor => ksb::StatusMonitor->new(),
projects_db => undef, # See getProjectDataReader
);
@@ -1058,13 +1056,6 @@ sub moduleBranchGroupResolver
return $self->{logical_module_resolver};
}
-# Manages the output of the TTY to keep the user in the know
-sub statusViewer
-{
- my $self = shift;
- return $self->{status_view};
-}
-
# An event-based aggregator for update events, to be used by user interfaces,
# including remote interfaces.
sub statusMonitor
diff --git a/modules/ksb/UserInterface/TTY.pm b/modules/ksb/UserInterface/TTY.pm
new file mode 100755
index 0000000..6452ba7
--- /dev/null
+++ b/modules/ksb/UserInterface/TTY.pm
@@ -0,0 +1,227 @@
+#!/usr/bin/env perl
+
+package ksb::UserInterface::TTY 0.10;
+
+=pod
+
+=head1 NAME
+
+ksb::UserInterface::TTY -- A command-line interface to the kdesrc-build backend
+
+=head1 DESCRIPTION
+
+This class is used to show a user interface for a kdesrc-build run at the
+command line (as opposed to a browser-based or GUI interface).
+
+Since the kdesrc-build backend is now meant to be headless and controlled via a
+Web-style API set (powered by Mojolicious), this class manages the interaction
+with that backend, also using Mojolicious to power the HTTP and WebSocket
+requests necessary.
+
+=head1 SYNOPSIS
+
+ my $app = BackendServer->new(@ARGV);
+ my $ui = ksb::UserInterface::TTY->new($app);
+ exit $ui->start(); # Blocks! Returns a shell-style return code
+
+=cut
+
+use strict;
+use warnings;
+use 5.014;
+
+use Mojo::Base -base;
+
+use Mojo::Server::Daemon;
+use Mojo::IOLoop;
+use Mojo::UserAgent;
+use Mojo::JSON qw(to_json);
+
+# This is essentially ksb::Application but across a socket connection. It reads
+# the options and module selectors like normal.
+use BackendServer;
+use ksb::StatusView;
+use ksb::Util;
+use ksb::Debug;
+
+use IO::Handle; # For methods on event_stream file
+use List::Util qw(max);
+
+has ua => sub { Mojo::UserAgent->new->inactivity_timeout(0) };
+has ui => sub { ksb::StatusView->new() };
+has 'app';
+
+sub new
+{
+ my ($class, $app) = @_;
+
+ my $self = $class->SUPER::new(app => $app);
+
+ # Mojo::UserAgent can be tied to a Mojolicious application server directly to
+ # handle relative URLs, which is perfect for what we want. Making this
+ # attachment will startup the Web server behind the scenes and allow $ua to
+ # make HTTP requests.
+ $self->ua->server->app($app);
+ $self->ua->server->app->log->level('fatal');
+
+ return $self;
+}
+
+sub _check_error {
+ my $tx = shift;
+ my $err = $tx->error or return;
+ my $body = $tx->res->body // '';
+ open my $fh, '<', \$body;
+ my ($first_line) = <$fh> // '';
+ $err->{message} .= "\n$first_line" if $first_line;
+ die $err;
+};
+
+# Just a giant huge promise handler that actually processes U/I events and
+# keeps the TTY up to date. Note the TTY-specific stuff is actually itself
+# buried in a separate class for now.
+sub start
+{
+ my $self = shift;
+
+ my $ui = $self->ui;
+ my $ua = $self->ua;
+ my $app = $self->app;
+ my $result = 0; # notes errors from module builds or internal errors
+
+ my @module_failures;
+
+ # Open a file to log the event stream
+ my $ctx = $app->context();
+ my $separator = ' ';
+ open my $event_stream, '>', $ctx->getLogDirFor($ctx) . '/event-stream'
+ or croak_internal("Unable to open event log $!");
+ $event_stream->say("["); # Try to make it valid JSON syntax
+
+ # This call just reads an option from the BuildContext as a sanity check
+ $ua->get_p('/context/options/pretend')->then(sub {
+ my $tx = shift;
+ _check_error($tx);
+
+ # If we get here things are mostly working?
+ my $selectorsRef = $app->{selectors};
+
+ # We need to specifically ask for all modules if we're not passing a
+ # specific list of modules to build.
+ my $headers = { };
+ $headers->{'X-BuildAllModules'} = 1 unless @{$selectorsRef};
+
+ # Tell the backend which modules to build.
+ return $ua->post_p('/modules', $headers, json => $selectorsRef);
+ })->then(sub {
+ my $tx = shift;
+ _check_error($tx);
+
+ # We've received a successful response from the backend that it's able to
+ # build the requested modules, so proceed to setup the U/I and start the
+ # build.
+
+ return $ua->websocket_p('/events');
+ })->then(sub {
+ # Websocket Event handler
+ my $ws = shift;
+ my $everFailed = 0;
+ my $stop_promise = Mojo::Promise->new;
+
+ # Websockets seem to be inherently event-driven instead of simply
+ # client/server. So attach the event handlers and then return to the event
+ # loop to await progress.
+ $ws->on(json => sub {
+ # This handler is called by the backend when there is something notable
+ # to report
+ my ($ws, $resultRef) = @_;
+ foreach my $modRef (@{$resultRef}) {
+ # Update the U/I
+ eval {
+ $ui->notifyEvent($modRef);
+ $event_stream->say($separator . to_json($modRef));
+ $separator = ', ';
+ };
+
+ if ($@) {
+ $ws->finish;
+ $stop_promise->reject($@);
+ }
+
+ # See ksb::StatusMonitor for where events defined
+ if ($modRef->{event} eq 'phase_completed') {
+ my $results = $modRef->{phase_completed};
+ push @module_failures, $results
+ if $results->{result} eq 'error';
+ }
+
+ if ($modRef->{event} eq 'build_done') {
+ # We've reported the build is complete, activate the promise
+ # holding things together. The value we pass is what is passed
+ # to the next promise handler.
+ $stop_promise->resolve(scalar @module_failures);
+ }
+ }
+ });
+
+ $ws->on(finish => sub {
+ # Shouldn't happen in a normal build but it's probably possible
+ $stop_promise->reject; # ignored if we resolved first
+ });
+
+ # Blocking call to kick off the build
+ my $tx = $ua->post('/build');
+ if (my $err = $tx->error) {
+ $stop_promise->reject('Unable to start build: ' . $err->{message});
+ }
+
+ # Once we return here we'll wait in Mojolicious event loop for awhile until
+ # the build is done, before moving into the promise handler below
+ return $stop_promise;
+ })->then(sub {
+ # Build done, value comes from stop_promise->resolve above
+ $result ||= shift;
+ })->catch(sub {
+ # Catches all errors in any of the prior promises
+ my $err = shift;
+
+ say "Error: ", $err->{code}, " ", $err->{message};
+
+ # See if we made it to an rc-file
+ my $ctx = $app->ksb->context();
+ my $rcFile = $ctx ? $ctx->rcFile() // 'Unknown' : undef;
+ say "Using configuration file found at $rcFile" if $rcFile;
+
+ $result = 1; # error
+ })->wait;
+
+ $event_stream->say("]");
+ $event_stream->close() or $result = 1;
+
+ _report_on_failures(@module_failures);
+
+ say $result == 0 ? ":-)" : ":-(";
+ return $result;
+};
+
+sub _report_on_failures
+{
+ my @failures = @_;
+ my $max_width = max map { length ($_->{module}) } @failures;
+
+ foreach my $mod (@failures) {
+ my $module = $mod->{module};
+ my $phase = $mod->{phase};
+ my $log = $mod->{error_file};
+ my $padding = $max_width - length $module;
+
+ $module .= (' ' x $padding); # Left-align
+ $phase = 'setup buildsystem' if $phase eq 'buildsystem';
+
+ error("b[*] r[b[$module] failed to b[$phase]");
+ error("b[*]\tFind the log at file://$log") if $log;
+ }
+}
+
+
+1;
diff --git a/modules/ksb/Util.pm b/modules/ksb/Util.pm
index d1eaaa9..3e27247 100644
--- a/modules/ksb/Util.pm
+++ b/modules/ksb/Util.pm
@@ -83,7 +83,8 @@ sub make_exception
# Remove this subroutine from the backtrace
local $Carp::CarpLevel = 1 + $levels;
- $message = Carp::cluck($message) if $exception_type eq 'Internal';
+ $message = Carp::longmess($message)
+ if $exception_type eq 'Internal';
return ksb::BuildException->new($exception_type, $message);
}
diff --git a/modules/templates/event_viewer.html.ep b/modules/templates/event_viewer.html.ep
new file mode 100644
index 0000000..ac5b47b
--- /dev/null
+++ b/modules/templates/event_viewer.html.ep
@@ -0,0 +1,206 @@
+% layout 'default';
+% title 'kdesrc-build status viewer';
+
+<h1>kdesrc-build status</h1>
+<div id="divBanner"></div>
+<div id="divStatus">
+ Building...
+</div>
+<table id="tblResult">
+ <tr><th>Module</th><th>Update</th><th>Build / Install</th></tr>
+</table>
+<div class="progress_div" style="float: right">
+ <label for="update_progress_bar">Update progress:</label>
+ <progress id="update_progress_bar" max="100"></progress> <br/>
+
+ <label for="build_progress_bar">Build progress:</label>
+ <progress id="build_progress_bar" max="100"></progress>
+</div>
+
+<textarea id="logEntries" cols="80" rows="50">
+</textarea>
+
+<script defer>
+let addRow = (moduleName) => {
+ if (!moduleName) {
+ console.trace("Stupidity afoot");
+ return;
+ }
+
+ let eventTable = lkup('tblResult');
+ let newRow = document.createElement('tr');
+ let moduleNameCell = document.createElement('td');
+ let updateDoneCell = document.createElement('td');
+ let buildDoneCell = document.createElement('td');
+
+ newRow.id = 'row_' + moduleName;
+ moduleNameCell.textContent = moduleName;
+ updateDoneCell.id = 'updateCell_' + moduleName;
+ updateDoneCell.className = 'pending';
+ buildDoneCell.id = 'buildCell_' + moduleName;
+ buildDoneCell.className = 'pending';
+
+ newRow.appendChild(moduleNameCell);
+ newRow.appendChild(updateDoneCell);
+ newRow.appendChild(buildDoneCell);
+ eventTable.appendChild(newRow);
+}
+
+const divStatus = document.getElementById('divStatus');
+let entriesDiv = document.getElementById('logEntries');
+
+const logEvent = (module, event) => {
+ newText = `${module}: ${event}\n`;
+ entriesDiv.value += newText;
+}
+
+let updates_complete = 0;
+let builds_complete = 0;
+let handleEvent = (ev) => {
+
+ if (ev.event === "build_plan") {
+ const text = "Working on " + ev.build_plan.length + " modules";
+
+ let num_updates = 0;
+ let num_builds = 0;
+ ev.build_plan.forEach((module_plan) => {
+ module_plan.phases.forEach((phase) => {
+ num_updates += phase === "update" ? 1 : 0;
+ num_builds += phase === "build" ? 1 : 0;
+ });
+ });
+
+ lkup('update_progress_bar').setAttribute('max', num_updates);
+ lkup('build_progress_bar' ).setAttribute('max', num_builds );
+ }
+ else if (ev.event === "build_done") {
+ const resetLink = '<%= url_for q(reset) %>';
+ const homeLink = '<%= url_for q(index) %>';
+
+ divStatus.innerHTML = `Build complete, <button id="btnReset">click to reset</button>`;
+
+ const btnReset = document.getElementById('btnReset');
+ btnReset.addEventListener('click', (ev) => {
+ fetch(resetLink, {
+ method: 'POST'
+ })
+ .then(resp => {
+ if (!resp.ok) {
+ throw new Error("Invalid response resetting kdesrc-build");
+ }
+ return resp.json();
+ })
+ .then(last_result => {
+ console.log("Last result was ", last_result.last_result)
+ document.location.assign (homeLink);
+ })
+ .catch(error => console.error(error));
+ }, { passive: false });
+ }
+ else if (ev.event === "phase_started") {
+ const phase = ev.phase_started.phase;
+ const module = ev.phase_started.module;
+
+ let row = lkup("row_" + module);
+ if (!row) {
+ addRow(module);
+ }
+
+ let cell = lkup(phase + "Cell_" + module);
+ if (!cell) {
+ return;
+ }
+
+ cell.className = 'working';
+ cell.textContent = 'Working...';
+ }
+ else if (ev.event === "phase_progress") {
+ const phase = ev.phase_progress.phase;
+ const module = ev.phase_progress.module;
+ const progressAry = ev.phase_progress.progress;
+
+ let cell = lkup(phase + "Cell_" + module);
+ if (!cell) {
+ return;
+ }
+
+ if (progressAry[1] == 0) {
+ cell.textContent = "???????";
+ } else {
+ cell.textContent = `${progressAry[0]} / ${progressAry[1]}`;
+ }
+ }
+ else if (ev.event === "phase_completed") {
+ const phase = ev.phase_completed.phase;
+ const module = ev.phase_completed.module;
+
+ let cell = lkup(phase + "Cell_" + module);
+ if (!cell) {
+ return;
+ }
+
+ if (phase === "update") {
+ updates_complete++;
+ } else {
+ builds_complete++;
+ }
+
+ lkup('update_progress_bar').setAttribute('value', updates_complete);
+ lkup('build_progress_bar' ).setAttribute('value', builds_complete );
+
+ cell.className = 'done';
+ if (['success', 'error'].
+ includes(ev.phase_completed.result))
+ {
+ cell.classList.add(ev.phase_completed.result);
+ }
+
+ if (ev.phase_completed.error_log) {
+ const logUrl = ev.phase_completed.error_log;
+ cell.innerHTML = `<a target='_blank' href='${logUrl}'>${ev.phase_completed.result}</a>`;
+ logEvent(module, 'Failed to ' + phase);
+ } else if (phase !== 'update') {
+ // If all successful for the module, remove the row
+ const updateResult = lkup('updateCell_' + module);
+ if (updateResult && updateResult.classList.contains('success')) {
+ const moduleRow = lkup('row_' + module);
+ if (moduleRow) {
+ moduleRow.remove(); // "Experimental" ChildNode iface
+ }
+ logEvent(module, 'Success');
+ }
+ } else {
+ cell.innerHTML = ev.phase_completed.result;
+ }
+ }
+ else if (ev.event === "log_entries") {
+ const phase = ev.log_entries.phase;
+ const module = ev.log_entries.module;
+ const entries = ev.log_entries.entries;
+
+ for(const entry of entries) {
+ logEvent(module, entry);
+ }
+ }
+ else {
+ console.error("Unhandled event ", ev.event);
+ console.error(ev);
+ }
+}
+
+let ws = new WebSocket('<%= url_for("events")->to_abs %>');
+
+ws.onmessage = (msg_event) => {
+ const events = JSON.parse(msg_event.data);
+
+ if (!events) {
+ console.log(`Received invalid JSON object in WebSocket handler ${msg_event}`);
+ return;
+ }
+
+ // event should be an array of JSON objects
+ for (const e of events) {
+ handleEvent(e);
+ }
+}
+</script>
diff --git a/modules/templates/index.html.ep b/modules/templates/index.html.ep
new file mode 100644
index 0000000..87fbcf5
--- /dev/null
+++ b/modules/templates/index.html.ep
@@ -0,0 +1,112 @@
+% layout 'default';
+% title 'kdesrc-build';
+<h1>kdesrc-build <%= $app %></h1>
+
+<label for="modules_list">List of modules to build:</label>
+<input id="modules_list"/><br />
+<button id="btnSubmitModuleList">Enter modules to build:</button>
+
+<div class="module_list_div">
+ <label for="module_select">Or use the dropdown to select</label>
+ <select multiple id="module_select" size="15">
+ <option value="">All</option>
+ <option value="###">--- Module names loading ---</option>
+ </select>
+</div>
+
+<div id="modules_result_div"></div>
+<br/>
+<button id="btnStartBuild" disabled>Start Build!</button>
+
+<script defer>
+const btnSubmitModuleList = lkup('btnSubmitModuleList');
+btnSubmitModuleList.addEventListener('click', (ev) => {
+ const modList = lkup('modules_list');
+ const modArray = modList.value.split(/[, ]+/);
+ console.dir(modArray);
+ if (modArray) {
+ const postUrl = '<%= url_for q(post_modules) %>';
+ fetch(postUrl, {
+ method: 'POST',
+ body: JSON.stringify(modArray),
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+ .then(resp => resp.json())
+ .then(resp => {
+ console.log(`Response received ${resp}`);
+ lkup('btnStartBuild').disabled = false;
+
+ const lookupUrl = '<%= url_for q(module_lookup) %>';
+ return fetch(lookupUrl); // TODO Merge with the result we already get?
+ })
+ .then(mod_list_resp => mod_list_resp.json())
+ .then(mod_list => {
+ const strOfMods = mod_list.join("<br/>");
+ lkup('modules_result_div').innerHTML = "Building: " + strOfMods;
+ })
+ .catch(err => console.error(err));
+ }
+}, { passive: false });
+
+const btnStartBuild = lkup('btnStartBuild');
+btnStartBuild.addEventListener('click', (ev) => {
+ const postUrl = '<%= url_for q(build) %>';
+ fetch(postUrl, {
+ method: 'POST',
+ })
+ .then(resp => {
+ if(!resp.ok) {
+ console.error(resp);
+ throw new Error ('Invalid response!');
+ }
+ return resp.text()
+ })
+ .then(text => {
+ console.log(`Response received ${text}`)
+ document.location.assign(text);
+ })
+ .catch(err => console.error(err));
+}, { passive: false });
+
+// Load the list of all modules to see where we're at
+fetch('<%= url_for q(known_modules) %>')
+.then(resp => resp.json())
+.then(mod_list => {
+ console.dir(mod_list);
+ const selList = lkup('module_select');
+ while (selList.firstChild) {
+ selList.removeChild(selList.firstChild);
+ }
+
+ const optTag = (name) => {
+ const newOpt = document.createElement('option');
+ const newText = document.createTextNode(name);
+ newOpt.appendChild(newText);
+ return newOpt;
+ }
+
+ console.group();
+ for (const module of mod_list) {
+ var tagToAdd;
+ if (typeof module === "string") {
+ console.log("Adding " + module);
+ tagToAdd = optTag(module);
+ }
+ else {
+ // module is really an array with [ set-name, @module-names ]
+ const setName = module.shift();
+ console.log("Adding module set " + setName);
+ tagToAdd = document.createElement('optgroup');
+ tagToAdd.setAttribute('label', setName);
+ module.forEach(name => tagToAdd.appendChild(optTag(name)));
+ }
+
+ selList.appendChild(tagToAdd);
+ }
+ console.groupEnd();
+})
+.catch(error => console.error(error));
+
+</script>
diff --git a/modules/templates/layouts/default.html.ep b/modules/templates/layouts/default.html.ep
new file mode 100644
index 0000000..eb7fecf
--- /dev/null
+++ b/modules/templates/layouts/default.html.ep
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8"/>
+<title><%= title %></title>
+
+<style>
+td.pending {
+ background-color: lightgray;
+}
+
+td.done {
+ background-color: lightblue;
+}
+
+td.done.success {
+ background-color: lightgreen;
+}
+
+td.done.error {
+ background-color: pink;
+}
+</style>
+
+<script defer>
+// Add common functions for use by all generated pages
+const lkup = (name) => document.getElementById(name);
+</script>
+</head>
+<body><%= content %></body>
+</html>
More information about the kde-doc-english
mailing list