Ce que Vanya a testé
Vanya savait qu'il allait tester le bundle "nginx + service".
Ici, je ferai immédiatement une remarque: un tel bundle a été choisi pour cet article simplement parce qu'il peut démontrer le plus clairement l'utilisation de divers utilitaires lors du débogage d'un problème et parce qu'il est très simple à configurer et à soulever. En termes réels, il peut s'agir d'un simple service ou d'une chaîne de services qui se demandent mutuellement.
Le serveur HTTP par défaut Python SimpleHTTPServer agit comme un service qui, en réponse à une requête sans paramètres, affiche le contenu du répertoire courant:
[root@ivan test_dir_srv]# ls -l
total 0
-rw-r--r-- 1 root root 0 Aug 25 11:23 test_file
[root@ivan test_dir_srv]# python3 -m http.server --bind 127.0.0.1 8000
Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...
Nginx est configuré comme suit:
upstream test {
server 127.0.0.1:8000;
}
server {
listen 80;
location / {
proxy_pass http://test;
}
}
Vanya avait besoin de tester un seul cas de test: pour vérifier que la demande de / fonctionne. Il a vérifié et tout a fonctionné:
MacBook-Pro-Ivan:~ ivantester$ curl http://12.34.56.78
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="test_file">test_file</a></li>
</ul>
<hr>
</body>
</html>
Mais à un moment donné sur le banc de test, les développeurs ont mis à jour quelque chose et Vanya a reçu une erreur:
MacBook-Pro-Ivan:~ ivantester$ curl http://12.34.56.78
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.14.2</center>
</body>
</html>
Il a décidé de ne pas lancer cette erreur incompréhensible aux développeurs, mais d'obtenir un accès ssh au serveur et de comprendre ce qui s'y passait. Il avait peu de connaissances dans le domaine de ce genre de débogage de problèmes, mais il voulait vraiment apprendre, alors il s'est armé de moteurs de recherche, de logique et est allé localiser le bug.
Première pensée de Vanya: les journaux
En effet, si une erreur s'est produite, il vous suffit de la retrouver dans le fichier journal. Mais vous devez d'abord trouver le fichier journal lui-même. Vanya est allé chez Google et a découvert que souvent les journaux se trouvent dans le répertoire / var / log . En effet, le répertoire nginx y a été trouvé:
[root@ivan ~]# ls /var/log/nginx/
access.log access.log-20200831 error.log error.log-20200831
Ivan a regardé les dernières lignes du journal des erreurs et s'est rendu compte de ce qui n'allait pas: les développeurs ont fait une erreur dans la configuration de nginx, une faute de frappe s'est glissée dans le port en amont.
[root@ivan ~]# tail /var/log/nginx/error.log
2020/08/31 04:36:21 [error] 15050#15050: *90452 connect() failed (111: Connection refused) while connecting to upstream, client: 31.170.95.221, server: , request: "GET / HTTP/1.0", upstream: "http://127.0.0.1:8009/", host: "12.34.56.78"
Quelle conclusion peut-on en tirer? Les journaux sont le meilleur ami des testeurs et des développeurs lors de la localisation des bogues. S'il y a un comportement inattendu du service, mais qu'il n'y a rien dans les journaux, c'est une raison pour renvoyer la tâche au développement avec une demande d'ajout de journaux. Après tout, si nginx n'avait pas écrit dans le journal à propos d'une tentative infructueuse d'atteindre en amont, alors, voyez-vous, il aurait été plus difficile de rechercher un problème?
À ce moment-là, Vanya pensa: «Et si les journaux nginx étaient dans un répertoire différent? Comment pourrais-je les trouver? " Dans quelques années, Vanya aura plus d'expérience avec les services sous Linux et il saura que le chemin d'accès au fichier journal est souvent passé au service en tant qu'argument de ligne de commande, ou qu'il est contenu dans un fichier de configuration, le chemin vers lequel est également souvent passé au service comme argument de ligne de commande. Eh bien, idéalement, le chemin d'accès au fichier journal doit être écrit dans la documentation du service.
À propos, via le fichier de configuration, vous pouvez trouver le chemin d'accès au fichier journal dans nginx:
[root@ivan ~]# ps ax | grep nginx | grep master
root 19899 0.0 0.0 57392 2872 ? Ss 2019 0:00 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
[root@ivan ~]# grep "log" /etc/nginx/nginx.conf
error_log /var/log/nginx/error.log warn;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
access_log /var/log/nginx/access.log main;
Et s'il n'y a rien dans les journaux?
Pendant son temps libre, Vanya a décidé de réfléchir à la façon dont il aurait fait face à la tâche si nginx n'avait rien écrit dans le journal. Vanya savait que le service écoutait sur le port 8000, il a donc décidé de regarder le trafic sur ce port sur le serveur. L'utilitaire tcpdump l'a aidé avec cela . Avec la configuration correcte, il a vu une requête et une réponse:
Décharger le trafic sur le port 8000
[root@ivan ~]# tcpdump -nn -i lo -A port 8000
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes
09:10:42.114284 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [S], seq 3390176024, win 43690, options [mss 65495,sackOK,TS val 830366494 ecr 0,nop,wscale 8], length 0
E..<..@.@..............@.............0.........
1~c.........
09:10:42.114293 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [S.], seq 4147196208, ack 3390176025, win 43690, options [mss 65495,sackOK,TS val 830366494 ecr 830366494,nop,wscale 8], length 0
E..<..@.@.<..........@...110.........0.........
1~c.1~c.....
09:10:42.114302 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [.], ack 1, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 0
E..4..@.@..............@.....111.....(.....
1~c.1~c.
09:10:42.114329 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [P.], seq 1:88, ack 1, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 87
E.....@.@..b...........@.....111...........
1~c.1~c.GET / HTTP/1.0
Host: test
Connection: close
User-Agent: curl/7.64.1
Accept: */*
09:10:42.114333 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [.], ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 0
E..4R/@.@............@...111...p.....(.....
1~c.1~c.
09:10:42.115062 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [P.], seq 1:155, ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 154
E...R0@.@............@...111...p...........
1~c.1~c.HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.7.2
Date: Mon, 07 Sep 2020 13:10:42 GMT
Content-type: text/html; charset=utf-8
Content-Length: 340
09:10:42.115072 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [.], ack 155, win 175, options [nop,nop,TS val 830366494 ecr 830366494], length 0
E..4.@.@..............@...p.11......(.....
1~c.1~c.
09:10:42.115094 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [P.], seq 155:495, ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 340
E...R1@.@..<.........@...11....p.....|.....
1~c.1~c.<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="test_file">test_file</a></li>
</ul>
<hr>
</body>
</html>
09:10:42.115098 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [.], ack 495, win 180, options [nop,nop,TS val 830366494 ecr 830366494], length 0
E..4.
@.@..............@...p.13......(.....
1~c.1~c.
09:10:42.115128 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [F.], seq 495, ack 88, win 171, options [nop,nop,TS val 830366494 ecr 830366494], length 0
E..4R2@.@............@...13....p.....(.....
1~c.1~c.
09:10:42.115264 IP 127.0.0.1.33296 > 127.0.0.1.8000: Flags [F.], seq 88, ack 496, win 180, options [nop,nop,TS val 830366495 ecr 830366494], length 0
E..4..@.@..............@...p.13 .....(.....
1~c.1~c.
09:10:42.115271 IP 127.0.0.1.8000 > 127.0.0.1.33296: Flags [.], ack 89, win 171, options [nop,nop,TS val 830366495 ecr 830366495], length 0
E..4R3@.@............@...13 ...q.....(.....
1~c.1~c.
^C
12 packets captured
24 packets received by filter
0 packets dropped by kernel
Avec une configuration incorrecte (avec le port 8009 dans le nginx en amont), il n'y avait pas de trafic sur le port 8000. Vanya était ravie: maintenant, même si les développeurs ont oublié d'écrire dans le journal en cas d'erreurs réseau, vous pouvez toujours au moins savoir si le trafic va vers l'hôte ou le port souhaité.
Quelle conclusion peut-on tirer de cette histoire? Même s'il n'y a pas de journaux, Linux a des utilitaires qui peuvent aider à localiser les problèmes.
Et si ce n'est pas un réseau?
Tout a bien fonctionné, mais un jour, Vanya a de nouveau reçu une erreur, cette fois différente:
MacBook-Pro-Ivan:~ ivantester$ curl http://12.34.56.78
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: File not found.</p>
<p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>
</body>
</html>
Vanya est retourné au serveur, mais cette fois, le problème n'était pas lié au réseau. Le journal de service indiquait également Fichier non trouvé et Vanya a décidé de comprendre pourquoi une telle erreur était soudainement apparue. Il sait qu'il existe un processus python3 -m http.server , mais il ne connaît pas le contenu de quel répertoire ce service affiche (ou, en d'autres termes, quel est le répertoire de travail courant de ce processus). Il découvre avec la commande lsof :
[root@ivan ~]# ps aux | grep python | grep "http.server"
root 20638 0.0 0.3 270144 13552 pts/2 S+ 08:29 0:00 python3 -m http.server
[root@ivan ~]# lsof -p 20638 | grep cwd
python3 20638 root cwd DIR 253,1 4096 1843551 /root/test_dir_srv2
Cela peut également être fait en utilisant la commande pwdx ou en utilisant le répertoire proc :
[root@ivan ~]# pwdx 20638 20638: /root/test_dir_srv2 [root@ivan ~]# ls -l /proc/20638/cwd lrwxrwxrwx 1 root root 0 Aug 31 08:37 /proc/20638/cwd -> /root/test_dir_srv2
Un tel répertoire existe vraiment sur le serveur et contient un fichier nommé test_file . Quel est le problème? Ivan a cherché sur Google et a trouvé l'utilitaire strace , avec lequel vous pouvez voir ce que le système appelle le processus (au fait, il y a un bon article sur Habré à propos de strace , et même pas un ). Vous pouvez soit démarrer un nouveau processus via strace , soit vous connecter avec cet utilitaire à un processus déjà en cours d'exécution. La deuxième option convenait à Vanya:
Sortie Strace
[root@ivan ~]# strace -ff -p 20638
strace: Process 20638 attached
restart_syscall(<... resuming interrupted poll ...>) = 0
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 1 ([{fd=4, revents=POLLIN}])
accept4(4, {sa_family=AF_INET, sin_port=htons(57530), sin_addr=inet_addr("127.0.0.1")}, [16], SOCK_CLOEXEC) = 5
clone(child_stack=0x7f2beeb28fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f2beeb299d0, tls=0x7f2beeb29700, child_tidptr=0x7f2beeb299d0) = 21062
futex(0x11204d0, FUTEX_WAIT_PRIVATE, 0, NULLstrace: Process 21062 attached
<unfinished ...>
[pid 21062] set_robust_list(0x7f2beeb299e0, 24) = 0
[pid 21062] futex(0x11204d0, FUTEX_WAKE_PRIVATE, 1) = 1
[pid 20638] <... futex resumed> ) = 0
[pid 20638] futex(0x921c9c, FUTEX_WAIT_BITSET_PRIVATE|FUTEX_CLOCK_REALTIME, 27, {1598879772, 978949000}, ffffffff <unfinished ...>
[pid 21062] futex(0x921c9c, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x921c98, {FUTEX_OP_SET, 0, FUTEX_OP_CMP_GT, 1}) = 1
[pid 20638] <... futex resumed> ) = 0
[pid 20638] futex(0x921cc8, FUTEX_WAIT_PRIVATE, 2, NULL <unfinished ...>
[pid 21062] futex(0x921cc8, FUTEX_WAKE_PRIVATE, 1) = 1
[pid 20638] <... futex resumed> ) = 0
[pid 20638] futex(0x921cc8, FUTEX_WAKE_PRIVATE, 1) = 0
[pid 20638] poll([{fd=4, events=POLLIN}], 1, 500 <unfinished ...>
[pid 21062] recvfrom(5, "GET / HTTP/1.1\r\nConnection: upgr"..., 8192, 0, NULL, NULL) = 153
[pid 21062] stat("/root/test_dir_srv/", 0x7f2beeb27350) = -1 ENOENT (No such file or directory)
[pid 21062] open("/root/test_dir_srv/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid 21062] write(2, "127.0.0.1 - - [31/Aug/2020 09:16"..., 70) = 70
[pid 21062] write(2, "127.0.0.1 - - [31/Aug/2020 09:16"..., 60) = 60
[pid 21062] sendto(5, "HTTP/1.0 404 File not found\r\nSer"..., 184, 0, NULL, 0) = 184
[pid 21062] sendto(5, "<!DOCTYPE HTML PUBLIC \"-//W3C//D"..., 469, 0, NULL, 0) = 469
[pid 21062] shutdown(5, SHUT_WR) = 0
[pid 21062] close(5) = 0
[pid 21062] madvise(0x7f2bee329000, 8368128, MADV_DONTNEED) = 0
[pid 21062] exit(0) = ?
[pid 21062] +++ exited with 0 +++
<... poll resumed> ) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500) = 0 (Timeout)
poll([{fd=4, events=POLLIN}], 1, 500^Cstrace: Process 20638 detached
<detached ...>
Habituellement, la sortie de strace est assez volumineuse (et peut-être très grande), il est donc plus pratique de la rediriger immédiatement vers un fichier et de rechercher les appels système nécessaires. Dans ce cas, vous pouvez immédiatement trouver que le service essaie d'ouvrir le répertoire / root / test_dir_srv / - quelqu'un l'a renommé et n'a pas redémarré le service après cela, il renvoie donc 404.
Si vous comprenez immédiatement quels appels système vous devez regarder, vous pouvez utiliser l'option -e :
[root@ivan ~]# strace -ff -e trace=open,stat -p 20638
strace: Process 20638 attached
strace: Process 21396 attached
[pid 21396] stat("/root/test_dir_srv/", 0x7f2beeb27350) = -1 ENOENT (No such file or directory)
[pid 21396] open("/root/test_dir_srv/", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
[pid 21396] +++ exited with 0 +++
^Cstrace: Process 20638 detached
: « » , strace. , , (, / ), . ltrace.
- ?
Vanya ne s'est pas arrêté là et a découvert qu'il existe un débogueur de projet GNU - GDB . Avec son aide, vous pouvez « entrer » dans le processus et même le modifier légèrement. Et Vanya a décidé d'essayer de trouver la dernière erreur en utilisant GDB . Il a suggéré que puisque le service affiche le contenu du répertoire, vous pouvez essayer de mettre un point d'arrêt sur la fonction open () et voir ce qui se passe:
Sortie de l'utilitaire Gdb
[root@ivan ~]# gdb -p 23998
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-119.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Attaching to process 23998
…
… < debugging symbols...>
...
0x00007f2284c0b20d in poll () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
Missing separate debuginfos, use: debuginfo-install keyutils-libs-1.5.8-3.el7.x86_64 krb5-libs-1.15.1-34.el7.x86_64 libcom_err-1.42.9-13.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libselinux-2.5-14.1.el7.x86_64 openssl-libs-1.0.2k-16.el7.x86_64 pcre-8.32-17.el7.x86_64 zlib-1.2.7-18.el7.x86_64
(gdb) set follow-fork-mode child
(gdb) b open
Breakpoint 1 at 0x7f2284c06d20: open. (2 locations)
(gdb) c
Continuing.
[New Thread 0x7f227a165700 (LWP 24030)]
[Switching to Thread 0x7f227a165700 (LWP 24030)]
Breakpoint 1, open64 () at ../sysdeps/unix/syscall-template.S:81
81 T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
(gdb) n
83 T_PSEUDO_END (SYSCALL_SYMBOL)
(gdb) n
_io_FileIO___init___impl (opener=<optimized out>, closefd=<optimized out>, mode=<optimized out>, nameobj=0x7f227a68f6f0, self=0x7f227a68f6c0) at ./Modules/_io/fileio.c:381
381 Py_END_ALLOW_THREADS
(gdb) n
379 self->fd = open(name, flags, 0666);
(gdb) n
381 Py_END_ALLOW_THREADS
(gdb) print name
$1 = 0x7f227a687c90 "/root/test_dir_srv/"
(gdb) q
A debugging session is active.
Inferior 1 [process 23998] will be detached.
Quit anyway? (y or n) y
Detaching from program: /usr/local/bin/python3.7, process 23998
[Inferior 1 (process 23998) detached]
Après la commande c ( continue ) , Vanya a lancé curl dans une autre console , a atteint un point d'arrêt dans le débogueur et a commencé à exécuter ce programme (c'est-à-dire le service) étape par étape. Dès qu'il a trouvé ouvert dans un nom de chemin , il a imprimé la valeur de cette variable et a vu " / root / test_dir_srv / ".
GDB est un outil puissant, et voici le cas d'utilisation le plus simple. Parfois, cela peut aider à reproduire des cas complexes (par exemple, vous pouvez suspendre le processus au bon moment et reproduire la condition de concurrence), cela aide également à lire les fichiers de vidage de mémoire.
Et si Docker?
À un moment donné, DevOps a décidé que le service serait maintenant déployé avec un conteneur Docker, et il était nécessaire de retester tous les cas trouvés par Vanya. Vanya a facilement googlé les éléments suivants:
- Vous pouvez utiliser tcpdump , strace et gdb dans un conteneur, mais vous devez garder à l'esprit les capacités Linux (il y a un article qui explique pourquoi strace ne fonctionnait pas dans un conteneur sans --cap-add = SYS_PTRACE ).
- L'option --pid peut être utilisée .
Mais il se demandait s'il était possible de voir tout le trafic allant vers le conteneur (ou depuis le conteneur) directement depuis l'hôte. Dans tcpdump ont la possibilité d'afficher n'importe quelle interface de trafic (option -i ), chaque conteneur correspond à une interface virtuelle Veth (cela peut être vu, par exemple, à travers l' ifconfig ou l'ip a ), mais comment savoir à quoi correspond le conteneur à quelle interface? Si le conteneur n'utilise pas de réseau hôte , il y aura à l'intérieur une interface réseau eth0 , à travers laquelle il pourra communiquer sur le réseau avec d'autres conteneurs et l'hôte. Il ne reste plus qu'à trouver l' ifindex dont l'interface sur l'hôte correspond à l' interface iflinketh0 du conteneur (vous pouvez lire ce que cela signifie ici ).
[root@ivan ~]# for f in `ls /sys/class/net/veth*/ifindex`; do echo $f; cat $f; done | grep -B 1 `docker exec test_service_container cat /sys/class/net/eth0/iflink` | head -1
/sys/class/net/veth6c18dba/ifindex
Vous pouvez maintenant exécuter tcpdump sur l'interface veth6c18dba :
tcpdump -i veth6c18dba
Mais il existe un moyen plus simple: vous pouvez trouver l'adresse IP du conteneur sur son réseau et écouter le trafic sur celui-ci:
[root@ivan ~]# docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' test_service_container
172.17.0.10
[root@ivan ~]# tcpdump -i any host 172.17.0.10
Conclusion: le débogage dans un conteneur Docker n'est pas un gros problème. Les utilitaires y fonctionnent et vous pouvez utiliser les journaux de docker pour lire les journaux .
conclusions
En tant qu'ingénieur responsable, Vanya a décidé de décrire brièvement de nouvelles informations pour lui-même dans la base de connaissances interne. Voici ce qu'il a écrit:
- Les journaux sont le meilleur ami de l'homme. Si un comportement inattendu du service est rencontré et qu'en même temps il n'écrit rien dans le journal, c'est une raison pour demander aux développeurs d'ajouter des journaux.
- , , . , Linux , .
- tcpdump. , .
- «» strace, ltrace gdb.
- , Docker-.
- /proc/PID. , /proc/PID/fd .
- Les utilitaires ps , ls , stat , lsof , ss , du , top , free , ip , ldconfig , ldd et autres peuvent également aider à obtenir diverses informations sur le système, les fichiers ou les processus .
J'espère que cette histoire vous a été utile, et au moins une fois, elle vous aidera à comprendre ce qui se passe lorsque vous déboguez quelque chose sous Linux. Si Vanya a raté quelque chose, partagez-le dans les commentaires!