Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package tcping for openSUSE:Factory checked in at 2023-11-13 22:21:42 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/tcping (Old) and /work/SRC/openSUSE:Factory/.tcping.new.17445 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "tcping" Mon Nov 13 22:21:42 2023 rev:5 rq:1125281 version:2.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/tcping/tcping.changes 2023-08-28 17:18:32.059802103 +0200 +++ /work/SRC/openSUSE:Factory/.tcping.new.17445/tcping.changes 2023-11-13 22:25:22.501452983 +0100 @@ -1,0 +2,17 @@ +Sun Nov 12 20:34:02 UTC 2023 - Dirk Müller <dmuel...@suse.com> + +- update to 2.4.0: + * new feature: add `-i` to specify the interval between sending + probes. + * new feature: add `-I` to specify the source interface to use + for sending probes. + * new feature: add `-t` to specify a custom timeout for probes. + * new feature: add `--db` to specify the path and file name to + store tcping output to sqlite database. e.g. `--db + /tmp/tcping.db`. + * fix: add `rtt` to JSON output + * refactor: remove unnecessary custom types + * refactor: memory align `structs` + * refactor: Debian packaging instructions + +------------------------------------------------------------------- Old: ---- tcping-2.0.0.tar.gz New: ---- tcping-2.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ tcping.spec ++++++ --- /var/tmp/diff_new_pack.Rq2kns/_old 2023-11-13 22:25:23.185478168 +0100 +++ /var/tmp/diff_new_pack.Rq2kns/_new 2023-11-13 22:25:23.185478168 +0100 @@ -18,7 +18,7 @@ Name: tcping -Version: 2.0.0 +Version: 2.4.0 Release: 0 Summary: A ping program for TCP ports License: MIT @@ -29,7 +29,6 @@ Source1: vendor.tar.gz BuildRequires: go >= 1.18 BuildRequires: golang-packaging -%{go_provides} %description TCPing will send TCP probes to an IP address or a hostname specified @@ -60,11 +59,10 @@ %autosetup -p 1 -a 1 %build -%{goprep} github.com/pouriyajamshidi/tcping -%{gobuild} -mod=vendor . +go build -mod=vendor -buildmode=pie -trimpath -o tcping %install -%{goinstall} +install -m 755 -D tcping %{buildroot}/%{_bindir}/tcping %files %license LICENSE ++++++ _service ++++++ --- /var/tmp/diff_new_pack.Rq2kns/_old 2023-11-13 22:25:23.209479051 +0100 +++ /var/tmp/diff_new_pack.Rq2kns/_new 2023-11-13 22:25:23.213479199 +0100 @@ -1,9 +1,9 @@ <services> - <service name="download_files" mode="disabled" /> - <service mode="disabled" name="set_version"> + <service name="download_files" mode="manual" /> + <service mode="manual" name="set_version"> <param name="basename">tcping</param> </service> - <service name="go_modules" mode="disabled"> + <service name="go_modules" mode="manual"> </service> </services> ++++++ tcping-2.0.0.tar.gz -> tcping-2.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/.github/workflows/container-publish.yml new/tcping-2.4.0/.github/workflows/container-publish.yml --- old/tcping-2.0.0/.github/workflows/container-publish.yml 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/.github/workflows/container-publish.yml 2023-09-10 16:07:16.000000000 +0200 @@ -37,14 +37,14 @@ - name: Extract Version id: version_step run: | - echo "##[set-output name=version;]VERSION=${GITHUB_REF#$"refs/tags/v"}" - echo "##[set-output name=version_tag;]$GITHUB_REPOSITORY:${GITHUB_REF#$"refs/tags/v"}" - echo "##[set-output name=latest_tag;]$GITHUB_REPOSITORY:latest" + echo "version=${GITHUB_REF#"refs/tags/v"}" >> $GITHUB_ENV + echo "version_tag=${GITHUB_REPOSITORY}:${GITHUB_REF#"refs/tags/v"}" >> $GITHUB_ENV + echo "latest_tag=${GITHUB_REPOSITORY}:latest" >> $GITHUB_ENV - name: Print Version run: | - echo ${{steps.version_step.outputs.version}} - echo ${{steps.version_step.outputs.version_tag}} - echo ${{steps.version_step.outputs.latest_tag}} + echo ${{ env.version }} + echo ${{ env.version_tag }} + echo ${{ env.latest_tag }} - name: Checkout repository uses: actions/checkout@v3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/.gitignore new/tcping-2.4.0/.gitignore --- old/tcping-2.0.0/.gitignore 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/.gitignore 2023-09-10 16:07:16.000000000 +0200 @@ -2,7 +2,9 @@ executables/ .idea/ tcping +tcping.exe .DS_Store .vscode *.code-workspace patches/ +*.db diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/CHANGELOG.md new/tcping-2.4.0/CHANGELOG.md --- old/tcping-2.0.0/CHANGELOG.md 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/CHANGELOG.md 2023-09-10 16:07:16.000000000 +0200 @@ -1,5 +1,38 @@ # Changelog +## v2.4.0 - 2023-09-10 + +- new feature: add `-i` to specify the interval between sending probes. Thanks to @luca-patrignani +- new feature: add `-I` to specify the source interface to use for sending probes. Thanks to @wizsk +- new feature: add `-t` to specify a custom timeout for probes. Thanks to @luca-patrignani +- new feature: add `--db` to specify the path and file name to store tcping output to sqlite database. e.g. `--db /tmp/tcping.db`. Thanks to @wizsk +- fix: add `rtt` to JSON output +- fix: CI warning thanks to @wizsk +- refactor: remove unnecessary custom types +- refactor: memory align `structs` +- refactor: Debian packaging instructions + +## v2.0.0 - 2023-08-05 + +- new feature: add `-c` or count flag to exit **TCPING** after a certain amount of probes specified by user thanks to @ravsii +- new feature: add **BSD** support +- new feature: add **Debian** package to make **TCPING** `apt installable` +- fix: packet loss `NaN` when program terminated too quickly thanks to @ravsii +- fix: random IP address selector index out of range bug +- fix: display format of IPv4 embedded in IPv6 addresses +- fix: time report bug. Everything is now accurate +- fix: Enter key detection for Windows machines +- refactor: complete overhaul of time calculation. **TCPING** now is hack-free when it comes to time handling thanks to @ravsii +- refactor: memory align `structs` +- refactor: improve code readability +- refactor: refactor `stats struct` and extract user input to a separate `struct` +- refactor: Enter key detection logic +- refactor: name resolution handling. The maximum allowed time to wait for DNS response is now 2 seconds +- refactor: and unify exit points thanks to @ravsii +- tests: add more test special thanks to @ravsii +- enhancement: add dependabot +- docs: improve documentation + ## v1.22.1 - 2023-5-14 - new feature: implement JSON output thanks to @ravsii Binary files old/tcping-2.0.0/Images/gifs/tcping.gif and new/tcping-2.4.0/Images/gifs/tcping.gif differ Binary files old/tcping-2.0.0/Images/gifs/tcping_json_pretty.gif and new/tcping-2.4.0/Images/gifs/tcping_json_pretty.gif differ Binary files old/tcping-2.0.0/Images/gifs/tcping_resolve.gif and new/tcping-2.4.0/Images/gifs/tcping_resolve.gif differ Binary files old/tcping-2.0.0/Images/old/tcping.gif and new/tcping-2.4.0/Images/old/tcping.gif differ Binary files old/tcping-2.0.0/Images/old/tcpingDockerDemo.gif and new/tcping-2.4.0/Images/old/tcpingDockerDemo.gif differ Binary files old/tcping-2.0.0/Images/old/tcpingrflag.gif and new/tcping-2.4.0/Images/old/tcpingrflag.gif differ Binary files old/tcping-2.0.0/Images/old/windowsVersion.png and new/tcping-2.4.0/Images/old/windowsVersion.png differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/Images/tapes/tcping.tape new/tcping-2.4.0/Images/tapes/tcping.tape --- old/tcping-2.0.0/Images/tapes/tcping.tape 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/Images/tapes/tcping.tape 2023-09-10 16:07:16.000000000 +0200 @@ -3,7 +3,7 @@ Set Width 1300 Set Height 800 -Type "./tcping example.com 80" +Type "tcping example.com 80" Sleep 500ms Enter Sleep 10s Ctrl+C diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/Images/tapes/tcping_json_pretty.tape new/tcping-2.4.0/Images/tapes/tcping_json_pretty.tape --- old/tcping-2.0.0/Images/tapes/tcping_json_pretty.tape 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/Images/tapes/tcping_json_pretty.tape 2023-09-10 16:07:16.000000000 +0200 @@ -3,7 +3,7 @@ Set Width 1300 Set Height 1000 -Type "./tcping example.com 80 -j --pretty" +Type "tcping example.com 80 -j --pretty" Sleep 500ms Enter Sleep 10s Ctrl+C diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/Images/tapes/tcping_resolve.tape new/tcping-2.4.0/Images/tapes/tcping_resolve.tape --- old/tcping-2.0.0/Images/tapes/tcping_resolve.tape 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/Images/tapes/tcping_resolve.tape 2023-09-10 16:07:16.000000000 +0200 @@ -3,7 +3,7 @@ Set Width 1300 Set Height 800 -Type "./tcping example.com 90 -r 3" Sleep 500ms Enter +Type "tcping example.com 90 -r 3" Sleep 500ms Enter Sleep 15s Ctrl+C diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/LICENSE new/tcping-2.4.0/LICENSE --- old/tcping-2.0.0/LICENSE 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/LICENSE 2023-09-10 16:07:16.000000000 +0200 @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 pouriya jamshidi +Copyright (c) 2021-2023 pouriya jamshidi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/Makefile new/tcping-2.4.0/Makefile --- old/tcping-2.0.0/Makefile 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/Makefile 2023-09-10 16:07:16.000000000 +0200 @@ -1,16 +1,15 @@ EXEC_DIR = executables/ TAPE_DIR = Images/tapes/ GIFS_DIR = Images/gifs/ -SOURCE_FILES = $(tcping.go statsprinter.go) PACKAGE_NAME = tcping -VERSION = 1.22.1 +VERSION = 2.0.0 ARCHITECTURE = amd64 DEB_PACKAGE_DIR = $(EXEC_DIR)/debian DEBIAN_DIR = $(DEB_PACKAGE_DIR)/DEBIAN CONTROL_FILE = $(DEBIAN_DIR)/control EXECUTABLE_PATH = $(EXEC_DIR)/tcping TARGET_EXECUTABLE_PATH = $(DEB_PACKAGE_DIR)/usr/bin/ -PACKAGE = $(PACKAGE_NAME)-$(VERSION)_$(ARCHITECTURE).deb +PACKAGE = $(PACKAGE_NAME)_$(ARCHITECTURE).deb MAINTAINER = https://github.com/pouriyajamshidi DESCRIPTION = Ping TCP ports using tcping. Inspired by Linux's ping utility. Written in Go @@ -29,7 +28,7 @@ @mkdir -p $(TARGET_EXECUTABLE_PATH) @echo "[+] Building the Linux version" - @go build -ldflags "-s -w" -o $(EXEC_DIR)tcping $(SOURCE_FILES) + @go build -ldflags "-s -w" -o $(EXEC_DIR)tcping @echo "[+] Packaging the Linux version" @tar -czvf $(EXEC_DIR)tcping_Linux.tar.gz -C $(EXEC_DIR) tcping > /dev/null @@ -40,7 +39,7 @@ @echo @echo "[+] Building the static Linux version" - @env GOOS=linux CGO_ENABLED=0 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping $(SOURCE_FILES) + @env GOOS=linux CGO_ENABLED=0 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping @echo "[+] Packaging the static Linux version" @tar -czvf $(EXEC_DIR)tcping_Linux_static.tar.gz -C $(EXEC_DIR) tcping > /dev/null @@ -50,21 +49,20 @@ @echo "[+] Building the Debian package" @cp $(EXECUTABLE_PATH) $(TARGET_EXECUTABLE_PATH) - @echo "[+] Creating control file" + @echo "[+] Creating the Debian control file" @echo "Package: $(PACKAGE_NAME)" > $(CONTROL_FILE) @echo "Version: $(VERSION)" >> $(CONTROL_FILE) @echo "Section: custom" >> $(CONTROL_FILE) @echo "Priority: optional" >> $(CONTROL_FILE) @echo "Architecture: amd64" >> $(CONTROL_FILE) @echo "Essential: no" >> $(CONTROL_FILE) - @echo "Installed-Size: 2048" >> $(CONTROL_FILE) @echo "Maintainer: $(MAINTAINER)" >> $(CONTROL_FILE) @echo "Description: $(DESCRIPTION)" >> $(CONTROL_FILE) - @echo "[+] Building package" + @echo "[+] Building the Debian package" @dpkg-deb --build $(DEB_PACKAGE_DIR) - @echo "[+] Renaming package" + @echo "[+] Renaming the Debian package" @mv $(DEB_PACKAGE_DIR).deb $(EXEC_DIR)/$(PACKAGE) @echo "[+] Removing the static Linux binary" @@ -72,7 +70,7 @@ @echo @echo "[+] Building the Windows version" - @env GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping.exe $(SOURCE_FILES) + @env GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping.exe @echo "[+] Packaging the Windows version" @zip -j $(EXEC_DIR)tcping_Windows.zip $(EXEC_DIR)tcping.exe > /dev/null @@ -83,7 +81,7 @@ @echo @echo "[+] Building the MacOS version" - @env GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping $(SOURCE_FILES) + @env GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping @echo "[+] Packaging the MacOS version" @tar -czvf $(EXEC_DIR)tcping_MacOS.tar.gz -C $(EXEC_DIR) tcping > /dev/null @@ -94,7 +92,7 @@ @echo @echo "[+] Building the MacOS ARM version" - @env GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping $(SOURCE_FILES) + @env GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping @echo "[+] Packaging the MacOS ARM version" @tar -czvf $(EXEC_DIR)tcping_MacOS_ARM.tar.gz -C $(EXEC_DIR) tcping > /dev/null @@ -105,11 +103,11 @@ @echo @echo "[+] Building the FreeBSD version" - @env GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping $(SOURCE_FILES) + @env GOOS=freebsd GOARCH=amd64 go build -ldflags "-s -w" -o $(EXEC_DIR)tcping @echo "[+] Packaging the FreeBSD AMD64 version" - @tar -czvf $(EXEC_DIR)tcping_freebsd.tar.gz -C $(EXEC_DIR) tcping > /dev/null - @sha256sum $(EXEC_DIR)tcping_freebsd.tar.gz + @tar -czvf $(EXEC_DIR)tcping_FreeBSD.tar.gz -C $(EXEC_DIR) tcping > /dev/null + @sha256sum $(EXEC_DIR)tcping_FreeBSD.tar.gz @echo "[+] Removing the FreeBSD binary" @rm $(EXEC_DIR)tcping diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/README.md new/tcping-2.4.0/README.md --- old/tcping-2.0.0/README.md 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/README.md 2023-09-10 16:07:16.000000000 +0200 @@ -41,11 +41,12 @@ - [Basic usage](#basic-usage) - [Retry hostname lookup (`-r`) flag](#retry-hostname-lookup--r-flag) - [JSON output (`-j --pretty`) flag](#json-output--j---pretty-flag) - - [Usage](#usage) - - [On `Linux` and `macOS`](#on-linux-and-macos) - - [On `Windows`](#on-windows) - - [Using Docker](#using-docker) - [Download](#download) + - [Usage](#usage) + - [Linux - Debian and Ubuntu](#linux---debian-and-ubuntu) + - [Linux, BSD and mac OS](#linux-bsd-and-mac-os) + - [Windows](#windows) + - [Docker](#docker) - [Flags](#flags) - [Tips](#tips) - [Notes](#notes) @@ -77,17 +78,84 @@ --- +## Download + +- ### [Windows](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_Windows.zip) + +- ### [Linux](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_Linux.tar.gz) - Also available through `brew` and [.deb package](#linux---debian-and-ubuntu) + +- ### [macOS](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_MacOS.tar.gz) - Also available through `brew` + +- ### [macOS M1 - ARM](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_MacOS_ARM.tar.gz) - Also available through `brew` + +- ### [FreeBSD](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_FreeBSD.tar.gz) + +When the download is complete, head to the [usage](#usage) section. + +**Alternatively**, you can: + +- Use the `Docker` images: + + ```bash + docker pull pouriyajamshidi/tcping:latest + ``` + + > Image is also available on GitHub container registry: + + ```bash + docker pull ghcr.io/pouriyajamshidi/tcping:latest + ``` + +- Install using `go install`: + + ```bash + go install github.com/pouriyajamshidi/tcping@latest + ``` + +- Install using `brew`: + + ```bash + brew install pouriyajamshidi/tap/tcping + ``` + +- Or compile the code yourself by running the `make` command in the `tcping` directory: + + ```bash + make build + ``` + + This will give you a compressed file with executables for all the supported operating systems inside the `executables` folder. + +--- + ## Usage -Download TCPING for your operating system [here](#download), extract it. Then, follow the instructions below: +Follow the instructions below for your operating system: -- [Linux and macOS](#on-linux-and-macos) -- [Windows](#on-windows) -- [Docker images](#using-docker) +- [Linux - Debian and Ubuntu](#linux---debian-and-ubuntu) +- [Linux, BSD and macOS](#linux-bsd-and-mac-os) +- [Windows](#windows) +- [Docker images](#docker) Also check the [available flags here](#flags). -### On `Linux` and `macOS` +### Linux - Debian and Ubuntu + +On **Debian** and its flavors such as **Ubuntu**, download the `.deb` package: + +```bash +wget https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_amd64.deb -O /tmp/tcping.deb +``` + +And install it: + +```bash +sudo apt install -y /tmp/tcping.deb +``` + +If you are using different Linux distros, proceed to [this section](#linux-bsd-and-mac-os). + +### Linux, BSD and mac OS Extract the file: @@ -101,6 +169,10 @@ # on Mac OS ARM # tar -xvf tcping_MacOS_ARM.tar.gz +# +# on BSD +# +tar -xvf tcping_FreeBSD.tar.gz ``` Make the file executable: @@ -123,7 +195,7 @@ tcping 10.10.10.1 22 ``` -### On `Windows` +### Windows We recommend [Windows Terminal](https://apps.microsoft.com/store/detail/windows-terminal/9N0DX20HK701) for the best experience and proper colorization. @@ -136,7 +208,7 @@ tcping www.example.com 443 -r 10 ``` -### Using Docker +### Docker The Docker image can be used like: @@ -150,70 +222,26 @@ --- -## Download - -- ### [Windows](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_Windows.zip) - -- ### [Linux](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_Linux.tar.gz) - Also available through `brew` - -- ### [macOS](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_MacOS.tar.gz) - Also available through `brew` - -- ### [macOS M1 - ARM](https://github.com/pouriyajamshidi/tcping/releases/latest/download/tcping_MacOS_ARM.tar.gz) - Also available through `brew` - -When the download is complete, head to the [usage](#usage) section. - -**Alternatively**, you can: - -- Use the `Docker` images: - - ```bash - docker pull pouriyajamshidi/tcping:latest - ``` - - > Image is also available on GitHub container registry: - - ```bash - docker pull ghcr.io/pouriyajamshidi/tcping:latest - ``` - -- Install using `go install`: - - ```bash - go install github.com/pouriyajamshidi/tcping@latest - ``` - -- Install using `brew`: - - ```bash - brew install pouriyajamshidi/tap/tcping - ``` - -- Or compile the code yourself by running the `make` command in the `tcping` directory: - - ```bash - make build - ``` - - This will give you a compressed file with executables for all the supported operating systems inside the `executables` folder. - ---- - ## Flags The following flags are available to control the behavior of application: -| Flag | Description | -| ---------- | ------------------------------------------------------------------------------------------------------------------------- | -| `-4` | Only use IPv4 addresses | -| `-6` | Only use IPv6 addresses | -| `-r` | Retry resolving target's hostname after `<n>` number of failed probes. e.g. -r 10 to retry after 10 failed probes | -| `-c` | Stop after `<n>` probes, regardless of the result. By default, no limit will be applied (available from version `v1.23`) | -| `-j` | Output in `JSON` format | -| `--pretty` | Prettify the `JSON` output | -| `-v` | Print version | -| `-u` | Check for updates | +| Flag | Description | +| ---------- | ----------------------------------------------------------------------------------------------------------------- | +| `-4` | Only use IPv4 addresses | +| `-6` | Only use IPv6 addresses | +| `-r` | Retry resolving target's hostname after `<n>` number of failed probes. e.g. -r 10 to retry after 10 failed probes | +| `-c` | Stop after `<n>` probes, regardless of the result. By default, no limit will be applied | +| `--db` | Path and file name to store tcping output to sqlite database. e.g. `--db /tmp/tcping.db` | +| `-t` | Time to wait for a response, in seconds. Real number allowed. 0 means infinite timeout | +| `-i` | Interval between sending probes | +| `-I` | Interface name to use for sending probes | +| `-j` | Output in `JSON` format | +| `--pretty` | Prettify the `JSON` output | +| `-v` | Print version | +| `-u` | Check for updates | -> Without specifying the `-4` and `-6` flags, tcping will use one randomly based on DNS lookups. +> Without specifying the `-4` and `-6` flags, tcping will randomly select an IP address based on DNS lookups. --- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/db.go new/tcping-2.4.0/db.go --- old/tcping-2.0.0/db.go 1970-01-01 01:00:00.000000000 +0100 +++ new/tcping-2.4.0/db.go 2023-09-10 16:07:16.000000000 +0200 @@ -0,0 +1,257 @@ +package main + +import ( + "database/sql" + "fmt" + "math" + "os" + "strings" + "time" + "unicode" + + _ "github.com/mattn/go-sqlite3" +) + +type database struct { + db *sql.DB + dbPath string + tableName string +} + +const ( + eventTypeStatistics = "statistics" + eventTypeHostnameChange = "hostname change" + tableSchema = ` +-- Organized row names together for better readability +CREATE TABLE %s ( + id INTEGER PRIMARY KEY, + event_type TEXT NOT NULL, -- for the data type eg. statistics, hostname change + timestamp DATETIME, + addr TEXT, + hostname TEXT, + port INTEGER, + hostname_resolve_retries INTEGER, + + hostname_changed_to TEXT, + hostname_change_time DATETIME, + + latency_min REAL, + latency_avg REAL, + latency_max REAL, + + total_duration TEXT, + start_time DATETIME, + end_time DATETIME, + + never_succeed_probe INTEGER, -- value will be 1 if a probe never succeeded + never_failed_probe INTEGER, -- value will be 1 if a probe never failed + last_successful_probe DATETIME, + last_unsuccessful_probe DATETIME, + + longest_uptime TEXT, + longest_uptime_start DATETIME, + longest_uptime_end DATETIME, + + longest_downtime TEXT, + longest_downtime_start DATETIME, + longest_downtime_end DATETIME, + + total_packets INTEGER, + total_packet_loss REAL, + total_successful_probes INTEGER, + total_unsuccessful_probes INTEGER, + + total_uptime TEXT, + total_downtime TEXT +);` +) + +// newDb creates a newDb with the given path and returns `database` struct +func newDb(args []string, dbPath string) database { + tableName := newTableName(args) + tableSchema := fmt.Sprintf(tableSchema, tableName) + + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + colorRed("\nError while creating the database %q: %s\n", dbPath, err) + os.Exit(1) + } + + _, err = db.Exec(tableSchema) + if err != nil { + colorRed("\nError while writing to the database %q \nerr: %s\n", dbPath, err) + os.Exit(1) + } + + return database{db, dbPath, tableName} +} + +// newTableName will return correctly formatted table name +// formatting the table name as "example_com_port_hour_minute_sec_day_month_year" +// table name can't have '.' and can't start with numbers +func newTableName(args []string) string { + tableName := fmt.Sprintf("%s_%s_%s", strings.ReplaceAll(args[0], ".", "_"), args[1], time.Now().Format("15_04_05_01_02_2006")) + + if unicode.IsNumber(rune(tableName[0])) { + tableName = "_" + tableName + } + + return tableName +} + +// save will insert the table name and +// saves the args to the database +func (s database) save(query string, args ...any) error { + // inserting the table name + statement := fmt.Sprintf(query, s.tableName) + + // saving to the db + _, err := s.db.Exec(statement, args...) + + return err +} + +// saveStats saves stats to the dedbase with proper fomatting +func (s database) saveStats(stat stats) error { + // %s will be replaced by the table name + schema := `INSERT INTO %s (event_type, timestamp, + addr, hostname, port, hostname_resolve_retries, + total_successful_probes, total_unsuccessful_probes, + never_succeed_probe, never_failed_probe, + last_successful_probe, last_unsuccessful_probe, + total_packets, total_packet_loss, + total_uptime, total_downtime, + longest_uptime, longest_uptime_end, longest_uptime_start, + longest_downtime, longest_downtime_start, longest_downtime_end, + latency_min, latency_avg, latency_max, + start_time, end_time, total_duration) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + + totalPackets := stat.totalSuccessfulProbes + stat.totalUnsuccessfulProbes + packetLoss := (float32(stat.totalUnsuccessfulProbes) / float32(totalPackets)) * 100 + if math.IsNaN(float64(packetLoss)) { + packetLoss = 0 + } + + // If the time is zero, that means it never failed. + // In this case, the time should be empty instead of "0001-01-01 00:00:00". + // Rather, it should be left empty. + lastSuccessfulProbe := stat.lastSuccessfulProbe.Format(timeFormat) + var neverSucceedProbe, neverFailedProbe bool + if stat.lastSuccessfulProbe.IsZero() { + lastSuccessfulProbe = "" + neverSucceedProbe = true + } + lastUnsuccessfulProbe := stat.lastUnsuccessfulProbe.Format(timeFormat) + if stat.lastUnsuccessfulProbe.IsZero() { + lastUnsuccessfulProbe = "" + neverFailedProbe = true + } + + // if the longest uptime is emtpy, then the column should also be empty + var longestUptimeDuration, longestUptimeStart, longestUptimeEnd string + var longestDowntimeDuration, longestDowntimeStart, longestDowntimeEnd string + if stat.longestUptime.start.IsZero() { + longestUptimeDuration = "0s" + longestUptimeStart = "" + longestUptimeEnd = "" + } else { + longestUptimeDuration = stat.longestUptime.duration.String() + longestUptimeStart = stat.longestUptime.start.Format(timeFormat) + longestUptimeEnd = stat.longestUptime.end.Format(timeFormat) + } + if stat.longestDowntime.start.IsZero() { + longestDowntimeDuration = "0s" + longestDowntimeStart = "" + longestDowntimeEnd = "" + } else { + longestDowntimeDuration = stat.longestDowntime.duration.String() + longestDowntimeStart = stat.longestDowntime.start.Format(timeFormat) + longestDowntimeEnd = stat.longestDowntime.end.Format(timeFormat) + } + + var totalDuration string + if stat.endTime.IsZero() { + totalDuration = time.Since(stat.startTime).String() + } else { + totalDuration = stat.endTime.Sub(stat.startTime).String() + } + + err := s.save(schema, + eventTypeStatistics, time.Now().Format(timeFormat), + stat.userInput.ip.String(), stat.userInput.hostname, stat.userInput.port, stat.retriedHostnameLookups, + stat.totalSuccessfulProbes, stat.totalUnsuccessfulProbes, + neverSucceedProbe, neverFailedProbe, + lastSuccessfulProbe, lastUnsuccessfulProbe, + totalPackets, packetLoss, + stat.totalUptime.String(), stat.totalDowntime.String(), + longestUptimeDuration, longestUptimeStart, longestUptimeEnd, + longestDowntimeDuration, longestDowntimeStart, longestDowntimeEnd, + stat.rttResults.min, stat.rttResults.average, stat.rttResults.max, + stat.startTime.Format(timeFormat), stat.endTime.Format(timeFormat), totalDuration, + ) + + return err +} + +// saveHostNameChang saves the hostname changes +// in multiple rows with event_type = eventTypeHostnameChange +func (s database) saveHostNameChange(h []hostnameChange) error { + // %s will be replaced by the table name + schema := `INSERT INTO %s + (event_type, hostname_changed_to, hostname_change_time) + VALUES (?, ?, ?)` + + for _, host := range h { + if host.Addr.String() == "" { + continue + } + err := s.save(schema, eventTypeHostnameChange, host.Addr.String(), host.When.Format(timeFormat)) + if err != nil { + return err + } + } + + return nil +} + +// printStart will let the user know the program is running by +// printing a msg with the hostname, and port number to stdout +func (s database) printStart(hostname string, port uint16) { + fmt.Printf("TCPinging %s on port %d\n", hostname, port) +} + +// printStatistics saves the statistics to the given database +// calls stat.printer.printError() on err +func (s database) printStatistics(stat stats) { + err := s.saveStats(stat) + if err != nil { + s.printError("\nError while writing stats to the database %q\nerr: %s", s.dbPath, err) + } + + // Hostname changes should be written during the final call. + // If the endtime is 0, it indicates that this is not the last call. + if !stat.endTime.IsZero() { + err = s.saveHostNameChange(stat.hostnameChanges) + if err != nil { + s.printError("\nError while writing hostname changes to the database %q\nerr: %s", s.dbPath, err) + } + + } + + colorYellow("\nStatistics for %q have been saved to %q in the table %q\n", stat.userInput.hostname, s.dbPath, s.tableName) +} + +// printError prints the err to the stderr and exits with status code 1 +func (s database) printError(format string, args ...any) { + fmt.Fprintf(os.Stderr, format, args...) + os.Exit(1) +} + +// Satisfying the "printer" interface. +func (s database) printProbeSuccess(hostname, ip string, port uint16, streak uint, rtt float32) {} +func (s database) printProbeFail(hostname, ip string, port uint16, streak uint) {} +func (s database) printRetryingToResolve(hostname string) {} +func (s database) printTotalDownTime(downtime time.Duration) {} +func (s database) printVersion() {} +func (s database) printInfo(format string, args ...any) {} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/db_test.go new/tcping-2.4.0/db_test.go --- old/tcping-2.0.0/db_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/tcping-2.4.0/db_test.go 2023-09-10 16:07:16.000000000 +0200 @@ -0,0 +1,233 @@ +package main + +import ( + "fmt" + "net/netip" + "strconv" + "testing" + "time" +) + +func TestNewDB(t *testing.T) { + arg := []string{"localhost", "8001"} + s := newDb(arg, ":memory:") + rows, err := s.db.Query("SELECT name FROM sqlite_master WHERE type='table';") + if err != nil { + t.Error(err) + return + } + defer rows.Close() + defer s.db.Close() + + rows.Next() + var tblName string + err = rows.Scan(&tblName) + isNil(t, err) + Equals(t, tblName, s.tableName) +} + +func TestDbSaveStats(t *testing.T) { + // There are many fields, so many things could go wrong; that's why this elaborate test. + arg := []string{"localhost", "8001"} + s := newDb(arg, ":memory:") + defer s.db.Close() + + stat := mockStats() + err := s.saveStats(stat) + isNil(t, err) + + query := `SELECT + addr, hostname, port, hostname_resolve_retries, + total_successful_probes, total_unsuccessful_probes, + never_succeed_probe, never_failed_probe, + last_successful_probe, last_unsuccessful_probe, + total_packets, total_packet_loss, + total_uptime, total_downtime, + longest_uptime, longest_uptime_end, longest_uptime_start, + longest_downtime, longest_downtime_start, longest_downtime_end, + latency_min, latency_avg, latency_max, + start_time, end_time, total_duration + FROM ` + fmt.Sprintf("%s WHERE event_type = '%s'", s.tableName, eventTypeStatistics) + + rows, err := s.db.Query(query) + isNil(t, err) + + if !rows.Next() { + t.Error("rows are empty; expted 1 row") + return + } + + var ( + addr, hostname, port string + hostNameResolveTries uint + totalSuccessfulProbes, totalUnsuccessfulProbes uint + neverSucceedProbe, neverFailedProbe bool + lastSuccessfulProbe, lastUnsuccessfulProbe time.Time + totalPackets uint + totalPacketsLoss float32 + totalUptime, totalDowntime string + longestUptime string + longestUptimeStart, longestUptimeEnd time.Time + longestDowntime string + longestDowntimeStart, longestDowntimeEnd time.Time + lMin, lAvg, lMax float32 + startTimestamp, endTimestamp time.Time + totalDuration string + ) + + err = rows.Scan( + &addr, &hostname, &port, &hostNameResolveTries, + &totalSuccessfulProbes, &totalUnsuccessfulProbes, + &neverSucceedProbe, &neverFailedProbe, + &lastSuccessfulProbe, &lastUnsuccessfulProbe, + &totalPackets, &totalPacketsLoss, + &totalUptime, &totalDowntime, + &longestUptime, &longestUptimeStart, &longestUptimeEnd, + &longestDowntime, &longestDowntimeStart, &longestDowntimeEnd, + &lMin, &lAvg, &lMax, + &startTimestamp, &endTimestamp, &totalDuration, + ) + + isNil(t, err) + rows.Close() + + t.Log("the line number will tell you where the error happend") + Equals(t, addr, stat.userInput.ip.String()) + Equals(t, hostname, stat.userInput.hostname) + Equals(t, totalSuccessfulProbes, stat.totalSuccessfulProbes) + Equals(t, totalUnsuccessfulProbes, stat.totalUnsuccessfulProbes) + Equals(t, port, strconv.Itoa(int(stat.userInput.port))) + + Equals(t, neverSucceedProbe, stat.lastSuccessfulProbe.IsZero()) + Equals(t, neverFailedProbe, stat.lastUnsuccessfulProbe.IsZero()) + + Equals(t, lMin, stat.rttResults.min) + Equals(t, lAvg, stat.rttResults.average) + Equals(t, lMax, stat.rttResults.max) + Equals(t, startTimestamp.Format(timeFormat), stat.startTime.Format(timeFormat)) + Equals(t, endTimestamp.Format(timeFormat), stat.endTime.Format(timeFormat)) + + actualDuration := stat.endTime.Sub(stat.startTime).String() + Equals(t, totalDuration, actualDuration) + Equals(t, totalUptime, stat.totalUptime.String()) + Equals(t, totalDowntime, stat.totalDowntime.String()) + Equals(t, totalPackets, stat.totalSuccessfulProbes+stat.totalUnsuccessfulProbes) + + Equals(t, longestUptime, stat.longestUptime.duration.String()) + Equals(t, longestUptimeStart.Format(timeFormat), stat.longestUptime.start.Format(timeFormat)) + Equals(t, longestUptimeEnd.Format(timeFormat), stat.longestUptime.end.Format(timeFormat)) + + Equals(t, longestDowntime, stat.longestDowntime.duration.String()) + Equals(t, longestDowntimeStart.Format(timeFormat), stat.longestDowntime.start.Format(timeFormat)) + Equals(t, longestDowntimeEnd.Format(timeFormat), stat.longestDowntime.end.Format(timeFormat)) + +} + +func TestSaveHostname(t *testing.T) { + // There are many fields, so many things could go wrong; that's why this elaborate test. + arg := []string{"localhost", "8001"} + s := newDb(arg, ":memory:") + defer s.db.Close() + stat := mockStats() + + err := s.saveHostNameChange(stat.hostnameChanges) + isNil(t, err) + // testing the host names if they are properly written + query := `SELECT + hostname_changed_to, hostname_change_time + FROM ` + fmt.Sprintf("%s WHERE event_type IS '%s';", s.tableName, eventTypeHostnameChange) + + rows, err := s.db.Query(query) + isNil(t, err) + + idx := 0 + for rows.Next() { + var hostName string + var cTime time.Time + err = rows.Scan(&hostName, &cTime) + isNil(t, err) + + actualHost := stat.hostnameChanges[idx] + idx++ + Equals(t, hostName, actualHost.Addr.String()) + Equals(t, cTime.Format(timeFormat), actualHost.When.Format(timeFormat)) + } + Equals(t, idx, len(stat.hostnameChanges)) + rows.Close() +} + +func hostNameChange() []hostnameChange { + ipAddresses := []string{ + "192.168.1.1", + "10.0.0.1", + "172.16.0.1", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + } + var hostNames []hostnameChange + for i, ip := range ipAddresses { + host := hostnameChange{ + Addr: netip.MustParseAddr(ip), + When: time.Now().Add(time.Duration(i) * time.Minute), + } + hostNames = append(hostNames, host) + } + return hostNames +} + +func mockStats() stats { + stat := stats{ + startTime: time.Now(), + endTime: time.Now().Add(10 * time.Minute), + lastSuccessfulProbe: time.Now().Add(1 * time.Minute), + // lastUnsuccessfulProbe is left with the default value "0" to simulate no probe failed + retriedHostnameLookups: 10, + longestUptime: longestTime{ + start: time.Now().Add(20 * time.Second), + end: time.Now().Add(80 * time.Second), + duration: time.Minute, + }, + longestDowntime: longestTime{ + start: time.Now().Add(20 * time.Second), + end: time.Now().Add(140 * time.Second), + duration: time.Minute * 2, + }, + userInput: userInput{ + ip: netip.MustParseAddr("192.168.1.1"), + hostname: "example.com", + port: 1234, + }, + totalUptime: time.Second * 32, + totalDowntime: time.Second * 60, + totalSuccessfulProbes: 201, + totalUnsuccessfulProbes: 123, + rttResults: rttResult{ + min: 2.832, + average: 3.8123, + max: 4.0932, + }, + + hostnameChanges: hostNameChange(), + } + + return stat +} + +// Equals compares two values. +// This is for avoiding code duplications. +func Equals[T comparable](t *testing.T, value, want T) { + t.Helper() + if want != value { + t.Errorf("wanted %v; got %v", want, value) + t.FailNow() + } +} + +// isNil compares a value to nil, in some cases you may need to do `Equals(t, value, nil)` +func isNil(t *testing.T, value any) { + t.Helper() + + if value != nil { + t.Logf(`expected "%v" to be nil`, value) + t.FailNow() + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/go.mod new/tcping-2.4.0/go.mod --- old/tcping-2.0.0/go.mod 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/go.mod 2023-09-10 16:07:16.000000000 +0200 @@ -1,10 +1,11 @@ -module github.com/pouriyajamshidi/tcping +module github.com/pouriyajamshidi/tcping/v2 -go 1.20 +go 1.21 require ( github.com/google/go-github/v45 v45.2.0 github.com/gookit/color v1.5.4 + github.com/mattn/go-sqlite3 v1.14.17 github.com/stretchr/testify v1.8.4 ) @@ -13,7 +14,7 @@ github.com/google/go-querystring v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/sys v0.10.0 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/sys v0.12.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/go.sum new/tcping-2.4.0/go.sum --- old/tcping-2.0.0/go.sum 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/go.sum 2023-09-10 16:07:16.000000000 +0200 @@ -8,17 +8,19 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= -golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/statsprinter.go new/tcping-2.4.0/statsprinter.go --- old/tcping-2.0.0/statsprinter.go 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/statsprinter.go 2023-09-10 16:07:16.000000000 +0200 @@ -203,7 +203,7 @@ e *json.Encoder } -func newJsonPrinter(withIndent bool) *jsonPrinter { +func newJSONPrinter(withIndent bool) *jsonPrinter { encoder := json.NewEncoder(os.Stdout) if withIndent { encoder.SetIndent("", "\t") @@ -262,6 +262,7 @@ HostnameChanges []hostnameChange `json:"hostname_changes,omitempty"` IsIP *bool `json:"is_ip,omitempty"` Port uint16 `json:"port,omitempty"` + Rtt float32 `json:"time,omitempty"` // Success is a special field from probe messages, containing information // whether request was successful or not. @@ -357,6 +358,7 @@ Hostname: hostname, Addr: ip, Port: port, + Rtt: rtt, IsIP: &t, Success: &t, TotalSuccessfulProbes: streak, @@ -365,11 +367,11 @@ if hostname != "" { data.IsIP = &f - data.Message = fmt.Sprintf("Reply from %s (%s) on port %d", - hostname, ip, port) + data.Message = fmt.Sprintf("Reply from %s (%s) on port %d time=%.3f", + hostname, ip, port, rtt) } else { - data.Message = fmt.Sprintf("Reply from %s on port %d", - ip, port) + data.Message = fmt.Sprintf("Reply from %s on port %d time=%.3f", + ip, port, rtt) } p.print(data) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/tcping.go new/tcping-2.4.0/tcping.go --- old/tcping-2.0.0/tcping.go 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/tcping.go 2023-09-10 16:07:16.000000000 +0200 @@ -17,6 +17,13 @@ "github.com/google/go-github/v45/github" ) +const ( + version = "2.4.0" + owner = "pouriyajamshidi" + repo = "tcping" + dnsTimeout = 2 * time.Second +) + // printer is a set of methods for printers to implement. // // Printers should NOT modify any existing data nor do any calculations. @@ -95,16 +102,25 @@ } type userInput struct { - ip ipAddress + ip netip.Addr hostname string + networkInterface networkInterface retryHostnameLookupAfter uint // Retry resolving target's hostname after a certain number of failed requests probesBeforeQuit uint + timeout time.Duration + intervalBetweenProbes time.Duration port uint16 useIPv4 bool useIPv6 bool shouldRetryResolve bool } +type networkInterface struct { + raddr *net.TCPAddr + dialer net.Dialer + use bool +} + type longestTime struct { start time.Time end time.Time @@ -118,28 +134,11 @@ hasResults bool } -type replyMsg struct { - msg string - rtt float32 -} - type hostnameChange struct { Addr netip.Addr `json:"addr,omitempty"` When time.Time `json:"when,omitempty"` } -type ( - ipAddress = netip.Addr - cliArgs = []string -) - -const ( - version = "2.0.0" - owner = "pouriyajamshidi" - repo = "tcping" - dnsTimeout = 2 * time.Second -) - // signalHandler catches SIGINT and SIGTERM then prints tcping stats func signalHandler(tcpStats *stats) { sigChan := make(chan os.Signal, 1) @@ -169,11 +168,11 @@ // This should be used instead, as it makes // all the necessary calculations beforehand. func (tcpStats *stats) printStats() { - calcLongestUptime(tcpStats, - time.Duration(tcpStats.ongoingSuccessfulProbes)*time.Second) - calcLongestDowntime(tcpStats, - time.Duration(tcpStats.ongoingUnsuccessfulProbes)*time.Second) - + if tcpStats.wasDown { + calcLongestDowntime(tcpStats, time.Since(tcpStats.startOfDowntime)) + } else { + calcLongestUptime(tcpStats, time.Since(tcpStats.startOfUptime)) + } tcpStats.rttResults = calcMinAvgMaxRttTime(tcpStats.rtt) tcpStats.printer.printStatistics(*tcpStats) @@ -182,9 +181,15 @@ // shutdown calculates endTime, prints statistics and calls os.Exit(0). // This should be used as a main exit-point. func shutdown(tcpStats *stats) { - totalRuntime := tcpStats.totalUnsuccessfulProbes + tcpStats.totalSuccessfulProbes - tcpStats.endTime = tcpStats.startTime.Add(time.Duration(totalRuntime) * time.Second) + tcpStats.endTime = time.Now() tcpStats.printStats() + + // if the printer type is `database`, then close the db before + // exiting to prevent any memory leaks + if db, ok := tcpStats.printer.(database); ok { + db.db.Close() + } + os.Exit(0) } @@ -216,10 +221,14 @@ useIPv6 := flag.Bool("6", false, "only use IPv6.") retryHostnameResolveAfter := flag.Uint("r", 0, "retry resolving target's hostname after <n> number of failed probes. e.g. -r 10 to retry after 10 failed probes.") probesBeforeQuit := flag.Uint("c", 0, "stop after <n> probes, regardless of the result. By default, no limit will be applied.") - outputJson := flag.Bool("j", false, "output in JSON format.") - prettyJson := flag.Bool("pretty", false, "use indentation when using json output format. No effect without the '-j' flag.") + outputJSON := flag.Bool("j", false, "output in JSON format.") + prettyJSON := flag.Bool("pretty", false, "use indentation when using json output format. No effect without the '-j' flag.") showVersion := flag.Bool("v", false, "show version.") shouldCheckUpdates := flag.Bool("u", false, "check for updates.") + secondsBetweenProbes := flag.Float64("i", 1, "interval between sending probes. Real number allowed with dot as a decimal separator. The default is one second") + timeout := flag.Float64("t", 1, "time to wait for a response, in seconds. Real number allowed. 0 means infinite timeout.") + outputDb := flag.String("db", "", "path and file name to store tcping output to sqlite database.") + interfaceName := flag.String("I", "", "interface name or address") flag.CommandLine.Usage = usage @@ -232,8 +241,10 @@ // we need to set printers first, because they're used for // errors reporting and other output. - if *outputJson { - tcpStats.printer = newJsonPrinter(*prettyJson) + if *outputJSON { + tcpStats.printer = newJSONPrinter(*prettyJSON) + } else if *outputDb != "" { + tcpStats.printer = newDb(args, *outputDb) } else { tcpStats.printer = &planePrinter{} } @@ -257,7 +268,7 @@ usage() } - if *prettyJson && !*outputJson { + if *prettyJSON && !*outputJSON { tcpStats.printer.printError("--pretty has no effect without the -j flag.") usage() } @@ -296,6 +307,13 @@ tcpStats.userInput.ip = resolveHostname(tcpStats) tcpStats.startTime = time.Now() tcpStats.userInput.probesBeforeQuit = *probesBeforeQuit + tcpStats.userInput.timeout = secondsToDuration(*timeout) + + tcpStats.userInput.intervalBetweenProbes = secondsToDuration(*secondsBetweenProbes) + if tcpStats.userInput.intervalBetweenProbes < 2*time.Millisecond { + tcpStats.printer.printError("Wait interval should be more than 2 ms") + os.Exit(1) + } // this serves as a default starting value for tracking changes. tcpStats.hostnameChanges = []hostnameChange{ @@ -309,6 +327,10 @@ if tcpStats.userInput.retryHostnameLookupAfter > 0 && !tcpStats.isIP { tcpStats.userInput.shouldRetryResolve = true } + + if *interfaceName != "" { + tcpStats.userInput.networkInterface = newNetworkInterface(tcpStats, *interfaceName) + } } /* @@ -316,17 +338,30 @@ see: https://pkg.go.dev/flag */ -func permuteArgs(args cliArgs) { +func permuteArgs(args []string) { var flagArgs []string var nonFlagArgs []string for i := 0; i < len(args); i++ { v := args[i] if v[0] == '-' { - optionName := v[1:] + var optionName string + if v[1] == '-' { + optionName = v[2:] + } else { + optionName = v[1:] + } switch optionName { case "c": fallthrough + case "t": + fallthrough + case "db": + fallthrough + case "I": + fallthrough + case "i": + fallthrough case "r": /* out of index */ if len(args) <= i+1 { @@ -354,6 +389,80 @@ } } +// newNetworkInterface uses the 1st ip address of the interface +// if any err occurs it calls `tcpStats.printer.printError` and exits with statuscode 1. +// or return `networkInterface` +func newNetworkInterface(tcpStats *stats, netInterface string) networkInterface { + var interfaceAddress net.IP + + // if netinterface is the addres `interfaceAddress` var will not be `nil` + interfaceAddress = net.ParseIP(netInterface) + + if interfaceAddress == nil { + ief, err := net.InterfaceByName(netInterface) + if err != nil { + tcpStats.printer.printError("Interface %s not found", netInterface) + os.Exit(1) + } + + addrs, err := ief.Addrs() + if err != nil { + tcpStats.printer.printError("Unable to get Interface addresses") + os.Exit(1) + } + + // Iterating through the available addresses to identify valid IP configurations + for _, addr := range addrs { + if ip := addr.(*net.IPNet).IP; ip != nil { + // netip.Addr + nipAddr, err := netip.ParseAddr(ip.String()) + if err != nil { + continue + } + + if nipAddr.Is4() && !tcpStats.userInput.useIPv6 { + interfaceAddress = ip + break + } else if nipAddr.Is6() && !tcpStats.userInput.useIPv4 { + if nipAddr.IsLinkLocalUnicast() { + continue + } + interfaceAddress = ip + break + } + } + } + + if interfaceAddress == nil { + tcpStats.printer.printError("Unable to get Interface's IP Address") + os.Exit(1) + } + } + + // Initializing a networkInterface struct and setting the 'use' field to true + ni := networkInterface{ + use: true, + } + + // remote address + ni.raddr = &net.TCPAddr{ + IP: net.ParseIP(tcpStats.userInput.ip.String()), + Port: int(tcpStats.userInput.port), + } + + // local address + laddr := &net.TCPAddr{ + IP: interfaceAddress, + } + + ni.dialer = net.Dialer{ + LocalAddr: laddr, + Timeout: tcpStats.userInput.timeout, // Set the timeout duration + } + + return ni +} + // checkLatestVersion checks for updates and print a message func checkLatestVersion(p printer) { c := github.NewClient(nil) @@ -387,10 +496,10 @@ } // selectResolvedIP returns a single IPv4 or IPv6 address from the net.IP slice of resolved addresses -func selectResolvedIP(tcpStats *stats, ipAddrs []netip.Addr) ipAddress { +func selectResolvedIP(tcpStats *stats, ipAddrs []netip.Addr) netip.Addr { var index int var ipList []netip.Addr - var ip ipAddress + var ip netip.Addr switch { case tcpStats.userInput.useIPv4: @@ -406,12 +515,12 @@ } if len(ipList) > 1 { - index = rand.Intn(len(ipAddrs)) + index = rand.Intn(len(ipList)) } else { index = 0 } - ip, _ = netip.ParseAddr(ipList[index].String()) + ip, _ = netip.ParseAddr(ipList[index].Unmap().String()) case tcpStats.userInput.useIPv6: for _, ip := range ipAddrs { @@ -426,12 +535,12 @@ } if len(ipList) > 1 { - index = rand.Intn(len(ipAddrs)) + index = rand.Intn(len(ipList)) } else { index = 0 } - ip, _ = netip.ParseAddr(ipList[index].String()) + ip, _ = netip.ParseAddr(ipList[index].Unmap().String()) default: if len(ipAddrs) > 1 { @@ -440,14 +549,14 @@ index = 0 } - ip, _ = netip.ParseAddr(ipAddrs[index].String()) + ip, _ = netip.ParseAddr(ipAddrs[index].Unmap().String()) } return ip } // resolveHostname handles hostname resolution with a timeout value of a second -func resolveHostname(tcpStats *stats) ipAddress { +func resolveHostname(tcpStats *stats) netip.Addr { ip, err := netip.ParseAddr(tcpStats.userInput.hostname) if err == nil { return ip @@ -577,17 +686,31 @@ return float32(nano) / float32(time.Millisecond) } +// secondsToDuration returns the corresonding duration from seconds expressed with a float. +func secondsToDuration(seconds float64) time.Duration { + return time.Duration(1000*seconds) * time.Millisecond +} + +// maxDuration is the implementation of the math.Max function for time.Duration types. +// returns the longest duration of x or y. +func maxDuration(x, y time.Duration) time.Duration { + if x > y { + return x + } + return y +} + // handleConnError processes failed probes -func (tcpStats *stats) handleConnError(connTime time.Time) { +func (tcpStats *stats) handleConnError(connTime time.Time, elapsed time.Duration) { if !tcpStats.wasDown { tcpStats.startOfDowntime = connTime - calcLongestUptime(tcpStats, - time.Duration(tcpStats.ongoingSuccessfulProbes)*time.Second) + uptime := tcpStats.startOfDowntime.Sub(tcpStats.startOfUptime) + calcLongestUptime(tcpStats, uptime) tcpStats.startOfUptime = time.Time{} tcpStats.wasDown = true } - tcpStats.totalDowntime += time.Second + tcpStats.totalDowntime += elapsed tcpStats.lastUnsuccessfulProbe = connTime tcpStats.totalUnsuccessfulProbes += 1 tcpStats.ongoingUnsuccessfulProbes += 1 @@ -601,10 +724,10 @@ } // handleConnSuccess processes successful probes -func (tcpStats *stats) handleConnSuccess(rtt float32, connTime time.Time) { +func (tcpStats *stats) handleConnSuccess(rtt float32, connTime time.Time, elapsed time.Duration) { if tcpStats.wasDown { tcpStats.startOfUptime = connTime - downtime := time.Since(tcpStats.startOfDowntime).Truncate(time.Second) + downtime := tcpStats.startOfUptime.Sub(tcpStats.startOfDowntime) calcLongestDowntime(tcpStats, downtime) tcpStats.printer.printTotalDownTime(downtime) tcpStats.startOfDowntime = time.Time{} @@ -617,7 +740,7 @@ tcpStats.startOfUptime = connTime } - tcpStats.totalUptime += time.Second + tcpStats.totalUptime += elapsed tcpStats.lastSuccessfulProbe = connTime tcpStats.totalSuccessfulProbes += 1 tcpStats.ongoingSuccessfulProbes += 1 @@ -634,30 +757,39 @@ // tcping pings a host, TCP style func tcping(tcpStats *stats) { - IPAndPort := netip.AddrPortFrom(tcpStats.userInput.ip, tcpStats.userInput.port) - + var err error + var conn net.Conn connStart := time.Now() - conn, err := net.DialTimeout("tcp", IPAndPort.String(), time.Second) - connEnd := time.Since(connStart) - rtt := nanoToMillisecond(connEnd.Nanoseconds()) + + if tcpStats.userInput.networkInterface.use { + // dialer already contains the timeout value + conn, err = tcpStats.userInput.networkInterface.dialer.Dial("tcp", tcpStats.userInput.networkInterface.raddr.String()) + } else { + IPAndPort := netip.AddrPortFrom(tcpStats.userInput.ip, tcpStats.userInput.port) + conn, err = net.DialTimeout("tcp", IPAndPort.String(), tcpStats.userInput.timeout) + } + + connDuration := time.Since(connStart) + rtt := nanoToMillisecond(connDuration.Nanoseconds()) + + elapsed := maxDuration(connDuration, tcpStats.userInput.intervalBetweenProbes) if err != nil { - tcpStats.handleConnError(connStart) + tcpStats.handleConnError(connStart, elapsed) } else { - tcpStats.handleConnSuccess(rtt, connStart) + tcpStats.handleConnSuccess(rtt, connStart, elapsed) conn.Close() } - <-tcpStats.ticker.C + } func main() { - tcpStats := &stats{ - ticker: time.NewTicker(time.Second), - } + tcpStats := &stats{} + processUserInput(tcpStats) + tcpStats.ticker = time.NewTicker(tcpStats.userInput.intervalBetweenProbes) defer tcpStats.ticker.Stop() - processUserInput(tcpStats) signalHandler(tcpStats) tcpStats.printer.printStart(tcpStats.userInput.hostname, tcpStats.userInput.port) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/tcping-2.0.0/tcping_test.go new/tcping-2.4.0/tcping_test.go --- old/tcping-2.0.0/tcping_test.go 2023-08-05 15:50:50.000000000 +0200 +++ new/tcping-2.4.0/tcping_test.go 2023-09-10 16:07:16.000000000 +0200 @@ -18,8 +18,10 @@ s := stats{ printer: &dummyPrinter{}, userInput: userInput{ - ip: addr, - port: 12345, + ip: addr, + port: 12345, + intervalBetweenProbes: time.Second, + timeout: time.Second, }, ticker: time.NewTicker(time.Second), } @@ -203,3 +205,37 @@ } }) } + +func TestSecondsToDuration(t *testing.T) { + tests := []struct { + name string + seconds float64 + duration time.Duration + }{ + { + name: "positive integer", + seconds: 2, + duration: 2 * time.Second, + }, + { + name: "positive float", + seconds: 1.5, // 1.5 = 3 / 2 + duration: time.Second * 3 / 2, + }, + { + name: "negative integer", + seconds: -3, + duration: -3 * time.Second, + }, + { + name: "negative float", + seconds: -2.5, // -2.5 = -5 / 2 + duration: time.Second * -5 / 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.duration, secondsToDuration(tt.seconds)) + }) + } +} ++++++ vendor.tar.gz ++++++ ++++ 268941 lines of diff (skipped)