自动检测校园网状态,自动联网并恢复 Ubuntu 下的 ToDesk 连接

1. 背景

实验室 Ubuntu 主机长期通过 ToDesk 远程连接,但校园网存在一个比较麻烦的问题:

校园网认证掉线 → Ubuntu 断网 → ToDesk 掉线 → 即使校园网重新恢复,ToDesk 有时也不会自动重新建立连接。

以前需要人工在本地执行:

复制代码
sudo kill ...
sudo systemctl stop ufw
sudo systemctl disable ufw

然后重新打开一个 Terminal:

复制代码
todesk

对于无人值守的实验室服务器,这显然不方便。

因此本文实现一个 Ubuntu 下的自动恢复脚本:

复制代码
定时检测校园网
        ↓
判断 Portal 是否已经登录
        ↓
未登录
        ↓
Selenium 自动登录校园网
        ↓
检测登录成功
        ↓
关闭原 ToDesk / ToDesk+ 进程
        ↓
关闭 UFW
        ↓
重新启动 ToDesk
        ↓
恢复远程连接

并进一步配置为 Ubuntu 登录桌面后自动启动


2. 参考项目

校园网 Selenium 自动登录部分参考:

ThreeStones1029 / AutoLoginCampusNetwork

GitHub - ThreeStones1029/AutoLoginCampusNetwork: 自动连接河海大学校园网 · GitHub

本文在此基础上增加了:

  • Ubuntu Chrome for Testing 支持

  • ChromeDriver 显式路径

  • 密码输入框兼容处理

  • 校园网状态周期检测

  • 自动重新登录

  • ToDesk / ToDesk+ 进程恢复

  • 自动关闭 UFW

  • 自动重新启动 ToDesk

  • Ubuntu 桌面登录后自动运行


3. 安装 Selenium

如果使用 Anaconda:

复制代码
pip install selenium

查看版本:

复制代码
python -c "import selenium; print(selenium.__version__)"

本文 Ubuntu 环境使用 Selenium 4。


4. 下载 Chrome 和 ChromeDriver

推荐直接使用 Google 官方的 Chrome for Testing

Chrome for Testing availability

Chrome for Testing 官方页面会同时提供不同平台的 Chrome 和 ChromeDriver 下载,并按版本对应。

本文建议使用 120 及以上版本,并保证:

Chrome 和 ChromeDriver 使用相同版本。

本文实际使用版本:

复制代码
153.0.7993.0

Chrome Linux 64 位

https://storage.googleapis.com/chrome-for-testing-public/153.0.7993.0/linux64/chrome-linux64.zip

ChromeDriver Linux 64 位

https://storage.googleapis.com/chrome-for-testing-public/153.0.7993.0/linux64/chromedriver-linux64.zip

下载完成后解压,例如放到:

复制代码
/home/hy/Downloads/chrome-linux64/
/home/hy/Downloads/chromedriver-linux64/

最终对应:

复制代码
/home/hy/Downloads/chrome-linux64/chrome
/home/hy/Downloads/chromedriver-linux64/chromedriver

添加执行权限:

复制代码
chmod +x /home/hy/Downloads/chrome-linux64/chrome
chmod +x /home/hy/Downloads/chromedriver-linux64/chromedriver

检查版本:

复制代码
/home/hy/Downloads/chrome-linux64/chrome --version
/home/hy/Downloads/chromedriver-linux64/chromedriver --version

5. 校园网状态如何判断

这里没有简单使用:

复制代码
ping baidu.com

来判断网络是否正常。

因为本文真正关心的是:

校园网 Portal 当前是否处于认证状态。

脚本每隔一段时间启动 Selenium,访问校园网 Portal:

复制代码
driver.get(LOGIN_URL)

然后检查页面中是否存在:

复制代码
id="toLogOut"

Python 判断:

复制代码
def is_portal_logged_in(driver):
    try:
        driver.find_element(By.ID, "toLogOut")
        return True
    except NoSuchElementException:
        return False

如果能够找到:

复制代码
toLogOut

说明当前 Portal 已经登录。

如果找不到,则进入自动登录流程。


6. 自动填写校园网账号密码

首先等待用户名输入框:

复制代码
username_input = wait.until(
    EC.presence_of_element_located(
        (By.ID, "username")
    )
)

username_input.clear()
username_input.send_keys(USERNAME)

校园网密码框比较特殊。

部分 Portal 页面中真正的:

复制代码
id="pwd"

一开始处于不可交互状态,上面覆盖了:

复制代码
id="pwd_tip"

如果直接执行:

复制代码
password_input.send_keys(PASSWORD)

可能出现:

复制代码
ElementNotInteractableException

因此先点击:

复制代码
pwd_tip = driver.find_element(By.ID, "pwd_tip")

if pwd_tip.is_displayed():
    pwd_tip.click()

之后重新寻找:

复制代码
password_input = driver.find_element(By.ID, "pwd")

如果普通 send_keys() 仍然失败,则使用 JavaScript 写入:

复制代码
def js_set_value(driver, element, value):
    driver.execute_script(
        """
        const el = arguments[0];
        const value = arguments[1];

        el.removeAttribute('readonly');
        el.removeAttribute('disabled');

        const setter =
            Object.getOwnPropertyDescriptor(
                window.HTMLInputElement.prototype,
                'value'
            ).set;

        setter.call(el, value);

        el.dispatchEvent(
            new Event('input', {bubbles:true})
        );

        el.dispatchEvent(
            new Event('change', {bubbles:true})
        );
        """,
        element,
        value,
    )

这种方式对一些老式校园网 Portal 页面更稳定。


7. 自动选择中国移动

我的校园网服务列表为:

复制代码
_service_0:校园网
_service_1:中国内网
_service_2:中国移动
_service_3:中国联通
_service_4:中国电信

因此设置:

复制代码
SERVICE_ID = "_service_2"

自动选择:

复制代码
service_item = wait.until(
    EC.presence_of_element_located(
        (By.ID, SERVICE_ID)
    )
)

driver.execute_script(
    "arguments[0].click();",
    service_item
)

不同学校的 Portal 页面 ID 可能不同,需要根据网页源码修改。


8. 自动点击登录

登录按钮:

复制代码
login_button = wait.until(
    EC.presence_of_element_located(
        (By.ID, "loginLink_div")
    )
)

为了避免某些网页普通:

复制代码
login_button.click()

失效,这里直接使用 JavaScript:

复制代码
driver.execute_script(
    """
    arguments[0].scrollIntoView({
        block: 'center'
    });

    arguments[0].click();
    """,
    login_button
)

登录以后继续检测:

复制代码
if is_portal_logged_in(driver):
    print("校园网登录成功")

只有确定校园网已经恢复以后,才开始处理 ToDesk。


9. 为什么校园网恢复后还需要重启 ToDesk

实际使用中遇到的问题是:

复制代码
校园网断开
 ↓
ToDesk 判断机器离线
 ↓
校园网恢复
 ↓
ToDesk 并不一定重新上线

人工解决方式通常是:

复制代码
关闭 ToDesk
↓
关闭 UFW
↓
重新执行 todesk

因此将这几个步骤也加入自动恢复流程。


10. 安全关闭 ToDesk 进程

这里有一个比较容易踩的坑。

一开始使用:

复制代码
pkill -9 -f todesk

结果 Python 监控脚本本身也被杀死。

原因是脚本名字中本身包含:

复制代码
todesk

例如:

复制代码
auto_login_and_todesk_watchdog.py

而:

复制代码
pkill -f todesk

匹配的是整个进程命令行

因此最终版本改成:

复制代码
ps -eo pid=,comm=,args=

获取进程列表,再判断真正的 ToDesk 进程,只杀目标 PID。

同时明确排除:

复制代码
if pid == os.getpid():
    continue

if "auto_login_and_todesk_watchdog" in args_lower:
    continue

if comm_lower.startswith("python"):
    continue

这样可以避免 watchdog 把自己杀掉。


11. 自动关闭 UFW

网络恢复后执行:

复制代码
sudo systemctl stop ufw
sudo systemctl disable ufw

注意正确命令是:

复制代码
sudo systemctl disable ufw

而不是:

复制代码
sudo systemctl ufw disable

为了方便观察恢复过程,我没有把这些操作完全隐藏在后台,而是自动打开一个新的 Terminal 执行。

效果类似:

复制代码
====================================
正在停止并禁用 UFW...
====================================

[1/2] systemctl stop ufw 完成
[2/2] systemctl disable ufw 完成

当前 UFW 状态:
inactive
disabled

注意:直接关闭 UFW 会降低系统防护能力。如果机器暴露在不可信网络环境,建议根据自己的网络环境配置 ToDesk 所需规则,而不是永久关闭整个防火墙。


12. 自动重新启动 ToDesk

UFW 操作完成后,再自动打开第二个 Terminal:

复制代码
todesk

为了防止 Terminal 一闪而过,脚本会先生成:

复制代码
/home/hy/start_todesk_recovery.sh

大致内容:

复制代码
#!/bin/bash

echo "========================================"
echo "       ToDesk 自动恢复启动终端"
echo "========================================"

echo "正在启动 ToDesk..."

todesk

RET=$?

echo "ToDesk 命令返回,退出码:$RET"

exec bash

这样即使 ToDesk 启动失败,Terminal 也不会立即关闭,可以直接看到错误信息。


13. 最终自动恢复逻辑

整个 watchdog 的逻辑如下:

复制代码
程序启动
   ↓
每 30 分钟检查一次
   ↓
启动 Chrome
   ↓
访问校园网 Portal
   ↓
检测 toLogOut
   ↓
┌──────────────────┐
│ 存在             │
│ 当前校园网正常   │
└────────┬─────────┘
         ↓
   等待下一次检测


如果不存在 toLogOut
         ↓
自动填写账号
         ↓
自动填写密码
         ↓
选择中国移动
         ↓
点击登录
         ↓
重新检测 toLogOut
         ↓
校园网恢复成功
         ↓
关闭 ToDesk / ToDesk+
         ↓
新开 Terminal
         ↓
stop ufw
disable ufw
         ↓
新开第二个 Terminal
         ↓
运行 todesk
         ↓
ToDesk 恢复远程连接

14. 每 30 分钟自动检测

监控周期:

复制代码
CHECK_INTERVAL = 30 * 60

即:

复制代码
30 × 60 = 1800 秒

正常情况下终端输出类似:

复制代码
开始执行本轮校园网状态检测......

打开 UPC Portal,检查当前登录状态......

检测到 toLogOut:当前校园网已登录

本轮判断结果:校园网正常 / 已登录

下一次检测将在 30 分钟后进行

如果掉线:

复制代码
未检测到 toLogOut:当前校园网未登录

开始执行校园网自动重连

填写校园网账号
填写校园网密码
选择网络服务
点击校园网登录按钮

校园网自动重连成功

随后:

复制代码
[1/3] Kill ToDesk / ToDesk+
[2/3] 新开 Terminal:停止并禁用 UFW
[3/3] 新开 Terminal:启动 ToDesk

15. 配置 Ubuntu 开机自动启动

因为这个程序需要:

  • 打开 Chrome

  • 打开 GNOME Terminal

  • 启动 ToDesk GUI

所以不建议直接作为纯后台 root systemd 服务运行。

更适合配置:

复制代码
Ubuntu 开机
 ↓
hy 用户登录 GNOME 桌面
 ↓
自动启动 watchdog

创建:

复制代码
mkdir -p ~/.config/autostart

然后:

复制代码
nano ~/.config/autostart/upc-todesk-watchdog.desktop

写入:

复制代码
[Desktop Entry]
Type=Application
Name=UPC ToDesk Watchdog
Comment=Auto reconnect UPC network and restart ToDesk
Exec=gnome-terminal --title=UPC-ToDesk-Watchdog -- bash -lc '/home/hy/anaconda3/bin/python /home/hy/net_watchdog/auto_login_and_todesk_watchdog_v7_autostart.py; exec bash'
Terminal=false
X-GNOME-Autostart-enabled=true
Hidden=false
NoDisplay=false

之后 Ubuntu 用户登录桌面时,会自动弹出:

复制代码
UPC-ToDesk-Watchdog

终端,并开始监控校园网状态。


16. 防止程序重复启动

如果开机自启动以后又手动执行脚本,有可能出现多个 watchdog。

因此可以通过文件锁保证只运行一个实例:

复制代码
import fcntl

LOCK_FILE = "/home/hy/.auto_login_todesk_watchdog.lock"

使用:

复制代码
fcntl.flock(
    lock_fp.fileno(),
    fcntl.LOCK_EX | fcntl.LOCK_NB
)

如果已经存在一个运行实例:

复制代码
已有一个校园网 + ToDesk watchdog 正在运行,本次退出。

这样可以避免重复检测和重复启动 ToDesk。


17. 常见问题

17.1 cannot find Chrome binary

错误:

复制代码
SessionNotCreatedException:
cannot find Chrome binary

说明 Selenium 找不到 Chrome。

显式指定:

复制代码
options.binary_location = (
    "/home/hy/Downloads/"
    "chrome-linux64/chrome"
)

17.2 找不到 ChromeDriver

指定:

复制代码
service = Service(
    "/home/hy/Downloads/"
    "chromedriver-linux64/chromedriver"
)

并确认:

复制代码
chmod +x /home/hy/Downloads/chromedriver-linux64/chromedriver

17.3 ElementNotInteractableException

如果账号可以输入,但密码出现:

复制代码
ElementNotInteractableException

一般是:

复制代码
pwd_tip

覆盖了真正密码框。

先点击:

复制代码
pwd_tip

再填写:

复制代码
pwd

仍然失败则使用 JavaScript 设置 value


17.4 ToDesk 被 kill 后 Python 自己也退出

如果看到:

复制代码
开始关闭 ToDesk / ToDesk+ 相关进程
已杀死

然后直接返回 Shell:

复制代码
hy@ubuntu:~$

检查是不是使用了:

复制代码
pkill -9 -f todesk

如果 Python 脚本名称也包含 todesk,会把自己一起杀掉。

应该根据 PID 和真实进程名精确 kill。


17.5 第二个 ToDesk Terminal 没有弹出来

不要简单执行:

复制代码
gnome-terminal -- bash -lc "todesk"

更稳定的方法是:

复制代码
Python
 ↓
生成 start_todesk_recovery.sh
 ↓
gnome-terminal
 ↓
bash start_todesk_recovery.sh
 ↓
todesk
 ↓
exec bash

这样终端会保留,也方便排查错误。


18. 总结

最终实现的不是单纯的"校园网自动登录",而是一套完整的 Ubuntu 无人值守远程恢复机制:

复制代码
校园网认证监测
+
Selenium 自动登录
+
ToDesk 进程恢复
+
UFW 自动处理
+
GNOME Terminal 可视化执行
+
30 分钟周期检测
+
Ubuntu 登录后自动启动

对于实验室 Ubuntu 工作站、GPU 服务器或者长期放在学校机房中的电脑,这种方式可以减少:

校园网偶发掉线 → ToDesk 永久离线 → 必须人工到现场恢复

的问题。

19. 完整代码

下面给出本文最终使用的完整 Python 代码。

注意:公开代码时请勿上传真实账号和密码。

本文已经将校园网账号、校园网密码以及 Ubuntu sudo 密码全部替换为 xxxxxx,使用时请自行修改。

建议文件名:

复制代码
auto_login_and_todesk_watchdog.py

完整代码如下:

复制代码
from selenium import webdriver
from selenium.common.exceptions import (
    NoSuchElementException,
    TimeoutException,
    WebDriverException,
    ElementNotInteractableException,
)
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import datetime
import fcntl
import os
import platform
import shlex
import shutil
import subprocess
import sys
import time


# ============================================================
# 1. 用户配置
# ============================================================

# =========================
# 校园网账号密码
# =========================
USERNAME = "xxxxxx"
PASSWORD = "xxxxxx"

# sudo 密码
# 注意:公开到 GitHub / CSDN 时一定要脱敏
SUDO_PASSWORD = "xxxxxx"


# ============================================================
# 校园网 Portal 地址
# ============================================================

LOGIN_URL = (
    "https://wlan.upc.edu.cn/eportal/index.jsp?"
    "wlanuserip=172.24.209.152&"
    "wlanacname=&"
    "nasip=172.22.242.141&"
    "wlanparameter=28-95-29-0d-cc-71&"
    "url=http://detectportal.firefox.com/canonical.html&"
    "uerlocation=ethtrunk/62:3081.0"
)


# ============================================================
# 网络服务
# ============================================================

# _service_0:校园网
# _service_1:中国内网
# _service_2:中国移动
# _service_3:中国联通
# _service_4:中国电信

SERVICE_ID = "_service_2"


# ============================================================
# 每 30 分钟检测一次校园网
# ============================================================

CHECK_INTERVAL = 30 * 60


# 登录成功检测最大等待时间
NETWORK_RECOVERY_TIMEOUT = 60


# Kill ToDesk 后等待时间
TODESK_KILL_WAIT = 3


# True:
#   Selenium 登录过程中显示 Chrome
#
# False:
#   后台运行 Chrome
#
# 为了方便调试,本文设置为 True
SHOW_BROWSER = True


# ============================================================
# Chrome / ChromeDriver
# ============================================================

CHROME_BINARY = (
    "/home/hy/Downloads/"
    "chrome-linux64/chrome"
)

CHROMEDRIVER = (
    "/home/hy/Downloads/"
    "chromedriver-linux64/chromedriver"
)


# ============================================================
# 日志
# ============================================================

LOG_FILE = (
    "/home/hy/"
    "network_todesk_watchdog.log"
)


# ============================================================
# 单实例锁
# 防止开机启动以后又手动启动一个相同程序
# ============================================================

LOCK_FILE = (
    "/home/hy/"
    ".auto_login_todesk_watchdog.lock"
)


# ============================================================
# 2. 日志输出
# ============================================================

def log(msg):
    line = (
        f"[{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
        f"{msg}"
    )

    print(
        line,
        flush=True
    )

    try:
        with open(
            LOG_FILE,
            "a",
            encoding="utf-8"
        ) as f:
            f.write(
                line + "\n"
            )

    except Exception:
        pass


# ============================================================
# 3. 防止重复启动
# ============================================================

def acquire_single_instance_lock():

    lock_fp = open(
        LOCK_FILE,
        "w"
    )

    try:
        fcntl.flock(
            lock_fp.fileno(),
            fcntl.LOCK_EX | fcntl.LOCK_NB
        )

    except BlockingIOError:

        print(
            "已有一个校园网 + ToDesk watchdog "
            "正在运行,本次退出。"
        )

        sys.exit(0)

    lock_fp.write(
        str(os.getpid())
    )

    lock_fp.flush()

    return lock_fp


# ============================================================
# 4. 创建 Chrome
# ============================================================

def create_driver():

    if (
        platform.system().lower()
        != "linux"
    ):
        raise RuntimeError(
            "当前版本按照 Ubuntu/Linux 环境编写"
        )

    if not os.path.isfile(
        CHROME_BINARY
    ):
        raise FileNotFoundError(
            f"找不到 Chrome:{CHROME_BINARY}"
        )

    if not os.path.isfile(
        CHROMEDRIVER
    ):
        raise FileNotFoundError(
            f"找不到 ChromeDriver:{CHROMEDRIVER}"
        )

    service = Service(
        CHROMEDRIVER
    )

    options = (
        webdriver.ChromeOptions()
    )

    # 指定 Chrome for Testing
    options.binary_location = (
        CHROME_BINARY
    )

    # 是否显示 Chrome
    if not SHOW_BROWSER:
        options.add_argument(
            "--headless=new"
        )

    options.add_argument(
        "--no-sandbox"
    )

    options.add_argument(
        "--disable-dev-shm-usage"
    )

    options.add_argument(
        "--disable-gpu"
    )

    options.add_argument(
        "--window-size=1920,1080"
    )

    options.add_argument(
        "--ignore-certificate-errors"
    )

    options.add_argument(
        "--ignore-ssl-errors"
    )

    options.add_argument(
        "--disable-notifications"
    )

    options.add_argument(
        "--disable-popup-blocking"
    )

    return webdriver.Chrome(
        service=service,
        options=options
    )


# ============================================================
# 5. JS 写入 input
# ============================================================

def js_set_value(
    driver,
    element,
    value
):

    driver.execute_script(
        """
        const el = arguments[0];
        const value = arguments[1];

        el.removeAttribute('readonly');
        el.removeAttribute('disabled');

        const setter =
            Object.getOwnPropertyDescriptor(
                window.HTMLInputElement.prototype,
                'value'
            ).set;

        setter.call(el, value);

        el.dispatchEvent(
            new Event(
                'input',
                {
                    bubbles: true
                }
            )
        );

        el.dispatchEvent(
            new Event(
                'change',
                {
                    bubbles: true
                }
            )
        );

        el.dispatchEvent(
            new Event(
                'blur',
                {
                    bubbles: true
                }
            )
        );
        """,
        element,
        value
    )


# ============================================================
# 6. 找到真正可见的元素
# ============================================================

def find_visible_element(
    driver,
    by,
    value
):

    elements = (
        driver.find_elements(
            by,
            value
        )
    )

    for element in elements:

        try:
            if element.is_displayed():
                return element

        except Exception:
            pass

    if elements:
        return elements[0]

    raise NoSuchElementException(
        f"找不到元素:{by}={value}"
    )


# ============================================================
# 7. 判断校园网是否已经登录
# ============================================================

def is_portal_logged_in(
    driver
):

    try:

        driver.find_element(
            By.ID,
            "toLogOut"
        )

        return True

    except NoSuchElementException:

        return False


# ============================================================
# 8. Portal 状态检测
# ============================================================

def portal_login_status():

    """
    判断方式不是 ping,
    而是直接访问校园网 Portal。

    找到:
        id="toLogOut"

    表示已经登录。

    返回:
        True  -> 已登录
        False -> 未登录
        None  -> 检测异常
    """

    driver = None

    try:

        log(
            "打开校园网 Portal,"
            "检查当前登录状态......"
        )

        driver = create_driver()

        driver.get(
            LOGIN_URL
        )

        # 等待 Portal JS 加载
        time.sleep(2)

        if is_portal_logged_in(
            driver
        ):

            log(
                "检测到 toLogOut:"
                "当前校园网已登录"
            )

            return True

        log(
            "未检测到 toLogOut:"
            "当前校园网未登录"
        )

        return False

    except Exception as e:

        log(
            "Portal 状态检测异常:"
            f"{type(e).__name__}: {e}"
        )

        try:

            if driver is not None:

                driver.save_screenshot(
                    "/home/hy/"
                    "portal_status_check_error.png"
                )

        except Exception:
            pass

        return None

    finally:

        if driver is not None:

            try:
                driver.quit()

            except Exception:
                pass


# ============================================================
# 9. 输入账号
# ============================================================

def fill_username(
    driver,
    wait
):

    log(
        "填写校园网账号"
    )

    username_input = (
        wait.until(
            EC.presence_of_element_located(
                (
                    By.ID,
                    "username"
                )
            )
        )
    )

    try:

        username_input.clear()

        username_input.send_keys(
            USERNAME
        )

    except ElementNotInteractableException:

        log(
            "账号输入框不可交互,"
            "改用 JavaScript 输入"
        )

        js_set_value(
            driver,
            username_input,
            USERNAME
        )


# ============================================================
# 10. 输入密码
# ============================================================

def fill_password(
    driver,
    wait
):

    log(
        "填写校园网密码"
    )

    # --------------------------------------------
    # 一些校园网 Portal 会使用 pwd_tip
    # 覆盖真正的密码输入框
    # --------------------------------------------

    try:

        pwd_tip = (
            driver.find_element(
                By.ID,
                "pwd_tip"
            )
        )

        if pwd_tip.is_displayed():

            try:

                pwd_tip.click()

            except Exception:

                driver.execute_script(
                    "arguments[0].click();",
                    pwd_tip
                )

            time.sleep(0.5)

    except NoSuchElementException:

        pass


    # --------------------------------------------
    # 重新获取真正的 pwd
    # --------------------------------------------

    password_input = (
        wait.until(
            EC.presence_of_element_located(
                (
                    By.ID,
                    "pwd"
                )
            )
        )
    )

    try:

        password_input = (
            find_visible_element(
                driver,
                By.ID,
                "pwd"
            )
        )

    except Exception:

        pass


    # --------------------------------------------
    # 正常 send_keys
    # --------------------------------------------

    try:

        driver.execute_script(
            """
            arguments[0].scrollIntoView(
                {
                    block:'center'
                }
            );
            """,
            password_input
        )

        password_input.click()

        password_input.clear()

        password_input.send_keys(
            PASSWORD
        )

    except Exception:

        log(
            "send_keys 无法输入密码,"
            "改用 JavaScript"
        )

        js_set_value(
            driver,
            password_input,
            PASSWORD
        )


# ============================================================
# 11. 自动登录校园网
# ============================================================

def campus_login():

    driver = None

    try:

        log(
            "启动 Chrome,"
            "开始自动登录校园网"
        )

        driver = create_driver()

        wait = WebDriverWait(
            driver,
            15
        )

        driver.get(
            LOGIN_URL
        )

        time.sleep(2)


        # --------------------------------------------
        # 已经登录
        # --------------------------------------------

        if is_portal_logged_in(
            driver
        ):

            log(
                "Portal 当前已经登录"
            )

            return True


        # --------------------------------------------
        # 等待用户名
        # --------------------------------------------

        wait.until(
            EC.presence_of_element_located(
                (
                    By.ID,
                    "username"
                )
            )
        )


        # --------------------------------------------
        # 输入账号密码
        # --------------------------------------------

        fill_username(
            driver,
            wait
        )

        fill_password(
            driver,
            wait
        )


        # --------------------------------------------
        # 选择运营商
        # --------------------------------------------

        log(
            "选择网络服务"
        )

        try:

            select_service = (
                wait.until(
                    EC.presence_of_element_located(
                        (
                            By.ID,
                            "selectDisname"
                        )
                    )
                )
            )

            try:

                select_service.click()

            except Exception:

                driver.execute_script(
                    "arguments[0].click();",
                    select_service
                )

            time.sleep(0.5)

        except TimeoutException:

            log(
                "未找到 selectDisname,"
                "直接尝试选择运营商"
            )


        # --------------------------------------------
        # 中国移动
        # --------------------------------------------

        try:

            service_item = (
                wait.until(
                    EC.presence_of_element_located(
                        (
                            By.ID,
                            SERVICE_ID
                        )
                    )
                )
            )

            driver.execute_script(
                "arguments[0].click();",
                service_item
            )

            log(
                f"已选择运营商:"
                f"{SERVICE_ID}"
            )

            time.sleep(0.5)

        except TimeoutException:

            log(
                f"找不到运营商选项:"
                f"{SERVICE_ID}"
            )


        # --------------------------------------------
        # 点击登录
        # --------------------------------------------

        log(
            "点击校园网登录按钮"
        )

        login_button = (
            wait.until(
                EC.presence_of_element_located(
                    (
                        By.ID,
                        "loginLink_div"
                    )
                )
            )
        )

        driver.execute_script(
            """
            arguments[0].scrollIntoView(
                {
                    block:'center'
                }
            );

            arguments[0].click();
            """,
            login_button
        )


        # --------------------------------------------
        # 等待登录成功
        # --------------------------------------------

        log(
            "已提交登录请求,"
            "等待 Portal 返回"
        )

        end_time = (
            time.time()
            + NETWORK_RECOVERY_TIMEOUT
        )

        while (
            time.time()
            < end_time
        ):

            try:

                if is_portal_logged_in(
                    driver
                ):

                    log(
                        "检测到 toLogOut:"
                        "校园网登录成功"
                    )

                    return True

            except Exception:

                pass

            time.sleep(2)


        # --------------------------------------------
        # 登录失败
        # --------------------------------------------

        log(
            "登录后仍未检测到 "
            "toLogOut"
        )

        try:

            driver.save_screenshot(
                "/home/hy/"
                "campus_login_failed.png"
            )

        except Exception:

            pass

        return False


    except Exception as e:

        log(
            "校园网自动登录异常:"
            f"{type(e).__name__}: {e}"
        )

        try:

            if driver is not None:

                driver.save_screenshot(
                    "/home/hy/"
                    "campus_login_error.png"
                )

        except Exception:

            pass

        return False


    finally:

        if driver is not None:

            try:

                driver.quit()

            except Exception:

                pass


# ============================================================
# 12. sudo 命令
# ============================================================

def run_sudo_command(
    command
):

    """
    使用 sudo -S 自动输入 sudo 密码。

    公开代码时 SUDO_PASSWORD
    一定要替换为 xxxxxx。
    """

    try:

        result = subprocess.run(
            [
                "sudo",
                "-S"
            ]
            + command,
            input=(
                SUDO_PASSWORD
                + "\n"
            ),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=15
        )

        if (
            result.returncode
            != 0
        ):

            log(
                "sudo 命令失败:"
                + " ".join(command)
                + " | "
                + result.stderr.strip()
            )

            return False

        return True

    except Exception as e:

        log(
            "sudo 命令异常:"
            f"{type(e).__name__}: {e}"
        )

        return False


# ============================================================
# 13. 安全 Kill ToDesk
# ============================================================

def kill_todesk():

    """
    不使用:

        pkill -9 -f todesk

    因为 Python 文件名本身可能包含 todesk,
    会把 watchdog 自己杀死。
    """

    log(
        "开始关闭 ToDesk / "
        "ToDesk+ 相关进程"
    )

    current_pid = (
        os.getpid()
    )

    try:

        result = subprocess.run(
            [
                "ps",
                "-eo",
                "pid=,comm=,args="
            ],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            timeout=5
        )

        if (
            result.returncode
            != 0
        ):

            log(
                "读取进程列表失败"
            )

            return False


        killed_any = False


        for line in (
            result.stdout.splitlines()
        ):

            line = line.strip()

            if not line:
                continue


            parts = (
                line.split(
                    None,
                    2
                )
            )

            if len(parts) < 2:
                continue


            try:

                pid = int(
                    parts[0]
                )

            except ValueError:

                continue


            comm = (
                parts[1]
            )

            args = (
                parts[2]
                if len(parts) >= 3
                else ""
            )


            # --------------------------------------------
            # 不杀自己
            # --------------------------------------------

            if (
                pid
                == current_pid
            ):
                continue


            comm_lower = (
                comm.lower()
            )

            args_lower = (
                args.lower()
            )


            # --------------------------------------------
            # 排除 watchdog
            # --------------------------------------------

            if (
                "auto_login_and_todesk_watchdog"
                in args_lower
            ):
                continue


            # --------------------------------------------
            # 排除 Python
            # --------------------------------------------

            if (
                comm_lower.startswith(
                    "python"
                )
            ):
                continue


            # --------------------------------------------
            # 判断真正 ToDesk
            # --------------------------------------------

            is_todesk = (

                comm_lower
                in {
                    "todesk",
                    "todesk+",
                    "todesk_service",
                    "todeskservice"
                }

                or "/todesk"
                in args_lower

                or "/todesk+"
                in args_lower
            )


            if not is_todesk:
                continue


            log(
                f"发现 ToDesk 进程:"
                f"PID={pid}, "
                f"COMM={comm}"
            )


            # --------------------------------------------
            # 普通 kill
            # --------------------------------------------

            try:

                kill_result = (
                    subprocess.run(
                        [
                            "kill",
                            "-9",
                            str(pid)
                        ],
                        stdout=(
                            subprocess.DEVNULL
                        ),
                        stderr=(
                            subprocess.DEVNULL
                        ),
                        timeout=5
                    )
                )

                if (
                    kill_result.returncode
                    == 0
                ):

                    log(
                        f"已关闭 ToDesk:"
                        f"PID={pid}"
                    )

                    killed_any = True

                    continue

            except Exception:

                pass


            # --------------------------------------------
            # sudo kill
            # --------------------------------------------

            if run_sudo_command(
                [
                    "kill",
                    "-9",
                    str(pid)
                ]
            ):

                log(
                    "已通过 sudo "
                    f"关闭 ToDesk:PID={pid}"
                )

                killed_any = True


        if not killed_any:

            log(
                "没有发现需要关闭的 "
                "ToDesk 进程"
            )


        time.sleep(
            TODESK_KILL_WAIT
        )

        return True


    except Exception as e:

        log(
            "关闭 ToDesk 异常:"
            f"{type(e).__name__}: {e}"
        )

        return False


# ============================================================
# 14. 查找 Terminal
# ============================================================

def find_terminal():

    candidates = [
        "gnome-terminal",
        "x-terminal-emulator",
        "konsole",
        "xfce4-terminal"
    ]

    for terminal in candidates:

        if shutil.which(
            terminal
        ):
            return terminal

    return None


# ============================================================
# 15. 新建 Terminal 执行命令
# ============================================================

def open_terminal_and_run(
    command,
    title=None,
    hold=True
):

    terminal = (
        find_terminal()
    )

    if not terminal:

        log(
            "没有找到可用的 "
            "图形 Terminal"
        )

        return False


    if hold:

        shell_cmd = (

            command

            + '; echo ""; '

            + 'echo "命令执行完成。'
              '按 Enter 关闭此终端..."; '

            + 'read'
        )

    else:

        shell_cmd = (
            command
        )


    try:

        # ========================================
        # GNOME Terminal
        # ========================================

        if (
            terminal
            == "gnome-terminal"
        ):

            args = [
                "gnome-terminal"
            ]

            if title:

                args += [
                    "--title",
                    title
                ]

            args += [
                "--",
                "bash",
                "-lc",
                shell_cmd
            ]


        # ========================================
        # x-terminal-emulator
        # ========================================

        elif (
            terminal
            == "x-terminal-emulator"
        ):

            args = [
                "x-terminal-emulator",
                "-e",
                "bash",
                "-lc",
                shell_cmd
            ]


        # ========================================
        # KDE Konsole
        # ========================================

        elif (
            terminal
            == "konsole"
        ):

            args = [
                "konsole"
            ]

            if title:

                args += [
                    "-p",
                    f"tabtitle={title}"
                ]

            args += [
                "-e",
                "bash",
                "-lc",
                shell_cmd
            ]


        # ========================================
        # XFCE Terminal
        # ========================================

        elif (
            terminal
            == "xfce4-terminal"
        ):

            args = [
                "xfce4-terminal"
            ]

            if title:

                args += [
                    "--title",
                    title
                ]

            args += [
                "--command",
                (
                    "bash -lc "
                    + shlex.quote(
                        shell_cmd
                    )
                )
            ]


        else:

            return False


        subprocess.Popen(
            args,
            stdout=(
                subprocess.DEVNULL
            ),
            stderr=(
                subprocess.DEVNULL
            ),
            start_new_session=True
        )

        return True


    except Exception as e:

        log(
            "打开 Terminal 失败:"
            f"{type(e).__name__}: {e}"
        )

        return False


# ============================================================
# 16. 停止 UFW
# ============================================================

def disable_ufw():

    log(
        "准备打开新 Terminal "
        "执行 UFW 操作"
    )


    quoted_password = (
        shlex.quote(
            SUDO_PASSWORD
        )
    )


    command = (

        'echo "===================================="; '

        'echo "正在停止并禁用 UFW..."; '

        'echo "===================================="; '

        f"printf '%s\\n' "
        f"{quoted_password} "
        f"| sudo -S systemctl stop ufw; "

        'echo ""; '

        'echo "[1/2] '
        'systemctl stop ufw 完成"; '

        f"printf '%s\\n' "
        f"{quoted_password} "
        f"| sudo -S systemctl disable ufw; "

        'echo ""; '

        'echo "[2/2] '
        'systemctl disable ufw 完成"; '

        'echo ""; '

        'echo "当前 UFW 状态:"; '

        'systemctl is-active ufw '
        '|| true; '

        'systemctl is-enabled ufw '
        '|| true'
    )


    ok = (
        open_terminal_and_run(

            command,

            title=(
                "ToDesk Recovery "
                "- Disable UFW"
            ),

            hold=True
        )
    )


    if ok:

        log(
            "UFW 操作 Terminal "
            "已打开"
        )

    else:

        log(
            "UFW Terminal "
            "打开失败"
        )


    # 给 UFW 操作留出时间
    time.sleep(8)

    return ok


# ============================================================
# 17. 查找 ToDesk
# ============================================================

def find_todesk_command():

    path = (
        shutil.which(
            "todesk"
        )
    )

    if path:
        return path


    candidates = [

        "/usr/bin/todesk",

        "/usr/local/bin/todesk",

        "/opt/todesk/todesk",

        "/opt/ToDesk/todesk",

        "/opt/todesk/bin/todesk"
    ]


    for candidate in candidates:

        if os.path.isfile(
            candidate
        ):

            return candidate


    return None


# ============================================================
# 18. 新建第二个 Terminal 启动 ToDesk
# ============================================================

def start_todesk():

    """
    为了防止第二个 Terminal 一闪而过:

    Python
        ↓
    生成 start_todesk_recovery.sh
        ↓
    gnome-terminal
        ↓
    bash start_todesk_recovery.sh
        ↓
    todesk
        ↓
    exec bash
    """

    todesk = (
        find_todesk_command()
    )


    if not todesk:

        log(
            "没有找到 todesk "
            "可执行文件"
        )

        log(
            "请执行:which todesk"
        )

        return False


    log(
        f"检测到 ToDesk 路径:"
        f"{todesk}"
    )


    launcher = (
        "/home/hy/"
        "start_todesk_recovery.sh"
    )


    launcher_text = f"""#!/bin/bash

echo "========================================"
echo "       ToDesk 自动恢复启动终端"
echo "========================================"

echo ""

echo "ToDesk 路径:{todesk}"

echo "当前用户:$(whoami)"

echo "DISPLAY=$DISPLAY"

echo ""

echo "正在启动 ToDesk..."

echo ""

"{todesk}"

RET=$?

echo ""

echo "========================================"

echo "ToDesk 命令返回,退出码:$RET"

echo "========================================"

echo ""

echo "此终端将保持打开,方便查看错误。"

echo "如需关闭,请输入 exit。"

exec bash
"""


    # --------------------------------------------
    # 生成 launcher
    # --------------------------------------------

    try:

        with open(
            launcher,
            "w",
            encoding="utf-8"
        ) as f:

            f.write(
                launcher_text
            )


        os.chmod(
            launcher,
            0o755
        )


        log(
            f"已生成 ToDesk 启动脚本:"
            f"{launcher}"
        )


    except Exception as e:

        log(
            "生成 ToDesk 启动脚本失败:"
            f"{type(e).__name__}: {e}"
        )

        return False


    terminal = (
        find_terminal()
    )


    if not terminal:

        log(
            "没有找到图形 Terminal"
        )

        return False


    log(
        f"使用图形终端:"
        f"{terminal}"
    )


    try:

        # ========================================
        # GNOME
        # ========================================

        if (
            terminal
            == "gnome-terminal"
        ):

            args = [

                "gnome-terminal",

                "--title="
                "ToDesk Recovery "
                "- Start ToDesk",

                "--",

                "bash",

                launcher
            ]


        # ========================================
        # x-terminal-emulator
        # ========================================

        elif (
            terminal
            == "x-terminal-emulator"
        ):

            args = [

                "x-terminal-emulator",

                "-e",

                "bash",

                launcher
            ]


        # ========================================
        # KDE
        # ========================================

        elif (
            terminal
            == "konsole"
        ):

            args = [

                "konsole",

                "-p",

                "tabtitle="
                "ToDesk Recovery "
                "- Start ToDesk",

                "-e",

                "bash",

                launcher
            ]


        # ========================================
        # XFCE
        # ========================================

        elif (
            terminal
            == "xfce4-terminal"
        ):

            args = [

                "xfce4-terminal",

                "--title="
                "ToDesk Recovery "
                "- Start ToDesk",

                "--command",

                (
                    "bash "
                    + shlex.quote(
                        launcher
                    )
                )
            ]


        else:

            log(
                f"不支持 Terminal:"
                f"{terminal}"
            )

            return False


        log(
            "正在打开第二个 "
            "ToDesk Terminal"
        )


        proc = (
            subprocess.Popen(

                args,

                stdout=(
                    subprocess.PIPE
                ),

                stderr=(
                    subprocess.PIPE
                ),

                text=True,

                start_new_session=True
            )
        )


        # --------------------------------------------
        # 判断 Terminal 是否立即启动失败
        # --------------------------------------------

        time.sleep(2)


        ret = (
            proc.poll()
        )


        if (
            ret is not None
            and ret != 0
        ):

            stdout, stderr = (
                proc.communicate(
                    timeout=2
                )
            )


            log(
                "第二个 Terminal "
                f"启动失败,返回码={ret}"
            )


            if stdout.strip():

                log(
                    "Terminal stdout:"
                    + stdout.strip()
                )


            if stderr.strip():

                log(
                    "Terminal stderr:"
                    + stderr.strip()
                )


            return False


        log(
            "第二个 ToDesk Terminal "
            "启动命令已经提交"
        )


        return True


    except Exception as e:

        log(
            "启动第二个 ToDesk "
            "Terminal 失败:"
            f"{type(e).__name__}: {e}"
        )

        return False


# ============================================================
# 19. ToDesk 完整恢复流程
# ============================================================

def restart_todesk():

    log(
        "================================"
    )

    log(
        "网络恢复,开始执行 "
        "ToDesk 恢复流程"
    )

    log(
        "================================"
    )


    # ========================================
    # 1. Kill ToDesk
    # ========================================

    log(
        "[1/3] "
        "Kill ToDesk / ToDesk+"
    )

    kill_todesk()


    # ========================================
    # 2. 关闭 UFW
    # ========================================

    log(
        "[2/3] "
        "新开 Terminal:"
        "停止并禁用 UFW"
    )

    disable_ufw()


    # ========================================
    # 3. 启动 ToDesk
    # ========================================

    log(
        "[3/3] "
        "新开 Terminal:"
        "启动 ToDesk"
    )

    start_todesk()


    log(
        "ToDesk 恢复流程执行结束"
    )


# ============================================================
# 20. 主监控程序
# ============================================================

def main():

    # --------------------------------------------
    # 单实例
    # --------------------------------------------

    _lock_fp = (
        acquire_single_instance_lock()
    )


    log(
        "========================================"
    )

    log(
        "校园网 + ToDesk "
        "自动恢复监控启动"
    )

    log(
        "========================================"
    )

    log(
        "校园网状态判断方式:"
        "Portal 页面 toLogOut"
    )

    log(
        "检测周期:30 分钟"
    )


    while True:

        try:

            log(
                "========================================"
            )

            log(
                "开始执行本轮 "
                "校园网状态检测......"
            )


            # ========================================
            # 检测 Portal
            # ========================================

            status = (
                portal_login_status()
            )


            # ========================================
            # 当前已登录
            # ========================================

            if status is True:

                log(
                    "本轮判断结果:"
                    "校园网正常 / 已登录"
                )

                log(
                    "无需操作 ToDesk"
                )

                log(
                    "下一次检测将在 "
                    "30 分钟后进行"
                )

                time.sleep(
                    CHECK_INTERVAL
                )

                continue


            # ========================================
            # Portal 检测异常
            # ========================================

            if status is None:

                log(
                    "Portal 检测异常,"
                    "本轮不操作 ToDesk"
                )

                log(
                    "30 分钟后重新检测"
                )

                time.sleep(
                    CHECK_INTERVAL
                )

                continue


            # ========================================
            # 当前未登录
            # ========================================

            log(
                "本轮判断结果:"
                "校园网未登录"
            )

            log(
                "开始执行校园网 "
                "自动重连"
            )


            # ========================================
            # 自动登录
            # ========================================

            login_success = (
                campus_login()
            )


            # ========================================
            # 登录成功
            # ========================================

            if login_success:

                log(
                    "校园网自动重连成功"
                )


                # ------------------------------------
                # 登录成功后重新恢复 ToDesk
                # ------------------------------------

                restart_todesk()


                log(
                    "本轮恢复完成"
                )

                log(
                    "下一次检测将在 "
                    "30 分钟后进行"
                )


                time.sleep(
                    CHECK_INTERVAL
                )


            # ========================================
            # 登录失败
            # ========================================

            else:

                log(
                    "本次校园网 "
                    "自动重连失败"
                )

                log(
                    "30 分钟后重新尝试"
                )

                time.sleep(
                    CHECK_INTERVAL
                )


        # ============================================
        # Ctrl+C
        # ============================================

        except KeyboardInterrupt:

            log(
                "收到 Ctrl+C,"
                "程序退出"
            )

            break


        # ============================================
        # 其它异常
        # ============================================

        except Exception as e:

            log(
                "主循环异常:"
                f"{type(e).__name__}: {e}"
            )

            log(
                "30 分钟后重新检测"
            )

            time.sleep(
                CHECK_INTERVAL
            )


# ============================================================
# 21. 程序入口
# ============================================================

if __name__ == "__main__":

    main()

20. 使用前必须修改的位置

至少修改下面几项。

① 校园网账号

复制代码
USERNAME = "xxxxxx"

例如:

复制代码
USERNAME = "你的校园网账号"

② 校园网密码

复制代码
PASSWORD = "xxxxxx"

③ sudo 密码

复制代码
SUDO_PASSWORD = "xxxxxx"

如果代码需要上传 GitHub,强烈建议进一步改成环境变量或者 sudoers NOPASSWD,不要提交真实 sudo 密码。

④ Chrome 路径

复制代码
CHROME_BINARY = (
    "/home/hy/Downloads/"
    "chrome-linux64/chrome"
)

⑤ ChromeDriver 路径

复制代码
CHROMEDRIVER = (
    "/home/hy/Downloads/"
    "chromedriver-linux64/chromedriver"
)

⑥ Ubuntu 用户目录

本文机器的用户名为:

复制代码
hy

因此代码中使用了:

复制代码
/home/hy/

其他用户需要统一替换为自己的 Ubuntu 用户目录,例如:

复制代码
/home/ubuntu/

或者:

复制代码
/home/zhangsan/

21. 运行

首先给 Chrome 和 ChromeDriver 添加权限:

复制代码
chmod +x ~/Downloads/chrome-linux64/chrome
chmod +x ~/Downloads/chromedriver-linux64/chromedriver

运行:

复制代码
python auto_login_and_todesk_watchdog.py

程序启动后会看到:

复制代码
校园网 + ToDesk 自动恢复监控启动
校园网状态判断方式:Portal 页面 toLogOut
检测周期:30 分钟

开始执行本轮校园网状态检测......

网络正常时:

复制代码
检测到 toLogOut:当前校园网已登录
本轮判断结果:校园网正常 / 已登录
无需操作 ToDesk
下一次检测将在 30 分钟后进行

如果校园网掉线:

复制代码
未检测到 toLogOut:当前校园网未登录
开始执行校园网自动重连

自动登录成功以后:

复制代码
校园网自动重连成功

[1/3] Kill ToDesk / ToDesk+
[2/3] 新开 Terminal:停止并禁用 UFW
[3/3] 新开 Terminal:启动 ToDesk

最终实现:

复制代码
校园网掉线
      ↓
自动检测
      ↓
Selenium 自动认证
      ↓
校园网恢复
      ↓
自动 Kill 旧 ToDesk
      ↓
关闭 UFW
      ↓
重新启动 ToDesk
      ↓
恢复远程控制

22. Ubuntu 开机自动启动

如果希望 Ubuntu 登录桌面后自动运行,可以创建:

复制代码
mkdir -p ~/.config/autostart

然后:

复制代码
nano ~/.config/autostart/upc-todesk-watchdog.desktop

内容:

复制代码
[Desktop Entry]
Type=Application
Name=UPC ToDesk Watchdog
Comment=Auto reconnect campus network and restart ToDesk
Exec=gnome-terminal --title=UPC-ToDesk-Watchdog -- bash -lc '/home/hy/anaconda3/bin/python /home/hy/net_watchdog/auto_login_and_todesk_watchdog.py; exec bash'
Terminal=false
X-GNOME-Autostart-enabled=true
Hidden=false
NoDisplay=false

注意根据自己的路径修改:

复制代码
/home/hy/anaconda3/bin/python

以及:

复制代码
/home/hy/net_watchdog/auto_login_and_todesk_watchdog.py

配置完成以后:

复制代码
Ubuntu 开机
   ↓
进入用户桌面
   ↓
自动弹出 Watchdog Terminal
   ↓
自动开始校园网监控
   ↓
每 30 分钟检测一次

23. 代码与参考

校园网 Selenium 自动登录部分参考:

https://github.com/ThreeStones1029/AutoLoginCampusNetwork/tree/main

Chrome for Testing:

Chrome for Testing availability

本文使用的 Chrome Linux 64:

https://storage.googleapis.com/chrome-for-testing-public/153.0.7993.0/linux64/chrome-linux64.zip

本文使用的 ChromeDriver Linux 64:

https://storage.googleapis.com/chrome-for-testing-public/153.0.7993.0/linux64/chromedriver-linux64.zip

建议 Chrome 与 ChromeDriver 使用完全相同的版本号


安全提示:

本文代码为了实现无人值守自动恢复,演示了 sudo -S 自动输入密码的方法。个人内网实验机使用比较方便,但如果代码准备公开到 CSDN、GitHub、Gitee 等平台,请务必像本文一样把:

复制代码
USERNAME = "xxxxxx"
PASSWORD = "xxxxxx"
SUDO_PASSWORD = "xxxxxx"

全部脱敏,切勿提交任何真实密码。

参考

校园网自动登录参考项目:

GitHub - ThreeStones1029/AutoLoginCampusNetwork: 自动连接河海大学校园网 · GitHub

Chrome for Testing 官方下载:

Chrome for Testing availability

本文使用的 Linux Chrome:

https://storage.googleapis.com/chrome-for-testing-public/153.0.7993.0/linux64/chrome-linux64.zip

本文使用的 Linux ChromeDriver:

https://storage.googleapis.com/chrome-for-testing-public/153.0.7993.0/linux64/chromedriver-linux64.zip

Chrome for Testing 官方页面提供不同版本和平台的 Chrome、ChromeDriver 对应下载信息。

相关推荐
三言老师1 小时前
K8s集群运行时自动化运维全覆盖落地实操(下)
linux·运维·服务器·网络
葡萄城技术团队2 小时前
InfluxDB 2\.x 深度解析:核心架构、Flux 函数与制造业落地指南(三)
java·开发语言·架构
Super 含2 小时前
Android 启动优化(五):线程、GC 与 IO 为什么会拖慢启动?
java·服务器·数据库
counting money2 小时前
Java IO流详解:从InputStream到文件操作实战
java·开发语言·python
wuyk5552 小时前
98.C语言易混难点:字符数组与字符串指针的底层差异
c语言·开发语言·c++·stm32·嵌入式硬件·算法
坚持学习前端日记3 小时前
Python SQLAlchemy ORM 从0到1精通实战手册(基础到复杂高阶)
数据库·python·oracle
峥无3 小时前
从0到1手撕红黑树:封装实现 my_map 与 my_set(SGI-STL 源码级深度解析)
开发语言·c++·笔记·算法·stl
波特率1152003 小时前
C++新特性---属性说明符与标准属性
开发语言·c++
程序员小八7773 小时前
上海百度B端java后端日常实习一面
java·开发语言
北斗落凡尘3 小时前
LangGraph 入门实战(11)--输出模式
后端·python·langchain