Команда командной строки для автоматического уничтожения команды через определенное время

Я хотел бы автоматически убить команду через определенное время. Я имею в виду такой интерфейс:

% constrain 300 ./foo args

Который будет запускать "./foo" с "args", но автоматически убивает его, если он все еще работает через 5 минут.

Возможно, было бы полезно обобщить идею для других ограничений, таких как автоматическое уничтожение процесса, если он использует слишком много памяти.

Существуют ли какие-либо инструменты, которые это делают, или кто-нибудь написал такое?

ДОБАВЛЕНО: решение Джонатана - именно то, что я имел в виду, и оно работает как прелесть в Linux, но я не могу заставить его работать на Mac OSX. Я избавился от SIGRTMIN, который позволяет нормально его компилировать, но сигнал просто не отправляется дочернему процессу. Кто-нибудь знает, как заставить это работать на Mac?

[Добавлено: обратите внимание, что от Джонатана доступно обновление, которое работает на Mac и в других местах.]

15 ответов

Решение

Я приехал довольно поздно на эту вечеринку, но не вижу своего любимого трюка в ответах.

Под *NIX, alarm(2) наследуется через execve(2) и SIGALRM является фатальным по умолчанию. Таким образом, вы часто можете просто:

$ doalarm () { perl -e 'alarm shift; exec @ARGV' "$@"; } # define a helper function

$ doalarm 300 ./foo.sh args

или установите тривиальную оболочку C, чтобы сделать это для вас.

Преимущества Используется только один PID, и механизм прост. Вы не убьете неправильный процесс, если, например, ./foo.sh выход "слишком быстро" и его PID был повторно использован. Вам не нужно, чтобы несколько подпроцессов оболочки работали согласованно, что может быть сделано правильно, но скорее склонно к гонкам.

Недостатки Процесс, ограниченный во времени, не может манипулировать своим будильником (например, alarm(2), ualarm(2), setitimer(2)), так как это, вероятно, очистит унаследованную тревогу. Очевидно, что он также не может блокировать или игнорировать SIGALRM, хотя то же самое можно сказать и о SIGINT, SIGTERM и т. Д. Для некоторых других подходов.

Некоторые (очень старые, я думаю) системы реализуют sleep(2) с точки зрения alarm(2)и даже сегодня некоторые программисты используют alarm(2) как грубый механизм внутреннего тайм-аута для операций ввода-вывода и других операций. Однако, по моему опыту, эта техника применима к подавляющему большинству процессов, которые вы хотите ограничить по времени.

GNU Coreutils включает в себя команду timeout, установленную по умолчанию во многих системах.

https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html

Смотреть free -m на одну минуту, затем убейте его, отправив сигнал TERM:

timeout 1m watch free -m

Может быть, я не понимаю вопроса, но это звучит выполнимо, по крайней мере, в bash:

( /path/to/slow command with options ) & sleep 5 ; kill $!

Это запускает первую команду в круглых скобках в течение пяти секунд, а затем убивает ее. Вся операция выполняется синхронно, то есть вы не сможете использовать свою оболочку, пока она занята, ожидая медленной команды. Если это не то, что вы хотели, должна быть возможность добавить еще один &.

$! Переменная - это встроенная функция Bash, содержащая идентификатор процесса последней запущенной подоболочки. Важно не иметь & в круглых скобках, так как при этом теряется идентификатор процесса.

Существует также ulimit, который можно использовать для ограничения времени выполнения, доступного подпроцессам.

ulimit -t 10

Ограничивает процесс до 10 секунд процессорного времени.

Чтобы фактически использовать его для ограничения нового процесса, а не текущего процесса, вы можете использовать скрипт-обертку:

#! /usr/bin/env python

import os
os.system("ulimit -t 10; other-command-here")

Командой other может быть любой инструмент. Я запускал версии различных алгоритмов сортировки на Java, Python, C и Scheme и записывал, сколько времени они занимали, в то время как время выполнения ограничивалось 30 секундами. Приложение Какао-Python сгенерировало различные командные строки - включая аргументы - и сопоставило времена в CSV-файл, но на самом деле это просто пух над командой, представленной выше.

У меня есть программа под названием timeout это делает то, что написано на C, первоначально в 1989 году, но с тех пор периодически обновляется.


Обновление: этот код не может быть скомпилирован в MacOS X, потому что SIGRTMIN не определен, и не имеет тайм-аута при запуске в MacOS X, потому что signal() функция там возобновляет wait() после истечения времени тревоги - что не является обязательным поведением. У меня новая версия timeout.c которая решает обе эти проблемы (используя sigaction() вместо signal()). Как и прежде, свяжитесь со мной для получения 10K gzip-файла tar с исходным кодом и страницей руководства (см. Мой профиль).


/*
@(#)File:           $RCSfile: timeout.c,v $
@(#)Version:        $Revision: 4.6 $
@(#)Last changed:   $Date: 2007/03/01 22:23:02 $
@(#)Purpose:        Run command with timeout monitor
@(#)Author:         J Leffler
@(#)Copyright:      (C) JLSS 1989,1997,2003,2005-07
*/

#define _POSIX_SOURCE       /* Enable kill() in <unistd.h> on Solaris 7 */
#define _XOPEN_SOURCE 500

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "stderr.h"

#define CHILD       0
#define FORKFAIL    -1

static const char usestr[] = "[-vV] -t time [-s signal] cmd [arg ...]";

#ifndef lint
/* Prevent over-aggressive optimizers from eliminating ID string */
const char jlss_id_timeout_c[] = "@(#)$Id: timeout.c,v 4.6 2007/03/01 22:23:02 jleffler Exp $";
#endif /* lint */

static void catcher(int signum)
{
    return;
}

int main(int argc, char **argv)
{
    pid_t   pid;
    int     tm_out;
    int     kill_signal;
    pid_t   corpse;
    int     status;
    int     opt;
    int     vflag = 0;

    err_setarg0(argv[0]);

    opterr = 0;
    tm_out = 0;
    kill_signal = SIGTERM;
    while ((opt = getopt(argc, argv, "vVt:s:")) != -1)
    {
        switch(opt)
        {
        case 'V':
            err_version("TIMEOUT", &"@(#)$Revision: 4.6 $ ($Date: 2007/03/01 22:23:02 $)"[4]);
            break;
        case 's':
            kill_signal = atoi(optarg);
            if (kill_signal <= 0 || kill_signal >= SIGRTMIN)
                err_error("signal number must be between 1 and %d\n", SIGRTMIN - 1);
            break;
        case 't':
            tm_out = atoi(optarg);
            if (tm_out <= 0)
                err_error("time must be greater than zero (%s)\n", optarg);
            break;
        case 'v':
            vflag = 1;
            break;
        default:
            err_usage(usestr);
            break;
        }
    }

    if (optind >= argc || tm_out == 0)
        err_usage(usestr);

    if ((pid = fork()) == FORKFAIL)
        err_syserr("failed to fork\n");
    else if (pid == CHILD)
    {
        execvp(argv[optind], &argv[optind]);
        err_syserr("failed to exec command %s\n", argv[optind]);
    }

    /* Must be parent -- wait for child to die */
    if (vflag)
        err_remark("time %d, signal %d, child PID %u\n", tm_out, kill_signal, (unsigned)pid);
    signal(SIGALRM, catcher);
    alarm((unsigned int)tm_out);
    while ((corpse = wait(&status)) != pid && errno != ECHILD)
    {
        if (errno == EINTR)
        {
            /* Timed out -- kill child */
            if (vflag)
                err_remark("timed out - send signal %d to process %d\n", (int)kill_signal, (int)pid);
            if (kill(pid, kill_signal) != 0)
                err_syserr("sending signal %d to PID %d - ", kill_signal, pid);
            corpse = wait(&status);
            break;
        }
    }

    alarm(0);
    if (vflag)
    {
        if (corpse == (pid_t) -1)
            err_syserr("no valid PID from waiting - ");
        else
            err_remark("child PID %u status 0x%04X\n", (unsigned)corpse, (unsigned)status);
    }

    if (corpse != pid)
        status = 2; /* Dunno what happened! */
    else if (WIFEXITED(status))
        status = WEXITSTATUS(status);
    else if (WIFSIGNALED(status))
        status = WTERMSIG(status);
    else
        status = 2; /* Dunno what happened! */

    return(status);
}

Если вам нужен "официальный" код для "stderr.h" и "stderr.c", свяжитесь со мной (см. Мой профиль).

Perl один лайнер, просто для пинков:

perl -e '$s = shift; $SIG{ALRM} = sub { print STDERR "Timeout!\n"; kill INT => $p }; exec(@ARGV) unless $p = fork; alarm $s; waitpid $p, 0' 10 yes foo

Это печатает 'foo' в течение десяти секунд, затем время ожидания. Замените '10' на любое количество секунд, а 'yes foo' - любой командой.

Моя вариация на perl one-liner дает вам статус выхода без использования muck с помощью fork () и wait () и без риска убить неправильный процесс:

#!/bin/sh
# Usage: timelimit.sh secs cmd [ arg ... ]
exec perl -MPOSIX -e '$SIG{ALRM} = sub { print "timeout: @ARGV\n"; kill(SIGTERM, -$$); }; alarm shift; $exit = system @ARGV; exit(WIFEXITED($exit) ? WEXITSTATUS($exit) : WTERMSIG($exit));' "$@"

По сути, fork () и wait () скрыты внутри system(). SIGALRM доставляется родительскому процессу, который затем убивает себя и своего потомка, отправляя SIGTERM всей группе процессов (-$$). В маловероятном случае выхода дочернего элемента и повторного использования pid дочернего элемента до того, как произойдет kill (), это НЕ убьет неправильный процесс, поскольку новый процесс с pid старого дочернего элемента не будет находиться в той же группе процессов родительского процесса perl,

В качестве дополнительного преимущества скрипт также завершает работу с тем, что, вероятно, является правильным состоянием выхода.

Команда timeout из Ubuntu/Debian при компиляции из исходного кода для работы на Mac. Дарвин

10.4. *

http://packages.ubuntu.com/lucid/timeout

#!/bin/sh
( some_slow_task ) & pid=$!
( sleep $TIMEOUT && kill -HUP $pid ) 2>/dev/null & watcher=$!
wait $pid 2>/dev/null && pkill -HUP -P $watcher

Наблюдатель убивает медленную задачу после заданного времени ожидания; скрипт ожидает медленной задачи и завершает работу наблюдателя.

Примеры:

  • Медленная задача выполнялась более 2 секунд и была прервана

Медленная задача прервана

( sleep 20 ) & pid=$!
( sleep 2 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "Slow task finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "Slow task interrupted"
fi
  • Это медленное задание завершено до истечения заданного времени ожидания

Медленное задание выполнено

( sleep 2 ) & pid=$!
( sleep 20 && kill -HUP $pid ) 2>/dev/null & watcher=$!
if wait $pid 2>/dev/null; then
    echo "Slow task finished"
    pkill -HUP -P $watcher
    wait $watcher
else
    echo "Slow task interrupted"
fi

Чистый баш:


#!/bin/bash

if [[ $# < 2 ]]; then
  echo "Usage: $0 timeout cmd [options]"
  exit 1
fi

TIMEOUT="$1"
shift

BOSSPID=$$

(
  sleep $TIMEOUT
  kill -9 -$BOSSPID
)&
TIMERPID=$!

trap "kill -9 $TIMERPID" EXIT

eval "$@"

Как насчет использования инструмента ожидания?

## run a command, aborting if timeout exceeded, e.g. timed-run 20 CMD ARGS ...
timed-run() {
  # timeout in seconds
  local tmout="$1"
  shift
  env CMD_TIMEOUT="$tmout" expect -f - "$@" <<"EOF"
# expect script follows
eval spawn -noecho $argv
set timeout $env(CMD_TIMEOUT)
expect {
   timeout {
      send_error "error: operation timed out\n"
      exit 1
   }
   eof
}
EOF
}

Попробуйте что-то вроде:

# This function is called with a timeout (in seconds) and a pid.
# After the timeout expires, if the process still exists, it attempts
# to kill it.
function timeout() {
    sleep $1
    # kill -0 tests whether the process exists
    if kill -0 $2 > /dev/null 2>&1 ; then
        echo "killing process $2"
        kill $2 > /dev/null 2>&1
    else
        echo "process $2 already completed"
    fi
}

<your command> &
cpid=$!
timeout 3 $cpid
wait $cpid > /dev/null 2>&
exit $?

Недостатком является то, что если pid вашего процесса повторно используется в течение тайм-аута, это может убить неправильный процесс. Это маловероятно, но вы можете запускать более 20000 процессов в секунду. Это можно исправить.

Я использую "timelimit", который является пакетом, доступным в репозитории debian.

http://devel.ringlet.net/sysutils/timelimit/

Небольшая модификация perl one-liner даст правильный статус выхода.

perl -e '$s = shift; $SIG{ALRM} = sub { print STDERR "Timeout!\n"; kill INT => $p; exit 77 }; exec(@ARGV) unless $p = fork; alarm $s; waitpid $p, 0; exit ($? >> 8)' 10 yes foo

По сути, выход ($? >> 8) переадресует состояние выхода подпроцесса. Я просто выбрал 77 в состоянии выхода для тайм-аута.

Разве нет способа установить определенное время с помощью "at", чтобы сделать это?

$ at 05:00 PM kill -9 $pid

Кажется, намного проще.

Если вы не знаете, какой будет номер pid, я предполагаю, что есть способ прочитать его с помощью ps aux и grep, но не уверен, как это реализовать.

$   | grep someprogram
tony     11585  0.0  0.0   3116   720 pts/1    S+   11:39   0:00 grep someprogram
tony     22532  0.0  0.9  27344 14136 ?        S    Aug25   1:23 someprogram

Ваш скрипт должен прочитать pid и присвоить ему переменную. Я не слишком опытен, но предполагаю, что это выполнимо.

Другие вопросы по тегам