beeping 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- beeping-1.0.0/.gitignore +8 -0
- beeping-1.0.0/LICENSE +21 -0
- beeping-1.0.0/PKG-INFO +95 -0
- beeping-1.0.0/README.md +73 -0
- beeping-1.0.0/beeping.py +722 -0
- beeping-1.0.0/pyproject.toml +36 -0
beeping-1.0.0/.gitignore
ADDED
beeping-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergey Pankov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
beeping-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: beeping
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Classic ping you can also hear: a high beep on send, a low blip on reply, the gap is the RTT
|
|
5
|
+
Project-URL: Homepage, https://github.com/trapwalker/beeping
|
|
6
|
+
Project-URL: Repository, https://github.com/trapwalker/beeping
|
|
7
|
+
Project-URL: Issues, https://github.com/trapwalker/beeping/issues
|
|
8
|
+
Author: Sergey Pankov
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: audio,beep,icmp,latency,network,ping,sound
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Topic :: System :: Networking :: Monitoring
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: numpy>=1.24
|
|
20
|
+
Requires-Dist: sounddevice>=0.4.6
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# beeping
|
|
24
|
+
|
|
25
|
+
> **English TL;DR:** classic `ping` you can also *hear* — a high «пик» beep on
|
|
26
|
+
> every packet sent, a low «пук» on every reply. The gap between them is your
|
|
27
|
+
> RTT, a lost packet is a «пик» without its «пук». CLI-compatible with the
|
|
28
|
+
> classic `ping`. Run: `uv run beeping.py ya.ru` (or `pipx install beeping`).
|
|
29
|
+
|
|
30
|
+
Классический `ping`, который ещё и **слышно**: каждый отправленный пакет —
|
|
31
|
+
«пик», каждый ответ — «пук». Пауза между ними = RTT, потеря пакета — «пик»
|
|
32
|
+
без «пука». Задержку и её изменения видно на слух.
|
|
33
|
+
|
|
34
|
+
## Запуск
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
uv run beeping.py [параметры] host # deps подтягиваются из PEP-723 шапки
|
|
38
|
+
uv run beeping.py ya.ru -i 0.5
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Зависимости: `numpy`, `sounddevice`. Python ≥ 3.10. Права root не нужны
|
|
42
|
+
(dgram-ICMP): macOS 10.14+ из коробки; на Linux —
|
|
43
|
+
`sysctl -w net.ipv4.ping_group_range="0 2147483647"` либо `sudo`.
|
|
44
|
+
|
|
45
|
+
## Совместимость с классическим ping
|
|
46
|
+
|
|
47
|
+
CLI совместим с BSD ping на macOS и iputils ping на Linux: getopt-стиль
|
|
48
|
+
(кластеризация `-qc5`, присоединённые значения `-i0.5`), long-опции GNU,
|
|
49
|
+
пер-платформенная семантика `-t`/`-W`/`-A` — как у «родного» ping каждой ОС.
|
|
50
|
+
Вывод в stdout — в формате ping своей платформы (BSD-стиль на macOS,
|
|
51
|
+
iputils-стиль на Linux); служебные сообщения beeping идут в stderr.
|
|
52
|
+
|
|
53
|
+
| Флаг | Значение | Поддержка |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `-c N` | стоп после N пакетов (0 = вечно) | ✅ |
|
|
56
|
+
| `-i SEC` | интервал, дробные | ✅ |
|
|
57
|
+
| `-s BYTES` | data bytes, default 56 (как у ping) | ✅ |
|
|
58
|
+
| `-l N` | предзагрузка N пакетов | ✅ |
|
|
59
|
+
| `-f` | flood: до 100/с, точки как у ping | ✅ |
|
|
60
|
+
| `-p HEX` | payload-паттерн (повторяется) | ✅ |
|
|
61
|
+
| `-t` | macOS: общий timeout (BSD) / Linux: TTL (iputils) | ✅ |
|
|
62
|
+
| `--ttl N` | TTL на любой платформе | ✅ |
|
|
63
|
+
| `-w SEC` | deadline | ✅ |
|
|
64
|
+
| `-W` | ожидание ответа: ms на macOS / сек на Linux | ✅ |
|
|
65
|
+
| `-a`, `-A` | BEL в выводе на ответ (macOS `-A` = audible, Linux `-A` = adaptive) | ✅ |
|
|
66
|
+
| `-D` | метки времени | ✅ |
|
|
67
|
+
| `-n`, `-q`, `-v`, `-o` | numeric / тихо / подроб / выход по 1-му ответу | ✅ |
|
|
68
|
+
| `-I iface\|addr`, `-S addr` | привязка к интерфейсу / адресу | ✅ |
|
|
69
|
+
| `-Q tos`, `-r`, `-b`, `--ttl` | TOS / bypass routing / broadcast | ✅ |
|
|
70
|
+
| `-4`, `-6` | IPv4 / IPv6 (ICMPv6) | ✅ |
|
|
71
|
+
| `-h`, `--help` | справка | ✅ |
|
|
72
|
+
| `-R`, `-m`, `-T`, `-L`, `-d`, `-U` | record route, mark, … | ⚠️ берутся, но игнорируются с warning в stderr |
|
|
73
|
+
|
|
74
|
+
Коды выхода, как у iputils ping: `0` — был хоть один ответ, `1` — ответов
|
|
75
|
+
нет, `2` — ошибка запуска (нераспознанный флаг, резолв, сокет). Работает и
|
|
76
|
+
`SIGINFO` (Ctrl+T на macOS): промежуточная статистика в stderr.
|
|
77
|
+
|
|
78
|
+
Кластеризация и long-опции: `ping -qc5 ya.ru` == `beeping -qc5 ya.ru` ==
|
|
79
|
+
`beeping --quiet --count 5 ya.ru`.
|
|
80
|
+
|
|
81
|
+
## Что не отключается
|
|
82
|
+
|
|
83
|
+
Звук. Это суть beeping: «пи́к»/«пук» всегда, любыми флагами. `-q` глушит только
|
|
84
|
+
текстовую выдачу; звук не пишется в stdout, поэтому парсеры вывода ping
|
|
85
|
+
работают как обычно.
|
|
86
|
+
|
|
87
|
+
## Примеры
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
uv run beeping.py ya.ru # по умолчанию: -i 1, -s 56
|
|
91
|
+
uv run beeping.py 1.1.1.1 -i 0.3 # джиттер слышнее
|
|
92
|
+
uv run beeping.py -f -c 50 ya.ru # шторм пи́ков на потерях
|
|
93
|
+
uv run beeping.py -6 ::1 # IPv6
|
|
94
|
+
uv run beeping.py ya.ru -v # подробности в stderr
|
|
95
|
+
```
|
beeping-1.0.0/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# beeping
|
|
2
|
+
|
|
3
|
+
> **English TL;DR:** classic `ping` you can also *hear* — a high «пик» beep on
|
|
4
|
+
> every packet sent, a low «пук» on every reply. The gap between them is your
|
|
5
|
+
> RTT, a lost packet is a «пик» without its «пук». CLI-compatible with the
|
|
6
|
+
> classic `ping`. Run: `uv run beeping.py ya.ru` (or `pipx install beeping`).
|
|
7
|
+
|
|
8
|
+
Классический `ping`, который ещё и **слышно**: каждый отправленный пакет —
|
|
9
|
+
«пик», каждый ответ — «пук». Пауза между ними = RTT, потеря пакета — «пик»
|
|
10
|
+
без «пука». Задержку и её изменения видно на слух.
|
|
11
|
+
|
|
12
|
+
## Запуск
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv run beeping.py [параметры] host # deps подтягиваются из PEP-723 шапки
|
|
16
|
+
uv run beeping.py ya.ru -i 0.5
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Зависимости: `numpy`, `sounddevice`. Python ≥ 3.10. Права root не нужны
|
|
20
|
+
(dgram-ICMP): macOS 10.14+ из коробки; на Linux —
|
|
21
|
+
`sysctl -w net.ipv4.ping_group_range="0 2147483647"` либо `sudo`.
|
|
22
|
+
|
|
23
|
+
## Совместимость с классическим ping
|
|
24
|
+
|
|
25
|
+
CLI совместим с BSD ping на macOS и iputils ping на Linux: getopt-стиль
|
|
26
|
+
(кластеризация `-qc5`, присоединённые значения `-i0.5`), long-опции GNU,
|
|
27
|
+
пер-платформенная семантика `-t`/`-W`/`-A` — как у «родного» ping каждой ОС.
|
|
28
|
+
Вывод в stdout — в формате ping своей платформы (BSD-стиль на macOS,
|
|
29
|
+
iputils-стиль на Linux); служебные сообщения beeping идут в stderr.
|
|
30
|
+
|
|
31
|
+
| Флаг | Значение | Поддержка |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `-c N` | стоп после N пакетов (0 = вечно) | ✅ |
|
|
34
|
+
| `-i SEC` | интервал, дробные | ✅ |
|
|
35
|
+
| `-s BYTES` | data bytes, default 56 (как у ping) | ✅ |
|
|
36
|
+
| `-l N` | предзагрузка N пакетов | ✅ |
|
|
37
|
+
| `-f` | flood: до 100/с, точки как у ping | ✅ |
|
|
38
|
+
| `-p HEX` | payload-паттерн (повторяется) | ✅ |
|
|
39
|
+
| `-t` | macOS: общий timeout (BSD) / Linux: TTL (iputils) | ✅ |
|
|
40
|
+
| `--ttl N` | TTL на любой платформе | ✅ |
|
|
41
|
+
| `-w SEC` | deadline | ✅ |
|
|
42
|
+
| `-W` | ожидание ответа: ms на macOS / сек на Linux | ✅ |
|
|
43
|
+
| `-a`, `-A` | BEL в выводе на ответ (macOS `-A` = audible, Linux `-A` = adaptive) | ✅ |
|
|
44
|
+
| `-D` | метки времени | ✅ |
|
|
45
|
+
| `-n`, `-q`, `-v`, `-o` | numeric / тихо / подроб / выход по 1-му ответу | ✅ |
|
|
46
|
+
| `-I iface\|addr`, `-S addr` | привязка к интерфейсу / адресу | ✅ |
|
|
47
|
+
| `-Q tos`, `-r`, `-b`, `--ttl` | TOS / bypass routing / broadcast | ✅ |
|
|
48
|
+
| `-4`, `-6` | IPv4 / IPv6 (ICMPv6) | ✅ |
|
|
49
|
+
| `-h`, `--help` | справка | ✅ |
|
|
50
|
+
| `-R`, `-m`, `-T`, `-L`, `-d`, `-U` | record route, mark, … | ⚠️ берутся, но игнорируются с warning в stderr |
|
|
51
|
+
|
|
52
|
+
Коды выхода, как у iputils ping: `0` — был хоть один ответ, `1` — ответов
|
|
53
|
+
нет, `2` — ошибка запуска (нераспознанный флаг, резолв, сокет). Работает и
|
|
54
|
+
`SIGINFO` (Ctrl+T на macOS): промежуточная статистика в stderr.
|
|
55
|
+
|
|
56
|
+
Кластеризация и long-опции: `ping -qc5 ya.ru` == `beeping -qc5 ya.ru` ==
|
|
57
|
+
`beeping --quiet --count 5 ya.ru`.
|
|
58
|
+
|
|
59
|
+
## Что не отключается
|
|
60
|
+
|
|
61
|
+
Звук. Это суть beeping: «пи́к»/«пук» всегда, любыми флагами. `-q` глушит только
|
|
62
|
+
текстовую выдачу; звук не пишется в stdout, поэтому парсеры вывода ping
|
|
63
|
+
работают как обычно.
|
|
64
|
+
|
|
65
|
+
## Примеры
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
uv run beeping.py ya.ru # по умолчанию: -i 1, -s 56
|
|
69
|
+
uv run beeping.py 1.1.1.1 -i 0.3 # джиттер слышнее
|
|
70
|
+
uv run beeping.py -f -c 50 ya.ru # шторм пи́ков на потерях
|
|
71
|
+
uv run beeping.py -6 ::1 # IPv6
|
|
72
|
+
uv run beeping.py ya.ru -v # подробности в stderr
|
|
73
|
+
```
|
beeping-1.0.0/beeping.py
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["numpy>=1.24", "sounddevice>=0.4.6"]
|
|
5
|
+
# ///
|
|
6
|
+
"""beeping — классический ping, который ещё и слышно.
|
|
7
|
+
|
|
8
|
+
CLI-флаги совместимы с классическим ping: getopt-стиль (кластеризация -qc5,
|
|
9
|
+
присоединённые значения -c5), семантика BSD ping на macOS (iputils на Linux),
|
|
10
|
+
GNU long-опции. Вывод — в формате ping своей платформы. Коды выхода:
|
|
11
|
+
0 — был хотя бы один ответ, 1 — ответов нет, 2 — ошибка запуска.
|
|
12
|
+
|
|
13
|
+
Туда — «пик», обратно — «пук»; пауза = RTT, потеря = «пик» без «пука».
|
|
14
|
+
Звук в beeping есть всегда — флагами он не отключается (звук не пишется
|
|
15
|
+
в stdout и не мешает парсингу вывода).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import random
|
|
21
|
+
import select
|
|
22
|
+
import signal
|
|
23
|
+
import socket
|
|
24
|
+
import struct
|
|
25
|
+
import sys
|
|
26
|
+
import threading
|
|
27
|
+
import time
|
|
28
|
+
from types import SimpleNamespace
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
import numpy as np
|
|
32
|
+
import sounddevice as sd
|
|
33
|
+
except ImportError as e: # запущено без uv
|
|
34
|
+
sys.exit(f"не хватает зависимостей ({e}) — запускай через uv: uv run {sys.argv[0]}")
|
|
35
|
+
|
|
36
|
+
SR = 44100
|
|
37
|
+
IS_MAC = sys.platform == "darwin"
|
|
38
|
+
|
|
39
|
+
# --- звук ---------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def tone(f0: float, f1: float, dur: float, vol: float) -> np.ndarray:
|
|
43
|
+
"""Синус с глиссандо f0→f1 и короткими фейдами, моно float32."""
|
|
44
|
+
n = int(dur * SR)
|
|
45
|
+
phase = 2.0 * np.pi * np.cumsum(np.linspace(f0, f1, n)) / SR
|
|
46
|
+
env = np.ones(n)
|
|
47
|
+
k = max(1, int(0.006 * SR))
|
|
48
|
+
env[:k] = np.linspace(0.0, 1.0, k)
|
|
49
|
+
env[-k:] = np.linspace(1.0, 0.0, k)
|
|
50
|
+
return (np.sin(phase) * env * vol).astype(np.float32)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# «пик» — короткий высокий тон; «пук» — низкий, съезжающий вниз («бульк»)
|
|
54
|
+
PICK = tone(1568, 1568, 0.050, 0.40)
|
|
55
|
+
PUK = tone(700, 320, 0.110, 0.55)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Mixer:
|
|
59
|
+
"""Одновременное воспроизведение налагающихся тонов без обрезания."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
self._pos = 0 # сколько сэмплов уже отдано в колонки
|
|
64
|
+
self._events: list[list] = [] # [start, samples]
|
|
65
|
+
try:
|
|
66
|
+
self.stream = sd.OutputStream(
|
|
67
|
+
samplerate=SR, channels=1, dtype="float32",
|
|
68
|
+
blocksize=0, latency="low", callback=self._cb,
|
|
69
|
+
)
|
|
70
|
+
except OSError as e:
|
|
71
|
+
sys.exit(f"beeping: не удалось открыть аудиовыход: {e}")
|
|
72
|
+
self.stream.start()
|
|
73
|
+
|
|
74
|
+
def play(self, samples: np.ndarray) -> None:
|
|
75
|
+
with self._lock:
|
|
76
|
+
if len(self._events) > 64: # защита от лавины тонов при -f
|
|
77
|
+
del self._events[: len(self._events) - 64]
|
|
78
|
+
self._events.append([self._pos + int(0.015 * SR), samples])
|
|
79
|
+
|
|
80
|
+
def _cb(self, out, frames, t, status) -> None:
|
|
81
|
+
try:
|
|
82
|
+
out[:] = 0.0
|
|
83
|
+
with self._lock:
|
|
84
|
+
base = self._pos
|
|
85
|
+
self._pos += frames
|
|
86
|
+
for start, arr in self._events:
|
|
87
|
+
i0 = max(base - start, 0)
|
|
88
|
+
i1 = min(len(arr), base + frames - start)
|
|
89
|
+
if i1 > i0:
|
|
90
|
+
j0 = start + i0 - base
|
|
91
|
+
j1 = j0 + (i1 - i0)
|
|
92
|
+
out[j0:j1, 0] += arr[i0:i1]
|
|
93
|
+
self._events = [ev for ev in self._events
|
|
94
|
+
if ev[0] + len(ev[1]) > self._pos]
|
|
95
|
+
np.clip(out, -1.0, 1.0, out=out)
|
|
96
|
+
except Exception:
|
|
97
|
+
pass # звук не должен ронять пинг
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# --- ICMP / ICMPv6 ------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def checksum(data: bytes) -> int:
|
|
104
|
+
if len(data) & 1:
|
|
105
|
+
data += b"\x00"
|
|
106
|
+
s = sum(struct.unpack(f"!{len(data) // 2}H", data))
|
|
107
|
+
s = (s >> 16) + (s & 0xFFFF)
|
|
108
|
+
s += s >> 16
|
|
109
|
+
return ~s & 0xFFFF
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_echo(fam: str, ident: int, seq: int, payload: bytes) -> bytes:
|
|
113
|
+
"""v6: чексумму считает ядро (IPV6_CHECKSUM, offset 2)."""
|
|
114
|
+
if fam == "6":
|
|
115
|
+
return struct.pack("!BBHHH", 128, 0, 0, ident, seq) + payload
|
|
116
|
+
pkt = struct.pack("!BBHHH", 8, 0, 0, ident, seq) + payload
|
|
117
|
+
return struct.pack("!BBHHH", 8, 0, checksum(pkt), ident, seq) + payload
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def open_icmp(v6: bool):
|
|
121
|
+
"""SOCK_DGRAM ICMP не требует root (macOS; Linux при ping_group_range).
|
|
122
|
+
Возвращает (sock, is_raw): raw — это фоллбэк, там нужен sudo."""
|
|
123
|
+
proto = socket.IPPROTO_ICMPV6 if v6 else socket.IPPROTO_ICMP
|
|
124
|
+
fam = socket.AF_INET6 if v6 else socket.AF_INET
|
|
125
|
+
for t in (socket.SOCK_DGRAM, socket.SOCK_RAW):
|
|
126
|
+
try:
|
|
127
|
+
return socket.socket(fam, t, proto), t == socket.SOCK_RAW
|
|
128
|
+
except OSError:
|
|
129
|
+
continue
|
|
130
|
+
sys.exit(
|
|
131
|
+
"beeping: не удалось открыть ICMP-сокет. Попробуй sudo, а на Linux:\n"
|
|
132
|
+
' sysctl -w net.ipv4.ping_group_range="0 2147483647"'
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_reply(data: bytes):
|
|
137
|
+
"""→ (type, code, ident, seq) или None. SOCK_RAW несёт IP-заголовок спереди."""
|
|
138
|
+
if data and data[0] >> 4 == 4:
|
|
139
|
+
data = data[(data[0] & 0x0F) * 4:]
|
|
140
|
+
if len(data) < 8:
|
|
141
|
+
return None
|
|
142
|
+
return data[0], data[1], *struct.unpack("!HH", data[4:8])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def parse_error_ref(data: bytes):
|
|
146
|
+
"""id/seq нашего пакета, вложенного в ICMP-ошибку."""
|
|
147
|
+
body = data[8:]
|
|
148
|
+
if len(body) >= 48 and body[0] >> 4 == 6: # внутри исходный IPv6-пакет
|
|
149
|
+
body = body[40:]
|
|
150
|
+
elif body and body[0] >> 4 == 4: # внутри исходный IPv4-пакет
|
|
151
|
+
body = body[(body[0] & 0x0F) * 4:]
|
|
152
|
+
if len(body) >= 8:
|
|
153
|
+
return struct.unpack("!HH", body[4:8])
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
ERR_NAMES = {
|
|
158
|
+
"4": {3: "Destination Net Unreachable", 11: "Time to live exceeded"},
|
|
159
|
+
"6": {1: "Destination Unreachable", 2: "Packet Too Big",
|
|
160
|
+
3: "Time Exceeded", 4: "Parameter Problem"},
|
|
161
|
+
}
|
|
162
|
+
ECHO_REQ = {"4": 8, "6": 128}
|
|
163
|
+
ECHO_REP = {"4": 0, "6": 129}
|
|
164
|
+
|
|
165
|
+
# --- CLI ----------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def usage(code: int) -> None:
|
|
169
|
+
t_desc = "timeout, s (BSD-style)" if IS_MAC else "ttl (iputils-style)"
|
|
170
|
+
w_desc = "waittime, ms (BSD-style)" if IS_MAC else "timeout, s (iputils-style)"
|
|
171
|
+
print(f"""Usage: beeping [options] host
|
|
172
|
+
|
|
173
|
+
Options:
|
|
174
|
+
-c count send count packets, then exit (0 = forever)
|
|
175
|
+
-i interval seconds between sends (fractional ok)
|
|
176
|
+
-s packetsize data bytes, default 56 (like ping)
|
|
177
|
+
-l preload preloaded burst, then normal mode
|
|
178
|
+
-f flood: packets as fast as replies come or 100/s
|
|
179
|
+
-p pattern payload pattern, hex (up to 16 bytes, repeats)
|
|
180
|
+
-t timeout {t_desc}
|
|
181
|
+
-w deadline hard deadline, seconds, then exit
|
|
182
|
+
-W waittime per-reply wait: {w_desc}
|
|
183
|
+
-a (and -A) audible: BEL (\\a) in the output per reply
|
|
184
|
+
-A (Linux) adaptive: interval follows RTT
|
|
185
|
+
-D timestamps on every output line
|
|
186
|
+
-n numeric output, no names
|
|
187
|
+
-o exit after first reply
|
|
188
|
+
-q quiet: startup line + summary only (sound keeps going)
|
|
189
|
+
-v verbose
|
|
190
|
+
-I iface|addr bind to interface (macOS: IP_BOUND_IF) or source address
|
|
191
|
+
-S srcaddr source address
|
|
192
|
+
-Q tos set TOS/DSCP (int or 0x..)
|
|
193
|
+
-r bypass routing tables
|
|
194
|
+
-b allow pinging broadcast
|
|
195
|
+
--ttl n IP TTL (any platform)
|
|
196
|
+
-4 / -6 force IPv4 / IPv6
|
|
197
|
+
-h --help this text
|
|
198
|
+
|
|
199
|
+
Sound (the whole point of beeping): every send = «пик», every reply = «пук».
|
|
200
|
+
The gap between them is the RTT, losses are «пик» with no «пук».""")
|
|
201
|
+
sys.exit(code)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def parse_args(argv: list[str]) -> SimpleNamespace:
|
|
205
|
+
cfg = SimpleNamespace(
|
|
206
|
+
host=None, count=0, interval=1.0, size=56, preload=0,
|
|
207
|
+
deadline=None, wait=None, flood=False, numeric=False, quiet=False,
|
|
208
|
+
verbose=False, bel=False, adaptive=False, timestamps=False,
|
|
209
|
+
onefire=False, broadcast=False, dontroute=False, report_out=False,
|
|
210
|
+
pattern_bytes=None, source=None, iface_name=None,
|
|
211
|
+
tos=None, ttl=None, family=socket.AF_UNSPEC,
|
|
212
|
+
)
|
|
213
|
+
warns: list[str] = []
|
|
214
|
+
|
|
215
|
+
def die(msg: str, code: int = 2) -> None:
|
|
216
|
+
print(f"beeping: {msg}", file=sys.stderr)
|
|
217
|
+
sys.exit(code)
|
|
218
|
+
|
|
219
|
+
def _int(v: str, flag: str) -> int:
|
|
220
|
+
try:
|
|
221
|
+
return int(v, 0)
|
|
222
|
+
except ValueError:
|
|
223
|
+
die(f"invalid value for {flag}: {v!r}")
|
|
224
|
+
|
|
225
|
+
def _float(v: str, flag: str) -> float:
|
|
226
|
+
try:
|
|
227
|
+
return float(v)
|
|
228
|
+
except ValueError:
|
|
229
|
+
die(f"invalid value for {flag}: {v!r}")
|
|
230
|
+
|
|
231
|
+
def apply_value(ch: str, v: str) -> None:
|
|
232
|
+
if ch == "c":
|
|
233
|
+
cfg.count = _int(v, "-c")
|
|
234
|
+
if cfg.count < 0:
|
|
235
|
+
die("count must be >= 0")
|
|
236
|
+
elif ch == "i":
|
|
237
|
+
cfg.interval = _float(v, "-i")
|
|
238
|
+
elif ch == "s":
|
|
239
|
+
cfg.size = _int(v, "-s")
|
|
240
|
+
elif ch == "l":
|
|
241
|
+
cfg.preload = _int(v, "-l")
|
|
242
|
+
elif ch == "t":
|
|
243
|
+
if IS_MAC:
|
|
244
|
+
cfg.deadline = _float(v, "-t")
|
|
245
|
+
else:
|
|
246
|
+
cfg.ttl = _int(v, "-t")
|
|
247
|
+
elif ch == "w":
|
|
248
|
+
cfg.deadline = _float(v, "-w")
|
|
249
|
+
elif ch == "W":
|
|
250
|
+
cfg.wait = _float(v, "-W") / 1000.0 if IS_MAC else _float(v, "-W")
|
|
251
|
+
elif ch == "I":
|
|
252
|
+
cfg.iface_raw = v
|
|
253
|
+
elif ch == "S":
|
|
254
|
+
cfg.source = v
|
|
255
|
+
elif ch == "p":
|
|
256
|
+
if len(v) % 2 or any(c not in "0123456789abcdefABCDEF" for c in v):
|
|
257
|
+
die("patterns must be specified as even-length hex digits")
|
|
258
|
+
cfg.pattern_bytes = bytes.fromhex(v) or b"\x00"
|
|
259
|
+
elif ch == "Q":
|
|
260
|
+
cfg.tos = _int(v, "-Q")
|
|
261
|
+
else: # T, m, M — известные, но не поддержанные
|
|
262
|
+
warns.append(f"flag -{ch} is not supported, ignored")
|
|
263
|
+
|
|
264
|
+
def apply_bool(ch: str) -> None:
|
|
265
|
+
if ch == "a":
|
|
266
|
+
cfg.bel = True
|
|
267
|
+
elif ch == "A":
|
|
268
|
+
if IS_MAC:
|
|
269
|
+
cfg.bel = True # BSD-style: -A audible
|
|
270
|
+
else:
|
|
271
|
+
cfg.adaptive = True # iputils-style: -A adaptive
|
|
272
|
+
elif ch == "D":
|
|
273
|
+
cfg.timestamps = True
|
|
274
|
+
elif ch == "f":
|
|
275
|
+
cfg.flood = True
|
|
276
|
+
elif ch == "n":
|
|
277
|
+
cfg.numeric = True
|
|
278
|
+
elif ch == "q":
|
|
279
|
+
cfg.quiet = True
|
|
280
|
+
elif ch == "v":
|
|
281
|
+
cfg.verbose = True
|
|
282
|
+
elif ch == "r":
|
|
283
|
+
cfg.dontroute = True
|
|
284
|
+
elif ch == "b":
|
|
285
|
+
cfg.broadcast = True
|
|
286
|
+
elif ch == "o":
|
|
287
|
+
cfg.onefire = True
|
|
288
|
+
elif ch == "O":
|
|
289
|
+
cfg.report_out = True
|
|
290
|
+
elif ch == "4":
|
|
291
|
+
cfg.family = socket.AF_INET
|
|
292
|
+
elif ch == "6":
|
|
293
|
+
cfg.family = socket.AF_INET6
|
|
294
|
+
else: # R, L, d, U — известные, но не поддержанные
|
|
295
|
+
warns.append(f"flag -{ch} is not supported, ignored")
|
|
296
|
+
|
|
297
|
+
LONG_VALUE = {"count": "c", "interval": "i", "size": "s", "packetsize": "s",
|
|
298
|
+
"preload": "l", "deadline": "w", "timeout": "t", "wait": "W",
|
|
299
|
+
"waittime": "W", "interface": "I", "source": "S",
|
|
300
|
+
"pattern": "p", "tos": "Q"}
|
|
301
|
+
LONG_BOOL = {"flood": "f", "numeric": "n", "quiet": "q", "verbose": "v",
|
|
302
|
+
"audible": "a", "timestamps": "D", "broadcast": "b",
|
|
303
|
+
"dontroute": "r", "one-reply": "o", "outstanding": "O",
|
|
304
|
+
"adaptive": "A", "ipv4": "4", "ipv6": "6"}
|
|
305
|
+
|
|
306
|
+
hosts: list[str] = []
|
|
307
|
+
VALUE_SHORT = "cisltwWIpSQTmM"
|
|
308
|
+
BOOL_SHORT = "aAbDfnqrvbo46OLRdU"
|
|
309
|
+
i = 0
|
|
310
|
+
args = list(argv)
|
|
311
|
+
while i < len(args):
|
|
312
|
+
a = args[i]
|
|
313
|
+
if a == "--":
|
|
314
|
+
hosts.extend(args[i + 1:])
|
|
315
|
+
break
|
|
316
|
+
if a.startswith("--"):
|
|
317
|
+
key, _, val = a[2:].partition("=")
|
|
318
|
+
if key == "help":
|
|
319
|
+
usage(0)
|
|
320
|
+
if key == "ttl":
|
|
321
|
+
if not val:
|
|
322
|
+
i += 1
|
|
323
|
+
if i >= len(args):
|
|
324
|
+
die("--ttl requires an argument")
|
|
325
|
+
val = args[i]
|
|
326
|
+
cfg.ttl = _int(val, "--ttl")
|
|
327
|
+
elif key in LONG_VALUE:
|
|
328
|
+
if not val:
|
|
329
|
+
i += 1
|
|
330
|
+
if i >= len(args):
|
|
331
|
+
die(f"--{key} requires an argument")
|
|
332
|
+
val = args[i]
|
|
333
|
+
apply_value(LONG_VALUE[key], val)
|
|
334
|
+
elif key in LONG_BOOL:
|
|
335
|
+
apply_bool(LONG_BOOL[key])
|
|
336
|
+
else:
|
|
337
|
+
die(f"unrecognized option --{key} (try -h)")
|
|
338
|
+
i += 1
|
|
339
|
+
continue
|
|
340
|
+
if len(a) > 1 and a[0] == "-":
|
|
341
|
+
body = a[1:]
|
|
342
|
+
j = 0
|
|
343
|
+
while j < len(body):
|
|
344
|
+
ch = body[j]
|
|
345
|
+
if ch == "h":
|
|
346
|
+
usage(0)
|
|
347
|
+
if ch in VALUE_SHORT:
|
|
348
|
+
v = body[j + 1:]
|
|
349
|
+
if not v:
|
|
350
|
+
i += 1
|
|
351
|
+
if i >= len(args):
|
|
352
|
+
die(f"option -{ch} requires an argument")
|
|
353
|
+
v = args[i]
|
|
354
|
+
apply_value(ch, v)
|
|
355
|
+
j = len(body)
|
|
356
|
+
elif ch in BOOL_SHORT:
|
|
357
|
+
apply_bool(ch)
|
|
358
|
+
j += 1
|
|
359
|
+
else:
|
|
360
|
+
die(f"invalid option -- '{ch}' (try -h)")
|
|
361
|
+
i += 1
|
|
362
|
+
continue
|
|
363
|
+
hosts.append(a)
|
|
364
|
+
i += 1
|
|
365
|
+
|
|
366
|
+
if len(hosts) == 0:
|
|
367
|
+
usage(2)
|
|
368
|
+
if len(hosts) > 1:
|
|
369
|
+
die("only one destination allowed")
|
|
370
|
+
cfg.host = hosts[0]
|
|
371
|
+
|
|
372
|
+
if cfg.family == socket.AF_UNSPEC:
|
|
373
|
+
cfg.family = socket.AF_INET6 if ":" in cfg.host else socket.AF_INET
|
|
374
|
+
|
|
375
|
+
if cfg.size < 0 or cfg.size > 65527:
|
|
376
|
+
die(f"invalid packetsize {cfg.size}")
|
|
377
|
+
if cfg.preload < 0:
|
|
378
|
+
die("preload must be >= 0")
|
|
379
|
+
if cfg.wait is not None and cfg.wait <= 0:
|
|
380
|
+
die("waittime must be positive")
|
|
381
|
+
if cfg.deadline is not None and cfg.deadline <= 0:
|
|
382
|
+
die("deadline must be positive")
|
|
383
|
+
if not cfg.flood and cfg.interval < 0.01:
|
|
384
|
+
die("interval below 10 ms needs -f")
|
|
385
|
+
if cfg.count and cfg.preload > cfg.count:
|
|
386
|
+
cfg.preload = cfg.count
|
|
387
|
+
cfg.iface_raw = getattr(cfg, "iface_raw", None)
|
|
388
|
+
if cfg.iface_raw:
|
|
389
|
+
if ":" in cfg.iface_raw:
|
|
390
|
+
cfg.source = cfg.iface_raw # IPv6-литерал
|
|
391
|
+
else:
|
|
392
|
+
try:
|
|
393
|
+
socket.inet_aton(cfg.iface_raw)
|
|
394
|
+
cfg.source = cfg.iface_raw # IPv4-литерал
|
|
395
|
+
except OSError:
|
|
396
|
+
cfg.iface_name = cfg.iface_raw
|
|
397
|
+
for w in warns:
|
|
398
|
+
print(f"beeping: {w}", file=sys.stderr)
|
|
399
|
+
return cfg
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# --- розетка ------------------------------------------------------------
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def build_socket(cfg: SimpleNamespace, v6: bool):
|
|
406
|
+
warns: list[str] = []
|
|
407
|
+
sock, is_raw = open_icmp(v6)
|
|
408
|
+
|
|
409
|
+
def so(level, opt, val, what):
|
|
410
|
+
if opt is None:
|
|
411
|
+
warns.append(f"{what}: not supported on this platform")
|
|
412
|
+
return
|
|
413
|
+
try:
|
|
414
|
+
sock.setsockopt(level, opt, val)
|
|
415
|
+
except OSError as e:
|
|
416
|
+
warns.append(f"{what}: {e}")
|
|
417
|
+
|
|
418
|
+
if cfg.iface_name:
|
|
419
|
+
if v6:
|
|
420
|
+
so(socket.IPPROTO_IPV6, getattr(socket, "IPV6_BOUND_IF", 125),
|
|
421
|
+
socket.if_nametoindex(cfg.iface_name), f"-I {cfg.iface_name}")
|
|
422
|
+
else:
|
|
423
|
+
so(socket.IPPROTO_IP, getattr(socket, "IP_BOUND_IF", 25),
|
|
424
|
+
socket.if_nametoindex(cfg.iface_name), f"-I {cfg.iface_name}")
|
|
425
|
+
if cfg.source:
|
|
426
|
+
try:
|
|
427
|
+
sock.bind((cfg.source, 0))
|
|
428
|
+
except OSError as e:
|
|
429
|
+
warns.append(f"bind {cfg.source}: {e}")
|
|
430
|
+
if cfg.broadcast:
|
|
431
|
+
so(socket.SOL_SOCKET, socket.SO_BROADCAST, 1, "-b")
|
|
432
|
+
if cfg.dontroute:
|
|
433
|
+
so(socket.SOL_SOCKET, socket.SO_DONTROUTE, 1, "-r")
|
|
434
|
+
if v6:
|
|
435
|
+
if is_raw: # на dgram-сокетах чексумму считает ядро (darwin: EPERM)
|
|
436
|
+
so(socket.IPPROTO_ICMPV6, getattr(socket, "IPV6_CHECKSUM", 26), 2, "checksum")
|
|
437
|
+
if cfg.tos is not None:
|
|
438
|
+
so(socket.IPPROTO_IPV6, getattr(socket, "IPV6_TCLASS", None), cfg.tos, "-Q")
|
|
439
|
+
if cfg.ttl is not None:
|
|
440
|
+
so(socket.IPPROTO_IPV6, getattr(socket, "IPV6_UNICAST_HOPS", None),
|
|
441
|
+
cfg.ttl, "ttl")
|
|
442
|
+
hop_opts = [getattr(socket, "IPV6_RECVHOPLIMIT", None)]
|
|
443
|
+
if IS_MAC:
|
|
444
|
+
hop_opts += [37, 20] # darwin: RFC3542 и RFC2292 версии
|
|
445
|
+
set_hlim = False
|
|
446
|
+
for opt in hop_opts:
|
|
447
|
+
if opt is None:
|
|
448
|
+
continue
|
|
449
|
+
try:
|
|
450
|
+
sock.setsockopt(socket.IPPROTO_IPV6, opt, 1)
|
|
451
|
+
set_hlim = True
|
|
452
|
+
break
|
|
453
|
+
except OSError:
|
|
454
|
+
continue
|
|
455
|
+
if not set_hlim:
|
|
456
|
+
warns.append("hlim recv: not supported on this platform")
|
|
457
|
+
else:
|
|
458
|
+
if cfg.tos is not None:
|
|
459
|
+
so(socket.IPPROTO_IP, socket.IP_TOS, cfg.tos, "-Q")
|
|
460
|
+
if cfg.ttl is not None:
|
|
461
|
+
so(socket.IPPROTO_IP, socket.IP_TTL, cfg.ttl, "ttl")
|
|
462
|
+
so(socket.IPPROTO_IP, getattr(socket, "IP_RECVTTL", 24 if IS_MAC else None),
|
|
463
|
+
1, "ttl recv")
|
|
464
|
+
sock.setblocking(False)
|
|
465
|
+
return sock, warns
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def anc_ttl(anc: list) -> int | None:
|
|
469
|
+
"""TTL/hlim из ancillary: на linux — 4-байтовый int, на darwin — один
|
|
470
|
+
байт. Уровень IP/IPv6 + короткий буфер однозначно указывают на TTL,
|
|
471
|
+
т.к. других cmsg мы не просим."""
|
|
472
|
+
for level, _typ, val in anc:
|
|
473
|
+
if level not in (socket.IPPROTO_IP, socket.IPPROTO_IPV6):
|
|
474
|
+
continue
|
|
475
|
+
if len(val) == 4:
|
|
476
|
+
try:
|
|
477
|
+
return struct.unpack("i", val)[0]
|
|
478
|
+
except struct.error:
|
|
479
|
+
pass
|
|
480
|
+
if len(val) == 1:
|
|
481
|
+
return val[0]
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
# --- main ---------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def main() -> None:
|
|
489
|
+
cfg = parse_args(sys.argv[1:])
|
|
490
|
+
v6 = cfg.family == socket.AF_INET6
|
|
491
|
+
fam = "6" if v6 else "4"
|
|
492
|
+
try:
|
|
493
|
+
sockaddr = socket.getaddrinfo(cfg.host, None, cfg.family)[0][4]
|
|
494
|
+
except OSError as e:
|
|
495
|
+
print(f"beeping: cannot resolve {cfg.host!r}: {e}", file=sys.stderr)
|
|
496
|
+
sys.exit(2)
|
|
497
|
+
ip = sockaddr[0].split("%")[0]
|
|
498
|
+
|
|
499
|
+
sock, warns = build_socket(cfg, v6)
|
|
500
|
+
for w in warns:
|
|
501
|
+
print(f"beeping: {w}", file=sys.stderr)
|
|
502
|
+
|
|
503
|
+
ident = random.randrange(1, 0x10000)
|
|
504
|
+
mixer = Mixer()
|
|
505
|
+
|
|
506
|
+
sent = received = 0
|
|
507
|
+
rtts: list[float] = []
|
|
508
|
+
pending: dict[int, list] = {} # seq → [t0(perf_counter), timer(monotonic)|None]
|
|
509
|
+
expired: dict[int, float] = {} # seq → t0 (снятые по -W; поздние ответы считаем)
|
|
510
|
+
seq_next = 0
|
|
511
|
+
preload_left = cfg.preload
|
|
512
|
+
t_start = time.monotonic()
|
|
513
|
+
deadline_at = t_start + cfg.deadline if cfg.deadline is not None else None
|
|
514
|
+
next_send = t_start
|
|
515
|
+
done_sending = False
|
|
516
|
+
done_at = None
|
|
517
|
+
stop = False
|
|
518
|
+
|
|
519
|
+
if IS_MAC: # классический BSD-заголовок
|
|
520
|
+
print(f"PING {cfg.host} ({ip}): {cfg.size} data bytes", flush=True)
|
|
521
|
+
else: # iputils-заголовок
|
|
522
|
+
print(f"PING {cfg.host} ({ip}) {cfg.size}({cfg.size + 28}) bytes of data.",
|
|
523
|
+
flush=True)
|
|
524
|
+
print("beeping: «пик» туда, «пук» обратно; пауза = задержка. Ctrl+C — итоги.",
|
|
525
|
+
file=sys.stderr, flush=True)
|
|
526
|
+
|
|
527
|
+
if hasattr(signal, "SIGINFO"): # Ctrl+T на macOS: промежуточная статистика
|
|
528
|
+
def _info(_sig, _frm):
|
|
529
|
+
print(f"beeping: {sent} sent, {received} received, "
|
|
530
|
+
f"{len(pending)} in flight", file=sys.stderr, flush=True)
|
|
531
|
+
try:
|
|
532
|
+
signal.signal(signal.SIGINFO, _info)
|
|
533
|
+
except Exception:
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
def make_payload() -> bytes:
|
|
537
|
+
pat = cfg.pattern_bytes or b"\x00"
|
|
538
|
+
if cfg.size >= 8: # как iputils: первые 8 байт — время отправки
|
|
539
|
+
n = cfg.size - 8
|
|
540
|
+
return struct.pack("!Q", time.perf_counter_ns()) + (pat * (n // len(pat) + 1))[:n]
|
|
541
|
+
return (pat * (cfg.size // len(pat) + 1))[:cfg.size]
|
|
542
|
+
|
|
543
|
+
def send_probe() -> None:
|
|
544
|
+
nonlocal sent, seq_next
|
|
545
|
+
try:
|
|
546
|
+
sock.sendto(build_echo(fam, ident, seq_next, make_payload()), sockaddr)
|
|
547
|
+
except OSError as e:
|
|
548
|
+
print(f"beeping: sendto: {e}", file=sys.stderr, flush=True)
|
|
549
|
+
return
|
|
550
|
+
sent += 1
|
|
551
|
+
now2 = time.monotonic()
|
|
552
|
+
if cfg.wait is not None:
|
|
553
|
+
timer = now2 + cfg.wait
|
|
554
|
+
elif cfg.report_out:
|
|
555
|
+
timer = now2 + max(0.05, cfg.interval)
|
|
556
|
+
else:
|
|
557
|
+
timer = None
|
|
558
|
+
pending[seq_next] = [time.perf_counter(), timer]
|
|
559
|
+
if cfg.flood and not cfg.quiet:
|
|
560
|
+
print(".", end="", flush=True)
|
|
561
|
+
seq_next = (seq_next + 1) & 0xFFFF
|
|
562
|
+
|
|
563
|
+
def handle(data: bytes, anc: list, src) -> None:
|
|
564
|
+
nonlocal received, stop
|
|
565
|
+
msg = parse_reply(data)
|
|
566
|
+
if msg is None:
|
|
567
|
+
return
|
|
568
|
+
mtype, mcode, mid, mseq = msg
|
|
569
|
+
src_ip = src[0].split("%")[0]
|
|
570
|
+
if mid != ident or src_ip != ip:
|
|
571
|
+
if cfg.verbose:
|
|
572
|
+
print(f"beeping: foreign ICMP type={mtype} code={mcode} from {src_ip}",
|
|
573
|
+
file=sys.stderr, flush=True)
|
|
574
|
+
return
|
|
575
|
+
if mtype == ECHO_REP[fam]: # эхо-ответ
|
|
576
|
+
e = pending.pop(mseq, None)
|
|
577
|
+
t0 = e[0] if e is not None else expired.get(mseq)
|
|
578
|
+
if t0 is None:
|
|
579
|
+
if cfg.verbose:
|
|
580
|
+
print(f"beeping: unexpected reply seq={mseq}", file=sys.stderr, flush=True)
|
|
581
|
+
return
|
|
582
|
+
rtt_ms = (time.perf_counter() - t0) * 1000.0
|
|
583
|
+
received += 1
|
|
584
|
+
rtts.append(rtt_ms)
|
|
585
|
+
mixer.play(PUK)
|
|
586
|
+
if cfg.wait is None or rtt_ms <= cfg.wait * 1000.0:
|
|
587
|
+
ttl = anc_ttl(anc)
|
|
588
|
+
if cfg.flood:
|
|
589
|
+
if not cfg.quiet:
|
|
590
|
+
print("\b" + ("\a" if cfg.bel else ""), end="", flush=True)
|
|
591
|
+
else:
|
|
592
|
+
line = f"{cfg.size} bytes from {ip}: icmp_seq={mseq}"
|
|
593
|
+
if ttl is not None:
|
|
594
|
+
label = "hlim" if v6 else "ttl"
|
|
595
|
+
line += f" {label}={ttl}"
|
|
596
|
+
line += f" time={rtt_ms:.3f} ms" if IS_MAC else f" time={rtt_ms:.1f} ms"
|
|
597
|
+
if cfg.timestamps:
|
|
598
|
+
line = f"[{time.time():.3f}] " + line
|
|
599
|
+
if cfg.bel:
|
|
600
|
+
line += "\a"
|
|
601
|
+
if not cfg.quiet:
|
|
602
|
+
print(line, flush=True)
|
|
603
|
+
elif cfg.verbose:
|
|
604
|
+
print(f"beeping: late reply seq={mseq} (counted, not printed)",
|
|
605
|
+
file=sys.stderr, flush=True)
|
|
606
|
+
if cfg.onefire:
|
|
607
|
+
stop = True
|
|
608
|
+
elif mtype in ERR_NAMES[fam]:
|
|
609
|
+
ref = parse_error_ref(data)
|
|
610
|
+
if ref and ref[0] == ident and ref[1] in pending:
|
|
611
|
+
pending.pop(ref[1])
|
|
612
|
+
if not cfg.quiet:
|
|
613
|
+
print(f"From {ip}: icmp_seq={ref[1]} "
|
|
614
|
+
f"{ERR_NAMES[fam][mtype]}", flush=True)
|
|
615
|
+
elif cfg.verbose:
|
|
616
|
+
print(f"beeping: ICMP type={mtype} code={mcode} from {ip}",
|
|
617
|
+
file=sys.stderr, flush=True)
|
|
618
|
+
|
|
619
|
+
def drain() -> None:
|
|
620
|
+
while True:
|
|
621
|
+
try:
|
|
622
|
+
data, anc, _flags, src = sock.recvmsg(2048, 512)
|
|
623
|
+
except (BlockingIOError, InterruptedError):
|
|
624
|
+
break
|
|
625
|
+
except OSError:
|
|
626
|
+
break
|
|
627
|
+
handle(data, anc, src)
|
|
628
|
+
|
|
629
|
+
def reap(now2: float) -> None:
|
|
630
|
+
if cfg.wait is not None:
|
|
631
|
+
for sq in [s for s, e in pending.items()
|
|
632
|
+
if e[1] is not None and now2 >= e[1]]:
|
|
633
|
+
# BSD-семантика: молча уходит в потери, поздний ответ учтётся
|
|
634
|
+
expired[sq] = pending.pop(sq)[0]
|
|
635
|
+
elif cfg.report_out:
|
|
636
|
+
for sq, e in list(pending.items()):
|
|
637
|
+
if e[1] is not None and now2 >= e[1]:
|
|
638
|
+
print(f"no answer yet for icmp_seq={sq}", flush=True)
|
|
639
|
+
e[1] = now2 + max(0.05, cfg.interval)
|
|
640
|
+
|
|
641
|
+
try:
|
|
642
|
+
while True:
|
|
643
|
+
now = time.monotonic()
|
|
644
|
+
if done_sending:
|
|
645
|
+
timeout = 0.05
|
|
646
|
+
elif cfg.flood:
|
|
647
|
+
timeout = max(0.0, min(next_send - now, 0.01))
|
|
648
|
+
else:
|
|
649
|
+
timeout = max(0.0, next_send - now)
|
|
650
|
+
if pending and (cfg.wait is not None or cfg.report_out):
|
|
651
|
+
nd = min(e[1] for e in pending.values())
|
|
652
|
+
timeout = min(timeout, max(0.0, nd - now))
|
|
653
|
+
ready, _, _ = select.select([sock], [], [], timeout)
|
|
654
|
+
if ready:
|
|
655
|
+
drain()
|
|
656
|
+
if stop:
|
|
657
|
+
break
|
|
658
|
+
now = time.monotonic()
|
|
659
|
+
reap(now)
|
|
660
|
+
if deadline_at is not None and now >= deadline_at:
|
|
661
|
+
break # как ping -t: выходим сразу, недождённые — потери
|
|
662
|
+
if not done_sending:
|
|
663
|
+
if cfg.flood:
|
|
664
|
+
want = (preload_left > 0 or not pending
|
|
665
|
+
or now >= next_send) and len(pending) < 100
|
|
666
|
+
else:
|
|
667
|
+
want = preload_left > 0 or now >= next_send
|
|
668
|
+
if want and (cfg.count == 0 or sent < cfg.count):
|
|
669
|
+
send_probe()
|
|
670
|
+
if preload_left > 0:
|
|
671
|
+
preload_left -= 1
|
|
672
|
+
if preload_left == 0:
|
|
673
|
+
next_send = now + cfg.interval
|
|
674
|
+
elif cfg.flood:
|
|
675
|
+
next_send = now + 0.01
|
|
676
|
+
else:
|
|
677
|
+
next_send = max(next_send + cfg.interval, now + 0.02)
|
|
678
|
+
if cfg.count and sent >= cfg.count:
|
|
679
|
+
done_sending = True
|
|
680
|
+
done_at = now + max(1.0, cfg.interval)
|
|
681
|
+
elif not pending or now >= done_at:
|
|
682
|
+
break
|
|
683
|
+
except KeyboardInterrupt:
|
|
684
|
+
pass
|
|
685
|
+
finally:
|
|
686
|
+
try:
|
|
687
|
+
mixer.stream.stop(); mixer.stream.close()
|
|
688
|
+
except Exception:
|
|
689
|
+
pass
|
|
690
|
+
try:
|
|
691
|
+
sock.close()
|
|
692
|
+
except Exception:
|
|
693
|
+
pass
|
|
694
|
+
if cfg.flood and not cfg.quiet:
|
|
695
|
+
print(flush=True)
|
|
696
|
+
elapsed = (time.monotonic() - t_start) * 1000
|
|
697
|
+
lost = sent - received
|
|
698
|
+
if IS_MAC:
|
|
699
|
+
print(f"\n--- {cfg.host} ping statistics ---")
|
|
700
|
+
print(f"{sent} packets transmitted, {received} packets received, "
|
|
701
|
+
f"{lost / sent * 100:.1f}% packet loss" if sent else
|
|
702
|
+
f"0 packets transmitted, 0 packets received, 100.0% packet loss")
|
|
703
|
+
if rtts:
|
|
704
|
+
mn, av, mx = min(rtts), sum(rtts) / len(rtts), max(rtts)
|
|
705
|
+
var = sum((r - av) ** 2 for r in rtts) / len(rtts)
|
|
706
|
+
print(f"round-trip min/avg/max/stddev = "
|
|
707
|
+
f"{mn:.3f}/{av:.3f}/{mx:.3f}/{var ** 0.5:.3f} ms")
|
|
708
|
+
else:
|
|
709
|
+
print(f"\n--- {cfg.host} ping statistics ---")
|
|
710
|
+
print(f"{sent} packets transmitted, {received} received, "
|
|
711
|
+
f"{lost / sent * 100:g}% packet loss, time {elapsed:.0f}ms"
|
|
712
|
+
if sent else "0 packets transmitted, 0 received, 100% packet loss")
|
|
713
|
+
if rtts:
|
|
714
|
+
mn, av, mx = min(rtts), sum(rtts) / len(rtts), max(rtts)
|
|
715
|
+
mdev = sum(abs(r - av) for r in rtts) / len(rtts)
|
|
716
|
+
print(f"rtt min/avg/max/mdev = "
|
|
717
|
+
f"{mn:.3f}/{av:.3f}/{mx:.3f}/{mdev:.3f} ms")
|
|
718
|
+
sys.exit(0 if received else 1)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
if __name__ == "__main__":
|
|
722
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "beeping"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Classic ping you can also hear: a high beep on send, a low blip on reply, the gap is the RTT"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Sergey Pankov" }]
|
|
13
|
+
keywords = ["ping", "icmp", "audio", "sound", "beep", "network", "latency"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: System Administrators",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: System :: Networking :: Monitoring",
|
|
21
|
+
]
|
|
22
|
+
dependencies = ["numpy>=1.24", "sounddevice>=0.4.6"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
beeping = "beeping:main"
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/trapwalker/beeping"
|
|
29
|
+
Repository = "https://github.com/trapwalker/beeping"
|
|
30
|
+
Issues = "https://github.com/trapwalker/beeping/issues"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
only-include = ["beeping.py"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.sdist]
|
|
36
|
+
only-include = ["beeping.py", "README.md", "LICENSE", "pyproject.toml"]
|