Vider un fichier sans perturber l'écriture du tuyau

12

J'ai un programme dont je redirige la sortie vers un fichier journal:

./my_app > log

Je voudrais effacer (c'est-à-dire vide) le journal de temps en temps (sur demande) et j'ai essayé diverses choses comme

cat "" > log

Cependant, il semble toujours que le canal d'origine est alors perturbé et que le programme ne redirige plus sa sortie vers le fichier journal.

Y a-t-il un moyen de le faire?

Mise à jour

Notez que je ne peux pas modifier l'application produisant la sortie. Il le crache simplement sur stdout et je veux l'enregistrer dans un journal afin de pouvoir l'inspecter quand j'en ai besoin et l'effacer quand je le veux. Cependant, je ne devrais pas avoir besoin de redémarrer l'application.

bangnab
la source
c'est pourquoi vous utilisez généralement un démon de journalisation pour enregistrer les choses ...
Kiwy
@Kiwy pouvez-vous expliquer comment cela résoudrait le problème?
bangnab
Eh bien, vous utilisez généralement un démon de journal ou laissez votre application gérer le journal, car écrire des choses dans la sortie et les rediriger n'est pas fiable. vous devriez jeter un oeil à syslogdoulogrotate
Kiwy
2
Les choses fonctionnent-elles si vous le faites ./my_app >> log(pour forcer l'ajout) et cp /dev/null logpour le tronquer?
Mark Plotnick
1
Quel message d'erreur obtenez-vous? Quel comportement voyez-vous? "Ne redirige plus sa sortie vers le fichier journal" n'est pas très spécifique. En outre, cat "" > logn'est pas une catcommande valide car aucun fichier n'est appelé "".
Mikel

Réponses:

13

Une autre forme de ce problème se produit avec les applications de longue durée dont les journaux sont périodiquement tournés. Même si vous déplacez le journal d'origine (par exemple, mv log.txt log.1) et le remplacez immédiatement par un fichier du même nom avant toute journalisation réelle, si le processus maintient le fichier ouvert, il finira par écrire dans log.1(car cela peut toujours être l'inode ouvert) ou à rien.

Une façon courante de résoudre ce problème (l'enregistreur système lui-même fonctionne de cette façon) consiste à implémenter un gestionnaire de signal dans le processus qui fermera et rouvrira ses journaux. Ensuite, lorsque vous souhaitez déplacer ou effacer (en supprimant) le journal, envoyez ce signal au processus immédiatement après.

Voici une démonstration simple pour bash - pardonnez mes compétences de shell cruddy (mais si vous allez modifier cela pour les meilleures pratiques, etc., assurez-vous de comprendre d'abord la fonctionnalité et de tester votre révision avant de la modifier):

#!/bin/bash

trap sighandler INT

function sighandler () {
    touch log.txt
    exec &> log.txt
}

echo $BASHPID
exec &> log.txt

count=0;
while [ $count -lt 60 ]; do
    echo "$BASHPID Count is now $count"
    sleep 2
    ((count++))
done          

Commencez par bifurquer en arrière-plan:

> ./test.sh &
12356

Notez qu'il signale son PID au terminal, puis commence à se connecter log.txt. Vous avez maintenant 2 minutes pour jouer. Attendez quelques secondes et essayez:

> mv log.txt log.1 && kill -s 2 12356

Tout simplement kill -2 12356peut aussi fonctionner pour vous ici. Le signal 2 est SIGINT (c'est aussi ce que fait Ctrl-C, vous pouvez donc essayer cela au premier plan et déplacer ou supprimer le fichier journal d'un autre terminal), ce qui trapdevrait intercepter. Vérifier;

> cat log.1
12356 Count is now 0
12356 Count is now 1
12356 Count is now 2
12356 Count is now 3
12356 Count is now 4
12356 Count is now 5
12356 Count is now 6
12356 Count is now 7
12356 Count is now 8
12356 Count is now 9
12356 Count is now 10
12356 Count is now 11
12356 Count is now 12
12356 Count is now 13
12356 Count is now 14

Voyons maintenant s'il écrit toujours dans un log.txtmême si nous l'avons déplacé:

> cat log.txt
12356 Count is now 15
12356 Count is now 16
12356 Count is now 17
12356 Count is now 18
12356 Count is now 19
12356 Count is now 20
12356 Count is now 21

Remarquez qu'il a continué là où il s'était arrêté. Si vous ne souhaitez pas conserver l'enregistrement, effacez simplement le journal en le supprimant

> rm -f log.txt && kill -s 2 12356

Vérifier:

> cat log.txt
12356 Count is now 29
12356 Count is now 30
12356 Count is now 31
12356 Count is now 32
12356 Count is now 33
12356 Count is now 34
12356 Count is now 35
12356 Count is now 36

Marche encore.

Vous ne pouvez pas le faire dans un script shell pour un sous-processus exécuté, malheureusement, car s'il est au premier plan, les propres gestionnaires de signaux de bash trapsont suspendus et si vous le bifurquez en arrière-plan, vous ne pouvez pas réaffecter son production. C'est-à-dire, c'est quelque chose que vous devez implémenter dans votre application.

Pourtant...

Si vous ne pouvez pas modifier l'application (par exemple, parce que vous ne l'avez pas écrite), j'ai un utilitaire CLI que vous pouvez utiliser comme intermédiaire. Vous pouvez également implémenter une version simple de ceci dans un script qui sert de canal vers le journal:

#!/bin/bash

trap sighandler INT

function sighandler () {
    touch log.txt
    exec 1> log.txt
}

echo "$0 $BASHPID"
exec 1> log.txt

count=0;
while read; do
    echo $REPLY
done  

Appelons ça pipetrap.sh. Maintenant, nous avons besoin d'un programme distinct pour tester, imitant l'application que vous souhaitez enregistrer:

#!/bin/bash

count=0
while [ $count -lt 60 ]; do
    echo "$BASHPID Count is now $count"
    sleep 2
    ((count++))
done           

Ce sera test.sh:

> (./test.sh | ./pipetrap.sh) &
./pipetrap.sh 15859

Il s'agit de deux processus distincts avec des PID distincts. Pour effacer test.shla sortie de, qui est acheminée via pipetrap.sh:

> rm -f log.txt && kill -s 2 15859

Vérifier:

>cat log.txt
15858 Count is now 6
15858 Count is now 7
15858 Count is now 8

15858, test.shest toujours en cours d'exécution et sa sortie est en cours d'enregistrement. Dans ce cas, aucune modification de l'application n'est nécessaire.

boucle d'or
la source
Merci pour les belles explications. Cependant dans mon cas je ne peux pas modifier l'application pour implémenter votre solution.
bangnab
2
Si vous ne pouvez pas implémenter un gestionnaire de signal dans votre application (parce que vous ne pouvez pas le modifier), vous pouvez utiliser cette technique pour diriger le journal à travers un piège de signal - voir le truc après "Cependant ..."
goldilocks
Ok je vais faire un essai et vous faire savoir comment ça s'est passé.
bangnab
J'ai enfin une application CLI écrite en C pour cela (désolé, cela a pris un peu plus de temps que prévu à l'origine): cognitivedissonance.ca/cogware/pipelog
goldilocks
6

TL; DR

Ouvrez votre fichier journal en mode ajout :

cmd >> log

Ensuite, vous pouvez le tronquer en toute sécurité avec:

: > log

Détails

Avec un shell de type Bourne, il existe 3 façons principales d'ouvrir un fichier pour l'écriture. En mode écriture seule ( >), lecture + écriture ( <>) ou ajout (et écriture seule >>).

Dans les deux premiers, le noyau se souvient de la position actuelle dans laquelle vous (par vous, je veux dire, la description du fichier ouvert , partagée par tous les descripteurs de fichier qui l'ont dupliqué ou hérité en forçant à partir de celui sur lequel vous avez ouvert le fichier) fichier.

Quand vous faites:

cmd > log

logest ouvert en mode écriture seule par le shell pour la sortie standard de cmd.

cmd(son processus initial engendré par le shell et tous les enfants possibles) lors de l'écriture sur leur stdout, écrivez à la position actuelle du curseur détenue par la description du fichier ouvert qu'ils partagent sur ce fichier.

Par exemple, si l' cmdécriture initiale zzz, la position sera au décalage d'octets 4 dans le fichier, et la prochaine fois que cmdses enfants écrivent dans le fichier, c'est là que les données seront écrites, que le fichier ait augmenté ou diminué dans l'intervalle. .

Si le fichier a diminué, par exemple s'il a été tronqué avec un

: > log

et cmdécrit xx, ceux- xxci seront écrits en offset 4, et les 3 premiers caractères seront remplacés par des caractères NUL.

$ exec 3> log # open file on fd 3.
$ printf zzz >&3
$ od -c log
0000000   z   z   z
0000003
$ printf aaaa >> log # other open file description -> different cursor
$ od -c log
0000000   z   z   z   a   a   a   a
0000007
$ printf bb >&3 # still write at the original position
$ od -c log
0000000   z   z   z   b   b   a   a
0000007
$ : > log
$ wc log
0 0 0 log
$ printf x >&3
$ od -c log
0000000  \0  \0  \0  \0  \0   x
0000006

Cela signifie que vous ne pouvez pas tronquer un fichier qui a été ouvert en mode écriture seule (et c'est la même chose en lecture + écriture ) comme si vous le faisiez, les processus qui avaient des descripteurs de fichier ouverts sur le fichier, laisseront des caractères NUL au début de la fichier (ceux-ci, sauf sur OS / X, ne prennent généralement pas d'espace sur le disque, ils deviennent des fichiers rares).

Au lieu de cela (et vous remarquerez que la plupart des applications le font lorsqu'elles écrivent dans des fichiers journaux), vous devez ouvrir le fichier en mode ajout :

cmd >> log

ou

: > log && cmd >> log

si vous souhaitez démarrer sur un fichier vide.

En mode ajout, toutes les écritures sont effectuées à la fin du fichier, quelle que soit la dernière écriture:

$ exec 4>> log
$ printf aa >&4
$ printf x >> log
$ printf bb >&4
$ od -c log
0000000   a   a   x   b   b
0000005
$ : > log
$ printf cc >&4
$ od -c log
0000000   c   c
0000002

C'est aussi plus sûr que si deux processus ont ouvert (de cette manière) le fichier par erreur (comme par exemple si vous avez démarré deux instances du même démon), leur sortie ne se remplacera pas.

Sur les versions récentes de Linux, vous pouvez vérifier la position actuelle et si un descripteur de fichier a été ouvert en mode ajout en regardant /proc/<pid>/fdinfo/<fd>:

$ cat /proc/self/fdinfo/4
pos:        2
flags:      0102001

Ou avec:

$ lsof +f G -p "$$" -ad 4
COMMAND  PID USER   FD   TYPE  FILE-FLAG DEVICE SIZE/OFF     NODE NAME
zsh     4870 root    4w   REG 0x8401;0x0 252,18        2 59431479 /home/chazelas/log
~# lsof +f g -p "$$" -ad 4
COMMAND  PID USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF     NODE NAME
zsh     4870 root    4w   REG   W,AP,LG 252,18        2 59431479 /home/chazelas/log

Ces drapeaux correspondent aux drapeaux O ..._ passés à l' openappel système.

$ gcc -E - <<< $'#include <fcntl.h>\nO_APPEND O_WRONLY' | tail -n1
02000 01

( O_APPENDest 0x400 ou octal 02000)

Ainsi, le shell >>ouvre le fichier avec O_WRONLY|O_APPEND(et 0100000 ici est O_LARGEFILE qui n'est pas pertinent pour cette question) alors que >est O_WRONLYseulement (et <>est O_RDWRseulement).

Si vous faites:

sudo lsof -nP +f g | grep ,AP

pour rechercher des fichiers ouverts avec O_APPEND, vous trouverez la plupart des fichiers journaux actuellement ouverts pour l'écriture sur votre système.

Stéphane Chazelas
la source
Pourquoi utilisez-vous le :(deux-points) dans : > ?
mvorisek
1
@Mvorisek, qui est de rediriger la sortie de commande qui ne produit aucune sortie: :. Sans commande, le comportement varie selon les shells.
Stéphane Chazelas
1

Si je comprends bien, cela teesemble être une approche raisonnable:

$ ./myapp-that-echoes-the-date-every-second | tee log > /dev/null &
[1] 20519
$ head log
Thu Apr  3 11:29:34 EDT 2014
Thu Apr  3 11:29:35 EDT 2014
Thu Apr  3 11:29:36 EDT 2014
$ > log
$ head log
Thu Apr  3 11:29:40 EDT 2014
Thu Apr  3 11:29:41 EDT 2014
Thu Apr  3 11:29:42 EDT 2014
évêque
la source
1

Comme solution rapide, vous pouvez utiliser un journal avec rotation (rotation quotidienne par exemple):

date=`date +%Y%m%d`
LOGFILE=/home/log$date.log

et rediriger la journalisation vers celui-ci ./my_app >> log$date.log

Charles nakhel
la source
J'aimerais pouvoir tourner sur demande. Il s'agit en fait d'un journal qui est produit lors d'un test automatisé et je voudrais l'effacer avant d'exécuter le test.
bangnab du
0

C'est un problème qui a été résolu depuis longtemps avec syslog (dans toutes ses variantes) mais il existe deux outils qui résoudraient votre problème particulier avec un minimum d'effort.

La première solution, plus portable mais moins polyvalente, est l'enregistreur (indispensable pour toute boîte à outils pour les administrateurs). Il s'agit d'un simple utilitaire qui copie l'entrée standard dans syslog. (passer la balle et faire de la rotation des fichiers le problème de logrotate et syslog)

La deuxième solution, plus élégante mais moins portable, est syslog-ng qui, en plus d'accepter les messages de journal des sockets de syslog standard, peut exécuter des programmes dont la sortie est filtrée via l'enregistreur. (Je n'ai pas encore utilisé cette fonctionnalité, mais elle semble parfaite pour ce que vous voulez faire.)

hildred
la source