11
\$\begingroup\$

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?

asked Apr 4, 2023 at 14:43
\$\endgroup\$
2
  • 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\$ Commented 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\$ Commented Apr 7, 2023 at 22:40

4 Answers 4

8
\$\begingroup\$

Interesting script.

Let's talk about reliability. You have a pair of concerns:

  1. A reboot (attempt) must happen every ten minutes, no matter what.
  2. 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.

answered Apr 4, 2023 at 17:11
\$\endgroup\$
2
  • \$\begingroup\$ Spawning from another process makes sense, and of course there's an obvious candidate specifically designed for that, namely cron. \$\endgroup\$ Commented 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 for ps 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 simple while true; do cmd.py; sleep $N; done naturally limits us to one. \$\endgroup\$ Commented Apr 5, 2023 at 2:20
2
\$\begingroup\$

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.

answered Apr 13, 2023 at 22:53
\$\endgroup\$
1
\$\begingroup\$

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.

answered Apr 4, 2023 at 15:46
\$\endgroup\$
3
  • 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\$ Commented 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\$ Commented 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\$ Commented Apr 5, 2023 at 6:40
1
\$\begingroup\$

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 retrying 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()
answered Apr 13, 2023 at 16:56
\$\endgroup\$
1
  • 1
    \$\begingroup\$ This seems like it should be separated into a new question \$\endgroup\$ Commented Apr 13, 2023 at 22:44

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.