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

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

% constrain 300 ./foo args

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

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

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

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

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

Ответ 1

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

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

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

$ doalarm 300 ./foo.sh args

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

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

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

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

Ответ 2

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

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

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

timeout 1m watch free -m

Ответ 3

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

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

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

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

Ответ 4

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


Обновление: этот код не удается скомпилировать в MacOS X, потому что SIGRTMIN не определен и не выполняется тайм-аут при запуске в MacOS X, потому что функция signal() возобновляет wait() после истечения времени будильника, что не является требуемым поведением. У меня есть новая версия timeout.c, которая касается обеих этих проблем (используя sigaction() вместо signal()). Как и прежде, свяжитесь со мной для 10K gzipped 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", свяжитесь со мной (см. мой профиль).

Ответ 5

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

ulimit -t 10

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

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

#! /usr/bin/env python

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

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

Ответ 6

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" на любое количество секунд и "да foo" с любой командой.

Ответ 7

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

10.4. *

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

Ответ 8

Мое изменение на одном слоте perl дает вам статус выхода без смещения 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));' "[email protected]"

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

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

Ответ 9

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

# 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+ процессов в секунду. Это может быть исправлено.

Ответ 11

Небольшая модификация 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

В принципе, exit ($? → 8) перенаправляет статус выхода подпроцесса. Я только что выбрал 77 в статусе выхода для тайм-аута.

Ответ 12

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

## 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 - "[email protected]" <<"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
}

Ответ 13

чистый bash:


#!/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 "[email protected]"

Ответ 14

Нет ли способа установить определенное время с помощью "at" для этого?

$ at 05:00 PM kill -9 $pid

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

Если вы не знаете, какой будет номер pid, я предполагаю, что есть способ script читать его с помощью 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

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

Ответ 15

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

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

Примеры:

  • Медленная задача выполняется более 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