This is a Python script I wrote to reboot the router every 600 seconds.
Why did I do it? Well, in case you don't know, I live in China, and I use VPNs to browse the international internet.
My ISP frequently interrupts my VPN connection, don't ask me why, they really have no better things to do. So I have to constantly reboot the router and reconnect VPN.
The router I am currently using takes a very short period of time to reboot, and rebooting doesn't disconnect the VPN, and I discovered somehow rebooting the router prevents my ISP from disconnecting my VPN, so I wrote this.
The script:
import time
from selenium import webdriver
from selenium.common.exceptions import ElementClickInterceptedException, ElementNotInteractableException, JavascriptException, MoveTargetOutOfBoundsException, NoAlertPresentException, NoSuchElementException, TimeoutException, UnexpectedAlertPresentException
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
Firefox = webdriver.Firefox()
wait = WebDriverWait(Firefox, 3)
def auto_reboot(n):
while True:
Firefox.get('http://192.168.0.1/login.html')
time.sleep(1)
while True:
try:
wait.until(EC.visibility_of_element_located(('id', "login-password"))).send_keys('U2VsZg1999')
break
except (TimeoutException, NoSuchElementException):
time.sleep(1)
while True:
try:
wait.until(EC.element_to_be_clickable(('id', "save"))).click()
break
except (ElementNotInteractableException, NoSuchElementException, TimeoutException):
time.sleep(1)
while True:
try:
wait.until(EC.element_to_be_clickable(('id', "system"))).click()
break
except (ElementNotInteractableException, NoSuchElementException, TimeoutException):
time.sleep(1)
while True:
try:
reboot = wait.until(EC.element_to_be_clickable(('id', "reboot")))
break
except (ElementNotInteractableException, NoSuchElementException, TimeoutException):
time.sleep(1)
while True:
try:
Firefox.execute_script("arguments[0].scrollIntoView();", reboot)
reboot.click()
break
except (ElementNotInteractableException, JavascriptException, MoveTargetOutOfBoundsException, TimeoutException):
time.sleep(1)
while True:
try:
Firefox.switch_to.alert
break
except NoAlertPresentException:
while True:
try:
reboot.click()
break
except ElementClickInterceptedException:
time.sleep(1)
time.sleep(1)
while True:
try:
Firefox.switch_to.alert.accept()
break
except UnexpectedAlertPresentException:
time.sleep(1)
time.sleep(n)
while True:
try:
auto_reboot(600)
except:
pass
I think my script is pretty self-explanatory, as you can see I use selenium to do exactly what I do manually using the GUI. Why don't I use telnet? Because my current router doesn't support it.
As you can see I use loads of try-except blocks inside while loops, why did I do that? Because if I don't, the script will eventually be stopped by exceptions, and I need the script to be running all the time. If I don't write the script like this, I can manually paste it into the console to run it and I never encountered exceptions, but if the script keeps running it always will be stopped by exceptions.
I have caught most exceptions and this version is much more stable than its predecessors, it can run hours without stopping, but the script still managed to throw exceptions, so I had to add another loop to catch everything.
I don't think my script can get any more efficient, but I think it can be more concise and elegant, how can I make it more concise and elegant?
-
1\$\begingroup\$ Perhaps I didn't make it clear, I use Windows 10, though I understood the bash commands in the currently most voted answer just by looking at them. But I would not use bash, perhaps I will write it in PowerShell. \$\endgroup\$Ξένη Γήινος– Ξένη Γήινος2023年04月05日 07:22:03 +00:00Commented Apr 5, 2023 at 7:22
-
2\$\begingroup\$ It is best to put all of the information in the question, rather than spreading it across the question and comments \$\endgroup\$Greenonline– Greenonline2023年04月07日 22:40:55 +00:00Commented Apr 7, 2023 at 22:40
4 Answers 4
Interesting script.
Let's talk about reliability. You have a pair of concerns:
- A reboot (attempt) must happen every ten minutes, no matter what.
- Four
.wait.until()
's will be tried, then retried at one-second intervals, until success, plus similar interactions.
Here's my perspective on those. Clearly there's more than one right answer.
Separate out item (1).
Turn the while
/ try
mainline into
just a single auto_reboot()
call
so it's a one-shot script that exits
within twenty seconds,
and have a "nanny" parent process spawn it.
Could be in python, but likely bash is simpler:
#! /bin/bash
while true
do
python reboot.py &
PID=$!
sleep 20
# Usually the child has succeeded and exited by this point.
jobs %% 2> /dev/null && (kill $PID; sleep 1; kill -9 $PID)
sleep 580
done
Very simple.
Now on to the second concern.
I am no Selenium expert, but I have to believe
that this requirement comes up all the time.
Apparently it has a RetryAnalyzer
that
overrides onTestFailure
, but you're
not within a test framework so let's look
outside the Selenium ecosystem.
You could write a while / try / do / sleep
wrapper for the repeated motif.
But tenacity
already offers wrappers for this common concern.
from tenacity import retry
@retry
def send_credentials():
wait.until(EC.visibility_of_element_located(('id', "login-password"))).send_keys('xxxx') # passwords never belong in the source code
@retry
def click(target: str):
wait.until(EC.element_to_be_clickable(('id', target))).click()
def auto_reboot(n):
Firefox.get('http://192.168.0.1/login.html')
time.sleep(1)
send_credentials()
click("save")
click("system")
...
It's clear you do not yet understand the occasional failure modes.
There's very little logging. We don't announce we're about to attempt an interaction. On success we don't report elapsed time, and on failure we don't report how it failed.
Consider reporting interaction details to the console or to a log file. The idea is to identify steps that fail more often so you can better examine and understand them. Also, elapsed time figures could help you to intelligently choose a strategy for sleeping "just long enough" to win.
Consider adding import logging
so you'll get timestamps
on each message.
This codebase achieves many of its design goals. There is room to improve its DRY issues and lack of logging.
-
\$\begingroup\$ Spawning from another process makes sense, and of course there's an obvious candidate specifically designed for that, namely
cron
. \$\endgroup\$Toby Speight– Toby Speight2023年04月04日 18:45:50 +00:00Commented Apr 4, 2023 at 18:45 -
1\$\begingroup\$ Agreed, cron
0-59/10
is cool. A daily task that misbehaves can only get into so much trouble, and there's time forps
to show you leftover jobs piling up. Frequently launched tasks run greater risk of Sorcerer's Apprentice Syndrome. So I usually flock against myself to inhibit "too many tasks", or do some kill $PID nonsense as above. An advantage of the nanny pattern for most situations is that simplewhile true; do cmd.py; sleep $N; done
naturally limits us to one. \$\endgroup\$J_H– J_H2023年04月05日 02:20:24 +00:00Commented Apr 5, 2023 at 2:20
Talking about your solution more broadly than your code:
Rebooting the router is the wrong thing to do. A reasonable network appliance would have a keep alive loop to monitor the VPN connection. If the VPN goes down, restart only the VPN service, and possibly also reissue a DHCP request on your main WAN. pfSense is very good at this kind of thing and can run on most commodity hardware, although it isn't the only game in town.
Philosophically, you should not interrupt the link to your clients needlessly; only interrupt the link to the internet.
I would recommend reading the password from a separate file, which can have more restrictive permissions than the script. Even if you're the only user today, that might not always be true.
And it's particularly important not to expose the password if you share the script for other people to use.
-
1\$\begingroup\$ I fail to see why I can't share my password to the router, after all no one online can use it because they wouldn't know the ip address to the router, and I configured the router to disable wan access. \$\endgroup\$Ξένη Γήινος– Ξένη Γήινος2023年04月04日 16:10:23 +00:00Commented Apr 4, 2023 at 16:10
-
5\$\begingroup\$ Perhaps I'm just naturally more cautious. In any case, it's generally a good idea to separate the functionality from its configuration. \$\endgroup\$Toby Speight– Toby Speight2023年04月04日 18:44:42 +00:00Commented Apr 4, 2023 at 18:44
-
5\$\begingroup\$ @ΞένηΓήινος also, knowing one password of a user often leads to some hints for other passwords of that user on other services/platforms. So it is a good practice to never share your actual password anywhere, even if nobody had access to the system. \$\endgroup\$justhalf– justhalf2023年04月05日 06:40:36 +00:00Commented Apr 5, 2023 at 6:40
I have updated the script to include many more functionalities, and I have achieved multi-threading for a long while now.
But the whole time I was using selenium
which as you see is inefficient and prone to exceptions. That is, until a few hours ago.
I have just discovered my router uses XMLHttpRequest, I have inspected them using Chrome's DevTools and I HAVE SUCCESSFULLY REPLICATED THOSE REQUESTS using, well, requests
.
I have eliminated the need to use selenium
and thus a major source of exceptions, and the code is tremendously more stable, but I will leave the retry
ing there for prosperity.
updated code relevant to this question:
import logging
import requests
import base64
import numpy as np
import random
import subprocess
import time
from datetime import datetime
from functools import cache
from ping3 import ping
from requests import ConnectionError, ConnectTimeout
from socket import gaierror
from tenacity import (
after_log,
before_sleep_log,
retry,
stop_after_attempt,
wait_random
)
from threading import Thread
from urllib3.exceptions import MaxRetryError, NewConnectionError
logging.basicConfig(
filename='D:/network_guard.log',
filemode='a',
format='%(asctime)s %(name)s %(levelname)s %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
level=logging.INFO
)
logger = logging.getLogger(__name__)
SIGNALS = {'router_busy': False}
persistent = retry(
reraise=True,
before_sleep=before_sleep_log(logger, logging.ERROR),
after=after_log(logger, logging.INFO),
stop=stop_after_attempt(5),
wait=wait_random(min=1, max=2)
)
class Router:
reboot_time = []
def __init__(self, password):
self.password = base64.b64encode(password.encode()).decode()
self.renew_session()
@persistent
def renew_session(self):
self.session = requests.Session()
self.session.post('http://192.168.0.1/login/Auth', data={'password': self.password})
@persistent
def check_session(self):
response = self.session.get('http://192.168.0.1/index.html')
if response.url == 'http://192.168.0.1/login.html':
self.renew_session()
@persistent
def reboot(self):
self.check_session()
start = time.perf_counter()
self.session.post('http://192.168.0.1/goform/sysReboot', data={'module1': 'sysOperate', 'action': 'reboot'})
time.sleep(20)
while not ping('192.168.0.1', 0.1):
time.sleep(0.25)
end = time.perf_counter()
Router.reboot_time.append(end - start)
@persistent
def disconnect(self):
self.check_session()
self.session.post('http://192.168.0.1/goform/setWAN', data={'module1': 'wanBasicCfg', 'wanType': 'dhcp'})
@persistent
def connect(self):
self.check_session()
self.session.post('http://192.168.0.1/goform/setWAN', data={'module1': 'wanBasicCfg', 'wanType': 'pppoe', 'wanPPPoEUser': 'NC2105221514', 'wanPPPoEPwd': '12345678'})
def reenable_adapter():
subprocess.run('devcon disable *dev_8168*', stdout=subprocess.DEVNULL)
time.sleep(3)
subprocess.run('devcon enable *dev_8168*', stdout=subprocess.DEVNULL)
logging.info('Successfully re-enabled adapter')
def google_accessible():
try:
google = requests.get('https://www.google.com', timeout=3)
if google.status_code == 200:
return True
except (ConnectionError, ConnectTimeout, gaierror, MaxRetryError, NewConnectionError):
pass
return False
def auto_reboot_worker():
logging.info('Attempting to reboot the router')
router.reboot()
logging.info('Successfully rebooted the router')
time.sleep(15)
reenable_adapter()
google_accessible()
def refresh_connection_worker():
logging.info('Attempting to refresh the connection')
router.disconnect()
time.sleep(5)
router.connect()
logging.info('Successfully refreshed the connection')
@cache
def agm(a, b, n):
if a > b:
a, b = b, a
for i in range(n):
c = (a * b) ** 0.5
d = (a + b) / 2
a, b = c, d
return (c + d) / 2
@cache
def gauss(x, weight):
return np.exp(-(x - 0.5) * (x - 0.5) / (2 * weight**2))
def randBias(base, top, bias, weight=0.5):
assert 0 < weight <= 1
influence = random.random()
x = random.random() * (top - base) + base
if x > bias:
return x + gauss(influence, weight) * (bias - x)
return x - gauss(influence, weight) * (x - bias)
def weighted_random(a, b):
if a > b:
a, b = b, a
return randBias(a, b, agm(a, b, 16), 0.1)
def notify_and_sleep(low, high, caller):
nap_time = weighted_random(low, high)
logging.info(
'Will sleep for {0} seconds, next {1} is scheduled at {2}'.format(
round(nap_time, 3), caller,
datetime.fromtimestamp(
time.time() + nap_time).strftime('%Y-%m-%d %H:%M:%S')
)
)
time.sleep(nap_time)
def refresh_connection():
logging.info('connection refreshment thread started')
while True:
while SIGNALS['router_busy'] or SIGNALS['testing'] or SIGNALS['VPN_disconnected']:
time.sleep(30)
SIGNALS['router_busy'] = True
refresh_connection_worker()
reenable_adapter()
SIGNALS['router_busy'] = False
notify_and_sleep(960, 1500, 'connection refreshment')
def auto_reboot():
logging.info('auto reboot thread started')
while True:
while SIGNALS['router_busy'] or SIGNALS['testing'] or SIGNALS['VPN_disconnected']:
time.sleep(30)
SIGNALS['router_busy'] = True
auto_reboot_worker()
SIGNALS['router_busy'] = False
notify_and_sleep(1800, 2400, 'router reboot')
if __name__ == '__main__':
router = Router('****')
auto_reboot_thread = Thread(target=auto_reboot)
auto_reboot_thread.start()
refresh_connection_thread = Thread(target=refresh_connection)
refresh_connection_thread.start()
-
1\$\begingroup\$ This seems like it should be separated into a new question \$\endgroup\$Reinderien– Reinderien2023年04月13日 22:44:08 +00:00Commented Apr 13, 2023 at 22:44