56 votes

Commande en ligne de commande pour tuer automatiquement une commande après un certain temps

Je voudrais tuer automatiquement une commande après un certain temps. J'ai en tête une interface comme celle-ci :

% constrain 300 ./foo args

Ce qui permet d'exécuter "./foo" avec "args" mais de le tuer automatiquement s'il est toujours en cours après 5 minutes.

Il pourrait être utile de généraliser l'idée à d'autres contraintes, comme l'arrêt automatique d'un processus s'il utilise trop de mémoire.

Existe-t-il des outils qui permettent de faire cela, ou quelqu'un a-t-il écrit un tel outil ?

AJOUTÉ : La solution de Jonathan est précisément ce que j'avais en tête et elle fonctionne comme un charme sous linux, mais je n'arrive pas à la faire fonctionner sous Mac OSX. Je me suis débarrassé du SIGRTMIN, ce qui permet de compiler sans problème, mais le signal n'est pas envoyé au processus enfant. Quelqu'un sait-il comment le faire fonctionner sur Mac ?

[Ajouté : Notez qu'une mise à jour est disponible auprès de Jonathan qui fonctionne sur Mac et ailleurs].

0 votes

Quelle est exactement votre question ?

0 votes

Oui, pour de vrai. Tu ne poses pas de question ici.

1 votes

Question similaire, réponses différentes : stackoverflow.com/questions/687948

47voto

Roger Dahl Points 8326

GNU Coreutils inclut le délai d'attente installée par défaut sur de nombreux systèmes.

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

A regarder free -m pendant une minute, puis le tuer en envoyant un signal TERM :

timeout 1m watch free -m

13 votes

Parfait ! Puisque les GNU Coreutils sont disponibles via Homebrew, il s'agit juste de brew install coreutils et ensuite gtimeout est disponible.

0 votes

@talyric : Par défaut, OS X ne fournit pas GNU Coreutils. Voir Comment remplacer les utilitaires de Mac OS X par les utilitaires de base de GNU ?

38voto

unwind Points 181987

Peut-être que je ne comprends pas la question, mais cela semble faisable directement, au moins en bash :

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

Cela exécute la première commande, à l'intérieur de la parenthèse, pendant cinq secondes, puis la tue. Toute l'opération se déroule de manière synchrone, c'est-à-dire que vous ne pourrez pas utiliser votre shell pendant qu'il est occupé à attendre la commande lente. Si ce n'est pas ce que vous vouliez, il devrait être possible d'ajouter un autre &.

El $! est une variable Bash qui contient l'ID du processus du sous-shell le plus récemment lancé. Il est important de ne pas avoir le & à l'intérieur de la parenthèse, car en procédant ainsi, on perd l'ID du processus.

0 votes

Très intelligent ! Je ne savais pas que Bash pouvait faire ça. Quelqu'un connaît-il une astuce similaire pour tcsh ?

0 votes

J'ai demandé un suivi de votre réponse astucieuse à l'adresse suivante stackoverflow.com/questions/687948/

4 votes

Très intéressant, mais il y a une condition de course si le processus se termine prématurément et que le PID est réutilisé pour un autre processus non lié du même utilisateur. Maintenant je dois lire les réponses au suivi du système PAUSE et voir s'ils ont le même problème.

29voto

pilcrow Points 20628

Je suis arrivé un peu tard à cette fête, mais je ne vois pas mon tour préféré dans les réponses.

Sous *NIX, un alarm(2) est héritée d'une execve(2) et SIGALRM est fatal par défaut. Donc, vous pouvez souvent simplement :

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

$ doalarm 300 ./foo.sh args

ou installer un wrapper C trivial pour le faire à votre place.

Avantages Un seul PID est impliqué, et le mécanisme est simple. Vous ne tuerez pas le mauvais processus si, par exemple, ./foo.sh est sorti "trop rapidement" et son PID a été réutilisé. Vous n'avez pas besoin de plusieurs sous-processus shell travaillant de concert, ce qui peut être fait correctement mais est plutôt sujet à la course.

Inconvénients Le processus soumis à des contraintes de temps ne peut pas manipuler son réveil (par ex, alarm(2) , ualarm(2) , setitimer(2) ), car cela permettrait d'effacer l'alarme héritée. Évidemment, il ne peut pas non plus bloquer ou ignorer SIGALRM, bien que l'on puisse dire la même chose de SIGINT, SIGTERM, etc. pour certaines autres approches.

Certains systèmes (très anciens, je pense) mettent en œuvre sleep(2) en termes de alarm(2) et, encore aujourd'hui, certains programmeurs utilisent alarm(2) comme un mécanisme de temporisation interne rudimentaire pour les E/S et autres opérations. D'après mon expérience, cependant, cette technique est applicable à la grande majorité des processus que vous souhaitez limiter dans le temps.

0 votes

Je l'ai essayé et il fonctionne à merveille. C'est peut-être mon nouveau préféré.

0 votes

J'ai utilisé le langage perl script. J'ai rencontré une difficulté étrange : il ne semble pas fonctionner lorsque la commande est une fonction bash. doalarm 7200 echo "Cool" fonctionne parfaitement, testfun () { echo $1 } ; testfun "Cool" fonctionne parfaitement, mais doalarm 7200 testfun "Cool" ne fonctionne pas. Merci pour toute suggestion.

1 votes

@EtienneLow-Décarie, oui, ce style de wrapper ne fonctionnera que sur les commandes qui sont exécutées()d en tant que processus séparés. Ainsi, les fonctions shell et les buildins ne pourront pas faire l'objet de doalarm.

13voto

Jonathan Leffler Points 299946

J'ai un programme appelé timeout qui fait cela - écrit en C, à l'origine en 1989 mais mis à jour périodiquement depuis.


Mise à jour : ce code ne compile pas sous MacOS X parce que SIGRTMIN n'est pas défini, et ne parvient pas à respecter le délai d'attente lorsqu'il est exécuté sous MacOS X parce que la fonction `signal()` y reprend le `wait()` après le délai d'attente de l'alarme - ce qui n'est pas le comportement requis. J'ai une nouvelle version de `timeout.c` qui traite ces deux problèmes (en utilisant `sigaction()` au lieu de `signal()`). Comme avant, contactez-moi pour obtenir un fichier tar gzippé de 10K avec le code source et une page de manuel (voir mon profil).

/*
@(#)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; /* I don't know what happened! */
    else if (WIFEXITED(status))
        status = WEXITSTATUS(status);
    else if (WIFSIGNALED(status))
        status = WTERMSIG(status);
    else
        status = 2; /* I don't know what happened! */

    return(status);
}

Si vous voulez le code 'officiel' de 'stderr.h' et 'stderr.c', contactez-moi (voir mon profil).

0 votes

Jonathan, merci beaucoup ! Cela fonctionne parfaitement sous linux mais pas sous Mac OSX pour moi. Il compile (après avoir supprimé le SIGRTMIN) et semble fonctionner mais n'envoie pas réellement le signal.

2 votes

Pour ceux qui sont trop timides pour contacter Jonathan, il a dit que j'étais le bienvenu pour héberger le code, alors le voici : yootles.com/outbox/timeout-4.09.tgz Merci encore, Jonathan, d'avoir rendu cette information disponible ! (Aussi de sa part : "Incidemment, je pense que la correction de sigaction() était également nécessaire pour Cygwin.")

13voto

Matthew Schinckel Points 15596

Il existe également ulimit, qui peut être utilisé pour limiter le temps d'exécution disponible pour les sous-processus.

ulimit -t 10

Limite le processus à 10 secondes de temps CPU.

Pour l'utiliser réellement pour limiter un nouveau processus, plutôt que le processus actuel, vous pouvez souhaiter utiliser un script enveloppant :

#! /usr/bin/env python

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

L'autre commande peut être n'importe quel outil. Je faisais tourner des versions Java, Python, C et Scheme de différents algorithmes de tri, et j'enregistrais le temps qu'ils prenaient, tout en limitant le temps d'exécution à 30 secondes. Une application Cocoa-Python générait les différentes lignes de commande - y compris les arguments - et rassemblait les temps dans un fichier CSV, mais ce n'était en fait que du vent en plus de la commande fournie ci-dessus.

0 votes

Aha, j'avais oublié l'ulimit. Mais y a-t-il un moyen de l'appliquer à un processus spécifique ? Par exemple, ce que Jonathan a fait peut-il être fait plus simplement en utilisant ulimit ?

0 votes

Je n'ai pas utilisé ulimit depuis des lustres. Ce que j'ai fait, c'est que j'avais un script python qui exécutait un autre outil : os.system("ulimit -t 600 ; gtime -f <command>java -Xms1024m -Xmx1024m Main")

0 votes

Merci Matthew ! Tu veux intégrer ça dans ta réponse ? Cela semble être une bonne alternative rapide au programme C de Jonathan. On pourrait même envelopper cela dans un script perl/python/shell et avoir la même interface de ligne de commande. Quels seraient les avantages/inconvénients de cette solution par rapport à celle de Jonathan ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X