Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

Re: Can I have a Perl script, initiated from a browser, fork itself, and not wait for the child to end?

by haukex (Bishop)
on Apr 07, 2022 at 13:59 UTC ( #11142798=note: print w/replies, xml ) Need Help??


in reply to Can I have a Perl script, initiated from a browser, fork itself, and not wait for the child to end?

Although the following is probably overkill - plus it won't run as-is as a CGI script - I've wanted to test out code like this for a while now so I took this oppertunity to write this example up using Mojolicious. And in the process, hopefully show some of the advantages of modern web technologies and frameworks over CGI.pm :-) This can be run as a standalone server in development mode via morbo script.pl, and as a simple server via perl test.pl daemon.

#!/usr/bin/env perl use 5.028; use Mojolicious::Lite -signatures; use Mojo::JSON qw/encode_json/; use Mojo::Util qw/sha1_sum/; # NOTICE: This script is designed to work in a single-threaded, # single-process server only! (morbo or Mojolicious::Command::daemon) get '/' => sub ($c) { $c->render(template => 'index') } => 'index'; my %runningprocs; post '/submit' => sub ($c) { # form variables my $foo = $c->param('foo'); my $bar = $c->param('bar'); # set up the event dispatcher my $ee = Mojo::EventEmitter->new; # hash collisions theoretically possible but very unlikely (could +check `exists $runningprocs{$id}`) my $id = sha1_sum( time."\0".rand."\0".(0+$ee) ); $runningprocs{$id} = $ee; $c->render(json => { eventurl=>$c->url_for('status', id=>$id) }); # set up and run the subprocess my $subproc = Mojo::IOLoop->subprocess; $subproc->on(spawn => sub ($sp) { $ee->emit(status => { progress=>"Subprocess spawned in PID + ".$sp->pid }) }); $subproc->on(progress => sub ($sp, @data) { $ee->emit(status => { progress=>\@data }) }); # give client a second to connect to event source Mojo::IOLoop->timer(1 => sub { $subproc->run( sub ($sp) { return long_running_subprocess($sp, $foo, $bar +) }, sub ($sp, $err, @results) { if ($err) { $ee->emit(status => { error=>"$err", done= +>"Error: $err" }) } else { $ee->emit(status => { done=>\@results }) } # don't clobber the event listener immediately (in cas +e client took longer to re/connect) Mojo::IOLoop->timer(10 => sub { delete $runningprocs{$ +id} }); }); }); } => 'formsubmit'; get '/status/:id' => sub ($c) { my $id = $c->stash('id'); my $ee = $runningprocs{$id} or return $c->reply->not_found; $c->inactivity_timeout(300); $c->res->headers->content_type('text/event-stream'); $c->write; my $timerid = Mojo::IOLoop->recurring(10 => sub { $c->write(":\n\n") }); # comment as keepalive my $cb = $ee->on(status => sub ($ev, $data) { my $json = encode_json($data) =~ s/\n//gr; $c->write("event: status\ndata: $json\n\n"); }); $c->on(finish => sub ($c) { $ee->unsubscribe(status => $cb); Mojo::IOLoop->remove($timerid); }); } => 'status'; sub long_running_subprocess { my ($subproc, $foo, $bar) = @_; # this code is now running in the subprocess! $subproc->progress("Beginning work on Foo='$foo'"); sleep 5; # ...foo... $subproc->progress("Finished work on Foo"); if ( length $bar ) { $subproc->progress("Beginning work on Bar='$bar'"); sleep 5; # ...bar... $subproc->progress("Finished work on Bar"); } return "All done!"; } app->start; __DATA__ @@ index.html.ep % layout 'main', title => 'Hello, World!'; <div> %= form_for formsubmit => ( method=>'post', id=>'myform' ) => begin <div> %= label_for foo => 'Foo' %= text_field foo => ( placeholder=>"Foo", required=>'required' ) </div><div> %= label_for bar => 'Bar' %= text_field bar => ( placeholder=>"Bar" ) </div><div> %= submit_button 'Process' </div> %= end </div> <pre id="myoutput" style="padding:3px 5px;border:1px solid black;"> Output will display here. </pre> <script> "use strict"; function addmsg(txt) { $(document.createTextNode(txt)).appendTo($('#myoutput')); } function getevents(url) { addmsg("Listening on "+JSON.stringify(url)+"\n"); var events = new EventSource(url); events.onerror = function(err) { // the event apparently doesn't contain any details var errmsg = "Error connecting to EventSource"; addmsg(errmsg); alert(errmsg); $("#myform :input").prop("disabled", false); }; events.addEventListener('status', function (event) { var data = JSON.parse(event.data); if ( 'progress' in data ) { addmsg("Progress: "+JSON.stringify(data.progress)+"\n"); } if ( 'error' in data ) { addmsg("Error: "+JSON.stringify(data.error)+"\n"); alert(data.error); } if ( 'done' in data ) { addmsg("Done: "+JSON.stringify(data.done)+"\n"); events.close(); $("#myform :input").prop("disabled", false); } }, false); } $(function () { $('#myform').on('submit', function (e) { e.preventDefault(); $("#myoutput").text("Submitting form\n"); var thedata = $('#myform').serialize(); // before disabling! $("#myform :input").prop("disabled", true); $.ajax({ type: 'post', url: '<%= url_for 'formsubmit' %>', data: thedata }) .done( function( data ) { getevents(data.eventurl); }) .fail( function( jqXHR, textStatus, errorThrown ) { var errmsg = "Form submission error: "+textStatus +" / "+jqXHR.status+" "+errorThrown; addmsg(errmsg); alert(errmsg); $("#myform :input").prop("disabled", false); }) }); }); </script> @@ layouts/main.html.ep <!DOCTYPE html> <html> <head> <title><%= title %></title> <meta name="viewport" content="width=device-width, initial-scale=1 +.0"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/n +ormalize.min.css" integrity="sha512-NhSC1YmyruXifcj/KFRWoC561YpHpc5Jtzgvbuzx5Voz +KpWvQ+4nXhPdFgmx8xqexRcpAglTj9sIBWINXa8x5w==" crossorigin="anonymous" referrerpolicy="no-referrer" /> <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4= +" crossorigin="anonymous"></script> </head> <body> %= content </body> </html>

If this needed to run in a threaded/multiprocess HTTP server, it would even be possible to replace the communication via EventEmitter objects with a system like Redis - it's pretty simple to spin up a server via Docker and connect to it using e.g. Mojo::Redis::PubSub.

  • Comment on Re: Can I have a Perl script, initiated from a browser, fork itself, and not wait for the child to end?
  • Select or Download Code

Replies are listed 'Best First'.
Re^2: Can I have a Perl script, initiated from a browser, fork itself, and not wait for the child to end?
by etj (Chaplain) on Apr 10, 2022 at 23:02 UTC
    Is this not reinventing part of Minion?
      Is this not reinventing part of Minion?

      Depends on which part you mean. Minion is a good suggestion, but it also depends on the OP's requirements - if it's just a single task, then I think my code is good enough, but if OP needs to run more subprocesses then Minion's features would certainly be an advantage. However, AFAICT Minion doesn't support EventSource, which was a major point of my post.

      Edit: Minor clarification.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: note [id://11142798]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others musing on the Monastery: (3)
As of 2022-07-06 19:01 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?