From 479d2d03d5f994a8c72a71f20f345a91788648e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 15:51:25 +0000 Subject: [PATCH] Add ping tool as a separately-releasable application module ping lives under tools/ping as its own Application-type DMOD module, built on dmicmp's existing Echo Request/Reply API (dmicmp_v4_send_echo_request(), dmicmp_register_echo_listener()). Like dmnet's tools/ifconfig, it gets its own CMakeLists.txt + .dmr, so every tagged release packages ping--.zip alongside dmicmp itself. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01V4i6Vs6fjG9cdHGj3AMpD5 --- CMakeLists.txt | 7 + README.md | 10 ++ tools/CMakeLists.txt | 2 + tools/ping/CMakeLists.txt | 56 +++++++ tools/ping/README.md | 108 ++++++++++++ tools/ping/docs/README.md | 27 +++ tools/ping/docs/ping.md | 114 +++++++++++++ tools/ping/ping.dmr | 45 +++++ tools/ping/src/ping.c | 289 ++++++++++++++++++++++++++++++++ tools/ping/tests/CMakeLists.txt | 23 +++ tools/ping/tests/ping_test.c | 163 ++++++++++++++++++ 11 files changed, 844 insertions(+) create mode 100644 tools/CMakeLists.txt create mode 100644 tools/ping/CMakeLists.txt create mode 100644 tools/ping/README.md create mode 100644 tools/ping/docs/README.md create mode 100644 tools/ping/docs/ping.md create mode 100644 tools/ping/ping.dmr create mode 100644 tools/ping/src/ping.c create mode 100644 tools/ping/tests/CMakeLists.txt create mode 100644 tools/ping/tests/ping_test.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8321b4d..94bc607 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,3 +98,10 @@ dmod_link_modules(${DMOD_MODULE_NAME} enable_testing() add_subdirectory(tests) + +# Application-type DMOD modules built as CLI tools around dmicmp's API. +# Added after dmod_add_library() above so ${DMOD_MODULE_NAME}_if already +# exists - tools/ping links it directly, the same way tests/CMakeLists.txt +# does, rather than fetching dmicmp's own headers back via +# dmod_link_modules() as if it were an external module. +add_subdirectory(tools) diff --git a/README.md b/README.md index 1daede9..6d8998b 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,13 @@ dmicmp_register_echo_listener(my_identifier, my_echo_reply_handler); dmicmp_v4_send_echo_request(&dst, my_identifier, 1, NULL, 0, DMARP_DEFAULT_TIMEOUT_MS); ``` +## Tools + +- **[tools/ping](tools/ping/README.md)** - CLI to send ICMPv4 Echo + Requests to a host and report the replies, built on the API below. + It's its own Application-type DMOD module (`ping`), released alongside + `dmicmp` itself in every tagged release. + ## API | Function | Description | @@ -112,6 +119,9 @@ dmicmp/ ├── tests/ │ ├── CMakeLists.txt │ └── dmicmp_test.c +├── tools/ +│ ├── CMakeLists.txt +│ └── ping/ # ping CLI - its own Application-type module, see tools/ping/README.md ├── CMakeLists.txt ├── Makefile ├── dmicmp.dmr diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..f6e656a --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,2 @@ +# Application-type DMOD modules that live under dmicmp/tools/. +add_subdirectory(ping) diff --git a/tools/ping/CMakeLists.txt b/tools/ping/CMakeLists.txt new file mode 100644 index 0000000..979a833 --- /dev/null +++ b/tools/ping/CMakeLists.txt @@ -0,0 +1,56 @@ +# Name of the module +set(DMOD_MODULE_NAME ping) + +# Author (should be string) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") + +# Stack size for the module (should be integer) +set(DMOD_STACK_SIZE 1024) + +# Priority for the thread of the module (should be integer) +set(DMOD_PRIORITY 0) + +# Path to the .dmr file - enables release package creation in build/packages/ +set(DMOD_DMR_PATH ${CMAKE_CURRENT_SOURCE_DIR}/ping.dmr) + +# +# dmod_add_executable - create an executable module +# it has the same signature as add_executable +# and can be used in the same way after the creation +# (for example, to link libraries) +# +dmod_add_executable(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + src/ping.c +) + +# dmicmp is this same repository's own top-level module, already built as +# part of this CMake configure by the time tools/ is added (see the root +# CMakeLists.txt) - link its dmicmp_if target directly, exactly like +# tests/CMakeLists.txt does, instead of fetching dmicmp's own published +# headers back via dmod_link_modules() as if it were an external module. +target_link_libraries(${DMOD_MODULE_NAME} dmicmp_if) + +# dmip: dmicmp.h #includes dmip.h directly (dmip_addr_t, dmip_send()'s +# family) - needed for that header to resolve even though ping.c +# itself never calls a dmip_* function (same reasoning dmicmp's own +# CMakeLists.txt gives for linking dmroute/dmnetif header-only). +# dmroute: dmip.h needs dmroute_addr_t (re-exported as dmip_addr_t) +# directly - dmicmp_if alone doesn't carry dmroute's own include +# dir. +# dmnetif: dmip.h needs dmnetif_iface_t for dmip_protocol_handler_t: not +# called from ping.c, just part of the header chain dmicmp.h +# pulls in. +# dmarp: DMARP_DEFAULT_TIMEOUT_MS, the ARP-resolution timeout forwarded to +# dmicmp_v4_send_echo_request(). +# dmosi: dmosi_semaphore_*() to block for the matching Echo Reply (with a +# timeout), dmosi_get_tick_count() for round-trip time, and +# dmosi_thread_sleep() for the delay between requests. +dmod_link_modules(${DMOD_MODULE_NAME} + dmip + dmroute + dmnetif + dmarp + dmosi +) + +add_subdirectory(tests) diff --git a/tools/ping/README.md b/tools/ping/README.md new file mode 100644 index 0000000..99c298d --- /dev/null +++ b/tools/ping/README.md @@ -0,0 +1,108 @@ +# ping + +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE) + +`ping` DMOD application module - a CLI to send ICMPv4 Echo Requests to a +host and report the matching Echo Replies, built on top of +[dmicmp](../../)'s own Echo Request/Reply API. It builds/parses no ICMP +bytes itself, only calls `dmicmp_v4_send_echo_request()` and +`dmicmp_register_echo_listener()`/`_unregister_echo_listener()`. + +## Usage + +``` +ping
Send 4 ICMPv4 Echo Requests to
+ping --help | -h Show this help + +Options: + -c Number of Echo Requests to send (default: 4) + -i Delay between requests, in milliseconds (default: 1000) + -W Time to wait for a reply before it's lost, in milliseconds (default: 1000) + -s Number of payload bytes to send, 0-1024 (default: 32) +``` + +`
` must be a literal dotted-decimal IPv4 address - dmicmp has no +hostname resolution, and no ICMPv6 send path yet (see +[include/dmicmp.h](../../include/dmicmp.h)'s top comment). + +Example output: + +``` +$ ping 192.168.1.1 +PING 192.168.1.1: 32 data bytes +32 bytes from 192.168.1.1: icmp_seq=0 time=3 ms +32 bytes from 192.168.1.1: icmp_seq=1 time=2 ms +32 bytes from 192.168.1.1: icmp_seq=2 time=2 ms +32 bytes from 192.168.1.1: icmp_seq=3 time=3 ms + +--- 192.168.1.1 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +``` + +## Building + +This module lives under `tools/ping` inside the `dmicmp` repository and is +built as part of the parent's CMake configure (the top-level +`CMakeLists.txt` calls `add_subdirectory(tools)`, whose own +`CMakeLists.txt` calls `add_subdirectory(ping)`) - it is not built +standalone. + +```bash +mkdir -p build +cd build +cmake .. +cmake --build . --target ping +``` + +Pass `-DDMOD_DIR=/path/to/local/dmod` to build against a local dmod checkout +instead of fetching `develop` from GitHub. + +It is also released as its own separate module-type application: any +tagged release of this repository packages `ping` alongside `dmicmp` +itself (see `build/packages/ping/` after a build with `DMOD_DMR_PATH` set, +and the repository's release workflow). + +## Testing + +`tests/ping_test.c` drives this module through `Dmod_RunModule("ping", +argc, argv)` (loads it, runs `main()`, unloads it again - see +`Dmod_RunModule` in `dmod.h`) rather than depending on `ping` as a linked +module: an Application-type module can't be loaded as another module's +dependency (only a Library-type module can), and `Dmod_RunModule` +sidesteps that entirely by loading/running/unloading it on demand instead +of holding it resident for the test's lifetime. + +## Documentation + +See the `docs/` directory: + +- **[ping.md](docs/ping.md)** - Usage, exit codes, and how this module is + tested + +View documentation using `dmf-man ping`. + +## Project Structure + +``` +ping/ +├── docs/ +│ ├── README.md +│ └── ping.md +├── src/ +│ └── ping.c +├── tests/ +│ ├── CMakeLists.txt +│ └── ping_test.c +├── CMakeLists.txt +└── ping.dmr +``` + +LICENSE is shared with the rest of the `dmicmp` repository (`../../LICENSE`). + +## Author + +Patryk Kubiak + +## License + +MIT diff --git a/tools/ping/docs/README.md b/tools/ping/docs/README.md new file mode 100644 index 0000000..facd0d6 --- /dev/null +++ b/tools/ping/docs/README.md @@ -0,0 +1,27 @@ +# ping Documentation + +Welcome to the ping module documentation. + +## Contents + +- **[ping.md](ping.md)** - Usage, exit codes, and how this module is tested + +## Quick Reference + +``` +ping
Send 4 ICMPv4 Echo Requests to
+ping --help | -h Show this help + +Options: + -c Number of Echo Requests to send (default: 4) + -i Delay between requests, in milliseconds (default: 1000) + -W Time to wait for a reply before it's lost, in milliseconds (default: 1000) + -s Number of payload bytes to send, 0-1024 (default: 32) +``` + +View documentation using `dmf-man`: + +```bash +dmf-man ping # Main documentation +dmf-man ping ping # ping.md +``` diff --git a/tools/ping/docs/ping.md b/tools/ping/docs/ping.md new file mode 100644 index 0000000..b729cea --- /dev/null +++ b/tools/ping/docs/ping.md @@ -0,0 +1,114 @@ +# ping + +## Overview + +`ping` is a CLI over [dmicmp](../../..)'s Echo Request/Reply API - it sends +one or more ICMPv4 Echo Requests to a literal IPv4 address and reports +each matching Echo Reply, or that the request was lost. It never builds +or parses an ICMP message itself; everything goes through +`dmicmp_v4_send_echo_request()` to send and +`dmicmp_register_echo_listener()`/`dmicmp_unregister_echo_listener()` to +learn about the reply (see `include/dmicmp.h`). + +`
` must be a literal dotted-decimal IPv4 address (e.g. +`192.168.1.1`) - dmicmp has no hostname resolution, and no ICMPv6 send +path yet (see `dmicmp.h`'s top comment), so `ping` cannot resolve a name +or reach an IPv6 destination either. + +## Usage + +``` +ping
Send 4 ICMPv4 Echo Requests to
+ping --help | -h Show this help + +Options: + -c Number of Echo Requests to send (default: 4) + -i Delay between requests, in milliseconds (default: 1000) + -W Time to wait for a reply before it's lost, in milliseconds (default: 1000) + -s Number of payload bytes to send, 0-1024 (default: 32) +``` + +Example output: + +``` +$ ping 192.168.1.1 +PING 192.168.1.1: 32 data bytes +32 bytes from 192.168.1.1: icmp_seq=0 time=3 ms +32 bytes from 192.168.1.1: icmp_seq=1 time=2 ms +32 bytes from 192.168.1.1: icmp_seq=2 time=2 ms +32 bytes from 192.168.1.1: icmp_seq=3 time=3 ms + +--- 192.168.1.1 ping statistics --- +4 packets transmitted, 4 packets received, 0% packet loss +``` + +`time=` is measured locally, from just before +`dmicmp_v4_send_echo_request()` is called to the moment the matching +reply's listener callback posts the semaphore `ping` is waiting on +(`dmosi_get_tick_count()` before/after) - it includes ARP resolution time +for the very first request to a given host, the same way a real ping's +first RTT sample often does. + +## How a request/reply round works + +Each Echo Request reuses the same ICMP identifier for the whole `ping` +run (derived from the tick count at startup, so two concurrent `ping` +invocations are unlikely to collide) but a fresh sequence number per +request. Before sending, `ping`: + +1. Calls `dmicmp_register_echo_listener(identifier, echo_reply_handler)`. +2. Calls `dmicmp_v4_send_echo_request(...)`. +3. Blocks on a semaphore (created with `dmosi_semaphore_create(0, 1)`) for + up to `-W`'s timeout, via `dmosi_semaphore_wait()`. +4. Calls `dmicmp_unregister_echo_listener(identifier)` unconditionally, + whether or not a reply arrived in time. + +`echo_reply_handler()` only posts the semaphore if the reply's sequence +number matches the request currently outstanding - a reply that arrives +late (after its own request already timed out and got unregistered) is +simply never delivered at all, since `dmicmp` drops a reply for an +identifier with no registered listener; the sequence check is an extra +guard against the much narrower race where a *new* registration for the +next request is already in place by the time a very late reply for the +*previous* one arrives. + +## Exit codes + +- `0` - at least one Echo Request got a matching reply, or `--help`/`-h` + was given +- `1` - bad arguments (missing/invalid address, unknown flag, an + out-of-range `-c`/`-W`/`-s` value), or every Echo Request was lost + (no reply within `-W`'s timeout, or `dmicmp_v4_send_echo_request()` + itself failed - e.g. no route to the destination) + +## Testing + +`tests/ping_test.c` drives this module through `Dmod_RunModule("ping", +argc, argv)` (loads it, runs `main()`, unloads it again - see +`Dmod_RunModule` in `dmod.h`) rather than depending on `ping` as a linked +module: an Application-type module can't be loaded as another module's +dependency (only a Library-type module can), and `Dmod_RunModule` +sidesteps that entirely by loading/running/unloading it on demand instead +of holding it resident for the test's lifetime. + +A "/dev/null"-backed `dmnetif` fixture, with a `dmroute` route and a +`dmarp` cache entry pointed at it, lets a send be exercised up through +`dmip_send()` itself - same pattern `dmicmp`'s own +`tests/dmicmp_test.c` uses for `dmicmp_v4_send_error()`/ +`_send_dest_unreachable()`. No real driver backs the fixture, so +`dmicmp_v4_send_echo_request()` still fails there (`dmnetif_send()` -> +`-EIO`) and no real Echo Reply can ever arrive - every test step expects +`ping` to report loss, not success; they only prove argument handling and +a full send attempt complete without crashing or hanging, and report the +honest outcome. + +## Dependencies + +- `dmicmp` - builds/sends the Echo Request and delivers the matching Echo + Reply (see the top-level [dmicmp](../../..) module) +- `dmroute` - the address type (`dmroute_addr_t`, re-exported by `dmicmp` + as `dmip_addr_t`) `ping` parses `
` into +- `dmarp` - `DMARP_DEFAULT_TIMEOUT_MS`, the ARP-resolution timeout + forwarded to `dmicmp_v4_send_echo_request()` +- `dmosi` - the semaphore `ping` waits for a reply on, the tick count used + to measure round-trip time, and the sleep between requests diff --git a/tools/ping/ping.dmr b/tools/ping/ping.dmr new file mode 100644 index 0000000..bd2e337 --- /dev/null +++ b/tools/ping/ping.dmr @@ -0,0 +1,45 @@ +# DMOD Resource File for ping +# This file specifies where resources should be installed +# +# ping lives nested under dmicmp/tools/ping rather than being its own +# top-level repo, so ${repo_dir}/${build_dir} below (which always resolve +# to the dmicmp repo/build root, not this subdirectory) are prefixed with +# tools/ping - same convention dmnet/tools/ifconfig/ifconfig.dmr uses. + +# === Core Module === +# Main module file - always installed +dmf=./${module}.dmf => ${destination}/${module}.dmf [origin=${dmf_dir}/${module}.dmf] + +# Compressed module +dmfc=./${module}.dmfc => ${destination}/${module}.dmfc [origin=${build_dir}/dmfc/${module}.dmfc] + +# Dependencies file (if exists) +dmd=./${module}.dmd => ${destination}/${module}.dmd [origin=${dmf_dir}/${module}.dmd] + +# Version information +version=./${module}_version.txt => ${destination}/${module}_version.txt [origin=${dmf_dir}/${module}_version.txt] + +# === Documentation === +# Module documentation in markdown format for dmf-man tool +docs=./docs => ${destination}/${module}/docs [origin=${repo_dir}/tools/ping/docs] + +# README file +readme=./README.md => ${destination}/${module}/README.md [origin=${repo_dir}/tools/ping/README.md] + +# === Header Files === +# No committed include/ dir of its own (its API is dmicmp's, see the +# top-level dmicmp module) - just the generated module definitions header +# every DMOD module gets, so dmf-man/consumers can still see it. +# Destination must be a full *file* path (not a bare "include" dir) since +# there's no separate directory-origin here to tell the packaging tool to +# treat it as one - see dmicmp.dmr's inc= entry for the directory+file +# combined case. +inc=./include/${module}_defs.h => ${destination}/${module}/include/${module}_defs.h [origin=${build_dir}/tools/ping/${module}_defs.h] + +# === License === +# License file - shared at the dmicmp repo root (same "nested module +# shares the parent repo's LICENSE" convention as dmnet/tools/ifconfig) +license=./LICENSE => ${destination}/${module}/LICENSE [origin=${repo_dir}/LICENSE] + +# Adding itself +dmr=./${module}.dmr => ${destination}/${module}/${module}.dmr [origin=${repo_dir}/tools/ping/${module}.dmr] diff --git a/tools/ping/src/ping.c b/tools/ping/src/ping.c new file mode 100644 index 0000000..51a8e3f --- /dev/null +++ b/tools/ping/src/ping.c @@ -0,0 +1,289 @@ +/** + * @file ping.c + * @brief ping - send ICMPv4 Echo Requests to a host and report the replies + * + * A thin CLI over dmicmp's Echo Request/Reply API + * (dmicmp_v4_send_echo_request(), dmicmp_register_echo_listener()/ + * _unregister_echo_listener() - see include/dmicmp.h). Registration is + * re-done for every request: the identifier is fixed for the whole run, + * but each iteration sets `g_expected_sequence` before sending and drops + * (via echo_reply_handler()'s own sequence check) any reply that doesn't + * match it - guards against a stale reply for an earlier request arriving + * after that request already timed out. + * + *
must be a literal dotted-decimal IPv4 address - dmicmp has no + * hostname resolution, and no ICMPv6 send path yet (see dmicmp.h's top + * comment), so this tool cannot ping an IPv6 destination either. + */ +#include "dmod.h" +#include "dmicmp.h" +#include "dmroute.h" +#include "dmarp.h" +#include "dmosi.h" +#include + +#define PING_DEFAULT_COUNT 4u +#define PING_DEFAULT_INTERVAL_MS 1000u +#define PING_DEFAULT_TIMEOUT_MS 1000u +#define PING_DEFAULT_PAYLOAD_LEN 32u +#define PING_MAX_PAYLOAD_LEN 1024u + +static dmosi_semaphore_t g_reply_sem; +static uint16_t g_expected_sequence; +static bool g_reply_matched; + +/* Parses a plain decimal uint32_t (dmod's minimal module runtime has no + * strtol()/atoi() - see dmod/src/module/string.c's replacement set). */ +static bool parse_uint32(const char* s, uint32_t* out) +{ + if (s == NULL || *s == '\0') + return false; + + uint64_t value = 0; + for (const char* p = s; *p != '\0'; p++) + { + if (*p < '0' || *p > '9') + return false; + + value = value * 10 + (uint64_t)(*p - '0'); + if (value > UINT32_MAX) + return false; + } + + *out = (uint32_t)value; + return true; +} + +/* Parses "A.B.C.D" (each octet 0-255) into an IPv4 dmroute_addr_t. */ +static bool parse_ipv4_address(const char* s, dmroute_addr_t* ip) +{ + if (s == NULL) + return false; + + ip->family = dmroute_family_v4; + + for (int i = 0; i < DMROUTE_IPV4_ADDR_LEN; i++) + { + if (*s < '0' || *s > '9') + return false; + + uint32_t octet = 0; + int digits = 0; + while (*s >= '0' && *s <= '9') + { + octet = octet * 10 + (uint32_t)(*s - '0'); + if (octet > 255 || ++digits > 3) + return false; + s++; + } + ip->addr.v4[i] = (uint8_t)octet; + + if (i < DMROUTE_IPV4_ADDR_LEN - 1) + { + if (*s != '.') + return false; + s++; + } + } + + return *s == '\0'; +} + +/* Only ever called while a single dmicmp_register_echo_listener() / + * dmicmp_v4_send_echo_request() pair is outstanding (send_one_ping() + * sends requests one at a time, never concurrently) - g_expected_sequence + * and g_reply_matched are safe as plain statics under that single-writer + * invariant. */ +static void echo_reply_handler(const dmip_addr_t* src, uint16_t identifier, uint16_t sequence, const uint8_t* payload, size_t payload_len) +{ + (void)src; + (void)identifier; + (void)payload; + (void)payload_len; + + if (sequence != g_expected_sequence) + return; + + g_reply_matched = true; + dmosi_semaphore_post(g_reply_sem, 1); +} + +/* Sends one Echo Request and blocks (up to wait_timeout_ms) for its + * reply. Returns 0 and prints a "bytes from ..." line if a matching reply + * arrived in time, -1 (and prints why) otherwise. */ +static int send_one_ping(const dmroute_addr_t* dst, const char* address_str, uint16_t identifier, uint16_t sequence, + const uint8_t* payload, size_t payload_len, uint32_t wait_timeout_ms) +{ + g_reply_matched = false; + g_expected_sequence = sequence; + + int reg_ret = dmicmp_register_echo_listener(identifier, echo_reply_handler); + if (reg_ret != 0) + { + Dmod_Printf("ping: failed to register reply listener for icmp_seq=%u (error %d)\n", (unsigned)sequence, reg_ret); + return -1; + } + + uint32_t start_tick = dmosi_get_tick_count(); + int send_ret = dmicmp_v4_send_echo_request(dst, identifier, sequence, payload, payload_len, DMARP_DEFAULT_TIMEOUT_MS); + if (send_ret != 0) + { + dmicmp_unregister_echo_listener(identifier); + Dmod_Printf("ping: send failed for icmp_seq=%u (error %d)\n", (unsigned)sequence, send_ret); + return -1; + } + + int wait_ret = dmosi_semaphore_wait(g_reply_sem, 1, (int32_t)wait_timeout_ms); + dmicmp_unregister_echo_listener(identifier); + + if (wait_ret == 0 && g_reply_matched) + { + uint32_t rtt_ms = dmosi_get_tick_count() - start_tick; + Dmod_Printf("%u bytes from %s: icmp_seq=%u time=%u ms\n", + (unsigned)payload_len, address_str, (unsigned)sequence, (unsigned)rtt_ms); + return 0; + } + + Dmod_Printf("Request timeout for icmp_seq %u\n", (unsigned)sequence); + return -1; +} + +static void print_usage(const char* prog) +{ + Dmod_Printf("Usage:\n"); + Dmod_Printf(" %s
Send %u ICMPv4 Echo Requests to
\n", prog, (unsigned)PING_DEFAULT_COUNT); + Dmod_Printf(" %s --help | -h Show this help\n", prog); + Dmod_Printf("\n"); + Dmod_Printf("Options:\n"); + Dmod_Printf(" -c Number of Echo Requests to send (default: %u)\n", (unsigned)PING_DEFAULT_COUNT); + Dmod_Printf(" -i Delay between requests, in milliseconds (default: %u)\n", (unsigned)PING_DEFAULT_INTERVAL_MS); + Dmod_Printf(" -W Time to wait for a reply before it's lost, in milliseconds (default: %u)\n", (unsigned)PING_DEFAULT_TIMEOUT_MS); + Dmod_Printf(" -s Number of payload bytes to send, 0-%u (default: %u)\n", (unsigned)PING_MAX_PAYLOAD_LEN, (unsigned)PING_DEFAULT_PAYLOAD_LEN); + Dmod_Printf("\n"); + Dmod_Printf("
must be a literal IPv4 address (e.g. 192.168.1.1) - dmicmp has no\n"); + Dmod_Printf("hostname resolution and no ICMPv6 send support yet.\n"); +} + +int main(int argc, char* argv[]) +{ + const char* prog = (argc > 0 && argv[0] != NULL) ? argv[0] : "ping"; + + if (argc >= 2 && (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) + { + print_usage(prog); + return 0; + } + + if (argc < 2) + { + print_usage(prog); + return 1; + } + + dmroute_addr_t dst = { 0 }; + if (!parse_ipv4_address(argv[1], &dst)) + { + Dmod_Printf("%s: invalid IPv4 address '%s'\n", prog, argv[1]); + return 1; + } + + uint32_t count = PING_DEFAULT_COUNT; + uint32_t interval_ms = PING_DEFAULT_INTERVAL_MS; + uint32_t wait_timeout_ms = PING_DEFAULT_TIMEOUT_MS; + uint32_t payload_len = PING_DEFAULT_PAYLOAD_LEN; + + for (int i = 2; i < argc; i++) + { + if (strcmp(argv[i], "-c") == 0 && i + 1 < argc) + { + if (!parse_uint32(argv[++i], &count) || count == 0) + { + Dmod_Printf("%s: invalid count '%s'\n", prog, argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "-i") == 0 && i + 1 < argc) + { + if (!parse_uint32(argv[++i], &interval_ms)) + { + Dmod_Printf("%s: invalid interval '%s'\n", prog, argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "-W") == 0 && i + 1 < argc) + { + if (!parse_uint32(argv[++i], &wait_timeout_ms) || wait_timeout_ms == 0) + { + Dmod_Printf("%s: invalid timeout '%s'\n", prog, argv[i]); + return 1; + } + } + else if (strcmp(argv[i], "-s") == 0 && i + 1 < argc) + { + if (!parse_uint32(argv[++i], &payload_len) || payload_len > PING_MAX_PAYLOAD_LEN) + { + Dmod_Printf("%s: invalid size '%s'\n", prog, argv[i]); + return 1; + } + } + else + { + print_usage(prog); + return 1; + } + } + + uint8_t* payload = NULL; + if (payload_len > 0) + { + payload = Dmod_Malloc(payload_len); + if (payload == NULL) + { + Dmod_Printf("%s: out of memory allocating a %u-byte payload\n", prog, (unsigned)payload_len); + return 1; + } + for (uint32_t i = 0; i < payload_len; i++) + payload[i] = (uint8_t)i; + } + + g_reply_sem = dmosi_semaphore_create(0, 1); + if (g_reply_sem == NULL) + { + Dmod_Printf("%s: failed to create the reply semaphore\n", prog); + if (payload != NULL) + Dmod_Free(payload); + return 1; + } + + /* Ticks-derived rather than a fixed constant, so two `ping` runs in + * flight at once (e.g. against different hosts) are unlikely to share + * an identifier and cross-deliver each other's replies. */ + uint16_t identifier = (uint16_t)(dmosi_get_tick_count() & 0xFFFFu); + + Dmod_Printf("PING %s: %u data bytes\n", argv[1], (unsigned)payload_len); + + uint32_t transmitted = 0; + uint32_t received = 0; + + for (uint32_t seq = 0; seq < count; seq++) + { + transmitted++; + if (send_one_ping(&dst, argv[1], identifier, (uint16_t)seq, payload, payload_len, wait_timeout_ms) == 0) + received++; + + if (seq + 1 < count) + dmosi_thread_sleep(interval_ms); + } + + dmosi_semaphore_destroy(g_reply_sem); + g_reply_sem = NULL; + if (payload != NULL) + Dmod_Free(payload); + + uint32_t loss_percent = (transmitted > 0) ? ((transmitted - received) * 100u) / transmitted : 0u; + Dmod_Printf("\n--- %s ping statistics ---\n", argv[1]); + Dmod_Printf("%u packets transmitted, %u packets received, %u%% packet loss\n", + (unsigned)transmitted, (unsigned)received, (unsigned)loss_percent); + + return (received > 0) ? 0 : 1; +} diff --git a/tools/ping/tests/CMakeLists.txt b/tools/ping/tests/CMakeLists.txt new file mode 100644 index 0000000..e39c20c --- /dev/null +++ b/tools/ping/tests/CMakeLists.txt @@ -0,0 +1,23 @@ +set(DMOD_MODULE_NAME test_ping) +set(DMOD_MODULE_VERSION ${PROJECT_VERSION}) +set(DMOD_AUTHOR_NAME "Patryk Kubiak") +set(DMOD_STACK_SIZE 1024) +set(DMOD_PRIORITY 0) + +dmod_add_test(${DMOD_MODULE_NAME} ${DMOD_MODULE_VERSION} + ping_test.c +) + +# `ping` itself is never linked as a dependency here - it's invoked on +# demand through Dmod_RunModule() (see ping_test.c), which loads/runs/ +# unloads it per call instead of holding it resident for the whole test +# run. +# dmroute/dmarp: seed a fixture route and ARP cache entry so a send can be +# exercised up to (but not through) a real driver, the same +# pattern dmicmp's own tests/dmicmp_test.c uses. +# dmnetif: the "/dev/null"-backed fixture interface the route points at. +dmod_link_modules(${DMOD_MODULE_NAME} + dmnetif + dmroute + dmarp +) diff --git a/tools/ping/tests/ping_test.c b/tools/ping/tests/ping_test.c new file mode 100644 index 0000000..28fd5c3 --- /dev/null +++ b/tools/ping/tests/ping_test.c @@ -0,0 +1,163 @@ +/** + * @file ping_test.c + * @brief Test steps for ping + * + * Drives the actual `ping` module through Dmod_RunModule("ping", argc, + * argv) (loads it, runs its main(), unloads it again - see + * Dmod_RunModule in dmod.h) rather than depending on `ping` as a linked + * module: an Application-type module can't be loaded as another module's + * dependency (only a Library-type module can) - same reasoning + * dmnet/tools/ifconfig/tests/ifconfig_test.c documents for itself. + * + * A "/dev/null"-backed dmnetif fixture, with a route and ARP cache entry + * pointed at it, lets a send be exercised up through dmip_send() the same + * way dmicmp's own tests/dmicmp_test.c does for dmicmp_v4_send_error()/ + * _send_dest_unreachable() - no real driver backs it, so + * dmicmp_v4_send_echo_request() itself still fails (dmnetif_send() -> + * -EIO), and no real Echo Reply can ever arrive. Every step below + * therefore expects ping to report loss, not success - it only proves + * argument handling and the full send attempt don't crash and report the + * honest outcome, the same boundary ifconfig_test.c's "up"/"down" steps + * document for themselves. + */ +#include "dmod_test.h" +#include "dmroute.h" +#include "dmarp.h" +#include + +#define TEST_DEVICE_PATH "/dev/null" + +static dmnetif_iface_t g_iface = NULL; +static dmroute_route_t g_route = NULL; + +void dmod_test_setup(void) +{ + g_iface = dmnetif_register("test0", TEST_DEVICE_PATH); + + dmroute_addr_t dest_net = { 0 }; + dest_net.family = dmroute_family_v4; + dest_net.addr.v4[0] = 203; dest_net.addr.v4[1] = 0; dest_net.addr.v4[2] = 113; dest_net.addr.v4[3] = 0; + + dmroute_addr_t netmask = { 0 }; + netmask.family = dmroute_family_v4; + netmask.addr.v4[0] = 255; netmask.addr.v4[1] = 255; netmask.addr.v4[2] = 255; netmask.addr.v4[3] = 0; + + g_route = dmroute_add(&dest_net, &netmask, NULL, "test0", DMROUTE_DEFAULT_METRIC, dmroute_origin_static); + + dmroute_addr_t dst = { 0 }; + dst.family = dmroute_family_v4; + dst.addr.v4[0] = 203; dst.addr.v4[1] = 0; dst.addr.v4[2] = 113; dst.addr.v4[3] = 7; + + dmnetif_mac_addr_t fake_mac = { { 0x02, 0x00, 0x00, 0x00, 0x00, 0x40 } }; + dmarp_cache_insert(g_iface, &dst, &fake_mac); +} + +void dmod_test_teardown(void) +{ + dmroute_addr_t dst = { 0 }; + dst.family = dmroute_family_v4; + dst.addr.v4[0] = 203; dst.addr.v4[1] = 0; dst.addr.v4[2] = 113; dst.addr.v4[3] = 7; + dmarp_cache_remove(g_iface, &dst); + + dmroute_remove(g_route); + g_route = NULL; + + dmnetif_unregister(g_iface); + g_iface = NULL; +} + +DMOD_TEST_STEP(no_args_fails) +{ + char* argv[] = { "ping" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 1, argv), 0); +} + +DMOD_TEST_STEP(run_module_unloads_ping_afterwards) +{ + char* argv[] = { "ping", "--help" }; + Dmod_RunModule("ping", 2, argv); + DMOD_TEST_EXPECT_FALSE(Dmod_IsModuleLoaded("ping")); +} + +DMOD_TEST_STEP(help_flag_succeeds) +{ + char* argv[] = { "ping", "--help" }; + DMOD_TEST_EXPECT_EQ(Dmod_RunModule("ping", 2, argv), 0); +} + +DMOD_TEST_STEP(short_help_flag_succeeds) +{ + char* argv[] = { "ping", "-h" }; + DMOD_TEST_EXPECT_EQ(Dmod_RunModule("ping", 2, argv), 0); +} + +DMOD_TEST_STEP(invalid_address_fails) +{ + char* argv[] = { "ping", "not-an-address" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 2, argv), 0); +} + +DMOD_TEST_STEP(address_octet_out_of_range_fails) +{ + char* argv[] = { "ping", "192.168.1.999" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 2, argv), 0); +} + +DMOD_TEST_STEP(unknown_flag_fails) +{ + char* argv[] = { "ping", "203.0.113.7", "--frobnicate" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 3, argv), 0); +} + +DMOD_TEST_STEP(count_zero_fails) +{ + char* argv[] = { "ping", "203.0.113.7", "-c", "0" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0); +} + +DMOD_TEST_STEP(count_non_numeric_fails) +{ + char* argv[] = { "ping", "203.0.113.7", "-c", "many" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0); +} + +DMOD_TEST_STEP(timeout_zero_fails) +{ + char* argv[] = { "ping", "203.0.113.7", "-W", "0" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0); +} + +DMOD_TEST_STEP(size_over_max_fails) +{ + char* argv[] = { "ping", "203.0.113.7", "-s", "999999" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 4, argv), 0); +} + +DMOD_TEST_STEP(unreachable_destination_reports_loss_without_crashing) +{ + /* No route anywhere in this file's fixtures covers this network. */ + char* argv[] = { "ping", "198.51.100.1", "-c", "1", "-W", "50" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 5, argv), 0); +} + +DMOD_TEST_STEP(routed_destination_without_real_driver_reports_loss) +{ + /* Route + ARP entry let dmicmp_v4_send_echo_request() reach + * dmnetif_send() itself, which fails (-EIO) since TEST_DEVICE_PATH is + * not a real driver - ping must still report the honest 100% loss + * outcome rather than crash or hang. */ + char* argv[] = { "ping", "203.0.113.7", "-c", "2", "-i", "1", "-W", "50" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 7, argv), 0); +} + +DMOD_TEST_STEP(custom_payload_size_is_accepted) +{ + char* argv[] = { "ping", "203.0.113.7", "-c", "1", "-s", "8", "-W", "50" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 7, argv), 0); +} + +DMOD_TEST_STEP(zero_payload_size_is_accepted) +{ + char* argv[] = { "ping", "203.0.113.7", "-c", "1", "-s", "0", "-W", "50" }; + DMOD_TEST_EXPECT_NE(Dmod_RunModule("ping", 7, argv), 0); +}