341 lines
10 KiB
Julia
341 lines
10 KiB
Julia
using Dates
|
|
using Printf
|
|
|
|
# Try to load HTTP; if not installed, give instruction and exit
|
|
try
|
|
@eval using HTTP
|
|
catch
|
|
println("HTTP.jl not found. Install it with: julia -e 'using Pkg; Pkg.add(\"HTTP\")'")
|
|
exit(1)
|
|
end
|
|
|
|
# Configuration
|
|
const URL = "https://www.xxx.cc"
|
|
const TIMEOUT_SECS = 6 # request timeout
|
|
const ATTEMPTS_PER_CHECK = 3 # number of HTTP attempts per check
|
|
const BACKOFF_BETWEEN_ATTEMPTS = 2 # seconds between attempts
|
|
const FAILS_TO_REBOOT = 3 # consecutive failed checks required to trigger reboot
|
|
const COOLDOWN_AFTER_REBOOT_SECS = 600 # do not reboot again within this many seconds
|
|
const DRY_RUN = true # set false to actually reboot (DRY RUN true for testing)
|
|
const LOGFILE = "./check_website_reboot_log.txt" # write logs here and also broadcast
|
|
const CHECK_INTERVAL_SECS = 60 # run a check every CHECK_INTERVAL_SECS seconds
|
|
|
|
# Persist state in current directory as requested
|
|
const STATE_FILE = "./check_and_reboot_state.json"
|
|
|
|
# Simple broadcast helper (safe Cmd construction)
|
|
function broadcast_msg(msg::AbstractString)
|
|
try
|
|
if Sys.islinux()
|
|
# Try wall if available by writing to its stdin
|
|
wall_paths = ("/usr/bin/wall", "/bin/wall")
|
|
for p in wall_paths
|
|
if isfile(p)
|
|
try
|
|
proc = open(`$p`, "w")
|
|
try
|
|
write(proc, msg * "\n")
|
|
finally
|
|
close(proc)
|
|
end
|
|
return true
|
|
catch
|
|
# ignore and try next
|
|
end
|
|
end
|
|
end
|
|
# Fallback to logger (safe arg passing)
|
|
try
|
|
run(Cmd(["logger", msg]))
|
|
return true
|
|
catch
|
|
end
|
|
elseif Sys.isapple()
|
|
# Use AppleScript notification as a fallback (escape double quotes)
|
|
try
|
|
escaped = replace(msg, "\"" => "\\\"")
|
|
applescript = "display notification \"" * escaped * "\" with title \"check_and_reboot\""
|
|
run(Cmd(["osascript", "-e", applescript]))
|
|
return true
|
|
catch
|
|
end
|
|
elseif Sys.iswindows()
|
|
# Try msg to all sessions (may require privileges); best-effort
|
|
try
|
|
run(Cmd(["msg", "*", msg]))
|
|
return true
|
|
catch
|
|
end
|
|
end
|
|
catch
|
|
# swallow any unexpected errors
|
|
end
|
|
return false
|
|
end
|
|
|
|
# Simple logging (prints, appends to LOGFILE, and broadcasts)
|
|
function logmsg(s::AbstractString)
|
|
t = Dates.format(now(), "yyyy-mm-dd HH:MM:SS")
|
|
out = "[$t] $s"
|
|
# write to logfile (append)
|
|
try
|
|
open(LOGFILE, "a") do io
|
|
println(io, out)
|
|
end
|
|
catch e
|
|
# If logfile write fails, fallback to stdout
|
|
println("[$t] (log write failed: $e) $s")
|
|
end
|
|
# Also print to stdout for immediate console visibility
|
|
println(out)
|
|
# Best-effort system broadcast so operators on console see it
|
|
try
|
|
broadcast_msg(out)
|
|
catch
|
|
# ignore broadcast failures
|
|
end
|
|
end
|
|
|
|
# Minimal JSON helper (uses JSON.jl if available, otherwise a tiny fallback)
|
|
module JSONHelper
|
|
export parse_obj, write_obj
|
|
function parse_obj(s::String)
|
|
try
|
|
@eval using JSON
|
|
return JSON.parse(s)
|
|
catch
|
|
try
|
|
return eval(Meta.parse(replace(s, "null"=>"nothing")))
|
|
catch
|
|
return Dict{String,Any}()
|
|
end
|
|
end
|
|
end
|
|
function write_obj(io, obj::Dict)
|
|
try
|
|
@eval using JSON
|
|
JSON.print(io, obj)
|
|
catch
|
|
# naive serializer for simple dict of numbers/strings
|
|
print(io, "{")
|
|
first = true
|
|
for (k,v) in obj
|
|
if !first; print(io, ","); end
|
|
first = false
|
|
if isa(v, String)
|
|
# escape quotes and backslashes minimally
|
|
esc = replace(replace(v, "\\"=>"\\\\") , "\"" => "\\\"")
|
|
print(io, "\"$k\":\"$esc\"")
|
|
else
|
|
print(io, "\"$k\":$v")
|
|
end
|
|
end
|
|
print(io, "}")
|
|
end
|
|
end
|
|
end
|
|
|
|
# State handling: store last_reboot as a DateTime
|
|
mutable struct State
|
|
consecutive_fails::Int
|
|
last_reboot::DateTime
|
|
end
|
|
|
|
# Default epoch for "never rebooted" state
|
|
const NEVER_REBOOTED = DateTime(1970,1,1)
|
|
|
|
function load_state()
|
|
try
|
|
if isfile(STATE_FILE)
|
|
s = read(STATE_FILE, String)
|
|
obj = JSONHelper.parse_obj(s)
|
|
cf = haskey(obj, "consecutive_fails") ? Int(obj["consecutive_fails"]) : 0
|
|
lr = NEVER_REBOOTED
|
|
if haskey(obj, "last_reboot") && isa(obj["last_reboot"], String)
|
|
try
|
|
lr = DateTime(obj["last_reboot"]) # expects ISO-like string
|
|
catch
|
|
# ignore parse error, keep NEVER_REBOOTED
|
|
end
|
|
end
|
|
return State(cf, lr)
|
|
end
|
|
catch e
|
|
logmsg("Warning loading state: $e")
|
|
end
|
|
return State(0, NEVER_REBOOTED)
|
|
end
|
|
|
|
# Atomic save_state to avoid partial/corrupted state across reboots
|
|
function save_state(st::State)
|
|
# write ISO-8601 string for DateTime
|
|
lr_str = Dates.format(st.last_reboot, Dates.ISODateTime)
|
|
obj = Dict("consecutive_fails" => st.consecutive_fails,
|
|
"last_reboot" => lr_str)
|
|
tmp = STATE_FILE * ".tmp"
|
|
try
|
|
open(tmp, "w") do io
|
|
JSONHelper.write_obj(io, obj)
|
|
end
|
|
mv(tmp, STATE_FILE; force=true)
|
|
catch e
|
|
logmsg("Warning: failed to write/replace state file: $e")
|
|
# attempt best-effort cleanup
|
|
try
|
|
isfile(tmp) && rm(tmp)
|
|
catch
|
|
end
|
|
end
|
|
end
|
|
|
|
# Helper: system uptime on Linux (seconds), Inf on other OS or error
|
|
function system_uptime_seconds()
|
|
try
|
|
if Sys.islinux()
|
|
s = read("/proc/uptime", String)
|
|
return parse(Float64, split(s)[1])
|
|
end
|
|
catch
|
|
end
|
|
return Inf
|
|
end
|
|
|
|
# HTTP check
|
|
function check_url_once(url::AbstractString; timeout=TIMEOUT_SECS)
|
|
try
|
|
resp = HTTP.request("GET", url; connect_timeout=timeout, read_timeout=timeout)
|
|
return 200 <= resp.status < 400, resp.status
|
|
catch e
|
|
return false, nothing
|
|
end
|
|
end
|
|
|
|
# Reboot command selection
|
|
# Return a tuple of strings (program and args) suitable for constructing Cmd
|
|
function reboot_command()
|
|
if Sys.iswindows()
|
|
return ("/usr/bin/cmd", "/C", "shutdown /r /t 0")
|
|
elseif Sys.isapple()
|
|
return ("/usr/bin/sudo", "shutdown", "-r", "now")
|
|
elseif Sys.islinux()
|
|
if isfile("/bin/systemctl") || isfile("/usr/bin/systemctl")
|
|
return ("/usr/bin/sudo", "systemctl", "reboot")
|
|
else
|
|
return ("/usr/bin/sudo", "reboot")
|
|
end
|
|
else
|
|
return nothing
|
|
end
|
|
end
|
|
|
|
function do_reboot()
|
|
cmd = reboot_command()
|
|
if cmd === nothing
|
|
logmsg("Reboot not supported on this OS")
|
|
return false
|
|
end
|
|
|
|
# Build a readable command string for logs (escape each arg safely)
|
|
cmd_str = join(map(x -> replace(x, '"' => "\\\""), cmd), " ")
|
|
|
|
if DRY_RUN
|
|
logmsg("DRY RUN: would run reboot command: $cmd_str")
|
|
return true
|
|
end
|
|
|
|
logmsg("Executing reboot command: $cmd_str")
|
|
try
|
|
# Construct a Cmd from an array so arguments are passed directly (no shell)
|
|
cmd_array = collect(cmd) # Tuple{String,...} -> Vector{String}
|
|
run(Cmd(cmd_array))
|
|
return true
|
|
catch e
|
|
logmsg("Failed to execute reboot command: $e")
|
|
return false
|
|
end
|
|
end
|
|
|
|
# Single check iteration
|
|
function perform_check!(st::State)
|
|
# If we're still within cooldown after a reboot, skip checks
|
|
if st.last_reboot != NEVER_REBOOTED
|
|
elapsed = now() - st.last_reboot
|
|
if elapsed < Second(COOLDOWN_AFTER_REBOOT_SECS)
|
|
remaining = Int(clamp(round((Second(COOLDOWN_AFTER_REBOOT_SECS) - elapsed).value), 0, typemax(Int)))
|
|
logmsg("In cooldown after recent reboot; skipping check for $remaining more seconds.")
|
|
return
|
|
end
|
|
end
|
|
|
|
# Boot grace: skip checks if system just booted (helps prevent immediate reboots while services settle)
|
|
upt = system_uptime_seconds()
|
|
if Sys.islinux() && upt < 120
|
|
logmsg("System boot grace active (uptime=$(round(upt))s); skipping check until uptime >= 120s.")
|
|
return
|
|
end
|
|
|
|
success = false
|
|
last_code = nothing
|
|
for i in 1:ATTEMPTS_PER_CHECK
|
|
ok, code = check_url_once(URL)
|
|
last_code = code
|
|
if ok
|
|
success = true
|
|
break
|
|
end
|
|
sleep(BACKOFF_BETWEEN_ATTEMPTS)
|
|
end
|
|
|
|
if success
|
|
if st.consecutive_fails > 0
|
|
logmsg("Website reachable; resetting consecutive failure counter.")
|
|
else
|
|
logmsg("Website reachable.")
|
|
end
|
|
st.consecutive_fails = 0
|
|
save_state(st)
|
|
return
|
|
else
|
|
st.consecutive_fails += 1
|
|
logmsg(@sprintf("Website unreachable (last HTTP status: %s). Consecutive fails: %d/%d.",
|
|
isnothing(last_code) ? "no response" : string(last_code),
|
|
st.consecutive_fails, FAILS_TO_REBOOT))
|
|
save_state(st)
|
|
end
|
|
|
|
if st.consecutive_fails >= FAILS_TO_REBOOT
|
|
st.last_reboot = now()
|
|
save_state(st)
|
|
ok = do_reboot()
|
|
if ok
|
|
logmsg("Reboot executed (or simulated). Resetting failure counter.")
|
|
st.consecutive_fails = 0
|
|
save_state(st)
|
|
else
|
|
logmsg("Reboot attempt failed; will retry after next interval.")
|
|
end
|
|
end
|
|
end
|
|
|
|
# Main loop: runs indefinitely every CHECK_INTERVAL_SECS
|
|
function main_loop()
|
|
logmsg("Starting check loop. Checking every $(CHECK_INTERVAL_SECS) seconds.")
|
|
logmsg("STATE_FILE path: $(abspath(STATE_FILE))")
|
|
st = load_state()
|
|
# Log loaded state for visibility
|
|
age = st.last_reboot == NEVER_REBOOTED ? "never" : string(Int(round((now() - st.last_reboot).value)))
|
|
lr_str = st.last_reboot == NEVER_REBOOTED ? "never" : Dates.format(st.last_reboot, Dates.ISODateTime)
|
|
logmsg("Loaded state: consecutive_fails=$(st.consecutive_fails) last_reboot=$(lr_str) (age=${age}s)")
|
|
while true
|
|
try
|
|
perform_check!(st)
|
|
catch e
|
|
logmsg("Error during check: $e")
|
|
end
|
|
sleep(CHECK_INTERVAL_SECS)
|
|
end
|
|
end
|
|
|
|
# Run
|
|
main_loop()
|