Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 167 additions & 47 deletions src/paping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ static std::string colored(std::string_view text, std::string_view color) {
return std::string(color) + std::string(text) + std::string(col::rst);
}

static std::string json_escape(std::string_view s) {
std::string out;
out.reserve(s.size());
for (char c : s) {
if (c == '"') out += "\\\"";
else if (c == '\\') out += "\\\\";
else if (c == '\n') out += "\\n";
else if (c == '\r') out += "\\r";
else if (c == '\t') out += "\\t";
else out += c;
}
return out;
}

static std::string csv_escape(std::string_view s) {
if (s.find_first_of(",\"\n\r") == std::string_view::npos)
return std::string(s);
std::string out = "\"";
for (char c : s) {
if (c == '"') out += "\"\"";
else out += c;
}
out += '"';
return out;
}

static void platform_init() {
#ifdef _WIN32
WSADATA wsa{};
Expand All @@ -82,11 +108,16 @@ static void platform_cleanup() {
#endif
}

enum class OutputFormat { Human, Json, Csv };

struct Config {
std::string host;
int port = 0;
int count = -1;
int timeout = 1000;
std::string host;
int port = 0;
int count = -1;
int timeout = 1000;
OutputFormat format = OutputFormat::Human;
bool silent = false;
bool stats_only = false;
};

struct HostInfo {
Expand Down Expand Up @@ -139,7 +170,7 @@ static std::optional<HostInfo> resolve(const std::string& hostname) {
return info;
}

enum class Probe { Connected, TimedOut, Failed };
enum class Probe { Connected, TimedOut, Failed };

static Probe tcp_probe(const std::string& ip, int port, int timeout_ms, double& elapsed_ms) {
elapsed_ms = 0.0;
Expand Down Expand Up @@ -207,66 +238,135 @@ static Probe tcp_probe(const std::string& ip, int port, int timeout_ms, double&
return result;
}

static void print_banner() {
static void print_banner(const Config& cfg) {
if (cfg.stats_only || cfg.format != OutputFormat::Human) return;
std::cout << std::format("Paping-NG v{} - Copyright (c) 2026 Oliver (arch3r.eu)\n\n", VERSION);
}

static void print_usage() {
std::cout <<
std::cout << std::format("Paping-NG v{} - Copyright (c) 2026 Oliver (arch3r.eu)\n\n", VERSION) <<
"Usage: paping <host> -p <port> [options]\n\n"
"Options:\n"
" -p, --port N TCP port to probe (required)\n"
" -c, --count N stop after N probes (default: run forever)\n"
" -t, --timeout N connection timeout in ms (default: 1000)\n"
" -h, --help show this help\n";
" -p, --port N TCP port to probe (required)\n"
" -c, --count N stop after N probes (default: run forever)\n"
" -t, --timeout N connection timeout in ms (default: 1000)\n"
" --format <fmt> output format: human (default), json, csv\n"
" --silent suppress per-probe lines; print stats at end\n"
" --stats-only suppress banner, target, and per-probe lines\n"
" -h, --help show this help\n";
}

static void print_target(const HostInfo& host, int port) {
static void print_target(const Config& cfg, const HostInfo& host) {
if (cfg.stats_only || cfg.format != OutputFormat::Human) return;
if (host.is_ip) {
std::cout << std::format("Connecting to {} on TCP port {}:\n\n",
colored(host.ip, col::yellow),
colored(std::to_string(port), col::yellow));
colored(std::to_string(cfg.port), col::yellow));
}
else {
std::cout << std::format("Connecting to {} [{}] on TCP port {}:\n\n",
colored(host.canonical, col::yellow),
colored(host.ip, col::yellow),
colored(std::to_string(port), col::yellow));
colored(std::to_string(cfg.port), col::yellow));
}
}

static void print_probe(const HostInfo& host, int port, Probe result, double ms) {
switch (result) {
case Probe::Connected:
std::cout << std::format("Connected to {}: time={} protocol={} port={}\n",
colored(host.ip, col::green),
colored(std::format("{:.2f}ms", ms), col::green),
colored("TCP", col::green),
colored(std::to_string(port), col::green));
static void print_csv_header() {
std::cout << "seq,host,ip,port,status,time_ms\n";
}

static void print_probe(const Config& cfg, const HostInfo& host, int seq, Probe result, double ms) {
if (cfg.silent) return;

switch (cfg.format) {
case OutputFormat::Human:
switch (result) {
case Probe::Connected:
std::cout << std::format("Connected to {}: time={} protocol={} port={}\n",
colored(host.ip, col::green),
colored(std::format("{:.2f}ms", ms), col::green),
colored("TCP", col::green),
colored(std::to_string(cfg.port), col::green));
break;
case Probe::TimedOut:
std::cout << colored("Connection timed out\n", col::red);
break;
case Probe::Failed:
std::cout << colored("Connection failed\n", col::red);
break;
}
break;
case Probe::TimedOut:
std::cout << colored("Connection timed out\n", col::red);

case OutputFormat::Json: {
const char* status =
result == Probe::Connected ? "connected" :
result == Probe::TimedOut ? "timed_out" : "failed";
if (result == Probe::Connected) {
std::cout << std::format(
"{{\"seq\":{},\"host\":\"{}\",\"ip\":\"{}\",\"port\":{},"
"\"status\":\"{}\",\"time_ms\":{:.3f}}}\n",
seq, json_escape(host.canonical), host.ip, cfg.port, status, ms);
} else {
std::cout << std::format(
"{{\"seq\":{},\"host\":\"{}\",\"ip\":\"{}\",\"port\":{},"
"\"status\":\"{}\",\"time_ms\":null}}\n",
seq, json_escape(host.canonical), host.ip, cfg.port, status);
}
break;
case Probe::Failed:
std::cout << colored("Connection failed\n", col::red);
}

case OutputFormat::Csv: {
const char* status =
result == Probe::Connected ? "connected" :
result == Probe::TimedOut ? "timed_out" : "failed";
std::cout << std::format("{},{},{},{},{},{}\n",
seq,
csv_escape(host.canonical),
host.ip,
cfg.port,
status,
result == Probe::Connected ? std::format("{:.3f}", ms) : "");
break;
}
}
}

static void print_stats(const Stats& s) {
static void print_stats(const Config& cfg, const Stats& s) {
if (s.attempts == 0) return;
const double fail_pct = 100.0 * s.failures / s.attempts;
const double min_v = s.connects > 0 ? s.min_ms : 0.0;
const double max_v = s.connects > 0 ? s.max_ms : 0.0;
std::cout << std::format(
"\n{} Probed = {}, Connected = {}, Failed = {} ({:.2f}%)\n"
"Approximate connection times:\n"
" Minimum = {:.2f}ms, Maximum = {:.2f}ms, Average = {:.2f}ms\n",
colored("Connection statistics:\n", col::blue),
colored(std::to_string(s.attempts), col::cyan),
colored(std::to_string(s.connects), col::green),
colored(std::to_string(s.failures), col::red),
fail_pct, min_v, max_v, s.avg());
const double min_v = s.connects > 0 ? s.min_ms : 0.0;
const double max_v = s.connects > 0 ? s.max_ms : 0.0;

switch (cfg.format) {
case OutputFormat::Human:
std::cout << std::format(
"\n{} Probed = {}, Connected = {}, Failed = {} ({:.2f}%)\n"
"Approximate connection times:\n"
" Minimum = {:.2f}ms, Maximum = {:.2f}ms, Average = {:.2f}ms\n",
colored("Connection statistics:\n", col::blue),
colored(std::to_string(s.attempts), col::cyan),
colored(std::to_string(s.connects), col::green),
colored(std::to_string(s.failures), col::red),
fail_pct, min_v, max_v, s.avg());
break;

case OutputFormat::Json:
std::cout << std::format(
"{{\"stats\":{{\"attempts\":{},\"connected\":{},\"failed\":{},"
"\"failure_pct\":{:.2f},\"min_ms\":{:.3f},\"max_ms\":{:.3f},"
"\"avg_ms\":{:.3f}}}}}\n",
s.attempts, s.connects, s.failures,
fail_pct, min_v, max_v, s.avg());
break;

case OutputFormat::Csv:
std::cout << std::format(
"\n#stats,attempts,connected,failed,failure_pct,min_ms,max_ms,avg_ms\n"
"#stats,{},{},{},{:.2f},{:.3f},{:.3f},{:.3f}\n",
s.attempts, s.connects, s.failures,
fail_pct, min_v, max_v, s.avg());
break;
}
}

static std::optional<Config> parse_args(int argc, char* argv[]) {
Expand All @@ -281,11 +381,26 @@ static std::optional<Config> parse_args(int argc, char* argv[]) {
const char* p = argv[i];
auto [end, ec] = std::from_chars(p, p + std::strlen(p), out);
return ec == std::errc{};
};
};

auto next_str = [&](std::string& out) -> bool {
if (++i >= argc) return false;
out = argv[i];
return true;
};

if (arg == "-p" || arg == "--port") { if (!next_int(cfg.port)) return std::nullopt; }
else if (arg == "-c" || arg == "--count") { if (!next_int(cfg.count)) return std::nullopt; }
else if (arg == "-c" || arg == "--count") { if (!next_int(cfg.count)) return std::nullopt; }
else if (arg == "-t" || arg == "--timeout") { if (!next_int(cfg.timeout)) return std::nullopt; }
else if (arg == "--format") {
std::string fmt;
if (!next_str(fmt)) return std::nullopt;
if (fmt == "json") cfg.format = OutputFormat::Json;
else if (fmt == "csv") cfg.format = OutputFormat::Csv;
else return std::nullopt;
}
else if (arg == "--silent") { cfg.silent = true; }
else if (arg == "--stats-only") { cfg.stats_only = true; cfg.silent = true; }
else if (arg == "-h" || arg == "--help" || arg == "-?") { return std::nullopt; }
else if (!got_host) { cfg.host = argv[i]; got_host = true; }
else { return std::nullopt; }
Expand All @@ -301,7 +416,6 @@ static void sig_handler(int) { g_stop = true; }

int main(int argc, char* argv[]) {
platform_init();
print_banner();

auto cfg_opt = parse_args(argc, argv);
if (!cfg_opt) {
Expand All @@ -311,6 +425,8 @@ int main(int argc, char* argv[]) {
}
const Config& cfg = *cfg_opt;

print_banner(cfg);

auto host_opt = resolve(cfg.host);
if (!host_opt) {
std::cerr << colored("Cannot resolve host: ", col::red) << cfg.host << '\n';
Expand All @@ -319,16 +435,20 @@ int main(int argc, char* argv[]) {
}
const HostInfo& host = *host_opt;

print_target(host, cfg.port);
print_target(cfg, host);

if (cfg.format == OutputFormat::Csv && !cfg.silent)
print_csv_header();

std::signal(SIGINT, sig_handler);

Stats stats;
int exit_code = 0;
bool infinite = (cfg.count < 0);
bool infinite = (cfg.count < 0);

for (int i = 0; (infinite || i < cfg.count) && !g_stop; ++i) {
double elapsed = 0.0;
Probe result = tcp_probe(host.ip, cfg.port, cfg.timeout, elapsed);
Probe result = tcp_probe(host.ip, cfg.port, cfg.timeout, elapsed);

++stats.attempts;
if (result == Probe::Connected) {
Expand All @@ -340,15 +460,15 @@ int main(int argc, char* argv[]) {
exit_code = 1;
}

print_probe(host, cfg.port, result, elapsed);
print_probe(cfg, host, i + 1, result, elapsed);

const int sleep_ms = 1000 - static_cast<int>(std::min(elapsed, 1000.0));
const bool more = infinite || (i + 1 < cfg.count);
const bool more = infinite || (i + 1 < cfg.count);
if (sleep_ms > 0 && more && !g_stop)
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}

print_stats(stats);
print_stats(cfg, stats);
platform_cleanup();
return exit_code;
}