xref: /curl/tests/processhelp.pm (revision c997f3e0)
1#***************************************************************************
2#                                  _   _ ____  _
3#  Project                     ___| | | |  _ \| |
4#                             / __| | | | |_) | |
5#                            | (__| |_| |  _ <| |___
6#                             \___|\___/|_| \_\_____|
7#
8# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
9#
10# This software is licensed as described in the file COPYING, which
11# you should have received as part of this distribution. The terms
12# are also available at https://curl.se/docs/copyright.html.
13#
14# You may opt to use, copy, modify, merge, publish, distribute and/or sell
15# copies of the Software, and permit persons to whom the Software is
16# furnished to do so, under the terms of the COPYING file.
17#
18# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
19# KIND, either express or implied.
20#
21# SPDX-License-Identifier: curl
22#
23###########################################################################
24
25package processhelp;
26
27use strict;
28use warnings;
29
30BEGIN {
31    use base qw(Exporter);
32
33    our @EXPORT = qw(
34        portable_sleep
35        pidfromfile
36        pidexists
37        pidwait
38        processexists
39        killpid
40        killsockfilters
41        killallsockfilters
42        set_advisor_read_lock
43        clear_advisor_read_lock
44    );
45
46    # portable sleeping needs Time::HiRes
47    eval {
48        no warnings "all";
49        require Time::HiRes;
50    };
51    # portable sleeping falls back to native Sleep on Windows
52    eval {
53        no warnings "all";
54        require Win32;
55    }
56}
57
58use serverhelp qw(
59    servername_id
60    mainsockf_pidfilename
61    datasockf_pidfilename
62    logmsg
63    );
64
65use pathhelp qw(
66    os_is_win
67    );
68
69#######################################################################
70# portable_sleep uses Time::HiRes::sleep if available and falls back
71# to the classic approach of using select(undef, undef, undef, ...).
72# even though that one is not portable due to being implemented using
73# select on Windows: https://perldoc.perl.org/perlport.html#select
74# Therefore it uses Win32::Sleep on Windows systems instead.
75#
76sub portable_sleep {
77    my ($seconds) = @_;
78
79    if($Time::HiRes::VERSION) {
80        Time::HiRes::sleep($seconds);
81    }
82    elsif (os_is_win()) {
83        Win32::Sleep($seconds*1000);
84    }
85    else {
86        select(undef, undef, undef, $seconds);
87    }
88}
89
90#######################################################################
91# pidfromfile returns the pid stored in the given pidfile.  The value
92# of the returned pid will never be a negative value. It will be zero
93# on any file related error or if a pid can not be extracted from the
94# given file.
95#
96sub pidfromfile {
97    my $pidfile = $_[0];
98    my $pid = 0;
99
100    if(-f $pidfile && -s $pidfile && open(my $pidfh, "<", "$pidfile")) {
101        $pid = 0 + <$pidfh>;
102        close($pidfh);
103        $pid = 0 if($pid < 0);
104    }
105    return $pid;
106}
107
108#######################################################################
109# return Cygwin pid from virtual pid
110#
111sub winpid_to_pid {
112    my $vpid = $_[0];
113    if(($^O eq 'cygwin' || $^O eq 'msys') && $vpid > 65536) {
114        my $pid = Cygwin::winpid_to_pid($vpid - 65536);
115        if($pid) {
116            return $pid;
117        } else {
118            return $vpid
119        }
120    }
121    return $vpid;
122}
123
124#######################################################################
125# pidexists checks if a process with a given pid exists and is alive.
126# This will return the positive pid if the process exists and is alive.
127# This will return the negative pid if the process exists differently.
128# This will return 0 if the process could not be found.
129#
130sub pidexists {
131    my $pid = $_[0];
132
133    if($pid > 0) {
134        # verify if currently existing Windows process
135        $pid = winpid_to_pid($pid);
136        if ($pid > 65536 && os_is_win()) {
137            $pid -= 65536;
138            if($^O ne 'MSWin32') {
139                my $filter = "PID eq $pid";
140                # https://ss64.com/nt/tasklist.html
141                my $result = `tasklist -fi \"$filter\" 2>nul`;
142                if(index($result, "$pid") != -1) {
143                    return -$pid;
144                }
145                return 0;
146            }
147        }
148
149        # verify if currently existing and alive
150        if(kill(0, $pid)) {
151            return $pid;
152        }
153    }
154
155    return 0;
156}
157
158#######################################################################
159# pidterm asks the process with a given pid to terminate gracefully.
160#
161sub pidterm {
162    my $pid = $_[0];
163
164    if($pid > 0) {
165        # request the process to quit
166        $pid = winpid_to_pid($pid);
167        if ($pid > 65536 && os_is_win()) {
168            $pid -= 65536;
169            if($^O ne 'MSWin32') {
170                # https://ss64.com/nt/taskkill.html
171                my $cmd = "taskkill -t -pid $pid >nul 2>&1";
172                logmsg "Executing: '$cmd'\n";
173                system($cmd);
174                return;
175            }
176        }
177
178        # signal the process to terminate
179        kill("TERM", $pid);
180    }
181}
182
183#######################################################################
184# pidkill kills the process with a given pid mercilessly and forcefully.
185#
186sub pidkill {
187    my $pid = $_[0];
188
189    if($pid > 0) {
190        # request the process to quit
191        $pid = winpid_to_pid($pid);
192        if ($pid > 65536 && os_is_win()) {
193            $pid -= 65536;
194            if($^O ne 'MSWin32') {
195                # https://ss64.com/nt/taskkill.html
196                my $cmd = "taskkill -f -t -pid $pid >nul 2>&1";
197                logmsg "Executing: '$cmd'\n";
198                system($cmd);
199                return;
200            }
201        }
202
203        # signal the process to terminate
204        kill("KILL", $pid);
205    }
206}
207
208#######################################################################
209# pidwait waits for the process with a given pid to be terminated.
210#
211sub pidwait {
212    my $pid = $_[0];
213    my $flags = $_[1];
214
215    $pid = winpid_to_pid($pid);
216    # check if the process exists
217    if ($pid > 65536 && os_is_win()) {
218        if($flags == &WNOHANG) {
219            return pidexists($pid)?0:$pid;
220        }
221        while(pidexists($pid)) {
222            portable_sleep(0.01);
223        }
224        return $pid;
225    }
226
227    # wait on the process to terminate
228    return waitpid($pid, $flags);
229}
230
231#######################################################################
232# processexists checks if a process with the pid stored in the given
233# pidfile exists and is alive. This will return 0 on any file related
234# error or if a pid can not be extracted from the given file. When a
235# process with the same pid as the one extracted from the given file
236# is currently alive this returns that positive pid. Otherwise, when
237# the process is not alive, will return the negative value of the pid.
238#
239sub processexists {
240    use POSIX ":sys_wait_h";
241    my $pidfile = $_[0];
242
243    # fetch pid from pidfile
244    my $pid = pidfromfile($pidfile);
245
246    if($pid > 0) {
247        # verify if currently alive
248        if(pidexists($pid)) {
249            return $pid;
250        }
251        else {
252            # get rid of the certainly invalid pidfile
253            unlink($pidfile) if($pid == pidfromfile($pidfile));
254            # reap its dead children, if not done yet
255            pidwait($pid, &WNOHANG);
256            # negative return value means dead process
257            return -$pid;
258        }
259    }
260    return 0;
261}
262
263#######################################################################
264# killpid attempts to gracefully stop processes in the given pid list
265# with a SIGTERM signal and SIGKILLs those which haven't died on time.
266#
267sub killpid {
268    my ($verbose, $pidlist) = @_;
269    use POSIX ":sys_wait_h";
270    my @requested;
271    my @signalled;
272    my @reapchild;
273
274    # The 'pidlist' argument is a string of whitespace separated pids.
275    return if(not defined($pidlist));
276
277    # Make 'requested' hold the non-duplicate pids from 'pidlist'.
278    @requested = split(' ', $pidlist);
279    return if(not @requested);
280    if(scalar(@requested) > 2) {
281        @requested = sort({$a <=> $b} @requested);
282    }
283    for(my $i = scalar(@requested) - 2; $i >= 0; $i--) {
284        if($requested[$i] == $requested[$i+1]) {
285            splice @requested, $i+1, 1;
286        }
287    }
288
289    # Send a SIGTERM to processes which are alive to gracefully stop them.
290    foreach my $tmp (@requested) {
291        chomp $tmp;
292        if($tmp =~ /^(\d+)$/) {
293            my $pid = $1;
294            if($pid > 0) {
295                if(pidexists($pid)) {
296                    print("RUN: Process with pid $pid signalled to die\n")
297                        if($verbose);
298                    pidterm($pid);
299                    push @signalled, $pid;
300                }
301                else {
302                    print("RUN: Process with pid $pid already dead\n")
303                        if($verbose);
304                    # if possible reap its dead children
305                    pidwait($pid, &WNOHANG);
306                    push @reapchild, $pid;
307                }
308            }
309        }
310    }
311
312    # Allow all signalled processes five seconds to gracefully die.
313    if(@signalled) {
314        my $twentieths = 5 * 20;
315        while($twentieths--) {
316            for(my $i = scalar(@signalled) - 1; $i >= 0; $i--) {
317                my $pid = $signalled[$i];
318                if(!pidexists($pid)) {
319                    print("RUN: Process with pid $pid gracefully died\n")
320                        if($verbose);
321                    splice @signalled, $i, 1;
322                    # if possible reap its dead children
323                    pidwait($pid, &WNOHANG);
324                    push @reapchild, $pid;
325                }
326            }
327            last if(not scalar(@signalled));
328            # give any zombies of us a chance to move on to the afterlife
329            pidwait(0, &WNOHANG);
330            portable_sleep(0.05);
331        }
332    }
333
334    # Mercilessly SIGKILL processes still alive.
335    if(@signalled) {
336        foreach my $pid (@signalled) {
337            if($pid > 0) {
338                print("RUN: Process with pid $pid forced to die with SIGKILL\n")
339                    if($verbose);
340                pidkill($pid);
341                # if possible reap its dead children
342                pidwait($pid, &WNOHANG);
343                push @reapchild, $pid;
344            }
345        }
346    }
347
348    # Reap processes dead children for sure.
349    if(@reapchild) {
350        foreach my $pid (@reapchild) {
351            if($pid > 0) {
352                pidwait($pid, 0);
353            }
354        }
355    }
356}
357
358#######################################################################
359# killsockfilters kills sockfilter processes for a given server.
360#
361sub killsockfilters {
362    my ($piddir, $proto, $ipvnum, $idnum, $verbose, $which) = @_;
363    my $server;
364    my $pidfile;
365    my $pid;
366
367    return if($proto !~ /^(ftp|imap|pop3|smtp)$/);
368
369    die "unsupported sockfilter: $which"
370        if($which && ($which !~ /^(main|data)$/));
371
372    $server = servername_id($proto, $ipvnum, $idnum) if($verbose);
373
374    if(!$which || ($which eq 'main')) {
375        $pidfile = mainsockf_pidfilename($piddir, $proto, $ipvnum, $idnum);
376        $pid = processexists($pidfile);
377        if($pid > 0) {
378            printf("* kill pid for %s-%s => %d\n", $server,
379                ($proto eq 'ftp')?'ctrl':'filt', $pid) if($verbose);
380            pidkill($pid);
381            pidwait($pid, 0);
382        }
383        unlink($pidfile) if(-f $pidfile);
384    }
385
386    return if($proto ne 'ftp');
387
388    if(!$which || ($which eq 'data')) {
389        $pidfile = datasockf_pidfilename($piddir, $proto, $ipvnum, $idnum);
390        $pid = processexists($pidfile);
391        if($pid > 0) {
392            printf("* kill pid for %s-data => %d\n", $server,
393                $pid) if($verbose);
394            pidkill($pid);
395            pidwait($pid, 0);
396        }
397        unlink($pidfile) if(-f $pidfile);
398    }
399}
400
401#######################################################################
402# killallsockfilters kills sockfilter processes for all servers.
403#
404sub killallsockfilters {
405    my ($piddir, $verbose) = @_;
406
407    for my $proto (('ftp', 'imap', 'pop3', 'smtp')) {
408        for my $ipvnum (('4', '6')) {
409            for my $idnum (('1', '2')) {
410                killsockfilters($piddir, $proto, $ipvnum, $idnum, $verbose);
411            }
412        }
413    }
414}
415
416
417sub set_advisor_read_lock {
418    my ($filename) = @_;
419
420    my $fileh;
421    if(open($fileh, ">", "$filename") && close($fileh)) {
422        return;
423    }
424    printf "Error creating lock file $filename error: $!\n";
425}
426
427
428sub clear_advisor_read_lock {
429    my ($filename) = @_;
430
431    if(-f $filename) {
432        unlink($filename);
433    }
434}
435
436
4371;
438