diff --git a/lib/App/Yath/Command/client/recent.pm b/lib/App/Yath/Command/client/recent.pm index a8f553b63..246749ae6 100644 --- a/lib/App/Yath/Command/client/recent.pm +++ b/lib/App/Yath/Command/client/recent.pm @@ -12,7 +12,7 @@ use Getopt::Yath; include_options( 'App::Yath::Options::Yath', 'App::Yath::Options::Recent', - 'App::Yath::Options::Client', + 'App::Yath::Options::WebClient', ); sub summary { "Show a list of recent runs on a yathui server" } diff --git a/lib/App/Yath/Command/recent.pm b/lib/App/Yath/Command/recent.pm index 86e77bab8..e6363b513 100644 --- a/lib/App/Yath/Command/recent.pm +++ b/lib/App/Yath/Command/recent.pm @@ -16,7 +16,7 @@ use Getopt::Yath; include_options( 'App::Yath::Options::Yath', 'App::Yath::Options::Recent', - 'App::Yath::Options::Client', + 'App::Yath::Options::WebClient', 'App::Yath::Options::DB', ); diff --git a/lib/App/Yath/Command/run.pm b/lib/App/Yath/Command/run.pm index 3e965f875..1648db9d0 100644 --- a/lib/App/Yath/Command/run.pm +++ b/lib/App/Yath/Command/run.pm @@ -38,7 +38,7 @@ include_options( 'App::Yath::Options::Run', 'App::Yath::Options::Tests', 'App::Yath::Options::Yath', - 'App::Yath::Options::Client', + 'App::Yath::Options::WebClient', 'App::Yath::Options::DB', ); diff --git a/lib/App/Yath/Command/server.pm b/lib/App/Yath/Command/server.pm index 99e28342d..d7c515d4b 100644 --- a/lib/App/Yath/Command/server.pm +++ b/lib/App/Yath/Command/server.pm @@ -2,10 +2,17 @@ package App::Yath::Command::server; use strict; use warnings; +use App::Yath::Server; + +use App::Yath::Schema::Util qw/schema_config_from_settings/; + our $VERSION = '2.000000'; use parent 'App::Yath::Command'; -use Test2::Harness::Util::HashBase; +use Test2::Harness::Util::HashBase qw{ + server->launcher_args} => @$dot_args; + push @{$settings->webserver->launcher_args} => @$dot_args; return; } use Getopt::Yath; include_options( + 'App::Yath::Options::Term', 'App::Yath::Options::Yath', 'App::Yath::Options::DB', - 'App::Yath::Options::Term', + 'App::Yath::Options::WebServer', ); option_group {group => 'server', category => "Server Options"} => sub { option ephemeral => ( type => 'Auto', autofill => 'Auto', - long_examples => ['', '=Auto', '=PostgreSQL', '=MySQL'], + long_examples => ['', '=Auto', '=PostgreSQL', '=MySQL', '=MariaDB' ], description => "Use a temporary 'ephemeral' database that will be destroyed when the server exits.", autofill_text => 'If no db type is specified it will use "auto" which will try PostgreSQL first, then MySQL.', - allowed_values => [qw/Auto PostgreSQL MySQL/], + allowed_values => [qw/Auto PostgreSQL MySQL MariaDB/], ); option shell => ( @@ -58,61 +66,188 @@ option_group {group => 'server', category => "Server Options"} => sub { description => 'Launches in "developer mode" which accepts some developer commands while the server is running.', ); - option launcher => ( - type => 'Scalar', - default => 'starman', - description => 'Command to use to launch the server ` path/to/share/psgi/yath.psgi`', - notes => "You can pass custom args to the launcher after a '::' like `yath server [ARGS] [LOG FILES(s)]:: [LAUNCHER ARGS]`", + option single_user => ( + type => 'Bool', + default => 0, + description => "When using an ephemeral database you can use this to enable single user mode to avoid login and user credentials.", ); - option port_command => ( - type => 'Scalar', - description => 'Command to run that returns a port number.', + option single_run => ( + type => 'Bool', + default => 0, + description => "When using an ephemeral database you can use this to enable single run mode which causes the server to take you directly to the first run.", ); - option port => ( - type => 'Scalar', - description => 'Port to listen on.', - notes => 'This is passed to the launcher via `launcher --port PORT`', - default => sub { - my ($option, $settings) = @_; - - if (my $cmd = $settings->server->port_command) { - local $?; - my $port = `$cmd`; - die "Port command `$cmd` exited with error code $?.\n" if $?; - die "Port command `$cmd` did not return a valid port.\n" unless $port; - chomp($port); - die "Port command `$cmd` did not return a valid port: $port.\n" unless $port =~ m/^\d+$/; - return $port; - } - - return 8080; - }, + option no_upload => ( + type => 'Bool', + default => 0, + description => "When using an ephemeral database you can use this to enable no-upload mode which removes the upload workflow.", ); - option workers => ( + option email => ( type => 'Scalar', - default => sub { eval { require System::Info; System::Info->new->ncore } || 5 }, - default_text => "5, or number of cores if System::Info is installed.", - description => 'Number of workers. Defaults to the number of cores, or 5 if System::Info is not installed.', - notes => 'This is passed to the launcher via `launcher --workers WORKERS`', + description => "When using an ephemeral database you can use this to set a 'from' email address for email sent from this server.", ); +}; - option importers => ( - type => 'Scalar', - default => 2, - description => 'Number of log importer processes.', - ); - option launcher_args => ( - type => 'List', - initialize => sub { [] }, - description => "Set additional options for the loader.", - notes => "It is better to put loader arguments after '::' at the end of the command line.", - long_examples => [' "--reload"', '="--reload"'], - ); -}; +sub run { + my $self = shift; + my $pid = $$; + + my $args = $self->args; + my $settings = $self->settings; + + my $ephemeral = $settings->server->ephemeral; + + my $config = $self->{+CONFIG} = schema_config_from_settings($settings, ephemeral => $ephemeral); + + my $qdb_params = { + single_user => $settings->server->single_user // 0, + single_run => $settings->server->single_run // 0, + no_upload => $settings->server->no_upload // 0, + email => $settings->server->email // undef, + }; + + my $server = $self->{+SERVER} = App::Yath::Server->new(schema_config => $config, $settings->webserver->all, qdb_params => $qdb_params); + + $server->start_server; + + my $done = 0; + $SIG{TERM} = sub { $done++; print "Caught SIGTERM shutting down...\n"; $SIG{TERM} = 'DEFAULT' }; + $SIG{INT} = sub { $done++; print "Caught SIGINT shutting down...\n"; $SIG{INT} = 'DEFAULT' }; + + SERVER_LOOP: until ($done) { + if ($settings->server->dev) { + unless(eval { $done = $self->shell($pid); 1 }) { + warn $@; + $done = 1; + } + } + else { + sleep 1; + } + } + + if ($pid == $$) { + $server->stop_server if $server->pid; + } + else { + die "Scope leak, wrong PID"; + } + + return 0; +} + +sub shell { + my $self = shift; + my ($pid, $doneref) = @_; + + # Return that we should exit if the PID is wrong. + return 1 unless $pid == $$; + + my $settings = $self->settings; + my $server = $self->{+SERVER}; + my $config = $self->{+CONFIG}; + + $SIG{TERM} = sub { $SIG{TERM} = 'DEFAULT'; die "Cought SIGTERM exiting...\n" }; + $SIG{INT} = sub { $SIG{INT} = 'DEFAULT'; die "Cought SIGINT exiting...\n" }; + + STDERR->autoflush(); + sleep 1; + + my $dsn = $config->dbi_dsn; + + print "DBI_DSN: $dsn\n\n"; + print "\n"; + print "| Yath Server Developer Shell |\n"; + print "| type 'help', 'h', or '?' for help |\n"; + + while(1) { + print "\n> "; + + my $in = ; + return 1 if !defined($in) && eof(STDIN); + chomp($in); + next unless length($in); + + return 1 if $in =~ m/^(q|x|exit|quit)$/; + + if ($in =~ m/^(help|h|\?)(?:\s(.+))?$/) { + $self->shell_help($1); + next; + } + + my ($cmd, $args) = split /\s/, $in, 2; + + my $meth = "shell_$cmd"; + if ($self->can($meth)) { + eval { $self->$meth($args); 1 } or warn $@; + } + else { + print STDERR "Invalid command '$in'\n"; + } + } +} + +sub shell_help_text { "Show command list." } +sub shell_help { + my $self = shift; + my $class = ref($self); + my $stash = do { no strict 'refs'; \%{"$class\::"} }; + + print "\nAvailable commands:\n"; + printf(" %-12s %s\n", "[q]uit", "Quit the program."); + printf(" %-12s %s\n", "e[x]it", "Exit the program."); + printf(" %-12s %s\n", "[h]elp", "Show this help."); + printf(" %-12s %s\n", "?", "Show this help."); + + for my $sym (sort keys %$stash) { + next unless $sym =~ m/^shell_(.*)/; + my $cmd = $1; + next if $cmd eq 'help'; + next if $sym =~ m/_text$/; + next unless $self->can($sym); + + my $text = "${sym}_text"; + $text = $self->can($text) ? $self->$text() : 'No description.'; + printf(" %-12s %s\n", $cmd, $text); + } + print "\n"; +} + +sub shell_reload_text { "Restart web server (does not restart database or importers)." } +sub shell_reload { $_[0]->server->restart_server } + +sub shell_reloaddb_text { "Restart database (data is lost)." } +sub shell_reloaddb { + my $self = shift; + + my $server = $self->server; + $server->stop_server; + $server->stop_importers; + $server->reset_ephemeral_db; + $server->start_server; +} + +sub shell_reloadimp_text { "Restart the importers." } +sub shell_reloadimp { $_[0]->restart_importers() } + +sub shell_db_text { "Open the database." } +sub shell_db { $_[0]->server->qdb->shell } + +sub shell_load_text { "Load a database file (filename given as argument)" } +sub shell_load { die "TODO: fix me" } + +{ + no warnings 'once'; + *shell_r = \*shell_reload; + *shell_r_text = \*shell_reload_text; + *shell_rdb = \*shell_reloaddb; + *shell_rdb_text = \*shell_reloaddb_text; + *shell_ri = \*shell_reloadimp; + *shell_ri_text = \*shell_reloadimp_text; +} 1; diff --git a/lib/App/Yath/Command/start.pm b/lib/App/Yath/Command/start.pm index 736b4f8d0..274555d94 100644 --- a/lib/App/Yath/Command/start.pm +++ b/lib/App/Yath/Command/start.pm @@ -42,7 +42,7 @@ sub option_modules { 'App::Yath::Options::Renderer', 'App::Yath::Options::Tests', 'App::Yath::Options::DB', - 'App::Yath::Options::Client', + 'App::Yath::Options::WebClient', ); } diff --git a/lib/App/Yath/Options/Client.pm b/lib/App/Yath/Options/WebClient.pm similarity index 90% rename from lib/App/Yath/Options/Client.pm rename to lib/App/Yath/Options/WebClient.pm index 611b3059b..454a71ec1 100644 --- a/lib/App/Yath/Options/Client.pm +++ b/lib/App/Yath/Options/WebClient.pm @@ -1,4 +1,4 @@ -package App::Yath::Options::Client; +package App::Yath::Options::WebClient; use strict; use warnings; @@ -6,7 +6,7 @@ our $VERSION = '2.000000'; use Getopt::Yath; -option_group {group => 'client', prefix => 'client', category => "Web Client Options"} => sub { +option_group {group => 'webclient', category => "Web Client Options"} => sub { option url => ( type => 'Scalar', alt => ['uri'], @@ -44,7 +44,7 @@ __END__ =head1 NAME -App::Yath::Options::Client - FIXME +App::Yath::Options::WebClient - FIXME =head1 DESCRIPTION diff --git a/lib/App/Yath/Options/WebServer.pm b/lib/App/Yath/Options/WebServer.pm new file mode 100644 index 000000000..1ed22932e --- /dev/null +++ b/lib/App/Yath/Options/WebServer.pm @@ -0,0 +1,118 @@ +package App::Yath::Options::WebServer; +use strict; +use warnings; + +our $VERSION = '2.000000'; + +use Getopt::Yath; + +include_options( + 'App::Yath::Options::DB', +); + +option_group {group => 'webserver', category => "Web Server Options"} => sub { + option launcher => ( + type => 'Scalar', + default => sub { eval { require Starman; 1 } ? 'Starman' : undef }, + description => 'Command to use to launch the server (--server argument to Plack::Runner) ', + notes => "You can pass custom args to the launcher after a '::' like `yath server [ARGS] [LOG FILES(s)] :: [LAUNCHER ARGS]`", + default_text => "Will use 'Starman' if it installed otherwise whatever Plack::Runner uses by default.", + ); + + option port_command => ( + type => 'Scalar', + description => 'Command to run that returns a port number.', + ); + + option port => ( + type => 'Scalar', + description => 'Port to listen on.', + notes => 'This is passed to the launcher via `launcher --port PORT`', + default => sub { + my ($option, $settings) = @_; + + if (my $cmd = $settings->webserver->port_command) { + local $?; + my $port = `$cmd`; + die "Port command `$cmd` exited with error code $?.\n" if $?; + die "Port command `$cmd` did not return a valid port.\n" unless $port; + chomp($port); + die "Port command `$cmd` did not return a valid port: $port.\n" unless $port =~ m/^\d+$/; + return $port; + } + + return 8080; + }, + ); + + option workers => ( + type => 'Scalar', + default => sub { eval { require System::Info; System::Info->new->ncore } || 5 }, + default_text => "5, or number of cores if System::Info is installed.", + description => 'Number of workers. Defaults to the number of cores, or 5 if System::Info is not installed.', + notes => 'This is passed to the launcher via `launcher --workers WORKERS`', + ); + + option importers => ( + type => 'Scalar', + default => 2, + description => 'Number of log importer processes.', + ); + + option launcher_args => ( + type => 'List', + initialize => sub { [] }, + description => "Set additional options for the loader.", + notes => "It is better to put loader arguments after '::' at the end of the command line.", + long_examples => [' "--reload"', '="--reload"'], + ); +}; + +1; + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Yath::Options::WebServer - FIXME + +=head1 DESCRIPTION + +=head1 PROVIDED OPTIONS POD IS AUTO-GENERATED + +=head1 SOURCE + +The source code repository for Test2-Harness can be found at +L. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright Chad Granum Eexodist7@gmail.comE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See L + +=cut + diff --git a/lib/App/Yath/Renderer/DB.pm b/lib/App/Yath/Renderer/DB.pm index 13d4add2c..abed9b237 100644 --- a/lib/App/Yath/Renderer/DB.pm +++ b/lib/App/Yath/Renderer/DB.pm @@ -35,7 +35,7 @@ include_options( 'App::Yath::Options::Yath', 'App::Yath::Options::DB', 'App::Yath::Options::Upload', - 'App::Yath::Options::Client', + 'App::Yath::Options::WebClient', ); option_group {group => 'db', prefix => 'db', category => "Database Options"} => sub { diff --git a/lib/App/Yath/Schema.pm b/lib/App/Yath/Schema.pm index ed2b6a720..22f3ca347 100644 --- a/lib/App/Yath/Schema.pm +++ b/lib/App/Yath/Schema.pm @@ -6,6 +6,8 @@ use Carp qw/confess/; use App::Yath::Schema::UUID qw/uuid_inflate/; +use Test2::Harness::Util::UUID qw/gen_uuid/; + our $VERSION = '2.000000'; use base 'DBIx::Class::Schema'; @@ -13,7 +15,7 @@ use base 'DBIx::Class::Schema'; confess "You must first load a App::Yath::Schema::NAME module" unless $App::Yath::Schema::LOADED; -if ($App::Yath::Schema::LOADED =~ m/MySQL/ && eval { require DBIx::Class::Storage::DBI::mysql::Retryable; 1 }) { +if ($App::Yath::Schema::LOADED =~ m/(MySQL|Percona|MariaDB)/i && eval { require DBIx::Class::Storage::DBI::mysql::Retryable; 1 }) { __PACKAGE__->storage_type('::DBI::mysql::Retryable'); } @@ -22,6 +24,17 @@ __PACKAGE__->load_namespaces( default_resultset_class => 'ResultSet', ); +sub config { + my $self = shift; + my ($setting, @val) = @_; + + my $conf = $self->resultset('Config')->find_or_create({config_id => gen_uuid(), setting => $setting, @val ? (value => $val[0]) : ()}); + + $conf->update({value => $val[0]}) if @val; + + return $conf->value; +} + sub vague_run_search { my $self = shift; my (%params) = @_; diff --git a/lib/App/Yath/Schema/Config.pm b/lib/App/Yath/Schema/Config.pm index 2b4766844..e44cff420 100644 --- a/lib/App/Yath/Schema/Config.pm +++ b/lib/App/Yath/Schema/Config.pm @@ -10,7 +10,9 @@ use Carp qw/croak/; use Test2::Harness::Util::HashBase qw{ -_schema - -dbi_dsn -dbi_user -dbi_pass + schema->storage->disconnect } @@ -19,19 +21,56 @@ sub connect { shift->schema->storage->dbh } sub init { my $self = shift; - croak "'dbi_dsn' is a required attribute" - unless defined $self->{+DBI_DSN}; + unless ($self->{+EPHEMERAL}) { + croak "'dbi_dsn' is a required attribute" unless defined $self->{+DBI_DSN}; + croak "'dbi_user' is a required attribute" unless defined $self->{+DBI_USER}; + croak "'dbi_pass' is a required attribute" unless defined $self->{+DBI_PASS}; + } +} + +sub _check_for_creds { + my $self = shift; + + croak "'dbi_dsn' has not been set yet" unless defined $self->{+DBI_DSN}; + croak "'dbi_user' has not been set yet" unless defined $self->{+DBI_USER}; + croak "'dbi_pass' has not been set yet" unless defined $self->{+DBI_PASS}; +} - croak "'dbi_user' is a required attribute" - unless defined $self->{+DBI_USER}; +sub push_ephemeral_credentials { + my $self = shift; + my %params = @_; + + push @{$self->{+EPHEMERAL_STACK} //= []} => [@{$self}{DBI_DSN(), DBI_USER(), DBI_PASS()}, delete($self->{+_SCHEMA}), delete($ENV{YATH_DB_SCHEMA})]; + + $self->{$_} = $params{$_} // croak "'$_' is a required parameter" for DBI_DSN(), DBI_USER(), DBI_PASS(); - croak "'dbi_pass' is a required attribute" - unless defined $self->{+DBI_PASS}; + if (my $schema_type = $params{schema_type}) { + $ENV{YATH_DB_SCHEMA} = $schema_type; + } + + return; +} + +sub pop_ephemeral_credentials { + my $self = shift; + + my $set = pop(@{$self->{+EPHEMERAL_STACK} // []}) or croak "No db to pop"; + + my $schema; + (@{$self}{DBI_DSN(), DBI_USER(), DBI_PASS()}, $self->{+_SCHEMA}, $schema) = @$set; + + $ENV{YATH_DB_SCHEMA} = $schema if defined $schema; + + delete $self->{+EPHEMERAL_STACK} unless @{$self->{+EPHEMERAL_STACK}}; + + return; } sub guess_db_driver { my $self = shift; + $self->_check_for_creds(); + return 'MySQL' if $self->{+DBI_DSN} =~ m/(mysql|maria|percona)/i; return 'PostgreSQL' if $self->{+DBI_DSN} =~ m/(pg|postgre)/i; return 'PostgreSQL'; # Default @@ -39,7 +78,10 @@ sub guess_db_driver { sub db_driver { my $self = shift; - return $ENV{YATH_UI_SCHEMA} //= $self->guess_db_driver; + + $self->_check_for_creds(); + + return $ENV{YATH_DB_SCHEMA} //= $self->guess_db_driver; } sub schema { @@ -47,10 +89,12 @@ sub schema { return $self->{+_SCHEMA} if $self->{+_SCHEMA}; + $self->_check_for_creds(); + { no warnings 'once'; unless ($App::Yath::Schema::LOADED) { - my $schema = $ENV{YATH_UI_SCHEMA} //= $self->guess_db_driver; + my $schema = $ENV{YATH_DB_SCHEMA} //= $self->guess_db_driver; require(mod2file("App::Yath::Schema::$schema")); } } diff --git a/lib/App/Yath/Schema/Importer.pm b/lib/App/Yath/Schema/Importer.pm index 06e277208..125a91d5f 100644 --- a/lib/App/Yath/Schema/Importer.pm +++ b/lib/App/Yath/Schema/Importer.pm @@ -4,6 +4,8 @@ use warnings; our $VERSION = '2.000000'; +use POSIX; + use Carp qw/croak/; use App::Yath::Schema::RunProcessor; @@ -21,6 +23,22 @@ sub init { croak "'config' is a required attribute" unless $self->{+CONFIG}; + + $self->{+WORKER_ID} //= gen_uuid(); +} + +sub spawn { + my $self = shift; + + my $pid = fork // die "Could not fork: $!"; + return $pid if $pid; + + local $SIG{INT} = sub { POSIX::_exit(0) }; + local $SIG{TERM} = sub { POSIX::_exit(0) }; + + my $ok = eval { $self->run(); 1 }; + warn $@ unless $ok; + exit($ok ? 0 : 255); } sub run { diff --git a/lib/App/Yath/Schema/Queries.pm b/lib/App/Yath/Schema/Queries.pm index 67426834c..457ad005c 100644 --- a/lib/App/Yath/Schema/Queries.pm +++ b/lib/App/Yath/Schema/Queries.pm @@ -6,7 +6,7 @@ our $VERSION = '2.000000'; use Carp qw/croak/; -use Test2::Harness::Util::HashBase qw/-config/; +use Test2::Harness::Util::HashBase qw/{mysql_auto_reconnect} = 1 if $App::Yath::Server::Schema::LOADED =~ m/mysql/i; + $dbh->{mysql_auto_reconnect} = 1 if $App::Yath::Schema::LOADED =~ m/(mysql|percona|maraidb)/i; } my $e = decode_json(scalar $line); @@ -630,7 +630,7 @@ sub get_job { # Prevent duplicate coverage when --retry is used if ($job_try) { - if ($App::Yath::Schema::LOADED =~ m/mysql/i) { + if ($App::Yath::Schema::LOADED =~ m/(mysql|percona|mariadb)/i) { my $schema = $self->schema; $schema->storage->connected; # Make sure we are connected my $dbh = $schema->storage->dbh; diff --git a/lib/App/Yath/Schema/Util.pm b/lib/App/Yath/Schema/Util.pm index 18eef9167..9215d508c 100644 --- a/lib/App/Yath/Schema/Util.pm +++ b/lib/App/Yath/Schema/Util.pm @@ -55,15 +55,18 @@ sub dbd_driver { } sub schema_config_from_settings { - my ($settings) = @_; + my ($settings, %params) = @_; - my $db = $settings->group('db') or return; + my $config_class = delete $params{config_class} // 'App::Yath::Schema::Config'; + require(mod2file($config_class)); + + my $db = $settings->group('db') or croak "No database settings"; if (my $cmod = $db->config) { my $file = mod2file($cmod); require $file; - return $cmod->yath_ui_config(%$$db); + return $cmod->yath_db_config(%$$db); } my $dsn = $db->dsn; @@ -95,14 +98,18 @@ sub schema_config_from_settings { } } - return unless $dsn; + if ($dsn) { + return App::Yath::Schema::Config->new( + %params, + dbi_dsn => $dsn, + dbi_user => $db->user // '', + dbi_pass => $db->pass // '', + ); + } + + croak "Could not find a DSN" unless $params{ephemeral}; - require App::Yath::Schema::Config; - return App::Yath::Schema::Config->new( - dbi_dsn => $dsn, - dbi_user => $db->user // '', - dbi_pass => $db->pass // '', - ); + return App::Yath::Schema::Config->new(%params); } sub find_job { diff --git a/lib/App/Yath/Server.pm b/lib/App/Yath/Server.pm index a9a1fde92..53ef184de 100644 --- a/lib/App/Yath/Server.pm +++ b/lib/App/Yath/Server.pm @@ -2,245 +2,286 @@ package App::Yath::Server; use strict; use warnings; +use Carp qw/croak confess/; +use Test2::Harness::Util qw/parse_exit mod2file/; +use Test2::Harness::Util::UUID qw/gen_uuid/; + +use Plack::Runner; +use DBIx::QuickDB; + +use App::Yath::Server::Plack; +use App::Yath::Schema::Importer; + +use App::Yath::Util qw/share_file/; +use App::Yath::Schema::Util qw/qdb_driver dbd_driver/; + our $VERSION = '2.000000'; -use Router::Simple; -use Text::Xslate(qw/mark_raw/); -use Scalar::Util qw/blessed/; -use DateTime; +use Test2::Harness::Util::HashBase qw{ + {+SCHEMA_CONFIG}; + + $self->{+QDB_PARAMS} //= {}; +} -use App::Yath::Server::Controller::Interactions; -use App::Yath::Server::Controller::Binary; +sub plack { + my $self = shift; + return $self->{+PLACK} //= App::Yath::Server::Plack->new( + schema_config => $self->{+SCHEMA_CONFIG}, + ); +} -use App::Yath::Server::Util qw/share_dir/; -use App::Yath::Server::Response qw/resp error/; +sub restart_server { + my $self = shift; + my ($sig) = @_; -use Test2::Harness::Util::JSON qw/encode_json decode_json/; + my $exit = $self->stop_server($sig); + $self->start_server(); -use Test2::Harness::Util::HashBase qw/-config -router/; + return $exit; +} -sub init { +sub stop_server { my $self = shift; + my ($sig) = @_; - my $router = $self->{+ROUTER} ||= Router::Simple->new; - my $config = $self->{+CONFIG}; + $self->_root_proc_check(); - $router->connect('/' => {controller => 'App::Yath::Server::Controller::View'}); + my $pid = delete $self->{+PID} or croak "No server running"; - $router->connect('/upload' => {controller => 'App::Yath::Server::Controller::Upload'}) - unless $config->single_run; + return $self->stop_proc($pid, $sig); +} - $router->connect('/user' => {controller => 'App::Yath::Server::Controller::User'}) - unless $config->single_user; +sub stop_proc { + my $self = shift; + my ($pid, $sig) = @_; - $router->connect('/resources/data/:id' => {controller => 'App::Yath::Server::Controller::Resources', data => 1}); - $router->connect('/resources/data/:id/' => {controller => 'App::Yath::Server::Controller::Resources', data => 1}); - $router->connect('/resources/data/:id/:batch' => {controller => 'App::Yath::Server::Controller::Resources', data => 1}); - $router->connect('/resources/:id' => {controller => 'App::Yath::Server::Controller::Resources'}); - $router->connect('/resources/:id/' => {controller => 'App::Yath::Server::Controller::Resources'}); - $router->connect('/resources/:id/:batch' => {controller => 'App::Yath::Server::Controller::Resources'}); + $self->_root_proc_check(); - $router->connect('/interactions/:id' => {controller => 'App::Yath::Server::Controller::Interactions'}); - $router->connect('/interactions/:id/:context' => {controller => 'App::Yath::Server::Controller::Interactions'}); - $router->connect('/interactions/data/:id' => {controller => 'App::Yath::Server::Controller::Interactions', data => 1}); - $router->connect('/interactions/data/:id/:context' => {controller => 'App::Yath::Server::Controller::Interactions', data => 1}); + croak "'pid' is required" unless $pid; + $sig //= 'TERM'; - $router->connect('/project/:id' => {controller => 'App::Yath::Server::Controller::Project'}); - $router->connect('/project/:id/stats' => {controller => 'App::Yath::Server::Controller::Project', stats => 1}); - $router->connect('/project/:id/:n' => {controller => 'App::Yath::Server::Controller::Project'}); - $router->connect('/project/:id/:n/:count' => {controller => 'App::Yath::Server::Controller::Project'}); + local $?; + kill($sig, $pid); + my $got = waitpid($pid, 0); + my $exit = $?; - $router->connect('/recent/:project/:user/:count' => {controller => 'App::Yath::Server::Controller::Recent'}); - $router->connect('/recent/:project/:user' => {controller => 'App::Yath::Server::Controller::Recent'}); + croak "waitpid returned '$got', expected '$pid'" unless $got == $pid; + return parse_exit($exit); +} - $router->connect('/query/:name' => {controller => 'App::Yath::Server::Controller::Query'}); - $router->connect('/query/:name/:arg' => {controller => 'App::Yath::Server::Controller::Query'}); +sub reset_ephemeral_db { + my $self = shift; + my ($sig) = @_; - $router->connect('/run/:id' => {controller => 'App::Yath::Server::Controller::Run'}); - $router->connect('/run/:id/pin' => {controller => 'App::Yath::Server::Controller::Run', action => 'pin_toggle'}); - $router->connect('/run/:id/delete' => {controller => 'App::Yath::Server::Controller::Run', action => 'delete'}); - $router->connect('/run/:id/cancel' => {controller => 'App::Yath::Server::Controller::Run', action => 'cancel'}); - $router->connect('/run/:id/parameters' => {controller => 'App::Yath::Server::Controller::Run', action => 'parameters'}); + my $exit = $self->stop_ephemeral_db($sig); + $self->start_ephemeral_db(); - $router->connect('/run/field/:id' => {controller => 'App::Yath::Server::Controller::RunField'}); - $router->connect('/run/field/:id/delete' => {controller => 'App::Yath::Server::Controller::RunField', action => 'delete'}); + return $exit; +} - $router->connect('/job/field/:id' => {controller => 'App::Yath::Server::Controller::JobField'}); - $router->connect('/job/field/:id/delete' => {controller => 'App::Yath::Server::Controller::JobField', action => 'delete'}); +sub stop_ephemeral_db { + my $self = shift; + my ($sig) = @_; - $router->connect('/job/:job' => {controller => 'App::Yath::Server::Controller::Job'}); - $router->connect('/job/:job/:try' => {controller => 'App::Yath::Server::Controller::Job'}); - $router->connect('/event/:id' => {controller => 'App::Yath::Server::Controller::Events', from => 'single_event'}); - $router->connect('/event/:id/events' => {controller => 'App::Yath::Server::Controller::Events', from => 'event'}); + $self->_root_proc_check(); + $self->stop_server if $self->{+PID}; + $self->stop_importers if $self->{+IMPORTER_PIDS}; - $router->connect('/durations/:project' => {controller => 'App::Yath::Server::Controller::Durations'}); - $router->connect('/durations/:project/median' => {controller => 'App::Yath::Server::Controller::Durations', median => 1}); - $router->connect('/durations/:project/median/:user' => {controller => 'App::Yath::Server::Controller::Durations', median => 1}); - $router->connect('/durations/:project/:short/:medium' => {controller => 'App::Yath::Server::Controller::Durations'}); + my $db = delete $self->{+QDB} or croak "No ephemeral db running"; - $router->connect('/coverage/:source' => {controller => 'App::Yath::Server::Controller::Coverage'}); - $router->connect('/coverage/:source/:user' => {controller => 'App::Yath::Server::Controller::Coverage'}); - $router->connect('/coverage/:source/delete' => {controller => 'App::Yath::Server::Controller::Coverage', delete => 1}); + $db->stop; +} - $router->connect('/failed/:source' => {controller => 'App::Yath::Server::Controller::Files', failed => 1}); - $router->connect('/failed/:source/json' => {controller => 'App::Yath::Server::Controller::Files', failed => 1, json => 1}); - $router->connect('/failed/:project/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 1, json => 1}); - $router->connect('/failed/:project/:username/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 1, json => 1}); +sub start_ephemeral_db { + my $self = shift; - $router->connect('/files/:source' => {controller => 'App::Yath::Server::Controller::Files', failed => 0}); - $router->connect('/files/:source/json' => {controller => 'App::Yath::Server::Controller::Files', failed => 0, json => 1}); - $router->connect('/files/:project/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 0, json => 1}); - $router->connect('/files/:project/:username/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 0, json => 1}); + croak "Ephemeral DB already started" if $self->{+QDB}; - $router->connect('/rerun/:run_id' => {controller => 'App::Yath::Server::Controller::ReRun'}); - $router->connect('/rerun/:project/:username' => {controller => 'App::Yath::Server::Controller::ReRun'}); + $self->{+ROOT_PID} //= $$; + $self->_root_proc_check(); - $router->connect('/binary/:binary_id' => {controller => 'App::Yath::Server::Controller::Binary'}); + my $config = $self->{+SCHEMA_CONFIG}; + my $schema_type = $config->ephemeral // 'Auto'; - $router->connect('/download/:id' => {controller => 'App::Yath::Server::Controller::Download'}); + my $qdb_args; + if ($schema_type eq 'Auto') { + $qdb_args = {drivers => [qdb_driver('PostgreSQL'), qdb_driver('MySQL')]}; + $schema_type = undef; + } + else { + $qdb_args = {driver => qdb_driver($schema_type), dbd_driver => dbd_driver($schema_type)} + } - $router->connect('/lookup' => {controller => 'App::Yath::Server::Controller::Lookup'}); - $router->connect('/lookup/:lookup' => {controller => 'App::Yath::Server::Controller::Lookup'}); - $router->connect('/lookup/data/:lookup' => {controller => 'App::Yath::Server::Controller::Lookup', data => 1}); + my $db = DBIx::QuickDB->build_db(harness_ui => $qdb_args); + unless($schema_type) { + if (ref($db) =~ m/::(PostgreSQL|MySQL)$/) { + $schema_type = $1; + } + else { + die "$db does not look like PostgreSQL or MySQL"; + } + } - $router->connect('/view' => {controller => 'App::Yath::Server::Controller::View'}); - $router->connect('/view/:id' => {controller => 'App::Yath::Server::Controller::View'}); - $router->connect('/view/:run_id/:job' => {controller => 'App::Yath::Server::Controller::View'}); - $router->connect('/view/:run_id/:job/:try' => {controller => 'App::Yath::Server::Controller::View'}); + my $dbh = $db->connect('quickdb', AutoCommit => 1, RaiseError => 1); + $dbh->do('CREATE DATABASE harness_ui') or die "Could not create db " . $dbh->errstr; - $router->connect('/stream/run/:run_id' => {controller => 'App::Yath::Server::Controller::Stream', run_only => 1}); - $router->connect('/stream' => {controller => 'App::Yath::Server::Controller::Stream'}); - $router->connect('/stream/:id' => {controller => 'App::Yath::Server::Controller::Stream'}); - $router->connect('/stream/:run_id/:job' => {controller => 'App::Yath::Server::Controller::Stream'}); - $router->connect('/stream/:run_id/:job/:try' => {controller => 'App::Yath::Server::Controller::Stream'}); - $router->connect('/sweeper/:count/days' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'day'}); - $router->connect('/sweeper/:count/hours' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'hour'}); - $router->connect('/sweeper/:count/minutes' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'minute'}); - $router->connect('/sweeper/:count/seconds' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'second'}); -} + $db->load_sql(harness_ui => share_file("schema/$schema_type.sql")); + my $dsn = $db->connect_string('harness_ui'); -sub to_app { - my $self = shift; + $config->push_ephemeral_credentials(dbi_dsn => $dsn, dbi_user => '', dbi_pass => '', schema_type => $schema_type); + $ENV{YATH_DB_DSN} = $dsn; - my $router = $self->{+ROUTER}; + require(mod2file("App::Yath::Schema::$schema_type")); - return sub { - my $env = shift; + my $schema = $config->schema; - my $req = App::Yath::Server::Request->new(env => $env, config => $self->{+CONFIG}); + $schema->resultset('User')->create({username => 'root', password => 'root', realname => 'root', user_id => gen_uuid()}); - my $r = $router->match($env) || {}; + my $qdb_params = $self->{+QDB_PARAMS} // {}; + $schema->config(single_user => $qdb_params->{single_user} // 0); + $schema->config(single_run => $qdb_params->{single_run} // 0); + $schema->config(no_upload => $qdb_params->{no_upload} // 0); + $schema->config(email => $qdb_params->{email}) if $qdb_params->{email}; - $self->wrap($r->{controller}, $req, $r); - }; + return $self->{+QDB} = $db; } -sub wrap { +sub start_server { my $self = shift; - my ($class, $req, $r) = @_; + my %params = @_; + + croak "Server already started with pid $self->{+PID}" if $self->{+PID}; + + $self->{+ROOT_PID} //= $$; + $self->_root_proc_check(); + + if ($self->{+SCHEMA_CONFIG}->ephemeral && !$params{no_db} && !$self->{+QDB}) { + $self->start_ephemeral_db(); + } + + unless ($self->{+IMPORTER_PIDS} || $params{no_importers}) { + $self->start_importers(); + } + + my $pid = fork // die "Could not fork: $!"; + + return $self->{+PID} = $pid if $pid; - my ($controller, $res, $session); my $ok = eval { - die error(404) unless $class; + my $r = Plack::Runner->new; - if ($class->uses_session) { - $session = $req->session; - $req->session_host; # vivify this - } + my @options; + push @options => @{$self->{+LAUNCHER_ARGS} // []}; + push @options => ("--server" => $self->{+LAUNCHER}) if $self->{+LAUNCHER}; + push @options => ('--listen' => ":$self->{+PORT}") if $self->{+PORT}; + push @options => ('--workers' => ":$self->{+WORKERS}") if $self->{+WORKERS}; - $controller = $class->new(request => $req, config => $self->{+CONFIG}); - $res = $controller->handle($r); + $r->parse_options(@options); + $r->run($self->plack()->to_app()); 1; }; - my $err = $@ || 'Internal Error'; + my $err = $@; - unless ($ok && $res) { - if (blessed($err) && $err->isa('App::Yath::Server::Response')) { - $res = $err; - } - else { - warn $err; - my $msg = ($ENV{T2_HARNESS_UI_ENV} || '') eq 'dev' ? "$err\n" : undef; - $res = error(500 => $msg); - } + unless ($ok) { + eval { warn $err }; + exit 255; } - my $ct = $r->{json} ? 'application/json' : blessed($res) ? $res->content_type() : 'text/html'; - $ct ||= 'text/html'; - $ct = lc($ct); - $res->content_type($ct) if blessed($res); + exit(0); +} + +sub restart_importers { + my $self = shift; + $self->stop_importers(); + $self->start_importers(); +} + +sub start_importers { + my $self = shift; - if (my $stream = $res->stream) { - return $stream; + croak "Importers already started" if $self->{+IMPORTER_PIDS}; + + $self->{+ROOT_PID} //= $$; + $self->_root_proc_check(); + + # Gen uuids here before forking + my @pids; + for (1 .. $self->{+IMPORTERS} // 2) { + push @pids => App::Yath::Schema::Importer->new(config => $self->{+SCHEMA_CONFIG})->spawn(); } - if ($ct eq 'text/html') { - my $dt = DateTime->now(time_zone => 'local'); + $self->{+IMPORTER_PIDS} = \@pids; +} - my $tx = Text::Xslate->new(path => [share_dir('templates')]); - my $wrapped = $tx->render( - 'main.tx', - { - config => $self->{+CONFIG}, +sub stop_importers { + my $self = shift; - user => $req->user || undef, - errors => $res->errors || [], - messages => $res->messages || [], - add_css => $res->css || [], - add_js => $res->js || [], - title => $res->title || ($controller ? $controller->title : 'Test2-Harness-UI'), + my $pids = delete $self->{+IMPORTER_PIDS} or croak "Importers not started"; + $self->_root_proc_check(); - time_zone => $dt->strftime("%Z"), + kill('TERM', @$pids); - base_uri => $req->base->as_string || '', - content => mark_raw($res->raw_body) || '', - } - ); + for my $pid (@$pids) { + local $?; + my $got = waitpid($pid, 0); + my $exit = $?; - $res->body($wrapped); - } - elsif($ct eq 'application/json') { - if (my $data = $res->raw_body) { - $res->body(ref($data) ? encode_json($data) : $data); - } - elsif (my $errors = $res->errors) { - $res->body(encode_json({errors => $errors})); - } + warn "waitpid returned '$got' expected '$pid'" unless $got == $pid; + warn "importer process exited with $exit" if $exit; } - $res->cookies->{id} = {value => $session->session_id, httponly => 1, expires => '+1M'} - if $session; + return; +} - return $res->finalize; +sub _root_proc_check { + my $self = shift; + confess "root_pid is not set, did you start any servers?" unless $self->{+ROOT_PID}; + return if $$ == $self->{+ROOT_PID}; + confess "Attempt to manage processes from the wrong process"; } +sub shutdown { + my $self = shift; + + $self->_root_proc_check(); + + $self->stop_importers() if $self->importer_pids; + $self->stop_ephemeral_db() if $self->qdb; + $self->stop_server() if $self->pid; +} + +sub DESTROY { + my $self = shift; + + local $?; + + return unless $self->{+ROOT_PID}; + return unless $self->{+ROOT_PID} == $$; + + $self->shutdown(); +} __END__ @@ -250,22 +291,13 @@ __END__ =head1 NAME -App::Yath::Server - Web interface for viewing and inspecting yath test logs - -=head1 EARLY VERSION WARNING - -This program is still in early development. There are many bugs, missing -features, and things that will change. +App::Yath::Server - FIXME =head1 DESCRIPTION -This package provides a web UI for yath logs. =head1 SYNOPSIS -The easiest thing to do is use the C command, which -will create a temporary postgresql db, load your log into it, then launch the -app in starman on a local port that you can visit in your browser. =head1 SOURCE diff --git a/lib/App/Yath/Server/Config.pm b/lib/App/Yath/Server/Config.pm deleted file mode 100644 index 17cc6667f..000000000 --- a/lib/App/Yath/Server/Config.pm +++ /dev/null @@ -1,71 +0,0 @@ -package App::Yath::Server::Config; -use strict; -use warnings; - -our $VERSION = '2.000000'; - -use Test2::Util qw/get_tid pkg_to_file/; - -use Carp qw/croak/; - -use Test2::Harness::Util::HashBase qw{ - -single_user -single_run -no_upload - -show_user - -email -}; - -sub init { - my $self = shift; - - $self->{+SHOW_USER} //= 0; -} - -1; - -__END__ - -=pod - -=encoding UTF-8 - -=head1 NAME - -App::Yath::Server::Config - UI configuration - -=head1 DESCRIPTION - -=head1 SYNOPSIS - -TODO - -=head1 SOURCE - -The source code repository for Test2-Harness-UI can be found at -F. - -=head1 MAINTAINERS - -=over 4 - -=item Chad Granum Eexodist@cpan.orgE - -=back - -=head1 AUTHORS - -=over 4 - -=item Chad Granum Eexodist@cpan.orgE - -=back - -=head1 COPYRIGHT - -Copyright Chad Granum Eexodist7@gmail.comE. - -This program is free software; you can redistribute it and/or -modify it under the same terms as Perl itself. - -See F - -=cut diff --git a/lib/App/Yath/Server/Controller.pm b/lib/App/Yath/Server/Controller.pm index dc281c1f2..d124d89f4 100644 --- a/lib/App/Yath/Server/Controller.pm +++ b/lib/App/Yath/Server/Controller.pm @@ -8,21 +8,49 @@ use Carp qw/croak/; use App::Yath::Server::Response qw/error/; -use Test2::Harness::Util::HashBase qw/-request -config/; - -sub uses_session { 1 } +use Test2::Harness::Util::HashBase qw{ + {+REQUEST}; - croak "'config' is a required attribute" unless $self->{+CONFIG}; + croak "'request' is a required attribute" unless $self->{+REQUEST}; + croak "'schema_config' is a required attribute" unless $self->{+SCHEMA_CONFIG}; + + croak "'single_user' must be defined" unless defined $self->{+SINGLE_USER}; + croak "'single_run' must be defined" unless defined $self->{+SINGLE_RUN}; } -sub title { 'Test2-Harness-UI' } -sub handle { error(501) } +sub schema { $_[0]->{+SCHEMA} //= $_[0]->{+SCHEMA_CONFIG}->schema } + +sub title { 'Yath-Server' } + +sub handle { error(501 => "Controller '" . ref($_[0]) . "' did not implement handle()") } + +sub requires_user { 0 } + +sub auth_check { + my $self = shift; + + return unless $self->requires_user; + + return error(501 => "Controller '" . ref($_[0]) . "' did not implement verify_user_credentials()") + unless $self->can('verify_user_credentials'); + + return error(401) unless $self->verify_user_credentials(); + + return; +} -sub schema { $_[0]->{+CONFIG}->schema } 1; diff --git a/lib/App/Yath/Server/Controller/Binary.pm b/lib/App/Yath/Server/Controller/Binary.pm index 9d7d027d5..1a8009a98 100644 --- a/lib/App/Yath/Server/Controller/Binary.pm +++ b/lib/App/Yath/Server/Controller/Binary.pm @@ -24,7 +24,7 @@ sub handle { error(404 => 'No id') unless $binary_id; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $binary = $schema->resultset('Binary')->find({binary_id => $binary_id}); error(404 => 'No such binary file') unless $binary_id; diff --git a/lib/App/Yath/Server/Controller/Coverage.pm b/lib/App/Yath/Server/Controller/Coverage.pm index ed52b0342..0abe4f99d 100644 --- a/lib/App/Yath/Server/Controller/Coverage.pm +++ b/lib/App/Yath/Server/Controller/Coverage.pm @@ -22,7 +22,7 @@ sub handle { my $req = $self->{+REQUEST}; my $res = resp(200); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; die error(404 => 'Missing route') unless $route; my $source = $route->{source} or die error(404 => 'No source'); diff --git a/lib/App/Yath/Server/Controller/Download.pm b/lib/App/Yath/Server/Controller/Download.pm index 610fd5571..a5f996950 100644 --- a/lib/App/Yath/Server/Controller/Download.pm +++ b/lib/App/Yath/Server/Controller/Download.pm @@ -7,7 +7,7 @@ our $VERSION = '2.000000'; use Data::GUID; use List::Util qw/max/; use Text::Xslate(qw/mark_raw/); -use App::Yath::Server::Util qw/share_dir/; +use App::Yath::Util qw/share_dir/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -29,13 +29,13 @@ sub handle { my $run; - if ($self->{+CONFIG}->single_run) { + if ($self->{+REQUEST}->single_run) { $run = $user->runs->first or die error(404 => 'Invalid run'); } else { my $it = $route->{id} or die error(404 => 'No id'); $it = uuid_inflate($it) or die error(404 => 'Invalid Run'); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->{+REQUEST}->schema; $run = $schema->resultset('Run')->find({run_id => $it}) or die error(404 => 'Invalid Run'); } diff --git a/lib/App/Yath/Server/Controller/Durations.pm b/lib/App/Yath/Server/Controller/Durations.pm index 21eb5ec25..715ab6ee5 100644 --- a/lib/App/Yath/Server/Controller/Durations.pm +++ b/lib/App/Yath/Server/Controller/Durations.pm @@ -30,7 +30,7 @@ sub handle { my $median = $route->{median} || 0; my $username = $route->{user}; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $project = $schema->resultset('Project')->find({name => $project_name}); my $data = {}; diff --git a/lib/App/Yath/Server/Controller/Events.pm b/lib/App/Yath/Server/Controller/Events.pm index 4b02e1230..75fd9d426 100644 --- a/lib/App/Yath/Server/Controller/Events.pm +++ b/lib/App/Yath/Server/Controller/Events.pm @@ -21,7 +21,7 @@ sub handle { my $req = $self->{+REQUEST}; my $res = resp(200); my $user = $req->user; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; die error(404 => 'Missing route') unless $route; my $it = $route->{id} or die error(404 => 'No name or id'); diff --git a/lib/App/Yath/Server/Controller/Files.pm b/lib/App/Yath/Server/Controller/Files.pm index 07977c9b8..7881f08e4 100644 --- a/lib/App/Yath/Server/Controller/Files.pm +++ b/lib/App/Yath/Server/Controller/Files.pm @@ -32,7 +32,7 @@ sub handle { my $failed = $route->{failed}; error(404 => 'No source') unless $source || $project_name; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $query = {status => 'complete'}; my $attrs = {order_by => {'-desc' => 'run_ord'}, rows => 1}; diff --git a/lib/App/Yath/Server/Controller/Interactions.pm b/lib/App/Yath/Server/Controller/Interactions.pm index 5077d20c2..5b58094be 100644 --- a/lib/App/Yath/Server/Controller/Interactions.pm +++ b/lib/App/Yath/Server/Controller/Interactions.pm @@ -8,7 +8,8 @@ use DateTime; use Data::GUID; use Scalar::Util qw/blessed/; use App::Yath::Server::Response qw/resp error/; -use App::Yath::Server::Util qw/share_dir find_job/; +use App::Yath::Util qw/share_dir/; +use App::Yath::Schema::Util qw/find_job/; use Test2::Harness::Util::JSON qw/encode_json/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -59,7 +60,7 @@ sub data { my $self = shift; my ($id, $context) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; # Get event my $event = $schema->resultset('Event')->find({event_id => $id}) or die error(404 => 'Invalid Event'); @@ -179,7 +180,7 @@ sub interval { my $self = shift; my ($stamp, $op, $context) = @_; - my $driver = $self->{+CONFIG}->db_driver; + my $driver = $self->{+SCHEMA_CONFIG}->db_driver; return \"timestamp '$stamp' $op INTERVAL '$context' seconds" if $driver eq 'PostgreSQL'; diff --git a/lib/App/Yath/Server/Controller/Job.pm b/lib/App/Yath/Server/Controller/Job.pm index 568ed6d58..97723e1f6 100644 --- a/lib/App/Yath/Server/Controller/Job.pm +++ b/lib/App/Yath/Server/Controller/Job.pm @@ -7,7 +7,8 @@ our $VERSION = '2.000000'; use Data::GUID; use List::Util qw/max/; use Text::Xslate(qw/mark_raw/); -use App::Yath::Server::Util qw/share_dir find_job/; +use App::Yath::Util qw/share_dir/; +use App::Yath::Schema::Util qw/find_job/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; @@ -22,7 +23,7 @@ sub handle { my $res = resp(200); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $user = $req->user; die error(404 => 'Missing route') unless $route; diff --git a/lib/App/Yath/Server/Controller/JobField.pm b/lib/App/Yath/Server/Controller/JobField.pm index 37aaf0bb2..60ac1e6e5 100644 --- a/lib/App/Yath/Server/Controller/JobField.pm +++ b/lib/App/Yath/Server/Controller/JobField.pm @@ -7,7 +7,7 @@ our $VERSION = '2.000000'; use Data::GUID; use List::Util qw/max/; use Text::Xslate(qw/mark_raw/); -use App::Yath::Server::Util qw/share_dir/; +use App::Yath::Util qw/share_dir/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -28,7 +28,7 @@ sub handle { my $it = $route->{id} or die error(404 => 'No id'); $it = uuid_inflate($it) or die error(404 => "Invalid id"); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $field = $schema->resultset('JobField')->find({job_field_id => $it}) or die error(404 => 'Invalid Field'); if (my $act = $route->{action}) { diff --git a/lib/App/Yath/Server/Controller/Lookup.pm b/lib/App/Yath/Server/Controller/Lookup.pm index ef35b90ef..5534b9ea2 100644 --- a/lib/App/Yath/Server/Controller/Lookup.pm +++ b/lib/App/Yath/Server/Controller/Lookup.pm @@ -7,7 +7,8 @@ our $VERSION = '2.000000'; use Data::GUID; use Scalar::Util qw/blessed/; use App::Yath::Server::Response qw/resp error/; -use App::Yath::Server::Util qw/share_dir find_job/; +use App::Yath::Util qw/share_dir/; +use App::Yath::Schema::Util qw/find_job/; use Test2::Harness::Util::JSON qw/encode_json/; use parent 'App::Yath::Server::Controller'; @@ -99,7 +100,7 @@ sub lookup_run { return if $state->{run}->{$lookup}++; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $rs = $schema->resultset('Run'); my $run = eval { $rs->find({run_id => $lookup}) }; @@ -124,7 +125,7 @@ sub lookup_jobs { return if $state->{job}->{$lookup}++; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $rs = $schema->resultset('Job'); @@ -156,7 +157,7 @@ sub lookup_event { return if $state->{event}->{$lookup}++; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $rs = $schema->resultset('Event'); my $event = eval { $rs->find({event_id => $lookup}) }; diff --git a/lib/App/Yath/Server/Controller/Project.pm b/lib/App/Yath/Server/Controller/Project.pm index 12aa7dddc..8cc157eca 100644 --- a/lib/App/Yath/Server/Controller/Project.pm +++ b/lib/App/Yath/Server/Controller/Project.pm @@ -7,7 +7,8 @@ our $VERSION = '2.000000'; use Time::Elapsed qw/elapsed/; use List::Util qw/sum/; use Text::Xslate(); -use App::Yath::Server::Util qw/share_dir format_duration parse_duration is_invalid_subtest_name/; +use App::Yath::Util qw/share_dir/; +use App::Yath::Schema::Util qw/format_duration parse_duration is_invalid_subtest_name/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; use App::Yath::Schema::UUID qw/uuid_deflate uuid_inflate/; @@ -26,7 +27,7 @@ sub users { my $self = shift; my ($project) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my $query = <<" EOT"; @@ -65,7 +66,7 @@ sub handle { my $n = $route->{n} // 25; my $stats = $route->{stats} // 0; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $project; $project = $schema->resultset('Project')->single({name => $it}); @@ -196,7 +197,7 @@ sub get_add_query { return ("AND $user_query\n", @add_vals) unless $n || $range; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; if ($range) { @@ -246,7 +247,7 @@ sub _build_stat_run_list { my $self = shift; my ($project, $stat) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my ($add_query, @add_vals) = $self->get_add_query($project, $stat); @@ -273,7 +274,7 @@ sub _build_stat_expensive_files { my $self = shift; my ($project, $stat) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my ($add_query, @add_vals) = $self->get_add_query($project, $stat); @@ -326,7 +327,7 @@ sub _build_stat_expensive_subtests { my $self = shift; my ($project, $stat) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my ($add_query, @add_vals) = $self->get_add_query($project, $stat); @@ -379,7 +380,7 @@ sub _build_stat_expensive_users { my $self = shift; my ($project, $stat) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my ($add_query, @add_vals) = $self->get_add_query($project, $stat); @@ -429,7 +430,7 @@ sub _build_stat_user_summary { my $self = shift; my ($project, $stat) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my ($add_query, @add_vals) = $self->get_add_query($project, $stat); @@ -510,7 +511,7 @@ sub _build_stat_uncovered { my $self = shift; my ($project, $stat) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $users = $stat->{users}; my $field = $schema->resultset('RunField')->search( @@ -552,7 +553,7 @@ sub _build_stat_coverage { my $n = $stat->{n}; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $users = $stat->{users}; my @items = reverse $schema->resultset('RunField')->search( diff --git a/lib/App/Yath/Server/Controller/Query.pm b/lib/App/Yath/Server/Controller/Query.pm index f748ee16b..e0aafa94a 100644 --- a/lib/App/Yath/Server/Controller/Query.pm +++ b/lib/App/Yath/Server/Controller/Query.pm @@ -27,7 +27,7 @@ sub handle { my $req = $self->{+REQUEST}; my $res = resp(200); my $user = $req->user; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; die error(404 => 'Missing route') unless $route; my $it = $route->{name} or die error(400 => 'No query specified'); @@ -35,7 +35,7 @@ sub handle { my $arg = $route->{arg}; die error(400 => 'Missing Argument') if $spec->{args} && !defined($arg); - my $q = App::Yath::Schema::Queries->new(config => $self->{+CONFIG}); + my $q = App::Yath::Schema::Queries->new(config => $self->{+SCHEMA_CONFIG}); my $data = $q->$it($arg); $res->stream( diff --git a/lib/App/Yath/Server/Controller/ReRun.pm b/lib/App/Yath/Server/Controller/ReRun.pm index b82fc8d6d..0d58bef4d 100644 --- a/lib/App/Yath/Server/Controller/ReRun.pm +++ b/lib/App/Yath/Server/Controller/ReRun.pm @@ -31,7 +31,7 @@ sub handle { } error(404 => 'No source') unless $run_id || ($project_name && $username); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $query = {}; my $attrs = {order_by => {'-desc' => 'run_ord'}, rows => 1}; diff --git a/lib/App/Yath/Server/Controller/Recent.pm b/lib/App/Yath/Server/Controller/Recent.pm index 9c5756fd9..1e2e1bd0f 100644 --- a/lib/App/Yath/Server/Controller/Recent.pm +++ b/lib/App/Yath/Server/Controller/Recent.pm @@ -25,7 +25,7 @@ sub handle { my $user_name = $route->{user}; my $count = $route->{count} || 10; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $runs = $schema->vague_run_search( username => $user_name, project_name => $project_name, diff --git a/lib/App/Yath/Server/Controller/Resources.pm b/lib/App/Yath/Server/Controller/Resources.pm index d24b39eb1..0a45527f4 100644 --- a/lib/App/Yath/Server/Controller/Resources.pm +++ b/lib/App/Yath/Server/Controller/Resources.pm @@ -7,7 +7,8 @@ our $VERSION = '2.000000'; use DateTime; use Scalar::Util qw/blessed/; use App::Yath::Server::Response qw/resp error/; -use App::Yath::Server::Util qw/share_dir find_job/; +use App::Yath::Util qw/share_dir/; +use App::Yath::Schema::Util qw/find_job/; use App::Yath::Schema::DateTimeFormat qw/DTF/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; use Test2::Util::Times qw/render_duration/; @@ -69,7 +70,7 @@ sub get_thing { my $self = shift; my ($id) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my ($thing, $stamp_start, $done_check); my $search_args = {}; @@ -110,7 +111,7 @@ sub get_stamps { my $search_args = $params{search_args} || {}; my $start = $params{start}; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dbh = $schema->storage->dbh; my $fields = ""; @@ -223,7 +224,7 @@ sub render_stamp_resources { my $search_args = $params{search_args}; my $batch_id = uuid_inflate($params{batch}); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $res_rs = $schema->resultset('Resource'); my @res_list; diff --git a/lib/App/Yath/Server/Controller/Run.pm b/lib/App/Yath/Server/Controller/Run.pm index d7e19076e..7abb3e92c 100644 --- a/lib/App/Yath/Server/Controller/Run.pm +++ b/lib/App/Yath/Server/Controller/Run.pm @@ -7,7 +7,7 @@ our $VERSION = '2.000000'; use Data::GUID; use List::Util qw/max/; use Text::Xslate(qw/mark_raw/); -use App::Yath::Server::Util qw/share_dir/; +use App::Yath::Util qw/share_dir/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -28,13 +28,13 @@ sub handle { my $run; - if ($self->{+CONFIG}->single_run) { + if ($self->{+REQUEST}->single_run) { $run = $user->runs->first or die error(404 => 'Invalid run'); } else { my $it = $route->{id} or die error(404 => 'No id'); $it = uuid_inflate($it) or die error(404 => "Invalid run id"); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->{+REQUEST}->schema; $run = $schema->resultset('Run')->find({run_id => $it}) or die error(404 => 'Invalid Run'); } diff --git a/lib/App/Yath/Server/Controller/RunField.pm b/lib/App/Yath/Server/Controller/RunField.pm index fc3413ef1..631c55e03 100644 --- a/lib/App/Yath/Server/Controller/RunField.pm +++ b/lib/App/Yath/Server/Controller/RunField.pm @@ -7,7 +7,7 @@ our $VERSION = '2.000000'; use Data::GUID; use List::Util qw/max/; use Text::Xslate(qw/mark_raw/); -use App::Yath::Server::Util qw/share_dir/; +use App::Yath::Util qw/share_dir/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json decode_json/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -27,7 +27,7 @@ sub handle { die error(404 => 'Missing route') unless $route; my $it = uuid_inflate($route->{id}) or die error(404 => 'No id'); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $field = $schema->resultset('RunField')->find({run_field_id => $it}) or die error(404 => 'Invalid Field'); if (my $act = $route->{action}) { diff --git a/lib/App/Yath/Server/Controller/Stream.pm b/lib/App/Yath/Server/Controller/Stream.pm index 46c302746..4331d4b4f 100644 --- a/lib/App/Yath/Server/Controller/Stream.pm +++ b/lib/App/Yath/Server/Controller/Stream.pm @@ -7,7 +7,7 @@ our $VERSION = '2.000000'; use Data::GUID; use List::Util qw/max/; use Scalar::Util qw/blessed/; -use App::Yath::Server::Util qw/find_job/; +use App::Yath::Schema::Util qw/find_job/; use App::Yath::Schema::UUID qw/uuid_inflate/; use App::Yath::Server::Response qw/resp error/; use Test2::Harness::Util::JSON qw/encode_json/; @@ -75,7 +75,7 @@ sub stream_runs { my $self = shift; my ($req, $route) = @_; - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $opts = { remove_columns => [qw/log_data run_fields.data parameters/], @@ -183,7 +183,7 @@ sub stream_jobs { if (my $job_uuid = $route->{job}) { $job_uuid = uuid_inflate($job_uuid) or die error(404 => "Invalid job id"); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; return $self->stream_single(%params, item => find_job($schema, $job_uuid, $route->{try})); } @@ -314,7 +314,7 @@ sub stream_set { unless ($items) { my $val; if (blessed($ord) && $ord->isa('DateTime')) { - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $dtf = $schema->storage->datetime_parser; $val = $dtf->format_datetime($ord); } @@ -324,7 +324,7 @@ sub stream_set { my $query = { ($custom_query ? %$custom_query : ()), - $ord_field => {'>' => $val}, + defined($val) ? ($ord_field => {'>' => $val}) : (), }; my @ids = $track ? keys %$incomplete : (); diff --git a/lib/App/Yath/Server/Controller/Sweeper.pm b/lib/App/Yath/Server/Controller/Sweeper.pm index 3a0bdc5b1..961546e6e 100644 --- a/lib/App/Yath/Server/Controller/Sweeper.pm +++ b/lib/App/Yath/Server/Controller/Sweeper.pm @@ -28,7 +28,7 @@ sub handle { my $sweeper = App::Yath::Schema::Sweeper->new( interval => $interval, - config => $self->{+CONFIG}, + config => $self->{+SCHEMA_CONFIG}, ); my $purged = $sweeper->sweep; diff --git a/lib/App/Yath/Server/Controller/Upload.pm b/lib/App/Yath/Server/Controller/Upload.pm index a6b86306d..2d90eb491 100644 --- a/lib/App/Yath/Server/Controller/Upload.pm +++ b/lib/App/Yath/Server/Controller/Upload.pm @@ -6,13 +6,14 @@ our $VERSION = '2.000000'; use Text::Xslate(); -use App::Yath::Schema::UUID qw/uuid_inflate/; +use Test2::Harness::Util::UUID qw/gen_uuid/; use Test2::Harness::Util::JSON qw/decode_json/; use Test2::Harness::Util qw/open_file/; +use App::Yath::Schema::UUID qw/uuid_inflate/; use App::Yath::Schema::Queries(); -use App::Yath::Server::Util qw/share_dir/; +use App::Yath::Util qw/share_dir/; use App::Yath::Server::Response qw/resp error/; use parent 'App::Yath::Server::Controller'; @@ -44,9 +45,9 @@ sub handle { 'upload.tx', { base_uri => $req->base->as_string, - single_user => $self->{+CONFIG}->single_user, + single_user => $self->single_user, user => $user, - projects => App::Yath::Schema::Queries->new(config => $self->{+CONFIG})->projects, + projects => App::Yath::Schema::Queries->new(config => $self->{+SCHEMA_CONFIG})->projects, } ); @@ -74,7 +75,7 @@ sub process_form { my $tmp = $req->uploads->{log_file}->tempname; my $project_name = $req->parameters->{project} || return $res->add_error('project is required'); - my $project = $self->schema->resultset('Project')->find_or_create({name => $project_name}); + my $project = $self->schema->resultset('Project')->find_or_create({name => $project_name, project_id => gen_uuid()}); my $mode = $req->parameters->{mode} || 'qvfd'; diff --git a/lib/App/Yath/Server/Controller/User.pm b/lib/App/Yath/Server/Controller/User.pm index cf5c6fcd8..cac460803 100644 --- a/lib/App/Yath/Server/Controller/User.pm +++ b/lib/App/Yath/Server/Controller/User.pm @@ -5,7 +5,7 @@ use warnings; our $VERSION = '2.000000'; use Text::Xslate(); -use App::Yath::Server::Util qw/share_dir/; +use App::Yath::Util qw/share_dir/; use App::Yath::Server::Response qw/resp error/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -200,15 +200,15 @@ sub send_verification_code { my $schema = $self->schema; + my $our_email = $schema->config('email') or die "System email address is not set"; + my $code = $schema->resultset('EmailVerificationCode')->find_or_create({email_id => $email->email_id}); my $text = $code->evcode_id; - my $config = $self->{+CONFIG}; - my $msg = Email::Simple->create( header => [ To => $email->address, - From => $config->email, + From => $our_email, Subject => "Email verification code", ], body => "Verification code: $text\n", diff --git a/lib/App/Yath/Server/Controller/View.pm b/lib/App/Yath/Server/Controller/View.pm index 3e1c2eda7..99aeaf47e 100644 --- a/lib/App/Yath/Server/Controller/View.pm +++ b/lib/App/Yath/Server/Controller/View.pm @@ -6,7 +6,8 @@ our $VERSION = '2.000000'; use Data::GUID; use Text::Xslate(qw/mark_raw/); -use App::Yath::Server::Util qw/share_dir find_job/; +use App::Yath::Util qw/share_dir/; +use App::Yath::Schema::Util qw/find_job/; use App::Yath::Server::Response qw/resp error/; use App::Yath::Schema::UUID qw/uuid_inflate/; @@ -28,7 +29,7 @@ sub handle { $res->add_js('eventtable.js'); $res->add_js('view.js'); - my $schema = $self->{+CONFIG}->schema; + my $schema = $self->schema; my $id = $route->{id}; my $uuid = uuid_inflate($id); diff --git a/lib/App/Yath/Server/Plack.pm b/lib/App/Yath/Server/Plack.pm new file mode 100644 index 000000000..6c0659026 --- /dev/null +++ b/lib/App/Yath/Server/Plack.pm @@ -0,0 +1,345 @@ +package App::Yath::Server::Plack; +use strict; +use warnings; + +our $VERSION = '2.000000'; + +use Router::Simple; +use Data::GUID; +use DateTime; + +use Text::Xslate(qw/mark_raw/); +use Scalar::Util qw/blessed/; +use Carp qw/croak/; + +use Plack::Builder; +use Plack::App::Directory; +use Plack::App::File; + +use App::Yath::Schema::UUID qw/gen_uuid uuid_inflate/; + +use App::Yath::Server::Request; +use App::Yath::Server::Controller::Upload; +use App::Yath::Server::Controller::Recent; +use App::Yath::Server::Controller::User; +use App::Yath::Server::Controller::Run; +use App::Yath::Server::Controller::RunField; +use App::Yath::Server::Controller::Job; +use App::Yath::Server::Controller::JobField; +use App::Yath::Server::Controller::Download; +use App::Yath::Server::Controller::Sweeper; +use App::Yath::Server::Controller::Project; +use App::Yath::Server::Controller::Resources; + +use App::Yath::Server::Controller::Stream; +use App::Yath::Server::Controller::View; +use App::Yath::Server::Controller::Lookup; + +use App::Yath::Server::Controller::Query; +use App::Yath::Server::Controller::Events; + +use App::Yath::Server::Controller::Durations; +use App::Yath::Server::Controller::Coverage; +use App::Yath::Server::Controller::Files; +use App::Yath::Server::Controller::ReRun; + +use App::Yath::Server::Controller::Interactions; +use App::Yath::Server::Controller::Binary; + +use App::Yath::Server::Response qw/resp error/; + +use App::Yath::Util qw/share_dir/; + +use Test2::Harness::Util::JSON qw/encode_json decode_json/; + +use Test2::Harness::Util::HashBase qw{ + {+SCHEMA_CONFIG}; + + my $schema = $self->schema; + $self->{+SINGLE_RUN} //= $schema->config('single_run'); + $self->{+SINGLE_USER} //= $schema->config('single_user'); +} + +sub schema { $_[0]->{+SCHEMA_CONFIG}->schema } + +sub router { + my $self = shift; + + return $self->{+ROUTER} if $self->{+ROUTER}; + + my $router = Router::Simple->new; + my $schema = $self->schema; + + $router->connect('/' => {controller => 'App::Yath::Server::Controller::View'}); + + $router->connect('/upload' => {controller => 'App::Yath::Server::Controller::Upload'}) + unless $self->single_run; + + $router->connect('/user' => {controller => 'App::Yath::Server::Controller::User'}) + unless $self->single_user; + + $router->connect('/resources/data/:id' => {controller => 'App::Yath::Server::Controller::Resources', data => 1}); + $router->connect('/resources/data/:id/' => {controller => 'App::Yath::Server::Controller::Resources', data => 1}); + $router->connect('/resources/data/:id/:batch' => {controller => 'App::Yath::Server::Controller::Resources', data => 1}); + $router->connect('/resources/:id' => {controller => 'App::Yath::Server::Controller::Resources'}); + $router->connect('/resources/:id/' => {controller => 'App::Yath::Server::Controller::Resources'}); + $router->connect('/resources/:id/:batch' => {controller => 'App::Yath::Server::Controller::Resources'}); + + $router->connect('/interactions/:id' => {controller => 'App::Yath::Server::Controller::Interactions'}); + $router->connect('/interactions/:id/:context' => {controller => 'App::Yath::Server::Controller::Interactions'}); + $router->connect('/interactions/data/:id' => {controller => 'App::Yath::Server::Controller::Interactions', data => 1}); + $router->connect('/interactions/data/:id/:context' => {controller => 'App::Yath::Server::Controller::Interactions', data => 1}); + + $router->connect('/project/:id' => {controller => 'App::Yath::Server::Controller::Project'}); + $router->connect('/project/:id/stats' => {controller => 'App::Yath::Server::Controller::Project', stats => 1}); + $router->connect('/project/:id/:n' => {controller => 'App::Yath::Server::Controller::Project'}); + $router->connect('/project/:id/:n/:count' => {controller => 'App::Yath::Server::Controller::Project'}); + + $router->connect('/recent/:project/:user/:count' => {controller => 'App::Yath::Server::Controller::Recent'}); + $router->connect('/recent/:project/:user' => {controller => 'App::Yath::Server::Controller::Recent'}); + + $router->connect('/query/:name' => {controller => 'App::Yath::Server::Controller::Query'}); + $router->connect('/query/:name/:arg' => {controller => 'App::Yath::Server::Controller::Query'}); + + $router->connect('/run/:id' => {controller => 'App::Yath::Server::Controller::Run'}); + $router->connect('/run/:id/pin' => {controller => 'App::Yath::Server::Controller::Run', action => 'pin_toggle'}); + $router->connect('/run/:id/delete' => {controller => 'App::Yath::Server::Controller::Run', action => 'delete'}); + $router->connect('/run/:id/cancel' => {controller => 'App::Yath::Server::Controller::Run', action => 'cancel'}); + $router->connect('/run/:id/parameters' => {controller => 'App::Yath::Server::Controller::Run', action => 'parameters'}); + + $router->connect('/run/field/:id' => {controller => 'App::Yath::Server::Controller::RunField'}); + $router->connect('/run/field/:id/delete' => {controller => 'App::Yath::Server::Controller::RunField', action => 'delete'}); + + $router->connect('/job/field/:id' => {controller => 'App::Yath::Server::Controller::JobField'}); + $router->connect('/job/field/:id/delete' => {controller => 'App::Yath::Server::Controller::JobField', action => 'delete'}); + + $router->connect('/job/:job' => {controller => 'App::Yath::Server::Controller::Job'}); + $router->connect('/job/:job/:try' => {controller => 'App::Yath::Server::Controller::Job'}); + $router->connect('/event/:id' => {controller => 'App::Yath::Server::Controller::Events', from => 'single_event'}); + $router->connect('/event/:id/events' => {controller => 'App::Yath::Server::Controller::Events', from => 'event'}); + + $router->connect('/durations/:project' => {controller => 'App::Yath::Server::Controller::Durations'}); + $router->connect('/durations/:project/median' => {controller => 'App::Yath::Server::Controller::Durations', median => 1}); + $router->connect('/durations/:project/median/:user' => {controller => 'App::Yath::Server::Controller::Durations', median => 1}); + $router->connect('/durations/:project/:short/:medium' => {controller => 'App::Yath::Server::Controller::Durations'}); + + $router->connect('/coverage/:source' => {controller => 'App::Yath::Server::Controller::Coverage'}); + $router->connect('/coverage/:source/:user' => {controller => 'App::Yath::Server::Controller::Coverage'}); + $router->connect('/coverage/:source/delete' => {controller => 'App::Yath::Server::Controller::Coverage', delete => 1}); + + $router->connect('/failed/:source' => {controller => 'App::Yath::Server::Controller::Files', failed => 1}); + $router->connect('/failed/:source/json' => {controller => 'App::Yath::Server::Controller::Files', failed => 1, json => 1}); + $router->connect('/failed/:project/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 1, json => 1}); + $router->connect('/failed/:project/:username/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 1, json => 1}); + + $router->connect('/files/:source' => {controller => 'App::Yath::Server::Controller::Files', failed => 0}); + $router->connect('/files/:source/json' => {controller => 'App::Yath::Server::Controller::Files', failed => 0, json => 1}); + $router->connect('/files/:project/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 0, json => 1}); + $router->connect('/files/:project/:username/:idx' => {controller => 'App::Yath::Server::Controller::Files', failed => 0, json => 1}); + + $router->connect('/rerun/:run_id' => {controller => 'App::Yath::Server::Controller::ReRun'}); + $router->connect('/rerun/:project/:username' => {controller => 'App::Yath::Server::Controller::ReRun'}); + + $router->connect('/binary/:binary_id' => {controller => 'App::Yath::Server::Controller::Binary'}); + + $router->connect('/download/:id' => {controller => 'App::Yath::Server::Controller::Download'}); + + $router->connect('/lookup' => {controller => 'App::Yath::Server::Controller::Lookup'}); + $router->connect('/lookup/:lookup' => {controller => 'App::Yath::Server::Controller::Lookup'}); + $router->connect('/lookup/data/:lookup' => {controller => 'App::Yath::Server::Controller::Lookup', data => 1}); + + $router->connect('/view' => {controller => 'App::Yath::Server::Controller::View'}); + $router->connect('/view/:id' => {controller => 'App::Yath::Server::Controller::View'}); + $router->connect('/view/:run_id/:job' => {controller => 'App::Yath::Server::Controller::View'}); + $router->connect('/view/:run_id/:job/:try' => {controller => 'App::Yath::Server::Controller::View'}); + + $router->connect('/stream/run/:run_id' => {controller => 'App::Yath::Server::Controller::Stream', run_only => 1}); + $router->connect('/stream' => {controller => 'App::Yath::Server::Controller::Stream'}); + $router->connect('/stream/:id' => {controller => 'App::Yath::Server::Controller::Stream'}); + $router->connect('/stream/:run_id/:job' => {controller => 'App::Yath::Server::Controller::Stream'}); + $router->connect('/stream/:run_id/:job/:try' => {controller => 'App::Yath::Server::Controller::Stream'}); + + $router->connect('/sweeper/:count/days' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'day'}); + $router->connect('/sweeper/:count/hours' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'hour'}); + $router->connect('/sweeper/:count/minutes' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'minute'}); + $router->connect('/sweeper/:count/seconds' => {controller => 'App::Yath::Server::Controller::Sweeper', units => 'second'}); + + return $self->{+ROUTER} = $router; +} + +sub to_app { + my $self = shift; + + return $self->{+APP} //= builder { + mount '/js' => Plack::App::Directory->new({root => share_dir('js')})->to_app; + mount '/css' => Plack::App::Directory->new({root => share_dir('css')})->to_app; + mount '/img' => Plack::App::Directory->new({root => share_dir('img')})->to_app; + mount '/favicon.ico' => Plack::App::File->new({file => share_dir('img') . '/favicon.ico'})->to_app; + mount '/' => sub { $self->handle_request(@_) }; + }; +} + +sub handle_request { + my $self = shift; + my ($env) = @_; + + my $schema = $self->schema; + my $router = $self->router; + my $route = $router->match($env) || {}; + my $controller_class = $route->{controller} or return error(404); + + my $req = App::Yath::Server::Request->new(env => $env, schema => $schema); + + my ($controller, $res, $session, $session_host, $user); + my $ok = eval { + $session = $req->session(); + $session_host = $req->session_host(); + + if ($self->{+SINGLE_USER}) { + $user = $self->schema->resultset('User')->find({username => 'root'}); + } + elsif ($session_host) { + $user = $session_host->user if $session_host->user_id; + } + + $req->set_user($user) if $user; + + $controller = $controller_class->new( + request => $req, + route => $route, + schema => $self->schema, + schema_config => $self->schema_config, + session => $session, + session_host => $session_host, + single_run => $self->single_run, + single_user => $self->single_user, + user => $user, + ); + + $res = $controller->auth_check() // $controller->handle(); + + 1; + }; + my $err = $@ || 'Internal Error'; + + unless ($ok && $res) { + if (blessed($err) && $err->isa('App::Yath::Server::Response')) { + $res = $err; + } + else { + warn $err; + my $msg = ($ENV{T2_HARNESS_UI_ENV} || '') eq 'dev' ? "$err\n" : undef; + $res = error(500 => $msg); + } + } + + my $ct = $route->{json} ? 'application/json' : blessed($res) ? $res->content_type() : 'text/html'; + $ct ||= 'text/html'; + $ct = lc($ct); + $res->content_type($ct) if blessed($res); + + if (my $stream = $res->stream) { + return $stream; + } + + if ($ct eq 'text/html') { + my $dt = DateTime->now(time_zone => 'local'); + + my $tx = Text::Xslate->new(path => [share_dir('templates')]); + my $wrapped = $tx->render( + 'main.tx', + { + single_user => $self->single_user // 0, + single_run => $self->single_run // 0, + no_upload => $schema->config('no_upload') // 0, + + user => $req->user || undef, + errors => $res->errors || [], + messages => $res->messages || [], + add_css => $res->css || [], + add_js => $res->js || [], + title => $res->title || ($controller ? $controller->title : 'Yath-Server'), + + time_zone => $dt->strftime("%Z"), + + base_uri => $req->base->as_string || '', + content => mark_raw($res->raw_body) || '', + } + ); + + $res->body($wrapped); + } + elsif($ct eq 'application/json') { + if (my $data = $res->raw_body) { + $res->body(ref($data) ? encode_json($data) : $data); + } + elsif (my $errors = $res->errors) { + $res->body(encode_json({errors => $errors})); + } + } + + $res->cookies->{id} = {value => $session->session_id, httponly => 1, expires => '+1M'} + if $session; + + return $res->finalize; +} + + +__END__ + +=pod + +=encoding UTF-8 + +=head1 NAME + +App::Yath::Server::Plack - Plack app module for Yath Server. + +=head1 DESCRIPTION + + +=head1 SYNOPSIS + + +=head1 SOURCE + +The source code repository for Test2-Harness-UI can be found at +F. + +=head1 MAINTAINERS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 AUTHORS + +=over 4 + +=item Chad Granum Eexodist@cpan.orgE + +=back + +=head1 COPYRIGHT + +Copyright Chad Granum Eexodist7@gmail.comE. + +This program is free software; you can redistribute it and/or +modify it under the same terms as Perl itself. + +See F + +=cut diff --git a/lib/App/Yath/Server/Request.pm b/lib/App/Yath/Server/Request.pm index 142fd1080..a3798a9e3 100644 --- a/lib/App/Yath/Server/Request.pm +++ b/lib/App/Yath/Server/Request.pm @@ -4,30 +4,32 @@ use warnings; our $VERSION = '2.000000'; -use App::Yath::Schema::UUID qw/gen_uuid uuid_inflate/; -use Data::GUID; use Carp qw/croak/; +use App::Yath::Schema::UUID qw/gen_uuid uuid_inflate/; + use parent 'Plack::Request'; +use Test2::Harness::Util::HashBase qw{ + +session + +session_host + SUPER::new($env); - $self->{'config'} = delete $params{config} or croak "'config' is a required attribute"; - - return $self; + return bless(\%params, $class); } -sub schema { $_[0]->{config}->schema } - sub session { my $self = shift; - return $self->{session} if $self->{session}; + return $self->{+SESSION} if $self->{+SESSION}; my $schema = $self->schema; @@ -39,22 +41,17 @@ sub session { $session = undef unless $session && $session->active; } - $session ||= $self->schema->resultset('Session')->create( + $session ||= $schema->resultset('Session')->create( {session_id => gen_uuid}, ); - $self->{session} = $session; - - # Vivify this - $self->session_host; - - return $session; + return $self->{+SESSION} = $session; } sub session_host { my $self = shift; - return $self->{session_host} if $self->{session_host}; + return $self->{+SESSION_HOST} if $self->{+SESSION_HOST}; my $session = $self->session or return undef; @@ -79,20 +76,9 @@ sub session_host { $schema->txn_commit; - return $self->{session_host} = $host; + return $self->{+SESSION_HOST} = $host; } -sub user { - my $self = shift; - - return $self->schema->resultset('User')->find({username => 'root'}) - if $self->{config}->single_user; - - my $host = $self->session_host or return undef; - - return undef unless $host->user_id; - return $host->user; -} 1; diff --git a/lib/App/Yath/Server/Util.pm b/lib/App/Yath/Server/Util.pm deleted file mode 100644 index 1d66bc792..000000000 --- a/lib/App/Yath/Server/Util.pm +++ /dev/null @@ -1,267 +0,0 @@ -package App::Yath::Server::Util; -use strict; -use warnings; - -our $VERSION = '2.000000'; - -use Carp qw/croak/; - -use File::ShareDir(); - -use Test2::Harness::Util qw/mod2file/; -use App::Yath::Schema::UUID qw/uuid_inflate/; - -use Importer Importer => 'import'; - -our @EXPORT = qw/share_dir share_file qdb_driver dbd_driver config_from_settings find_job format_duration parse_duration is_invalid_subtest_name/; - -my %SCHEMA_TO_QDB_DRIVER = ( - mariadb => 'MySQL', - mysql => 'MySQL', - postgresql => 'PostgreSQL', -); - -my %SCHEMA_TO_DBD_DRIVER = ( - mariadb => 'DBD::MariaDB', - mysql => 'DBD::mysql', - postgresql => 'DBD::Pg', -); - -my %BAD_ST_NAME = ( - '__ANON__' => 1, - 'unnamed' => 1, - 'unnamed subtest' => 1, - 'unnamed summary' => 1, - '' => 1, -); - -sub is_invalid_subtest_name { - my ($name) = @_; - return $BAD_ST_NAME{$name} // 0; -} - -sub find_job { - my ($schema, $uuid, $try) = @_; - - $uuid = uuid_inflate($uuid) or croak "Invalid job identifier"; - - my $jobs = $schema->resultset('Job'); - - if (length $try) { - return $jobs->search({job_id => $uuid}, {order_by => {'-desc' => 'job_try'}, limit => 1})->first - if $try == -1; - - return $jobs->find({job_id => $uuid, job_try => $try}); - } - - return $jobs->find({job_key => $uuid}) - || $jobs->search({job_id => $uuid}, {order_by => {'-desc' => 'job_try'}, limit => 1})->first; -} - -sub base_name { - my ($in) = @_; - - my $out = lc($in); - $out =~ s/\.sql$//; - $out =~ s/\d+$//g; - - return $out; -} - -sub qdb_driver { - my $base = base_name(@_); - return $SCHEMA_TO_QDB_DRIVER{$base}; -} - -sub dbd_driver { - my $base = base_name(@_); - return $SCHEMA_TO_DBD_DRIVER{$base}; -} - -sub share_file { - my ($file) = @_; - - return File::ShareDir::dist_file('Test2-Harness-UI' => $file) - unless 'dev' eq ($ENV{T2_HARNESS_UI_ENV} || ''); - - my $path = "share/$file"; - croak "Could not find '$file'" unless -e $path; - - return $path; -} - -sub share_dir { - my ($dir) = @_; - - my $path; - - if ('dev' eq ($ENV{T2_HARNESS_UI_ENV} || '')) { - $path = "share/$dir"; - } - else { - my $root = File::ShareDir::dist_dir('Test2-Harness-UI'); - $path = "$root/$dir"; - } - - croak "Could not find '$dir'" unless -d $path; - - return $path; -} - -sub config_from_settings { - my ($settings) = @_; - - my $db = $settings->prefix('yathui-db') or die "No DB settings"; - - if (my $cmod = $db->config) { - my $file = mod2file($cmod); - require $file; - - return $cmod->yath_ui_config(%$$db); - } - - my $dsn = $db->dsn; - - unless ($dsn) { - $dsn = ""; - - my $driver = $db->driver; - my $name = $db->name; - - $dsn .= "dbi:$driver" if $driver; - $dsn .= ":dbname=$name" if $name; - - if (my $socket = $db->socket) { - my $ld = lc($driver); - if ($ld eq 'pg') { - $dsn .= ";host=$socket"; - } - else { - $dsn .= ";${ld}_socket=$socket"; - } - } - else { - my $host = $db->host; - my $port = $db->port; - - $dsn .= ";host=$host" if $host; - $dsn .= ";port=$port" if $port; - } - } - - require App::Yath::Server::Config; - return App::Yath::Server::Config->new( - dbi_dsn => $dsn, - dbi_user => $db->user // '', - dbi_pass => $db->pass // '', - ); -} - -sub format_duration { - my $seconds = shift; - - my $minutes = int($seconds / 60); - my $hours = int($minutes / 60); - my $days = int($hours / 24); - - $minutes %= 60; - $hours %= 24; - - $seconds -= $minutes * 60; - $seconds -= $hours * 60 * 60; - $seconds -= $days * 60 * 60 * 24; - - my @dur; - push @dur => sprintf("%02dd", $days) if $days; - push @dur => sprintf("%02dh", $hours) if @dur || $hours; - push @dur => sprintf("%02dm", $minutes) if @dur || $minutes; - push @dur => sprintf("%07.4fs", $seconds); - - return join ':' => @dur; -} - -sub parse_duration { - my $duration = shift; - - return 0 unless $duration; - - return $duration unless $duration =~ m/:?.*[dhms]$/i; - - my $out = 0; - - my (@parts) = split ':' => $duration; - for my $part (@parts) { - my ($num, $type) = ($part =~ m/^([0-9\.]+)([dhms])$/); - - unless ($num && $type) { - warn "invalid duration section '$part'"; - next; - } - - if ($type eq 'd') { - $out += ($num * 60 * 60 * 24); - } - elsif ($type eq 'h') { - $out += ($num * 60 * 60); - } - elsif ($type eq 'm') { - $out += ($num * 60); - } - else { - $out += $num; - } - } - - return $out; -} - - -1; - -__END__ - -=pod - -=encoding UTF-8 - -=head1 NAME - -App::Yath::Server::Util - General Utilities - -=head1 DESCRIPTION - -=head1 SYNOPSIS - -TODO - -=head1 SOURCE - -The source code repository for Test2-Harness-UI can be found at -F. - -=head1 MAINTAINERS - -=over 4 - -=item Chad Granum Eexodist@cpan.orgE - -=back - -=head1 AUTHORS - -=over 4 - -=item Chad Granum Eexodist@cpan.orgE - -=back - -=head1 COPYRIGHT - -Copyright Chad Granum Eexodist7@gmail.comE. - -This program is free software; you can redistribute it and/or -modify it under the same terms as Perl itself. - -See F - -=cut diff --git a/lib/App/Yath/Util.pm b/lib/App/Yath/Util.pm index af574bd0e..e93d4a877 100644 --- a/lib/App/Yath/Util.pm +++ b/lib/App/Yath/Util.pm @@ -4,18 +4,47 @@ use warnings; our $VERSION = '2.000000'; -use File::Spec; +use File::Spec(); +use File::ShareDir(); use Test2::Harness::Util qw/clean_path/; use Importer Importer => 'import'; use Config qw/%Config/; +use Carp qw/croak/; our @EXPORT_OK = qw{ is_generated_test_pl find_yath + share_dir share_file }; +sub share_file { + my ($file) = @_; + + my $path = "share/$file"; + return $path if -f $path; + + return File::ShareDir::dist_file('Test2-Harness' => $file); + + croak "Could not find '$file'"; +} + +sub share_dir { + my ($dir) = @_; + + my $path = "share/$dir"; + return $path if -d $path; + + my $root = File::ShareDir::dist_dir('Test2-Harness'); + + $path .= "/$dir"; + + croak "Could not find '$dir'" unless -d $path; + + return $path; +} + sub find_yath { return $App::Yath::Script::SCRIPT if defined $App::Yath::Script::SCRIPT; diff --git a/share/psgi/demo.psgi b/share/psgi/demo.psgi deleted file mode 100644 index b6399e12b..000000000 --- a/share/psgi/demo.psgi +++ /dev/null @@ -1,33 +0,0 @@ -use strict; -use warnings; - -BEGIN {$ENV{T2_HARNESS_UI_ENV} = 'dev'} - -use Plack::Builder; -use Plack::App::Directory; -use Plack::App::File; - -use Test2::Harness::UI::Util qw/share_dir share_file/; - -builder { - enable "DBIx::DisconnectAll"; - mount '/js' => Plack::App::Directory->new({root => share_dir('js')})->to_app; - mount '/css' => Plack::App::Directory->new({root => share_dir('css')})->to_app; - mount '/img' => Plack::App::Directory->new({root => share_dir('img')})->to_app; - mount '/favicon.ico' => Plack::App::File->new({file => share_file('img/favicon.ico')})->to_app; - - mount '/' => sub { - require Test2::Harness::UI; - require Test2::Harness::UI::Config; - - my $config = Test2::Harness::UI::Config->new( - dbi_dsn => $ENV{HARNESS_UI_DSN}, - dbi_user => '', - dbi_pass => '', - single_user => 1, - show_user => 1, - ); - - Test2::Harness::UI->new(config => $config)->to_app->(@_); - } -} diff --git a/share/psgi/test.psgi b/share/psgi/test.psgi deleted file mode 100644 index b6399e12b..000000000 --- a/share/psgi/test.psgi +++ /dev/null @@ -1,33 +0,0 @@ -use strict; -use warnings; - -BEGIN {$ENV{T2_HARNESS_UI_ENV} = 'dev'} - -use Plack::Builder; -use Plack::App::Directory; -use Plack::App::File; - -use Test2::Harness::UI::Util qw/share_dir share_file/; - -builder { - enable "DBIx::DisconnectAll"; - mount '/js' => Plack::App::Directory->new({root => share_dir('js')})->to_app; - mount '/css' => Plack::App::Directory->new({root => share_dir('css')})->to_app; - mount '/img' => Plack::App::Directory->new({root => share_dir('img')})->to_app; - mount '/favicon.ico' => Plack::App::File->new({file => share_file('img/favicon.ico')})->to_app; - - mount '/' => sub { - require Test2::Harness::UI; - require Test2::Harness::UI::Config; - - my $config = Test2::Harness::UI::Config->new( - dbi_dsn => $ENV{HARNESS_UI_DSN}, - dbi_user => '', - dbi_pass => '', - single_user => 1, - show_user => 1, - ); - - Test2::Harness::UI->new(config => $config)->to_app->(@_); - } -} diff --git a/share/templates/main.tx b/share/templates/main.tx index 5ca434a67..f79aec19f 100644 --- a/share/templates/main.tx +++ b/share/templates/main.tx @@ -36,8 +36,8 @@ - - + +