diff --git a/lib/Hitagi.pl b/lib/Hitagi.pl new file mode 100644 index 000000000..8304a76c6 --- /dev/null +++ b/lib/Hitagi.pl @@ -0,0 +1,162 @@ +#!/usr/bin/env perl + +# Transparent Process Manager. +# Self-contained process orchestrator for the server and helper processes. + +use v5.38; +use utf8; + +use local::lib; + +use threads; +use threads::shared; +use Config; +use Proc::Simple; +use Cwd 'abs_path'; +use File::Path qw(make_path); +use IO::Socket qw(SHUT_WR); +use IO::Socket::UNIX; +use Getopt::Long qw(GetOptionsFromString); + +use constant IS_UNIX => ( $Config{osname} ne 'MSWin32' ); + +my %pids :shared; # PIDs +my %run :shared; # Should process be restarted + +my $should_run :shared = 1; + +sub start_process( $process, $arg1, @argv ) { + $run{$process} = 1; + while ( $should_run ) { + while ( $should_run && $run{$process} ) { + my $proc = Proc::Simple->new(); + $proc->start( $Config{perlpath}, $arg1, @argv ); + $pids{$process} = $proc->pid; + $proc->wait(); + } + sleep 1; + } +} + +sub stop_processes { + kill INT => $pids{lanraragi}; + kill INT => $pids{shinobu}; + kill TERM => $pids{tsubasa}; +} + +sub restart_command ( $value, $client ) { + if ( $value eq "all" ) { + stop_processes(); + $client->send( 1 ); + } else { + my $pid = $pids{$value}; + if ( $pid ) { + $pids{$value} = 0; + kill INT => $pid; + } + + $run{$value} = 1; # Make sure process is enabled + + my $retries = 0; + while (!$pids{$value} && $retries < 5) { # Wait for start + $retries++; + sleep 1; + } + + $client->send( $pids{$value} ); + } +} + +sub pid_command ( $value, $client ) { + $client->send( $pids{$value} ); +} + +sub stop_command ( $value, $client ) { + $run{$value} = 0; + kill INT => $pids{$value}; + $pids{$value} = 0; + $client->send( 1 ); +} + +if ( $ENV{LRR_DATA_DIRECTORY} ) { + make_path( $ENV{LRR_DATA_DIRECTORY} ); +} + +if ( $ENV{LRR_THUMB_DIRECTORY} ) { + make_path( $ENV{LRR_THUMB_DIRECTORY} ); +} + +if ( $ENV{LRR_TEMP_DIRECTORY} ) { + make_path( $ENV{LRR_TEMP_DIRECTORY} ); +} else { + eval { make_path("./temp"); }; +} + +local $SIG{INT} = sub { + $should_run = 0; + stop_processes(); +}; + +local $SIG{TERM} = sub { + $should_run = 0; + stop_processes(); +}; + +my $socket = "/tmp/hitagi.sock"; + +if ( $ENV{XDG_RUNTIME_DIR} ) { + $socket = $ENV{XDG_RUNTIME_DIR} . "/hitagi.sock"; # Relocate socket to user run directory +} + +if ( !IS_UNIX ) { + $socket = $ENV{TEMP} . "/hitagi.sock"; # Relocate socket to user temp directory +} + +$ENV{HITAGI_SOCK} = $socket = abs_path($socket); + +say( "Starting processes" ); + +my @monitor_threads; + +push @monitor_threads, threads->create( \&start_process, "lanraragi", "./script/launcher.pl", @ARGV ); +push @monitor_threads, threads->create( \&start_process, "shinobu", "./lib/Shinobu.pm" ); +push @monitor_threads, threads->create( \&start_process, "tsubasa", "./lib/Tsubasa.pm" ); + +unlink $socket if -e $socket; + +my $server = IO::Socket::UNIX->new( + Type => SOCK_STREAM(), + Local => $socket, + Listen => 1, + Timeout => 1 +); + +while ( $should_run ) { + while ( my $client = $server->accept() ) { + my $message = ""; + $client->recv( $message, 512 ); + + GetOptionsFromString ( + $message, + "restart=s" => sub { restart_command($_[1], $client); }, + "stop=s" => sub { stop_command($_[1], $client); }, + "pid=s" => sub { pid_command($_[1], $client); }, + ); + + $client->shutdown( SHUT_WR ); + } +} + +$server->close(); + +say( "Waiting for processes..." ); + +foreach my $thread (@monitor_threads) { + $thread->join(); +} + +say( "Done! Exiting..." ); + +eval { unlink $socket; }; + +exit; diff --git a/lib/LANraragi.pm b/lib/LANraragi.pm index 4c1b6c296..d74bdeca1 100644 --- a/lib/LANraragi.pm +++ b/lib/LANraragi.pm @@ -21,6 +21,7 @@ use LANraragi::Utils::Routing; use LANraragi::Utils::Minion; use LANraragi::Utils::I18N; use LANraragi::Utils::I18NInitializer; +use LANraragi::Utils::Hitagi; use LANraragi::Model::Search; use LANraragi::Model::Config; @@ -157,9 +158,11 @@ sub startup { $self->LRR_LOGGER->info( "Downloader Detected: " . $name ); } - # Enable Minion capabilities in the app - if ( IS_UNIX ) { - shutdown_from_pid( get_temp . "/minion.pid" ); + if ( !LANraragi::Utils::Hitagi::available() ) { + # Enable Minion capabilities in the app + if ( IS_UNIX ) { + shutdown_from_pid( get_temp . "/minion.pid" ); + } } my $redisad = $self->LRR_CONF->get_redisad; @@ -186,26 +189,31 @@ sub startup { # Anything else can cause weird database lockups. $self->minion->enqueue('build_stat_hashes'); - # Start a Minion worker in a subprocess - start_minion($self); + if ( !LANraragi::Utils::Hitagi::available() ) { + # Start a Minion worker in a subprocess + start_minion($self); - # Start File Watcher - if ( IS_UNIX ) { - shutdown_from_pid( get_temp . "/shinobu.pid" ); + # Start File Watcher + if ( IS_UNIX ) { + shutdown_from_pid( get_temp . "/shinobu.pid" ); + } + start_shinobu($self); } - start_shinobu($self); # Check if this is a first-time installation. first_install_actions(); - # Hook to SIGTERM to cleanly kill minion+shinobu on server shutdown - # As this is executed during before_dispatch, this code won't work if you SIGTERM without loading a single page! - # (https://stackoverflow.com/questions/60814220/how-to-manage-myself-sigint-and-sigterm-signals) $self->hook( before_dispatch => sub { my $c = shift; - if ( IS_UNIX ) { - state $unused = add_sigint_handler(); + + if ( !LANraragi::Utils::Hitagi::available() ) { + # Hook to SIGTERM to cleanly kill minion+shinobu on server shutdown + # As this is executed during before_dispatch, this code won't work if you SIGTERM without loading a single page! + # (https://stackoverflow.com/questions/60814220/how-to-manage-myself-sigint-and-sigterm-signals) + if ( IS_UNIX ) { + state $unused = add_sigint_handler(); + } } my $prefix = $self->LRR_BASEURL; diff --git a/lib/LANraragi/Controller/Api/Shinobu.pm b/lib/LANraragi/Controller/Api/Shinobu.pm index c5abd3db3..29c6142b1 100644 --- a/lib/LANraragi/Controller/Api/Shinobu.pm +++ b/lib/LANraragi/Controller/Api/Shinobu.pm @@ -3,8 +3,9 @@ use Mojo::Base 'Mojolicious::Controller'; use Storable; use Config; -use LANraragi::Utils::Generic qw(start_shinobu render_api_response); +use LANraragi::Utils::Generic qw(start_shinobu render_api_response); use LANraragi::Utils::TempFolder qw(get_temp); +use LANraragi::Utils::Hitagi; use constant IS_UNIX => ( $Config{osname} ne 'MSWin32' ); @@ -18,7 +19,18 @@ BEGIN { sub shinobu_status { my $self = shift->openapi->valid_input or return; - if ( IS_UNIX ) { + if ( LANraragi::Utils::Hitagi::available() ) { + my $pid = LANraragi::Utils::Hitagi::pid( "shinobu" ); + + $self->render( + openapi => { + operation => "shinobu_status", + success => 1, + is_alive => $pid != 0 ? 1 : 0, + pid => $pid + } + ); + } elsif ( IS_UNIX ) { my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; $self->render( @@ -60,46 +72,62 @@ sub reset_filemap { $redis->del("LRR_FILEMAP"); $redis->quit(); - if ( IS_UNIX ) { - my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; + if ( LANraragi::Utils::Hitagi::available() ) { + my $pid = LANraragi::Utils::Hitagi::restart( "shinobu" ); - #commit sudoku - $shinobu->kill(); - } else { - open( my $fh, "<", get_temp() . "/shinobu.pid-s6" ); - chomp(my $pid = <$fh>); - close($fh); - kill HUP => $pid; - } - - # Create a new Process, automatically stored in TEMP_FOLDER/shinobu.pid - my $proc = start_shinobu($self); - - if ( IS_UNIX ) { $self->render( openapi => { operation => "shinobu_rescan", - success => $proc->poll(), - new_pid => $proc->pid + success => 1, + new_pid => $pid } ); } else { - eval { + if ( IS_UNIX ) { + my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; + + #commit sudoku + $shinobu->kill(); + } else { + open( my $fh, "<", get_temp() . "/shinobu.pid-s6" ); + chomp(my $pid = <$fh>); + close($fh); + kill HUP => $pid; + } + + # Create a new Process, automatically stored in TEMP_FOLDER/shinobu.pid + my $proc = start_shinobu($self); + + if ( IS_UNIX ) { $self->render( openapi => { operation => "shinobu_rescan", - success => ($proc->GetProcessID() != 0) ? 1 : 0, - new_pid => int($proc->GetProcessID()) + success => $proc->poll(), + new_pid => $proc->pid } ); - }; + } else { + eval { + $self->render( + openapi => { + operation => "shinobu_rescan", + success => ($proc->GetProcessID() != 0) ? 1 : 0, + new_pid => int($proc->GetProcessID()) + } + ); + }; + } } } sub stop_shinobu { my $self = shift->openapi->valid_input or return; - if ( IS_UNIX ) { + if ( LANraragi::Utils::Hitagi::available() ) { + + LANraragi::Utils::Hitagi::stop( "shinobu" ); + + } elsif ( IS_UNIX ) { my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; #commit sudoku @@ -117,39 +145,51 @@ sub stop_shinobu { sub restart_shinobu { my $self = shift->openapi->valid_input or return; - if ( IS_UNIX ) { - my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; - - #commit sudoku - $shinobu->kill(); - } else { - open( my $fh, "<", get_temp() . "/shinobu.pid-s6" ); - chomp(my $pid = <$fh>); - close($fh); - kill HUP => $pid; - } - - # Create a new Process, automatically stored in TEMP_FOLDER/shinobu.pid - my $proc = start_shinobu($self); + if ( LANraragi::Utils::Hitagi::available() ) { + my $pid = LANraragi::Utils::Hitagi::restart( "shinobu" ); - if ( IS_UNIX ) { $self->render( openapi => { operation => "shinobu_restart", - success => $proc->poll(), - new_pid => $proc->pid + success => 1, + new_pid => $pid } ); } else { - eval { + if ( IS_UNIX ) { + my $shinobu = ${ retrieve( get_temp . "/shinobu.pid" ) }; + + #commit sudoku + $shinobu->kill(); + } else { + open( my $fh, "<", get_temp() . "/shinobu.pid-s6" ); + chomp(my $pid = <$fh>); + close($fh); + kill HUP => $pid; + } + + # Create a new Process, automatically stored in TEMP_FOLDER/shinobu.pid + my $proc = start_shinobu($self); + + if ( IS_UNIX ) { $self->render( openapi => { operation => "shinobu_restart", - success => ($proc->GetProcessID() != 0) ? 1 : 0, - new_pid => int($proc->GetProcessID()) + success => $proc->poll(), + new_pid => $proc->pid } ); - }; + } else { + eval { + $self->render( + openapi => { + operation => "shinobu_restart", + success => ($proc->GetProcessID() != 0) ? 1 : 0, + new_pid => int($proc->GetProcessID()) + } + ); + }; + } } } diff --git a/lib/LANraragi/Utils/Generic.pm b/lib/LANraragi/Utils/Generic.pm index aa15cf1b0..8c764b619 100644 --- a/lib/LANraragi/Utils/Generic.pm +++ b/lib/LANraragi/Utils/Generic.pm @@ -148,7 +148,7 @@ sub start_minion { return $proc; } else { my $proc; - Win32::Process::Create( $proc, undef, "perl \"" . abs_path(".") . "/lib/Worker.pm\"", 0, NORMAL_PRIORITY_CLASS, "." ); + Win32::Process::Create( $proc, undef, "perl \"" . abs_path(".") . "/lib/Tsubasa.pm\"", 0, NORMAL_PRIORITY_CLASS, "." ); $logger->info( "Starting new Minion worker with PID " . $proc->GetProcessID() . "." ); return $proc; } diff --git a/lib/LANraragi/Utils/Hitagi.pm b/lib/LANraragi/Utils/Hitagi.pm new file mode 100644 index 000000000..8d75257f1 --- /dev/null +++ b/lib/LANraragi/Utils/Hitagi.pm @@ -0,0 +1,46 @@ +package LANraragi::Utils::Hitagi; + +use v5.38; +use utf8; + +use IO::Socket qw(SHUT_WR); +use IO::Socket::UNIX; + +sub hitagi_send ( $command ) { + my $client = IO::Socket::UNIX->new( + Type => SOCK_STREAM(), + Peer => $ENV{HITAGI_SOCK}, + ); + + $client->send( $command ); + $client->shutdown( SHUT_WR ); + + my $reply = ""; + $client->recv( $reply, 512 ); + + $client->close(); + + return $reply; +} + +sub restart_all() { + hitagi_send( "-r all" ); +} + +sub restart( $process ) { + return int(hitagi_send( "-r $process" )); +} + +sub stop( $process ) { + return hitagi_send( "-s $process" ); +} + +sub pid( $process ) { + return int(hitagi_send( "-p $process" )); +} + +sub available { + return defined($ENV{HITAGI_SOCK}); +} + +1; diff --git a/lib/Shinobu.pm b/lib/Shinobu.pm index fb258f412..88125eb8a 100644 --- a/lib/Shinobu.pm +++ b/lib/Shinobu.pm @@ -7,11 +7,8 @@ package Shinobu; # Tracking all files in the content folder and making sure they're sync'ed with the database # -use strict; -use warnings; +use v5.38; use utf8; -use feature qw(say signatures); -no warnings 'experimental::signatures'; use local::lib; @@ -99,9 +96,10 @@ sub initialize_from_new_process { my $running = 1; my $metrics_counter = 0; - while ($running) { - local $SIG{INT} = sub { $running = 0 }; + local $SIG{INT} = sub { $running = 0 }; + local $SIG{TERM} = sub { $running = 0 }; + while ($running) { # Check events on files for my $event ( $contentwatcher->new_events ) { $inotifysub->($event); diff --git a/lib/Tsubasa.pm b/lib/Tsubasa.pm new file mode 100644 index 000000000..dc50f0ac0 --- /dev/null +++ b/lib/Tsubasa.pm @@ -0,0 +1,101 @@ +package Worker; + +use v5.38; +use utf8; + +use local::lib; + +use FindBin; +use Sys::CpuAffinity; +use Minion; +use Config; + +#As this is a new process, reloading the LRR libs into INC is needed. +BEGIN { unshift @INC, "$FindBin::Bin/../lib"; } + +use Mojolicious; # Needed by Model::Config to read the Redis address/port. +use Mojo::Util qw(steady_time); + +use LANraragi::Utils::Logging qw(get_logger); + +use LANraragi::Utils::Minion; +use LANraragi::Model::Config; + +use constant IS_UNIX => ( $Config{osname} ne 'MSWin32' ); + +# Logger and Database objects +my $logger = get_logger( "Minion Worker", "minion" ); + +# Minion worker +sub initialize_from_new_process { + + if ( !IS_UNIX ) { + # Enable autoflush + $| = 1; + } + + $logger->info("Minion Worker started."); + + my $userdir = LANraragi::Model::Config->get_userdir; + + my $miniondb = LANraragi::Model::Config->get_redisad . "/" . LANraragi::Model::Config->get_miniondb; + my $redispassword = LANraragi::Model::Config->get_redispassword; + + # If the password is non-empty, add the required delimiters + if ($redispassword) { $redispassword = "x:" . $redispassword . "@"; } + + say "Minion Worker will use the Redis database at $miniondb"; + + my $minion = Minion->new(Redis => "redis://$redispassword$miniondb"); + + LANraragi::Utils::Minion::add_tasks( $minion ); + $logger->debug("Registered tasks with Minion."); + + my $worker = $minion->repair->worker; + + if ( IS_UNIX ) { + my $numcpus = Sys::CpuAffinity::getNumCpus(); + $logger->info("Starting new Minion worker in subprocess with $numcpus parallel jobs."); + + $worker->status->{jobs} = $numcpus; + $worker->on( dequeue => sub { pop->once( spawn => \&_spawn ) } ); + + $worker->run; + } else { + $worker->register; + my $running = 1; + + local $SIG{INT} = sub { $running = 0 }; + local $SIG{TERM} = sub { $running = 0 }; + + my $last_heartbeat = 0; + my $last_repair = 0; + while ($running) { + while(my $job = $worker->dequeue(3)) { + if (defined(my $err = eval { $job->execute })) { + $job->fail($err); + } else { + $job->finish; + } + } + $worker->register and $last_heartbeat = steady_time if ($last_heartbeat + 300) < steady_time; + + if (($last_repair + 21600) < steady_time) { + $minion->repair; + $last_repair = steady_time; + } + } + $worker->unregister; + } +} + +sub _spawn { + my ( $job, $pid ) = @_; + my ( $id, $task ) = ( $job->id, $job->task ); + my $logger = get_logger( "Minion Worker", "minion" ); + $job->app->log->debug(qq{Process $pid is performing job "$id" with task "$task"}); +} + +__PACKAGE__->initialize_from_new_process unless caller; + +1; diff --git a/lib/Worker.pm b/lib/Worker.pm deleted file mode 100644 index 617fec40f..000000000 --- a/lib/Worker.pm +++ /dev/null @@ -1,76 +0,0 @@ -package Worker; - -use strict; -use warnings; -use utf8; -use feature qw(say signatures); -no warnings 'experimental::signatures'; - -use FindBin; -use Minion; - -#As this is a new process, reloading the LRR libs into INC is needed. -BEGIN { unshift @INC, "$FindBin::Bin/../lib"; } - -use Mojolicious; # Needed by Model::Config to read the Redis address/port. -use Mojo::Util qw(steady_time); - -use LANraragi::Utils::Logging qw(get_logger); - -use LANraragi::Utils::Minion; -use LANraragi::Model::Config; - -# Logger and Database objects -my $logger = get_logger( "Minion Worker", "minion" ); - -# Windows-only worker. Single threaded and non-forking. -sub initialize_from_new_process { - - # Enable autoflush - $| = 1; - - $logger->info("Minion Worker started."); - - my $userdir = LANraragi::Model::Config->get_userdir; - - my $miniondb = LANraragi::Model::Config->get_redisad . "/" . LANraragi::Model::Config->get_miniondb; - my $redispassword = LANraragi::Model::Config->get_redispassword; - - # If the password is non-empty, add the required delimiters - if ($redispassword) { $redispassword = "x:" . $redispassword . "@"; } - - say "Minion Worker will use the Redis database at $miniondb"; - - my $minion = Minion->new(Redis => "redis://$redispassword$miniondb"); - - LANraragi::Utils::Minion::add_tasks( $minion ); - $logger->debug("Registered tasks with Minion."); - - my $worker = $minion->repair->worker->register; - my $running = 1; - - my $last_heartbeat = 0; - my $last_repair = 0; - while ($running) { - local $SIG{INT} = sub { $running = 0 }; - - while(my $job = $worker->dequeue(3)) { - if (defined(my $err = $job->execute)) { - $job->fail($err); - } else { - $job->finish; - } - } - $worker->register and $last_heartbeat = steady_time if ($last_heartbeat + 300) < steady_time; - - if (($last_repair + 21600) < steady_time) { - $minion->repair; - $last_repair = steady_time; - } - } - $worker->unregister; -} - -__PACKAGE__->initialize_from_new_process unless caller; - -1; diff --git a/tools/build/docker/s6/s6-rc.d/lanraragi/finish b/tools/build/docker/s6/s6-rc.d/lanraragi/finish deleted file mode 100644 index 7c9fc790a..000000000 --- a/tools/build/docker/s6/s6-rc.d/lanraragi/finish +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -pkill --signal SIGINT -F "/home/koyomi/lanraragi/temp/shinobu.pid-s6" -pkill --signal SIGTERM -F "/home/koyomi/lanraragi/temp/minion.pid-s6" - -pidwait -F "/home/koyomi/lanraragi/temp/shinobu.pid-s6" -pidwait -F "/home/koyomi/lanraragi/temp/minion.pid-s6" diff --git a/tools/build/docker/s6/s6-rc.d/lanraragi/run b/tools/build/docker/s6/s6-rc.d/lanraragi/run index b621c100b..1d899b551 100644 --- a/tools/build/docker/s6/s6-rc.d/lanraragi/run +++ b/tools/build/docker/s6/s6-rc.d/lanraragi/run @@ -1,4 +1,4 @@ #!/bin/sh cd /home/koyomi/lanraragi/ export HOME=/home/koyomi -exec s6-setuidgid koyomi perl /home/koyomi/lanraragi/script/launcher.pl -f /home/koyomi/lanraragi/script/lanraragi +exec s6-setuidgid koyomi perl /home/koyomi/lanraragi/lib/Hitagi.pl -f /home/koyomi/lanraragi/script/lanraragi