12
\$\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?

\$\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\$ Apr 5 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\$ Apr 7 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.

\$\endgroup\$
2
  • \$\begingroup\$ Spawning from another process makes sense, and of course there's an obvious candidate specifically designed for that, namely cron. \$\endgroup\$ Apr 4 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\$
    – J_H
    Apr 5 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.

\$\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.

\$\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\$ Apr 4 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\$ Apr 4 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
    Apr 5 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()
\$\endgroup\$
1
  • 1
    \$\begingroup\$ This seems like it should be separated into a new question \$\endgroup\$
    – Reinderien
    Apr 13 at 22:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.