|
#!/usr/bin/env bash |
|
|
|
# ============================================================================= |
|
# NetScope - Network Quality Analysis Tool |
|
# ============================================================================= |
|
# Description: Production-grade tool for generating comprehensive network |
|
# quality reports for ISP debugging and troubleshooting |
|
# Author: Md. Sazzad Hossain Sharkar |
|
# Email: [email protected] |
|
# Version: 1.2.0 |
|
# Created: 2025-08-19 |
|
# License: MIT |
|
# ============================================================================= |
|
|
|
# Removed set -e for debugging |
|
|
|
# ============================================================================= |
|
# CONFIGURATION & CONSTANTS |
|
# ============================================================================= |
|
|
|
SCRIPT_NAME="$(basename "${0}")" |
|
SCRIPT_VERSION="1.2.0" |
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
|
TIMESTAMP="$(date '+%Y-%m-%d_%I-%M-%S_%p')" |
|
LOG_DIR="${HOME}/Desktop/network-quality-reports" |
|
CONFIG_DIR="${HOME}/.config/network-quality-reports" |
|
CONFIG_FILE="${CONFIG_DIR}/config.env" |
|
|
|
# API Configuration |
|
IPAPI_BASE="http://ip-api.com/json" |
|
IPAPI_TIMEOUT=10 |
|
NETWORK_QUALITY_TIMEOUT=60 |
|
|
|
# Test Configuration |
|
DEFAULT_TEST_COUNT=3 |
|
DEFAULT_TEST_INTERVAL=30 |
|
MAX_TEST_COUNT=10 |
|
MIN_TEST_INTERVAL=10 |
|
|
|
# WhatsApp markdown formatting |
|
WA_BOLD_START="*" |
|
WA_BOLD_END="*" |
|
WA_ITALIC_START="_" |
|
WA_ITALIC_END="_" |
|
WA_CODE_START="\`" |
|
WA_CODE_END="\`" |
|
WA_QUOTE_PREFIX="> " |
|
|
|
# Colors for output (with NO_COLOR support) |
|
if [[ -t 1 ]] && [[ -z "${NO_COLOR:-}" ]]; then |
|
RED=$'\033[0;31m' |
|
GREEN=$'\033[0;32m' |
|
YELLOW=$'\033[1;33m' |
|
BLUE=$'\033[0;34m' |
|
MAGENTA=$'\033[0;35m' |
|
CYAN=$'\033[0;36m' |
|
WHITE=$'\033[1;37m' |
|
BOLD=$'\033[1m' |
|
NC=$'\033[0m' # No Color |
|
else |
|
RED='' |
|
GREEN='' |
|
YELLOW='' |
|
BLUE='' |
|
MAGENTA='' |
|
CYAN='' |
|
WHITE='' |
|
BOLD='' |
|
NC='' |
|
fi |
|
|
|
# ============================================================================= |
|
# GLOBAL VARIABLES |
|
# ============================================================================= |
|
|
|
# IP API doesn't require token (free service) |
|
OUTPUT_FORMAT="whatsapp" |
|
VERBOSE=false |
|
DEBUG=false |
|
TEST_COUNT=${DEFAULT_TEST_COUNT} |
|
TEST_INTERVAL=${DEFAULT_TEST_INTERVAL} |
|
OUTPUT_FILE="" |
|
INCLUDE_SYSTEM_INFO=true |
|
USE_IPV6=false |
|
|
|
# ============================================================================= |
|
# UTILITY FUNCTIONS |
|
# ============================================================================= |
|
|
|
# Logging functions |
|
log_debug() { |
|
[[ "${DEBUG}" == true ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 |
|
} |
|
|
|
log_info() { |
|
echo -e "${GREEN}[INFO]${NC} $*" >&2 |
|
} |
|
|
|
log_warn() { |
|
echo -e "${YELLOW}[WARN]${NC} $*" >&2 |
|
} |
|
|
|
log_error() { |
|
echo -e "${RED}[ERROR]${NC} $*" >&2 |
|
} |
|
|
|
log_fatal() { |
|
echo -e "${RED}[FATAL]${NC} $*" >&2 |
|
exit 1 |
|
} |
|
|
|
# Progress indicator |
|
show_progress() { |
|
local current=$1 |
|
local total=$2 |
|
local message=$3 |
|
local percentage=$((current * 100 / total)) |
|
printf "\r${BLUE}[%3d%%]${NC} %s" "${percentage}" "${message}" >&2 |
|
} |
|
|
|
# Spinner for long-running operations |
|
spinner() { |
|
local pid=$1 |
|
local message=$2 |
|
local spinchars="β β β Ήβ Έβ Όβ ΄β ¦β §β β " |
|
local i=0 |
|
|
|
while kill -0 "${pid}" 2>/dev/null; do |
|
printf "\r${CYAN}%c${NC} %s" "${spinchars:i++%${#spinchars}:1}" "${message}" |
|
sleep 0.1 |
|
done |
|
printf "\r${GREEN}β${NC} %s\n" "${message}" |
|
} |
|
|
|
# ============================================================================= |
|
# CONFIGURATION MANAGEMENT |
|
# ============================================================================= |
|
|
|
create_directories() { |
|
local dirs=("${LOG_DIR}" "${CONFIG_DIR}") |
|
for dir in "${dirs[@]}"; do |
|
if [[ ! -d "${dir}" ]]; then |
|
log_debug "Creating directory: ${dir}" |
|
mkdir -p "${dir}" || log_fatal "Failed to create directory: ${dir}" |
|
fi |
|
done |
|
} |
|
|
|
load_config() { |
|
if [[ -f "${CONFIG_FILE}" ]]; then |
|
log_debug "Loading configuration from: ${CONFIG_FILE}" |
|
# shellcheck source=/dev/null |
|
source "${CONFIG_FILE}" |
|
else |
|
log_debug "No configuration file found, using defaults" |
|
fi |
|
} |
|
|
|
save_config() { |
|
log_debug "Saving configuration to: ${CONFIG_FILE}" |
|
cat >"${CONFIG_FILE}" <<EOF |
|
# NetScope Configuration |
|
# Generated on $(date) |
|
|
|
# ip-api.com doesn't require API token (free service) |
|
|
|
# Default test settings |
|
DEFAULT_TEST_COUNT=${TEST_COUNT} |
|
DEFAULT_TEST_INTERVAL=${TEST_INTERVAL} |
|
|
|
# Output preferences |
|
OUTPUT_FORMAT="${OUTPUT_FORMAT}" |
|
INCLUDE_SYSTEM_INFO=${INCLUDE_SYSTEM_INFO} |
|
USE_IPV6=${USE_IPV6} |
|
EOF |
|
} |
|
|
|
# ============================================================================= |
|
# DEPENDENCY CHECKING |
|
# ============================================================================= |
|
|
|
check_dependencies() { |
|
local deps=("curl" "networkQuality" "system_profiler" "route") |
|
local missing=() |
|
|
|
log_debug "Checking dependencies..." |
|
|
|
for dep in "${deps[@]}"; do |
|
if ! command -v "${dep}" >/dev/null 2>&1; then |
|
missing+=("${dep}") |
|
fi |
|
done |
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then |
|
log_fatal "Missing required dependencies: ${missing[*]}" |
|
fi |
|
|
|
# Check macOS version for networkQuality |
|
if ! networkQuality -h >/dev/null 2>&1; then |
|
log_fatal "networkQuality command not available. Requires macOS 12.0+" |
|
fi |
|
|
|
log_debug "All dependencies satisfied" |
|
} |
|
|
|
# ============================================================================= |
|
# NETWORK INFORMATION GATHERING |
|
# ============================================================================= |
|
|
|
get_dns_servers() { |
|
# Get DNS servers but filter out local addresses (127.x.x.x and ::1) |
|
scutil --dns 2>/dev/null | grep "nameserver\[0\]" | awk '{print $3}' | |
|
grep -v '^127\.' | grep -v '^::1$' | head -3 | tr '\n' ', ' | sed 's/,$//' || echo "Unknown" |
|
} |
|
|
|
get_network_interface_info() { |
|
local interface="${1:-$(route get default 2>/dev/null | grep interface | awk '{print $2}')}" |
|
local interface_type="Unknown" |
|
local link_speed="Unknown" |
|
local hardware_port="Unknown" |
|
|
|
if [[ -n "${interface}" ]]; then |
|
# Get hardware port name from networksetup |
|
hardware_port=$(networksetup -listallhardwareports 2>/dev/null | grep -B1 "Device: ${interface}" | grep "Hardware Port:" | cut -d: -f2 | xargs) |
|
|
|
# Determine interface type and get link speed |
|
if [[ "${hardware_port}" == *"Wi-Fi"* ]] || [[ "${hardware_port}" == *"AirPort"* ]]; then |
|
interface_type="Wi-Fi" |
|
# For Wi-Fi, get current link speed |
|
link_speed=$(/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I 2>/dev/null | grep "lastTxRate:" | awk '{print $2 " Mbps"}') |
|
if [[ -z "${link_speed}" ]] || [[ "${link_speed}" == " Mbps" ]]; then |
|
link_speed="Connected" |
|
fi |
|
elif [[ "${hardware_port}" == *"Ethernet"* ]] || [[ "${hardware_port}" == *"LAN"* ]]; then |
|
interface_type="Ethernet" |
|
# Get Ethernet link speed from ifconfig |
|
local media_info=$(ifconfig "${interface}" 2>/dev/null | grep "media:" | sed 's/.*(//' | sed 's/).*//') |
|
if [[ "${media_info}" == *"1000baseT"* ]] || [[ "${media_info}" == *"1000Base"* ]]; then |
|
link_speed="1 Gbps" |
|
elif [[ "${media_info}" == *"100baseT"* ]] || [[ "${media_info}" == *"100Base"* ]]; then |
|
link_speed="100 Mbps" |
|
elif [[ "${media_info}" == *"10baseT"* ]] || [[ "${media_info}" == *"10Base"* ]]; then |
|
link_speed="10 Mbps" |
|
elif [[ "${media_info}" == *"10Gbase"* ]]; then |
|
link_speed="10 Gbps" |
|
elif [[ "${media_info}" == *"2500Base"* ]]; then |
|
link_speed="2.5 Gbps" |
|
elif [[ "${media_info}" == *"5000Base"* ]]; then |
|
link_speed="5 Gbps" |
|
else |
|
link_speed="${media_info:-Unknown}" |
|
fi |
|
elif [[ "${hardware_port}" == *"Thunderbolt"* ]]; then |
|
interface_type="Thunderbolt Bridge" |
|
elif [[ "${hardware_port}" == *"Bluetooth"* ]]; then |
|
interface_type="Bluetooth PAN" |
|
else |
|
interface_type="${hardware_port:-Unknown}" |
|
fi |
|
fi |
|
|
|
echo "${interface}|${interface_type}|${link_speed}|${hardware_port}" |
|
} |
|
|
|
get_system_info() { |
|
log_debug "Gathering system information..." |
|
|
|
local model_name |
|
local macos_version |
|
local network_interface |
|
|
|
model_name=$(system_profiler SPHardwareDataType 2>/dev/null | |
|
grep "Model Name" | awk -F': ' '{print $2}' | xargs || echo "Unknown") |
|
|
|
macos_version=$(sw_vers -productVersion 2>/dev/null || echo "Unknown") |
|
|
|
network_interface=$(route get default 2>/dev/null | |
|
grep interface | awk '{print $2}' || echo "Unknown") |
|
|
|
cat <<EOF |
|
{ |
|
"device_model": "${model_name}", |
|
"macos_version": "${macos_version}", |
|
"network_interface": "${network_interface}", |
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" |
|
} |
|
EOF |
|
} |
|
|
|
get_public_ip_info() { |
|
log_debug "Retrieving public IP information from ip-api.com..." |
|
|
|
# ip-api.com provides free API with no token required |
|
# It includes more fields like AS, ASN, mobile detection, proxy detection, etc. |
|
local api_url="${IPAPI_BASE}" |
|
local curl_opts=("--silent" "--max-time" "${IPAPI_TIMEOUT}" "--fail") |
|
|
|
# Add specific fields we want (comprehensive data) |
|
api_url="${api_url}?fields=status,message,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,reverse,mobile,proxy,hosting,query" |
|
|
|
local response |
|
if ! response=$(curl "${curl_opts[@]}" "${api_url}" 2>/dev/null); then |
|
log_warn "Failed to retrieve public IP information" |
|
echo '{"status": "fail", "message": "Failed to retrieve public IP information"}' |
|
return 1 |
|
fi |
|
|
|
# Check if the API returned success |
|
local status=$(echo "${response}" | jq -r '.status // "fail"' 2>/dev/null) |
|
if [[ "${status}" != "success" ]]; then |
|
log_warn "IP API returned error: $(echo "${response}" | jq -r '.message // "Unknown error"' 2>/dev/null)" |
|
echo "${response}" |
|
return 1 |
|
fi |
|
|
|
# Transform ip-api.com response to match our expected format |
|
# This ensures compatibility with existing code |
|
echo "${response}" | jq '{ |
|
ip: .query, |
|
hostname: .reverse // "", |
|
city: .city, |
|
region: .regionName, |
|
country: .countryCode, |
|
loc: "\(.lat),\(.lon)", |
|
org: .org, |
|
isp: .isp, |
|
as: .as, |
|
asname: .asname, |
|
timezone: .timezone, |
|
mobile: .mobile, |
|
proxy: .proxy, |
|
hosting: .hosting, |
|
postal: .zip // "" |
|
}' 2>/dev/null || echo "${response}" |
|
} |
|
|
|
run_network_quality_test() { |
|
local test_number=$1 |
|
log_debug "Running network quality test ${test_number}/${TEST_COUNT}..." |
|
|
|
local result |
|
|
|
# Run networkQuality with verbose output and configuration details |
|
if ! result=$(timeout "${NETWORK_QUALITY_TIMEOUT}" networkQuality -v -c 2>/dev/null); then |
|
log_warn "Network quality test ${test_number} failed or timed out" |
|
echo "ERROR: Test failed or timed out" |
|
return 1 |
|
fi |
|
|
|
echo "${result}" |
|
} |
|
|
|
# ============================================================================= |
|
# REPORT GENERATION |
|
# ============================================================================= |
|
|
|
generate_report_header() { |
|
cat <<EOF |
|
{ |
|
"report_metadata": { |
|
"generator": "${SCRIPT_NAME}", |
|
"version": "${SCRIPT_VERSION}", |
|
"generated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", |
|
"local_time": "$(date '+%B %d, %Y at %I:%M:%S %p')", |
|
"local_time_raw": "$(date)", |
|
"timezone": "$(date +%Z)", |
|
"test_configuration": { |
|
"test_count": ${TEST_COUNT}, |
|
"test_interval_seconds": ${TEST_INTERVAL}, |
|
"include_system_info": ${INCLUDE_SYSTEM_INFO}, |
|
"use_ipv6": ${USE_IPV6} |
|
} |
|
}, |
|
EOF |
|
} |
|
|
|
# Store test results in memory for report generation |
|
declare -a TEST_RESULTS |
|
declare -a TEST_TIMESTAMPS |
|
declare -a TEST_DURATIONS |
|
|
|
run_all_tests() { |
|
# Clear previous results |
|
TEST_RESULTS=() |
|
TEST_TIMESTAMPS=() |
|
TEST_DURATIONS=() |
|
|
|
log_info "Running network quality tests..." |
|
|
|
for ((i = 1; i <= TEST_COUNT; i++)); do |
|
show_progress "${i}" "${TEST_COUNT}" "Running test ${i}/${TEST_COUNT}..." |
|
|
|
local start_time=$(date +%s) |
|
local test_result=$(run_network_quality_test "${i}") |
|
local end_time=$(date +%s) |
|
local duration=$((end_time - start_time)) |
|
|
|
TEST_RESULTS+=("${test_result}") |
|
TEST_TIMESTAMPS+=("$(date '+%I:%M:%S %p')") |
|
TEST_DURATIONS+=("${duration}") |
|
|
|
if [[ ${i} -lt ${TEST_COUNT} && ${TEST_INTERVAL} -gt 0 ]]; then |
|
log_debug "Waiting ${TEST_INTERVAL} seconds before next test..." |
|
sleep "${TEST_INTERVAL}" |
|
fi |
|
done |
|
|
|
printf "\r\033[K" >&2 # Clear the progress line |
|
} |
|
|
|
generate_whatsapp_report() { |
|
local report_file=$1 |
|
|
|
# Run all tests first |
|
run_all_tests |
|
|
|
# Get system and IP info |
|
local device_model="" |
|
local macos_version="" |
|
local network_interface="" |
|
local network_info="" |
|
local interface_type="" |
|
local link_speed="" |
|
|
|
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then |
|
device_model=$(system_profiler SPHardwareDataType 2>/dev/null | grep "Model Name" | awk -F': ' '{print $2}' | xargs || echo "Unknown") |
|
macos_version=$(sw_vers -productVersion 2>/dev/null || echo "Unknown") |
|
network_interface=$(route get default 2>/dev/null | grep interface | awk '{print $2}' || echo "Unknown") |
|
|
|
# Get detailed network interface info |
|
network_info=$(get_network_interface_info "${network_interface}") |
|
interface_type=$(echo "${network_info}" | cut -d'|' -f2) |
|
link_speed=$(echo "${network_info}" | cut -d'|' -f3) |
|
fi |
|
|
|
# Get DNS servers |
|
local dns_servers=$(get_dns_servers) |
|
|
|
# Get IP information |
|
local ip_info=$(get_public_ip_info) |
|
local ip=$(echo "${ip_info}" | jq -r '.ip // "Unknown"' 2>/dev/null) |
|
local isp=$(echo "${ip_info}" | jq -r '.isp // ""' 2>/dev/null) |
|
local org=$(echo "${ip_info}" | jq -r '.org // ""' 2>/dev/null) |
|
local city=$(echo "${ip_info}" | jq -r '.city // "Unknown"' 2>/dev/null) |
|
local country=$(echo "${ip_info}" | jq -r '.country // "Unknown"' 2>/dev/null) |
|
local asn=$(echo "${ip_info}" | jq -r '.as // ""' 2>/dev/null) |
|
local asname=$(echo "${ip_info}" | jq -r '.asname // ""' 2>/dev/null) |
|
|
|
log_info "Generating WhatsApp markdown report..." |
|
|
|
{ |
|
echo "${WA_BOLD_START}π NETWORK QUALITY REPORT${WA_BOLD_END}" |
|
echo "" |
|
echo "${WA_ITALIC_START}$(date '+%B %d, %Y at %I:%M:%S %p')${WA_ITALIC_END}" |
|
echo "" |
|
|
|
# Device info |
|
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then |
|
echo "${WA_BOLD_START}DEVICE & NETWORK${WA_BOLD_END}" |
|
echo "β’ ${device_model} | macOS ${macos_version}" |
|
echo "β’ Network: ${interface_type} (${network_interface})" |
|
if [[ "${link_speed}" != "Unknown" ]]; then |
|
echo "β’ Link Speed: ${WA_BOLD_START}${link_speed}${WA_BOLD_END}" |
|
fi |
|
echo "" |
|
fi |
|
|
|
# Connection info |
|
echo "${WA_BOLD_START}CONNECTION${WA_BOLD_END}" |
|
echo "β’ IP: ${WA_BOLD_START}${ip}${WA_BOLD_END}" |
|
echo "β’ DNS: ${WA_BOLD_START}${dns_servers}${WA_BOLD_END}" |
|
if [[ -n "${isp}" && "${isp}" != "null" ]]; then |
|
echo "β’ ISP: ${isp}" |
|
elif [[ -n "${org}" && "${org}" != "null" ]]; then |
|
echo "β’ Provider: ${org}" |
|
fi |
|
echo "β’ Location: ${city}, ${country}" |
|
if [[ -n "${asn}" && "${asn}" != "null" && "${asn}" != "" ]]; then |
|
if [[ -n "${asname}" && "${asname}" != "null" && "${asname}" != "" ]]; then |
|
echo "β’ ASN: ${WA_BOLD_START}${asn} ${asname}${WA_BOLD_END}" |
|
else |
|
echo "β’ ASN: ${WA_BOLD_START}${asn}${WA_BOLD_END}" |
|
fi |
|
fi |
|
echo "" |
|
|
|
# Test tool info |
|
echo "${WA_BOLD_START}TESTING TOOL${WA_BOLD_END}" |
|
echo "β’ ${WA_BOLD_START}networkQuality${WA_BOLD_END} by Apple Inc." |
|
echo "β’ Built-in macOS 12+ network diagnostic tool" |
|
echo "β’ Tests real-world network performance" |
|
echo "" |
|
|
|
# Test results |
|
echo "${WA_BOLD_START}SPEED TESTS (${TEST_COUNT} tests)${WA_BOLD_END}" |
|
echo "" |
|
|
|
local total_dl=0 total_ul=0 total_latency=0 |
|
local valid_tests=0 |
|
|
|
for ((i = 0; i < ${#TEST_RESULTS[@]}; i++)); do |
|
local test_num=$((i + 1)) |
|
local result="${TEST_RESULTS[i]}" |
|
|
|
if [[ "${result}" != "ERROR"* ]]; then |
|
# Parse the networkQuality JSON output |
|
local dl_throughput=$(echo "${result}" | jq -r '.dl_throughput // 0' 2>/dev/null || echo "0") |
|
local ul_throughput=$(echo "${result}" | jq -r '.ul_throughput // 0' 2>/dev/null || echo "0") |
|
local base_rtt=$(echo "${result}" | jq -r '.base_rtt // 0' 2>/dev/null || echo "0") |
|
local test_endpoint=$(echo "${result}" | jq -r '.test_endpoint // ""' 2>/dev/null) |
|
|
|
# Convert to Mbps |
|
local dl_mbps=$(awk "BEGIN {printf \"%.1f\", ${dl_throughput}/1048576}" 2>/dev/null || echo "0") |
|
local ul_mbps=$(awk "BEGIN {printf \"%.1f\", ${ul_throughput}/1048576}" 2>/dev/null || echo "0") |
|
local latency_ms=$(awk "BEGIN {printf \"%.0f\", ${base_rtt}}" 2>/dev/null || echo "0") |
|
|
|
echo "${WA_BOLD_START}Test ${test_num}${WA_BOLD_END} (${TEST_TIMESTAMPS[i]})" |
|
if [[ -n "${test_endpoint}" && "${test_endpoint}" != "null" ]]; then |
|
echo "Server: ${WA_ITALIC_START}${test_endpoint}${WA_ITALIC_END}" |
|
fi |
|
echo "β ${dl_mbps} Mbps | β ${ul_mbps} Mbps | ${latency_ms}ms" |
|
echo "" |
|
|
|
# Add to totals |
|
total_dl=$(awk "BEGIN {print ${total_dl} + ${dl_mbps}}" 2>/dev/null) |
|
total_ul=$(awk "BEGIN {print ${total_ul} + ${ul_mbps}}" 2>/dev/null) |
|
total_latency=$(awk "BEGIN {print ${total_latency} + ${latency_ms}}" 2>/dev/null) |
|
((valid_tests++)) |
|
else |
|
echo "${WA_BOLD_START}Test ${test_num}${WA_BOLD_END}: β Failed" |
|
echo "" |
|
fi |
|
done |
|
|
|
# Averages |
|
if [[ ${valid_tests} -gt 0 ]]; then |
|
local avg_dl=$(awk "BEGIN {printf \"%.1f\", ${total_dl}/${valid_tests}}" 2>/dev/null) |
|
local avg_ul=$(awk "BEGIN {printf \"%.1f\", ${total_ul}/${valid_tests}}" 2>/dev/null) |
|
local avg_latency=$(awk "BEGIN {printf \"%.0f\", ${total_latency}/${valid_tests}}" 2>/dev/null) |
|
|
|
echo "${WA_BOLD_START}AVERAGE${WA_BOLD_END}" |
|
echo "β ${WA_BOLD_START}${avg_dl} Mbps${WA_BOLD_END}" |
|
echo "β ${WA_BOLD_START}${avg_ul} Mbps${WA_BOLD_END}" |
|
echo "Latency: ${avg_latency}ms" |
|
fi |
|
|
|
echo "" |
|
echo "βββββββββββββββ" |
|
echo "${WA_ITALIC_START}NetScope v${SCRIPT_VERSION}${WA_ITALIC_END}" |
|
echo "${WA_ITALIC_START}Author: Md. Sazzad Hossain Sharkar${WA_ITALIC_END}" |
|
echo "${WA_ITALIC_START}Contact: [email protected]${WA_ITALIC_END}" |
|
|
|
} >"${report_file}" |
|
|
|
log_info "Report saved to: ${report_file}" |
|
} |
|
|
|
|
|
generate_text_report() { |
|
local report_file=$1 |
|
|
|
# Run all tests first |
|
run_all_tests |
|
|
|
# Get system info |
|
local device_model="" |
|
local macos_version="" |
|
local network_interface="" |
|
local network_info="" |
|
local interface_type="" |
|
local link_speed="" |
|
|
|
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then |
|
device_model=$(system_profiler SPHardwareDataType 2>/dev/null | grep "Model Name" | awk -F': ' '{print $2}' | xargs || echo "Unknown") |
|
macos_version=$(sw_vers -productVersion 2>/dev/null || echo "Unknown") |
|
network_interface=$(route get default 2>/dev/null | grep interface | awk '{print $2}' || echo "Unknown") |
|
|
|
# Get detailed network interface info |
|
network_info=$(get_network_interface_info "${network_interface}") |
|
interface_type=$(echo "${network_info}" | cut -d'|' -f2) |
|
link_speed=$(echo "${network_info}" | cut -d'|' -f3) |
|
fi |
|
|
|
# Get DNS servers |
|
local dns_servers=$(get_dns_servers) |
|
|
|
# Get IP information |
|
local ip_info=$(get_public_ip_info) |
|
local ip=$(echo "${ip_info}" | jq -r '.ip // "Unknown"' 2>/dev/null) |
|
local isp=$(echo "${ip_info}" | jq -r '.isp // ""' 2>/dev/null) |
|
local org=$(echo "${ip_info}" | jq -r '.org // ""' 2>/dev/null) |
|
local city=$(echo "${ip_info}" | jq -r '.city // "Unknown"' 2>/dev/null) |
|
local country=$(echo "${ip_info}" | jq -r '.country // "Unknown"' 2>/dev/null) |
|
local asn=$(echo "${ip_info}" | jq -r '.as // ""' 2>/dev/null) |
|
|
|
log_info "Generating text report..." |
|
|
|
{ |
|
echo "NETWORK QUALITY REPORT" |
|
echo "======================" |
|
echo "" |
|
echo "Date: $(date '+%B %d, %Y at %I:%M:%S %p')" |
|
echo "Tests Performed: ${TEST_COUNT}" |
|
echo "" |
|
|
|
if [[ "${INCLUDE_SYSTEM_INFO}" == true ]]; then |
|
echo "DEVICE INFORMATION" |
|
echo "------------------" |
|
echo "Device: ${device_model}" |
|
echo "OS: macOS ${macos_version}" |
|
echo "Network Interface: ${network_interface} (${interface_type})" |
|
if [[ "${link_speed}" != "Unknown" ]]; then |
|
echo "Link Speed: ${link_speed}" |
|
fi |
|
echo "" |
|
fi |
|
|
|
echo "NETWORK INFORMATION" |
|
echo "-------------------" |
|
echo "Public IP: ${ip}" |
|
if [[ -n "${isp}" && "${isp}" != "null" ]]; then |
|
echo "ISP: ${isp}" |
|
elif [[ -n "${org}" && "${org}" != "null" ]]; then |
|
echo "Provider: ${org}" |
|
fi |
|
echo "Location: ${city}, ${country}" |
|
if [[ -n "${asn}" && "${asn}" != "null" && "${asn}" != "" ]]; then |
|
echo "ASN: ${asn}" |
|
fi |
|
echo "" |
|
|
|
echo "TESTING TOOL" |
|
echo "------------" |
|
echo "Tool: Apple networkQuality" |
|
echo "Type: Built-in macOS 12+ network diagnostic utility" |
|
echo "Method: Real-world performance testing with parallel connections" |
|
echo "Metrics: Download/Upload throughput, Latency, and Responsiveness" |
|
echo "" |
|
|
|
echo "SPEED TEST RESULTS" |
|
echo "------------------" |
|
|
|
local total_dl=0 total_ul=0 total_latency=0 |
|
local valid_tests=0 |
|
|
|
for ((i = 0; i < ${#TEST_RESULTS[@]}; i++)); do |
|
local test_num=$((i + 1)) |
|
local result="${TEST_RESULTS[i]}" |
|
|
|
echo "Test #${test_num} (${TEST_TIMESTAMPS[i]}, Duration: ${TEST_DURATIONS[i]}s)" |
|
|
|
if [[ "${result}" != "ERROR"* ]]; then |
|
# Parse the networkQuality JSON output |
|
local dl_throughput=$(echo "${result}" | jq -r '.dl_throughput // 0' 2>/dev/null || echo "0") |
|
local ul_throughput=$(echo "${result}" | jq -r '.ul_throughput // 0' 2>/dev/null || echo "0") |
|
local base_rtt=$(echo "${result}" | jq -r '.base_rtt // 0' 2>/dev/null || echo "0") |
|
local responsiveness=$(echo "${result}" | jq -r '.responsiveness // 0' 2>/dev/null || echo "0") |
|
local test_endpoint=$(echo "${result}" | jq -r '.test_endpoint // ""' 2>/dev/null) |
|
|
|
# Convert to Mbps |
|
local dl_mbps=$(awk "BEGIN {printf \"%.2f\", ${dl_throughput}/1048576}" 2>/dev/null || echo "0") |
|
local ul_mbps=$(awk "BEGIN {printf \"%.2f\", ${ul_throughput}/1048576}" 2>/dev/null || echo "0") |
|
local latency_ms=$(awk "BEGIN {printf \"%.1f\", ${base_rtt}}" 2>/dev/null || echo "0") |
|
local resp_rpm=$(awk "BEGIN {printf \"%.0f\", ${responsiveness}}" 2>/dev/null || echo "0") |
|
|
|
if [[ -n "${test_endpoint}" && "${test_endpoint}" != "null" ]]; then |
|
echo " Test Server: ${test_endpoint}" |
|
fi |
|
echo " Download: ${dl_mbps} Mbps" |
|
echo " Upload: ${ul_mbps} Mbps" |
|
echo " Latency: ${latency_ms} ms" |
|
echo " Responsiveness: ${resp_rpm} RPM" |
|
|
|
# Add to totals |
|
total_dl=$(awk "BEGIN {print ${total_dl} + ${dl_mbps}}" 2>/dev/null) |
|
total_ul=$(awk "BEGIN {print ${total_ul} + ${ul_mbps}}" 2>/dev/null) |
|
total_latency=$(awk "BEGIN {print ${total_latency} + ${latency_ms}}" 2>/dev/null) |
|
((valid_tests++)) |
|
else |
|
echo " Status: Test Failed" |
|
fi |
|
echo "" |
|
done |
|
|
|
if [[ ${valid_tests} -gt 0 ]]; then |
|
echo "AVERAGE PERFORMANCE" |
|
echo "-------------------" |
|
local avg_dl=$(awk "BEGIN {printf \"%.2f\", ${total_dl}/${valid_tests}}" 2>/dev/null) |
|
local avg_ul=$(awk "BEGIN {printf \"%.2f\", ${total_ul}/${valid_tests}}" 2>/dev/null) |
|
local avg_latency=$(awk "BEGIN {printf \"%.1f\", ${total_latency}/${valid_tests}}" 2>/dev/null) |
|
|
|
echo "Average Download: ${avg_dl} Mbps" |
|
echo "Average Upload: ${avg_ul} Mbps" |
|
echo "Average Latency: ${avg_latency} ms" |
|
echo "" |
|
fi |
|
|
|
echo "======================" |
|
echo "Report Generated by NetScope v${SCRIPT_VERSION}" |
|
echo "Author: Md. Sazzad Hossain Sharkar ([email protected])" |
|
|
|
} >"${report_file}" |
|
|
|
log_info "Text report saved to: ${report_file}" |
|
} |
|
|
|
# ============================================================================= |
|
# COMMAND LINE INTERFACE |
|
# ============================================================================= |
|
|
|
show_version() { |
|
echo "${SCRIPT_NAME} version ${SCRIPT_VERSION}" |
|
exit 0 |
|
} |
|
|
|
show_help() { |
|
cat <<EOF |
|
${BOLD}NetScope${NC} - Professional Network Quality Analysis Tool |
|
${CYAN}Version ${SCRIPT_VERSION}${NC} |
|
|
|
${BOLD}QUICK START${NC} |
|
${GREEN}${SCRIPT_NAME}${NC} # Run with defaults (3 tests, 30s interval) |
|
${GREEN}${SCRIPT_NAME} -f whatsapp${NC} # Generate WhatsApp-ready report |
|
${GREEN}${SCRIPT_NAME} -c 1 -i 10${NC} # Quick single test |
|
|
|
${BOLD}DESCRIPTION${NC} |
|
Generates comprehensive network performance reports perfect for ISP support. |
|
Tests your connection quality using Apple's networkQuality tool and provides |
|
detailed diagnostics in multiple formats. |
|
|
|
${CYAN}Reports are saved to: ~/Desktop/network-quality-reports/${NC} |
|
|
|
${BOLD}COMMON USE CASES${NC} |
|
|
|
${YELLOW}1. Quick Network Check:${NC} |
|
${GREEN}${SCRIPT_NAME} -c 1 -i 10${NC} |
|
β Single test with minimal wait time |
|
|
|
${YELLOW}2. Detailed ISP Report (Recommended):${NC} |
|
${GREEN}${SCRIPT_NAME} -c 5 -i 60 -f text${NC} |
|
β 5 tests over 5 minutes for reliable data |
|
|
|
${YELLOW}3. Share via WhatsApp/Social Media:${NC} |
|
${GREEN}${SCRIPT_NAME} -f whatsapp${NC} |
|
β Formatted with emojis for easy mobile sharing |
|
|
|
${BOLD}OUTPUT FORMATS${NC} |
|
${CYAN}whatsapp${NC} Mobile-friendly with emojis for messaging apps (default) |
|
${CYAN}text${NC} Human-readable report for ISP technicians |
|
|
|
${BOLD}OPTIONS${NC} |
|
${BOLD}Basic:${NC} |
|
-h, --help Show this help message |
|
-v, --version Show version information |
|
|
|
${BOLD}Test Settings:${NC} |
|
-c, --count N Number of tests (1-${MAX_TEST_COUNT}, default: ${DEFAULT_TEST_COUNT}) |
|
-i, --interval SECS Seconds between tests (min: ${MIN_TEST_INTERVAL}, default: ${DEFAULT_TEST_INTERVAL}) |
|
|
|
${BOLD}Output Control:${NC} |
|
-f, --format FORMAT Output format: whatsapp, text (default: whatsapp) |
|
-o, --output FILE Custom output path (default: auto-generated) |
|
--no-system-info Skip device information |
|
|
|
${BOLD}Advanced:${NC} |
|
# -t, --token TOKEN (Deprecated - ip-api.com doesn't need tokens) |
|
--ipv6 Test IPv6 connectivity |
|
--verbose Show detailed progress |
|
--debug Enable debug output |
|
|
|
${BOLD}Configuration:${NC} |
|
--save-config Save current settings as defaults |
|
--show-config Display saved configuration |
|
|
|
${BOLD}IP GEOLOCATION SERVICE${NC} |
|
|
|
This tool uses ${CYAN}ip-api.com${NC} for free IP geolocation data: |
|
β’ No API token required |
|
β’ Includes ISP, ASN, and network details |
|
β’ Mobile/Proxy detection |
|
β’ 45 requests per minute limit (more than enough) |
|
|
|
The service automatically provides comprehensive network information |
|
without any configuration needed. |
|
|
|
${BOLD}EXAMPLES${NC} |
|
|
|
${YELLOW}Basic report (no configuration needed):${NC} |
|
${GREEN}${SCRIPT_NAME}${NC} |
|
|
|
${YELLOW}ISP troubleshooting report:${NC} |
|
${GREEN}${SCRIPT_NAME} -c 5 -i 60 -f text${NC} |
|
β Creates detailed report with 5 tests over 5 minutes |
|
|
|
${YELLOW}Quick test for immediate results:${NC} |
|
${GREEN}${SCRIPT_NAME} -c 1 -i 10 -f whatsapp${NC} |
|
β Single test, WhatsApp format, ready in ~20 seconds |
|
|
|
${YELLOW}Continuous monitoring (maximum):${NC} |
|
${GREEN}${SCRIPT_NAME} -c 10 -i 120 --verbose${NC} |
|
β 10 tests over 20 minutes with live progress |
|
|
|
${YELLOW}Custom location with text output:${NC} |
|
${GREEN}${SCRIPT_NAME} -f text -o ~/Documents/network_test.txt${NC} |
|
|
|
${YELLOW}One-time API token setup:${NC} |
|
${GREEN}${SCRIPT_NAME} -t abc123def456 --save-config${NC} |
|
β Token saved for all future reports |
|
|
|
${BOLD}OUTPUT FILES${NC} |
|
Reports are saved with timestamps: |
|
β’ ${CYAN}network_report_2025-01-19_03-45-00_PM.txt${NC} |
|
|
|
Default location: ${CYAN}~/Desktop/network-quality-reports/${NC} |
|
|
|
${BOLD}TROUBLESHOOTING${NC} |
|
|
|
${YELLOW}If tests fail:${NC} |
|
β’ Ensure you're on macOS 12.0 or later |
|
β’ Check internet connection stability |
|
β’ Try with fewer tests: -c 1 |
|
|
|
${YELLOW}For slow networks:${NC} |
|
β’ Increase interval: -i 120 (2 minutes between tests) |
|
β’ Reduce test count: -c 3 |
|
|
|
${BOLD}AUTHOR${NC} |
|
Md. Sazzad Hossain Sharkar |
|
Email: ${CYAN}[email protected]${NC} |
|
|
|
${BOLD}SUPPORT${NC} |
|
For issues, feature requests, or contributions: |
|
Email: ${CYAN}[email protected]${NC} |
|
|
|
${BOLD}SEE ALSO${NC} |
|
networkQuality(1) - Apple's network testing tool |
|
${CYAN}https://ip-api.com${NC} - Free IP geolocation service |
|
|
|
EOF |
|
exit 0 |
|
} |
|
|
|
parse_arguments() { |
|
while [[ $# -gt 0 ]]; do |
|
case $1 in |
|
-h | --help) |
|
show_help |
|
;; |
|
-v | --version) |
|
show_version |
|
;; |
|
--verbose) |
|
VERBOSE=true |
|
shift |
|
;; |
|
--debug) |
|
DEBUG=true |
|
VERBOSE=true |
|
shift |
|
;; |
|
-c | --count) |
|
if [[ -z "${2:-}" ]] || [[ ! "${2}" =~ ^[0-9]+$ ]] || [[ "${2}" -lt 1 ]] || [[ "${2}" -gt ${MAX_TEST_COUNT} ]]; then |
|
log_fatal "Invalid test count. Must be 1-${MAX_TEST_COUNT}" |
|
fi |
|
TEST_COUNT="$2" |
|
shift 2 |
|
;; |
|
-i | --interval) |
|
if [[ -z "${2:-}" ]] || [[ ! "${2}" =~ ^[0-9]+$ ]] || [[ "${2}" -lt ${MIN_TEST_INTERVAL} ]]; then |
|
log_fatal "Invalid interval. Must be β₯${MIN_TEST_INTERVAL} seconds" |
|
fi |
|
TEST_INTERVAL="$2" |
|
shift 2 |
|
;; |
|
-o | --output) |
|
if [[ -z "${2:-}" ]]; then |
|
log_fatal "Output file cannot be empty" |
|
fi |
|
OUTPUT_FILE="$2" |
|
shift 2 |
|
;; |
|
-f | --format) |
|
if [[ "${2:-}" =~ ^(text|whatsapp)$ ]]; then |
|
OUTPUT_FORMAT="$2" |
|
else |
|
log_fatal "Invalid format. Must be 'text' or 'whatsapp'" |
|
fi |
|
shift 2 |
|
;; |
|
-t | --token) |
|
log_warn "API tokens are no longer needed. Using free ip-api.com service." |
|
shift 2 |
|
;; |
|
--ipv6) |
|
USE_IPV6=true |
|
shift |
|
;; |
|
--no-system-info) |
|
INCLUDE_SYSTEM_INFO=false |
|
shift |
|
;; |
|
--save-config) |
|
save_config |
|
log_info "Configuration saved to: ${CONFIG_FILE}" |
|
exit 0 |
|
;; |
|
--show-config) |
|
if [[ -f "${CONFIG_FILE}" ]]; then |
|
cat "${CONFIG_FILE}" |
|
else |
|
log_info "No configuration file found at: ${CONFIG_FILE}" |
|
fi |
|
exit 0 |
|
;; |
|
-*) |
|
log_fatal "Unknown option: $1" |
|
;; |
|
*) |
|
log_fatal "Unexpected argument: $1" |
|
;; |
|
esac |
|
done |
|
} |
|
|
|
# ============================================================================= |
|
# MAIN EXECUTION |
|
# ============================================================================= |
|
|
|
main() { |
|
# Initialize |
|
create_directories |
|
load_config |
|
parse_arguments "$@" |
|
check_dependencies |
|
|
|
# Validate jq is available for API response processing |
|
if ! command -v jq >/dev/null 2>&1; then |
|
log_fatal "jq is required. Please install with: brew install jq" |
|
fi |
|
|
|
# Determine output file |
|
if [[ -z "${OUTPUT_FILE}" ]]; then |
|
local ext="txt" |
|
[[ "${OUTPUT_FORMAT}" == "whatsapp" ]] && ext="txt" |
|
OUTPUT_FILE="${LOG_DIR}/network_report_${TIMESTAMP}.${ext}" |
|
fi |
|
|
|
# Ensure output directory exists |
|
local output_dir |
|
output_dir="$(dirname "${OUTPUT_FILE}")" |
|
if [[ ! -d "${output_dir}" ]]; then |
|
mkdir -p "${output_dir}" || log_fatal "Cannot create output directory: ${output_dir}" |
|
fi |
|
|
|
# Display configuration if verbose |
|
if [[ "${VERBOSE}" == true ]]; then |
|
log_info "Configuration:" |
|
log_info " Test count: ${TEST_COUNT}" |
|
log_info " Test interval: ${TEST_INTERVAL} seconds" |
|
log_info " Output format: ${OUTPUT_FORMAT}" |
|
log_info " Output file: ${OUTPUT_FILE}" |
|
log_info " Include system info: ${INCLUDE_SYSTEM_INFO}" |
|
log_info " Use IPv6: ${USE_IPV6}" |
|
log_info " IP API: ip-api.com (no token required)" |
|
echo "" |
|
fi |
|
|
|
# Generate report |
|
case "${OUTPUT_FORMAT}" in |
|
whatsapp) |
|
generate_whatsapp_report "${OUTPUT_FILE}" |
|
;; |
|
text) |
|
generate_text_report "${OUTPUT_FILE}" |
|
;; |
|
esac |
|
|
|
log_info "${GREEN}Report generation completed successfully!${NC}" |
|
|
|
if [[ "${VERBOSE}" == true ]]; then |
|
echo "" |
|
log_info "Next steps:" |
|
log_info " 1. Review the generated report" |
|
log_info " 2. Share with your ISP for network debugging" |
|
log_info " 3. Run additional tests if needed" |
|
fi |
|
} |
|
|
|
# ============================================================================= |
|
# SCRIPT EXECUTION |
|
# ============================================================================= |
|
|
|
# Trap signals for cleanup |
|
trap 'log_error "Script interrupted"; exit 130' INT TERM |
|
|
|
# Run main function |
|
main "$@" |