new API to change password
[pve-access-control.git] / PVE / RPCEnvironment.pm
1 package PVE::RPCEnvironment;
2
3 use strict;
4 use warnings;
5 use POSIX qw(:sys_wait_h EINTR);
6 use IO::Handle;
7 use IO::File;
8 use IO::Select;
9 use Fcntl qw(:flock);
10 use PVE::Exception qw(raise raise_perm_exc);
11 use PVE::SafeSyslog;
12 use PVE::Tools;
13 use PVE::INotify;
14 use PVE::Cluster;
15 use PVE::ProcFSTools;
16 use PVE::AccessControl;
17 use CGI;
18
19 # we use this singleton class to pass RPC related environment values
20
21 my $pve_env;
22
23 # save $SIG{CHLD} handler implementation.
24 # simply set $SIG{CHLD} = $worker_reaper;
25 # and register forked processes with &$register_worker(pid)
26 # Note: using $SIG{CHLD} = 'IGNORE' or $SIG{CHLD} = sub { wait (); } or ...
27 # has serious side effects, because perls built in system() and open()
28 # functions can't get the correct exit status of a child. So we cant use 
29 # that (also see perlipc)
30
31 my $WORKER_PIDS;
32
33 my $log_task_result = sub {
34     my ($upid, $user, $status) = @_;
35
36     my $msg = 'successful';
37     my $pri = 'info';
38     if ($status != 0) {
39         my $ec = $status >> 8;
40         my $ic = $status & 255;
41         $msg = $ec ? "failed ($ec)" : "interrupted ($ic)";
42         $pri = 'err';
43     }
44     my $tlist = active_workers($upid);
45     PVE::Cluster::broadcast_tasklist($tlist);
46     my $task;
47     foreach my $t (@$tlist) {
48         if ($t->{upid} eq $upid) {
49             $task = $t;
50             last;
51         }
52     }
53     if ($task && $task->{status}) {
54         $msg = $task->{status};
55     }
56     PVE::Cluster::log_msg($pri, $user, "end task $upid $msg");
57 };
58
59 my $worker_reaper = sub {
60     local $!; local $?;
61     foreach my $pid (keys %$WORKER_PIDS) {
62         my $waitpid = waitpid ($pid, WNOHANG);
63         if (defined($waitpid) && ($waitpid == $pid)) {
64             my $info = $WORKER_PIDS->{$pid};
65             if ($info && $info->{upid} && $info->{user}) {
66                 &$log_task_result($info->{upid}, $info->{user}, $?);
67             }
68             delete ($WORKER_PIDS->{$pid});
69         }
70     }
71 };
72
73 my $register_worker = sub {
74     my ($pid, $user, $upid) = @_;
75
76     return if !$pid;
77
78     # do not register if already finished
79     my $waitpid = waitpid ($pid, WNOHANG);
80     if (defined($waitpid) && ($waitpid == $pid)) {
81         delete ($WORKER_PIDS->{$pid});
82         return;
83     }
84
85     $WORKER_PIDS->{$pid} = {
86         user => $user,
87         upid => $upid,
88     };
89 };
90
91 # ACL cache
92
93 my $compile_acl = sub {
94     my ($self, $user) = @_;
95
96     my $res = {};
97     my $cfg = $self->{user_cfg};
98
99     return undef if !$cfg->{roles};
100
101     if ($user eq 'root@pam') { # root can do anything
102         return {'/' => $cfg->{roles}->{'Administrator'}};
103     } 
104
105     foreach my $path (sort keys %{$cfg->{acl}}) {
106         my @ra = PVE::AccessControl::roles($cfg, $user, $path);
107
108         my $privs = {};
109         foreach my $role (@ra) {
110             if (my $privset = $cfg->{roles}->{$role}) {
111                 foreach my $p (keys %$privset) {
112                     $privs->{$p} = 1;
113                 }
114             }
115         }
116
117         $res->{$path} = $privs;
118     }
119
120     return $res;
121 };
122
123 sub permissions {
124     my ($self, $user, $path) = @_;
125
126     $user = PVE::AccessControl::verify_username($user, 1);
127     return {} if !$user;
128
129     my $cache = $self->{aclcache};
130
131     my $acl = $cache->{$user};
132
133     if (!$acl) {
134         if (!($acl = &$compile_acl($self, $user))) {
135             return {};
136         }
137         $cache->{$user} = $acl;
138     }
139
140     my $perm;
141
142     if (!($perm = $acl->{$path})) {
143         $perm = {};
144         foreach my $p (sort keys %$acl) {
145             my $final = ($path eq $p);
146             
147             next if !(($p eq '/') || $final || ($path =~ m|^$p/|));
148
149             $perm = $acl->{$p};
150         }
151         $acl->{$path} = $perm;
152     }
153
154     return $perm;
155 }
156
157 sub check {
158     my ($self, $user, $path, $privs, $noerr) = @_;
159
160     my $perm = $self->permissions($user, $path);
161
162     foreach my $priv (@$privs) {
163         PVE::AccessControl::verify_privname($priv);
164         if (!$perm->{$priv}) {
165             return undef if $noerr;
166             raise_perm_exc("$path, $priv"); 
167         }
168     };
169
170     return 1;
171 };
172
173 sub check_any {
174     my ($self, $user, $path, $privs, $noerr) = @_;
175
176     my $perm = $self->permissions($user, $path);
177
178     my $found = 0;
179     foreach my $priv (@$privs) {
180         PVE::AccessControl::verify_privname($priv);
181         if ($perm->{$priv}) {
182             $found = 1;
183             last;
184         }
185     };
186
187     return 1 if $found;
188
189     return undef if $noerr;
190
191     raise_perm_exc("$path, " . join("|", @$privs)); 
192 };
193
194 sub check_user_enabled {
195     my ($self, $user, $noerr) = @_;
196     
197     my $cfg = $self->{user_cfg};
198     return PVE::AccessControl::check_user_enabled($cfg, $user, $noerr);
199 }
200
201 sub check_user_exist {
202     my ($self, $user, $noerr) = @_;
203     
204     my $cfg = $self->{user_cfg};
205     return PVE::AccessControl::check_user_exist($cfg, $user, $noerr);
206 }
207
208 sub is_group_member {
209     my ($self, $group, $user) = @_;
210
211     my $cfg = $self->{user_cfg};
212
213     return 0 if !$cfg->{groups}->{$group};
214
215     return defined($cfg->{groups}->{$group}->{users}->{$user});
216 }
217
218 sub filter_groups {
219     my ($self, $user, $getPath, $privs, $any) = @_;
220
221     my $cfg = $self->{user_cfg};
222
223     my $groups = {};
224     foreach my $group (keys %{$cfg->{groups}}) {
225         if ($any) {
226             if ($self->check_any($user, &$getPath($group), $privs, 1)) {
227                 $groups->{$group} = $cfg->{groups}->{$group};
228             }
229         } else {
230             if ($self->check($user, &$getPath($group), $privs, 1)) {
231                 $groups->{$group} = $cfg->{groups}->{$group};
232             }
233         }
234     }
235
236     return $groups;
237 }
238
239 sub group_member_join {
240     my ($self, $grouplist) = @_;
241
242     my $users = {};
243
244     my $cfg = $self->{user_cfg};
245     foreach my $group (@$grouplist) {
246         my $data = $cfg->{groups}->{$group};
247         next if !$data;
248         foreach my $user (keys %{$data->{users}}) {
249             $users->{$user} = 1;
250         }
251     }
252
253     return $users;
254 }
255
256 # initialize environment - must be called once at program startup
257 sub init {
258     my ($class, $type, %params) = @_;
259
260     $class = ref($class) || $class;
261
262     die "already initialized" if $pve_env;
263
264     die "unknown environment type" if !$type || $type !~ m/^(cli|pub|priv|ha)$/;
265
266     $SIG{CHLD} = $worker_reaper;
267
268     # environment types
269     # cli  ... command started fron command line
270     # pub  ... access from public server (apache)
271     # priv ... access from private server (pvedaemon)
272     # ha   ... access from HA resource manager agent (rgmanager)
273     
274     my $self = {
275         user_cfg => {},
276         aclcache => {},
277         aclversion => undef,
278         type => $type,
279     };
280
281     bless $self, $class;
282
283     foreach my $p (keys %params) {
284         if ($p eq 'atfork') {
285             $self->{$p} = $params{$p};
286         } else {
287             die "unknown option '$p'";
288         }
289     }
290
291     $pve_env = $self;
292
293     my ($sysname, $nodename) = POSIX::uname();
294
295     $nodename =~ s/\..*$//; # strip domain part, if any
296
297     $self->{nodename} = $nodename;
298
299     return $self;
300 }; 
301
302 # get the singleton 
303 sub get {
304
305     die "not initialized" if !$pve_env;
306
307     return $pve_env;
308 }
309
310 sub parse_params {
311     my ($self, $enable_upload) = @_;
312
313     if ($self->{request_rec}) {
314         my $cgi;
315         if ($enable_upload) {
316             $cgi = CGI->new($self->{request_rec});
317         } else {
318             # disable upload using empty upload_hook
319             $cgi = CGI->new($self->{request_rec}, sub {}, undef, 0);
320         }
321         $self->{cgi} = $cgi;
322         my $params = $cgi->Vars();
323         return PVE::Tools::decode_utf8_parameters($params);
324     } elsif ($self->{params}) {
325         return $self->{params};
326     } else {
327         die "no parameters registered";
328     }
329 }
330
331 sub get_upload_info {
332     my ($self, $param) = @_;
333
334     my $cgi = $self->{cgi};
335     die "CGI not initialized" if !$cgi;
336
337     my $pd = $cgi->param($param);
338     die "unable to get cgi parameter info\n" if !$pd;
339     my $info = $cgi->uploadInfo($pd);
340     die "unable to get cgi upload info\n" if !$info;
341
342     my $res = { %$info };
343
344     my $tmpfilename = $cgi->tmpFileName($pd);
345     die "unable to get cgi upload file name\n" if !$tmpfilename;
346     $res->{tmpfilename} = $tmpfilename;
347
348     #my $hndl = $cgi->upload($param);
349     #die "unable to get cgi upload handle\n" if !$hndl;
350     #$res->{handle} = $hndl->handle;
351
352     return $res;
353 }
354
355 # init_request - must be called before each RPC request
356 sub init_request {
357     my ($self, %params) = @_;
358
359     PVE::Cluster::cfs_update();
360
361     $self->{result_attributes} = {};
362
363     my $userconfig; # we use this for regression tests
364     foreach my $p (keys %params) {
365         if ($p eq 'userconfig') {
366             $userconfig = $params{$p};
367         } elsif ($p eq 'request_rec') {
368             # pass Apache2::RequestRec
369             $self->{request_rec} = $params{$p};
370         } elsif ($p eq 'params') {
371             $self->{params} = $params{$p};
372         } else {
373             die "unknown parameter '$p'";
374         }
375     }
376
377     eval {
378         $self->{aclcache} = {};
379         if ($userconfig) {
380             my $ucdata = PVE::Tools::file_get_contents($userconfig);
381             my $cfg = PVE::AccessControl::parse_user_config($userconfig, $ucdata);
382             $self->{user_cfg} = $cfg;
383         } else {
384             my $ucvers = PVE::Cluster::cfs_file_version('user.cfg'); 
385             if (!$self->{aclcache} || !defined($self->{aclversion}) || 
386                 !defined($ucvers) ||  ($ucvers ne $self->{aclversion})) {
387                 $self->{aclversion} = $ucvers;
388                 my $cfg = PVE::Cluster::cfs_read_file('user.cfg');
389                 $self->{user_cfg} = $cfg;
390             }
391         }
392     };
393     if (my $err = $@) {
394         $self->{user_cfg} = {};
395         die "Unable to load access control list: $err";
396     }
397 }
398
399 sub set_client_ip {
400     my ($self, $ip) = @_;
401
402     $self->{client_ip} = $ip;
403 }
404
405 sub get_client_ip {
406     my ($self) = @_;
407
408     return $self->{client_ip};
409 }
410
411 sub set_result_attrib {
412     my ($self, $key, $value) = @_;
413
414     $self->{result_attributes}->{$key} = $value;
415 }
416
417 sub get_result_attrib {
418     my ($self, $key) = @_;
419
420     return $self->{result_attributes}->{$key};
421 }
422
423 sub set_language {
424     my ($self, $lang) = @_;
425
426     # fixme: initialize I18N
427
428     $self->{language} = $lang;
429 }
430
431 sub get_language {
432     my ($self) = @_;
433
434     return $self->{language};
435 }
436
437 sub set_user {
438     my ($self, $user) = @_;
439
440     # fixme: get ACLs
441
442     $self->{user} = $user;
443 }
444
445 sub get_user {
446     my ($self) = @_;
447
448     die "user name not set\n" if !$self->{user};
449
450     return $self->{user};
451 }
452
453 # read/update list of active workers 
454 # we move all finished tasks to the archive index,
455 # but keep aktive and most recent task in the active file.
456 # $nocheck ... consider $new_upid still running (avoid that
457 # we try to read the reult to early.
458 sub active_workers  {
459     my ($new_upid, $nocheck) = @_;
460
461     my $lkfn = "/var/log/pve/tasks/.active.lock";
462
463     my $timeout = 10;
464
465     my $code = sub {
466
467         my $tasklist = PVE::INotify::read_file('active');
468
469         my @ta;
470         my $tlist = [];
471         my $thash = {}; # only list task once
472
473         my $check_task = sub {
474             my ($task, $running) = @_;
475
476             if ($running || PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart})) {
477                 push @$tlist, $task;
478             } else {
479                 delete $task->{pid};
480                 push @ta, $task;
481             }
482             delete $task->{pstart};
483         };
484
485         foreach my $task (@$tasklist) {
486             my $upid = $task->{upid};
487             next if $thash->{$upid};
488             $thash->{$upid} = $task;
489             &$check_task($task);
490         }
491
492         if ($new_upid && !(my $task = $thash->{$new_upid})) {
493             $task = PVE::Tools::upid_decode($new_upid);
494             $task->{upid} = $new_upid;
495             $thash->{$new_upid} = $task;
496             &$check_task($task, $nocheck);
497         }
498
499
500         @ta = sort { $b->{starttime} cmp $a->{starttime} } @ta;
501
502         my $save = defined($new_upid);
503
504         foreach my $task (@ta) {
505             next if $task->{endtime};
506             $task->{endtime} = time();
507             $task->{status} = PVE::Tools::upid_read_status($task->{upid});
508             $save = 1;
509         }
510
511         my $archive = '';
512         my @arlist = ();
513         foreach my $task (@ta) {
514             if (!$task->{saved}) {
515                 $archive .= sprintf("$task->{upid} %08X $task->{status}\n", $task->{endtime});
516                 $save = 1;
517                 push @arlist, $task;
518                 $task->{saved} = 1;
519             }
520         }
521
522         if ($archive) {
523             my $size = 0;
524             my $filename = "/var/log/pve/tasks/index";
525             eval {
526                 my $fh = IO::File->new($filename, '>>', 0644) ||
527                     die "unable to open file '$filename' - $!\n";
528                 PVE::Tools::safe_print($filename, $fh, $archive);
529                 $size = -s $fh;
530                 close($fh) ||
531                     die "unable to close file '$filename' - $!\n";
532             };
533             my $err = $@;
534             if ($err) {
535                 syslog('err', $err);
536                 foreach my $task (@arlist) { # mark as not saved
537                     $task->{saved} = 0;
538                 }
539             }
540             my $maxsize = 50000; # about 1000 entries
541             if ($size > $maxsize) {
542                 rename($filename, "$filename.1");
543             }
544         }
545
546         # we try to reduce the amount of data
547         # list all running tasks and task and a few others
548         # try to limit to 25 tasks
549         my $ctime = time();
550         my $max = 25 - scalar(@$tlist);
551         foreach my $task (@ta) {
552             last if $max <= 0;
553             push @$tlist, $task;
554             $max--;
555         }
556
557         PVE::INotify::write_file('active', $tlist) if $save;
558
559         return $tlist;
560     };
561
562     my $res = PVE::Tools::lock_file($lkfn, $timeout, $code);
563     die $@ if $@;
564
565     return $res;
566 }
567
568 my $kill_process_group = sub {
569     my ($pid, $pstart) = @_;
570
571     # send kill to process group (negative pid)
572     my $kpid = -$pid;
573
574     # always send signal to all pgrp members
575     kill(15, $kpid); # send TERM signal
576
577     # give max 5 seconds to shut down
578     for (my $i = 0; $i < 5; $i++) {
579         return if !PVE::ProcFSTools::check_process_running($pid, $pstart);
580         sleep (1);
581     }
582        
583     # to be sure
584     kill(9, $kpid); 
585 };
586
587 sub check_worker {
588     my ($upid, $killit) = @_;
589
590     my $task = PVE::Tools::upid_decode($upid);
591
592     my $running = PVE::ProcFSTools::check_process_running($task->{pid}, $task->{pstart});
593
594     return 0 if !$running;
595
596     if ($killit) {
597         &$kill_process_group($task->{pid});
598         return 0;
599     }
600
601     return 1;
602 }
603
604 # start long running workers
605 # STDIN is redirected to /dev/null
606 # STDOUT,STDERR are redirected to the filename returned by upid_decode
607 # NOTE: we simulate running in foreground if ($self->{type} eq 'cli')
608 sub fork_worker {
609     my ($self, $dtype, $id, $user, $function) = @_;
610
611     $dtype = 'unknown' if !defined ($dtype);
612     $id = '' if !defined ($id);
613
614     $user = 'root@pve' if !defined ($user);
615
616     my $sync = $self->{type} eq 'cli' ? 1 : 0;
617
618     local $SIG{INT} = 
619         local $SIG{QUIT} = 
620         local $SIG{PIPE} = 
621         local $SIG{TERM} = 'IGNORE';
622
623     my $starttime = time ();
624
625     my @psync = POSIX::pipe();
626     my @csync = POSIX::pipe();
627
628     my $node = $self->{nodename};
629
630     my $cpid = fork();
631     die "unable to fork worker - $!" if !defined($cpid);
632
633     my $workerpuid = $cpid ? $cpid : $$;
634
635     my $pstart = PVE::ProcFSTools::read_proc_starttime($workerpuid) ||
636         die "unable to read process start time";
637
638     my $upid = PVE::Tools::upid_encode ({
639         node => $node, pid => $workerpuid, pstart => $pstart, 
640         starttime => $starttime, type => $dtype, id => $id, user => $user });
641
642     my $outfh;
643
644     if (!$cpid) { # child
645
646         $0 = "task $upid";
647
648         $SIG{INT} = $SIG{QUIT} = $SIG{TERM} = sub { die "received interrupt\n"; };
649
650         $SIG{CHLD} = $SIG{PIPE} = 'DEFAULT';
651
652         # set sess/process group - we want to be able to kill the
653         # whole process group
654         POSIX::setsid(); 
655
656         POSIX::close ($psync[0]);
657         POSIX::close ($csync[1]);
658
659         $outfh = $sync ? $psync[1] : undef;
660
661         eval {
662             PVE::INotify::inotify_close();
663
664             if (my $atfork = $self->{atfork}) {
665                 &$atfork();
666             }
667
668             # same algorythm as used inside SA
669             # STDIN = /dev/null
670             my $fd = fileno (STDIN);
671
672             if (!$sync) {
673                 close STDIN;
674                 POSIX::close(0) if $fd != 0;
675
676                 die "unable to redirect STDIN - $!" 
677                     if !open(STDIN, "</dev/null");
678
679                 $outfh = PVE::Tools::upid_open($upid);
680             }
681
682
683             # redirect STDOUT
684             $fd = fileno(STDOUT);
685             close STDOUT;
686             POSIX::close (1) if $fd != 1;
687
688             die "unable to redirect STDOUT - $!" 
689                 if !open(STDOUT, ">&", $outfh);
690
691             STDOUT->autoflush (1);
692       
693             #  redirect STDERR to STDOUT
694             $fd = fileno (STDERR);
695             close STDERR;
696             POSIX::close(2) if $fd != 2;
697
698             die "unable to redirect STDERR - $!" 
699                 if !open(STDERR, ">&1");
700             
701             STDERR->autoflush(1);
702         };
703         if (my $err = $@) {
704             my $msg =  "ERROR: $err";
705             POSIX::write($psync[1], $msg, length ($msg));
706             POSIX::close($psync[1]);
707             POSIX::_exit(1); 
708             kill(-9, $$); 
709         }
710
711         # sync with parent (signal that we are ready)
712         if ($sync) {
713             print "$upid\n";
714         } else {
715             POSIX::write($psync[1], $upid, length ($upid));
716             POSIX::close($psync[1]);
717         }
718
719         my $readbuf = '';
720         # sync with parent (wait until parent is ready)
721         POSIX::read($csync[0], $readbuf, 4096);
722         die "parent setup error\n" if $readbuf ne 'OK';
723
724         if ($self->{type} eq 'ha') {
725             print "task started by HA resource agent\n";
726         }
727         eval { &$function($upid); };
728         my $err = $@;
729         if ($err) {
730             chomp $err;
731             $err =~ s/\n/ /mg;
732             syslog('err', $err);
733             print STDERR "TASK ERROR: $err\n";
734             POSIX::_exit(-1); 
735         } else {
736             print STDERR "TASK OK\n";
737             POSIX::_exit(0);
738         } 
739         kill(-9, $$); 
740     }
741
742     # parent
743
744     POSIX::close ($psync[1]);
745     POSIX::close ($csync[0]);
746
747     my $readbuf = '';
748     # sync with child (wait until child starts)
749     POSIX::read($psync[0], $readbuf, 4096);
750
751     if (!$sync) {
752         POSIX::close($psync[0]);
753         &$register_worker($cpid, $user, $upid);
754     } else {
755         chomp $readbuf;
756     }
757
758     eval {
759         die "got no worker upid - start worker failed\n" if !$readbuf;
760
761         if ($readbuf =~ m/^ERROR:\s*(.+)$/m) {
762             die "starting worker failed: $1\n";
763         }
764
765         if ($readbuf ne $upid) {
766             die "got strange worker upid ('$readbuf' != '$upid') - start worker failed\n";
767         }
768
769         if ($sync) {
770             $outfh = PVE::Tools::upid_open($upid);
771         }
772     };
773     my $err = $@;
774
775     if (!$err) {
776         my $msg = 'OK';
777         POSIX::write($csync[1], $msg, length ($msg));
778         POSIX::close($csync[1]);
779        
780     } else {
781         POSIX::close($csync[1]);
782         kill(-9, $cpid); # make sure it gets killed
783         die $err;
784     }
785
786     PVE::Cluster::log_msg('info', $user, "starting task $upid");
787
788     my $tlist = active_workers($upid, $sync);
789     PVE::Cluster::broadcast_tasklist($tlist);
790    
791     my $res = 0;
792
793     if ($sync) {
794         my $count;
795         my $outbuf = '';
796         my $int_count = 0;
797         eval {
798             local $SIG{INT} = local $SIG{QUIT} = local $SIG{TERM} = sub { 
799                 # always send signal to all pgrp members
800                 my $kpid = -$cpid;
801                 if ($int_count < 3) {
802                     kill(15, $kpid); # send TERM signal
803                 } else {
804                     kill(9, $kpid); # send KILL signal
805                 }
806                 $int_count++;
807             };
808             local $SIG{PIPE} = sub { die "broken pipe\n"; };
809
810             my $select = new IO::Select;    
811             my $fh = IO::Handle->new_from_fd($psync[0], 'r');
812             $select->add($fh);
813
814             while ($select->count) {
815                 my @handles = $select->can_read(1);
816                 if (scalar(@handles)) {
817                     my $count = sysread ($handles[0], $readbuf, 4096);
818                     if (!defined ($count)) {
819                         my $err = $!;
820                         die "sync pipe read error: $err\n";
821                     }
822                     last if $count == 0; # eof
823
824                     $outbuf .= $readbuf;
825                     while ($outbuf =~ s/^(([^\010\r\n]*)(\r|\n|(\010)+|\r\n))//s) {
826                         my $line = $1;
827                         my $data = $2;
828                         if ($data =~ m/^TASK OK$/) {
829                             # skip
830                         } elsif ($data =~ m/^TASK ERROR: (.+)$/) {
831                             print STDERR "$1\n";
832                         } else {
833                             print $line;
834                         }
835                         if ($outfh) {
836                             print $outfh $line;
837                             $outfh->flush();
838                         }
839                     }
840                 } else {
841                     # some commands daemonize without closing stdout
842                     last if !PVE::ProcFSTools::check_process_running($cpid);
843                 }
844             }
845         };
846         my $err = $@;
847
848         POSIX::close($psync[0]);
849
850         if ($outbuf) { # just to be sure
851             print $outbuf;
852             if ($outfh) {
853                 print $outfh $outbuf;
854             }
855         }
856
857         if ($err) {
858             $err =~ s/\n/ /mg;
859             print STDERR "$err\n";
860             if ($outfh) {
861                 print $outfh "TASK ERROR: $err\n";
862             }
863         }
864
865         &$kill_process_group($cpid, $pstart); # make sure it gets killed
866
867         close($outfh);
868
869         waitpid($cpid, 0);
870         $res = $?;
871         &$log_task_result($upid, $user, $res);
872     }
873
874     return wantarray ? ($upid, $res) : $upid;
875 }
876
877 1;