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

StackOverflow https://stackoverflow.com/questions/601543

Вопрос

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

% constrain 300 ./foo args

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

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

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

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

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

Это было полезно?

Решение

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

В *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 включает в себя тайм-аут команда, установленная по умолчанию во многих системах.

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

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

timeout 1m watch free -m

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

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

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

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

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


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


/*
@(#)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», свяжитесь со мной (см. мой профиль).

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

ulimit -t 10

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

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

#! /usr/bin/env python

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

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

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» на любую команду.

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

10.4.*

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

Мой вариант однострочника 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));' "$@"

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

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

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

# 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 вашего процесса повторно используется в течение таймаута, это может привести к уничтожению не того процесса.Это маловероятно, но вы можете запускать более 20 000 процессов в секунду.Это можно исправить.

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

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

#!/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

Небольшая модификация однострочника Perl позволит получить правильный статус выхода.

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 в статусе выхода для тайм-аута.

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

## 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
}

чистый баш:


#!/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 "$@"

Нет ли способа установить для этого конкретное время с помощью «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 и назначить ему переменную.Я не слишком опытен, но предполагаю, что это выполнимо.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top