module core import net import time import incubaid.herolib.ui.console import incubaid.herolib.core as herolib_core import math import os @[params] pub struct PingArgs { pub mut: address string = '8.8.8.8' nr_ping u16 = 2 // amount of ping requests we will do nr_ok u16 = 2 // how many of them need to be ok retry u8 // how many times fo we retry above sequence, basically we ping ourselves with -c 1 } // if ping ok, return true pub fn ping(args PingArgs) !bool { platform_ := herolib_core.platform()! mut cmd := 'ping' if args.address.contains(':') { cmd = 'ping6' } // if platform_ == .windows { // cmd += ' -n 1 -w 1000' if platform_ == .osx { cmd += ' -c1 -t2' } else { // linux cmd += ' -c1 -w2' } cmd += ' ${args.address}' if args.nr_ok > args.nr_ping { return error('nr_ok must be <= nr_ping') } for _ in 0 .. math.max(1, args.retry) { mut nrerrors := 0 for _ in 0 .. args.nr_ping { res := os.execute(cmd) if res.exit_code > 0 { nrerrors += 1 } console.print_debug('${cmd} ${res.exit_code} ${nrerrors}') } successes := args.nr_ping - nrerrors if successes >= args.nr_ok { return true } } return false } @[params] pub struct RebootWaitArgs { pub mut: address string @[required] // 192.168.8.8 timeout_down i64 = 60 // total time in seconds to wait till its down timeout_up i64 = 60 * 5 } // test if a tcp port answers //``` // address string //192.168.8.8 // port int = 22 // timeout u16 = 2000 // total time in milliseconds to keep on trying //``` pub fn reboot_wait(args RebootWaitArgs) ! { start_time := time.now().unix() mut run_time := 0.0 for true { console.print_debug('Waiting for server to go down...') run_time = time.now().unix() if run_time > start_time + args.timeout_down { return error('timeout in waiting for server down') } if ping(address: args.address)! == false { break } // println(ping(address: args.address)!) time.sleep(1) } for true { console.print_debug('Waiting for server to come back up...') run_time = time.now().unix() if run_time > start_time + args.timeout_up { return error('timeout in waiting for server up') } if ping(address: args.address)! == true { // println(ping(address: args.address)!) break } time.sleep(1) } } @[params] pub struct TcpPortTestArgs { pub mut: address string @[required] // 192.168.8.8 port int = 22 timeout u16 = 2000 // total time in milliseconds to keep on trying } // test if a tcp port answers //``` // address string //192.168.8.8 // port int = 22 // timeout u16 = 2000 // total time in milliseconds to keep on trying //``` pub fn tcp_port_test(args TcpPortTestArgs) bool { start_time := time.now().unix_milli() mut run_time := 0.0 for true { run_time = time.now().unix_milli() if run_time > start_time + args.timeout { return false } _ = net.dial_tcp('${args.address}:${args.port}') or { time.sleep(100 * time.millisecond) continue } // console.print_debug(socket) return true } return false } // return in milliseconds pub fn http_ping(args TcpPortTestArgs) !int { start_time := time.now().unix_milli() // Try to establish TCP connection console.print_debug('Pinging HTTP server at ${args.address}:${args.port}...') mut sock := net.dial_tcp('${args.address}:${args.port}') or { return error('failed to establish TCP connection to ${args.address}:${args.port}') } console.print_debug('TCP connection established to ${args.address}:${args.port}') // Send a simple HTTP GET request http_request := 'GET / HTTP/1.1\r\nHost: ${args.address}\r\nConnection: close\r\n\r\n' sock.write_string(http_request) or { sock.close()! return error('failed to send HTTP request to ${args.address}:${args.port}') } console.print_debug('HTTP request sent to ${args.address}:${args.port}') // Read response (at least some bytes to confirm it's an HTTP server) mut buf := []u8{len: 1024} _ = sock.read(mut buf) or { sock.close()! return error('failed to read HTTP response from ${args.address}:${args.port}') } console.print_debug('HTTP response received from ${args.address}:${args.port}') sock.close()! console.print_debug('TCP connection closed for ${args.address}:${args.port}') // Calculate and return the round-trip time end_time := time.now().unix_milli() return int(end_time - start_time) } // Wait until a web server responds properly to HTTP requests // Returns true when the server is responding, false on timeout pub fn http_wait(args TcpPortTestArgs) bool { start_time := time.now().unix_milli() mut run_time := 0.0 for true { run_time = time.now().unix_milli() if run_time > start_time + args.timeout { return false } // Try to ping the HTTP server _ = http_ping(args) or { // If http_ping fails, it means the server is not responding properly yet time.sleep(100 * time.millisecond) continue } // If http_ping succeeds, the server is responding properly return true } return false } // Returns the public IP address as known on the public side // Uses resolver4.opendns.com to fetch the IP address pub fn ipaddr_pub_get() !string { cmd := 'dig @resolver4.opendns.com myip.opendns.com +short' ipaddr := exec(cmd: cmd)! public_ip := ipaddr.output.trim('\n').trim(' \n') return public_ip } // also check the address is on local interface pub fn ipaddr_pub_get_check() !string { // Check if the public IP matches any local interface public_ip := ipaddr_pub_get()! if !is_ip_on_local_interface(public_ip)! { return error('Public IP ${public_ip} is NOT bound to any local interface (possibly behind a NAT firewall).') } return public_ip } // Check if the public IP matches any of the local network interfaces pub fn is_ip_on_local_interface(public_ip string) !bool { interfaces := exec(cmd: 'ip addr show', stdout: false) or { return error('Failed to enumerate network interfaces: ${err}') } lines := interfaces.output.split('\n') // Parse through the `ip addr show` output to find local IPs for line in lines { if line.contains('inet ') { parts := line.trim_space().split(' ') if parts.len > 1 { local_ip := parts[1].split('/')[0] // Extract the IP address if public_ip == local_ip { return true } } } } return false } // will give error if ssh test did not work pub fn ssh_check(args TcpPortTestArgs) ! { errmsg, res := ssh_testrun_internal(args)! if res != .ok { return error(errmsg) } } pub enum SSHResult { ok ping // timeout from ping tcpport // means we don't know the hostname its a dns issue ssh } pub fn ssh_test(args TcpPortTestArgs) !SSHResult { _, res := ssh_testrun_internal(args)! return res } // will give error if ssh test did not work pub fn ssh_wait(args TcpPortTestArgs) ! { start_time := time.now().unix_milli() mut run_time := 0.0 for true { run_time = time.now().unix_milli() errmsg, res := ssh_testrun_internal(args)! // console.print_debug(errmsg) if run_time > start_time + args.timeout { return error(errmsg) } if res == .ok { return } } } fn ssh_testrun_internal(args TcpPortTestArgs) !(string, SSHResult) { cmd := ' set -ex ssh -o BatchMode=yes -o ConnectTimeout=3 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -q root@${args.address} exit if [ $? -eq 0 ]; then echo "OK: SSH works" exit 0 fi timeout 1 nc -z "${args.address}" 22 >/dev/null 2>&1 if [ $? -eq 0 ]; then echo "ERROR: SSH failed but port ${args.port} open" exit 1 fi # Cross-platform ping (Linux vs macOS) if [[ "$(uname)" == "Darwin" ]]; then ping -c1 -t2 "${args.address}" >/dev/null 2>&1 else ping -c1 -w2 "${args.address}" >/dev/null 2>&1 fi if [ $? -eq 0 ]; then echo "ERROR: SSH & port test failed, host reachable by ping" exit 2 fi echo "ERROR: Host unreachable, over ping and ssh" exit 3 ' res := exec(cmd: cmd, ignore_error: true, stdout: false, debug: false)! // console.print_debug('ssh test ${res.exit_code}: ===== cmd:\n${cmd}\n=====\n${res.output}') res_output := res.output if res.exit_code == 0 { return res_output, SSHResult.ok } else if res.exit_code == 1 { return res_output, SSHResult.ssh } else if res.exit_code == 2 { return res_output, SSHResult.ping } else { return res_output, SSHResult.ssh } }