mirror of
https://github.com/Stone-Red-Code/HotspotAutoLogin.git
synced 2026-11-01 23:05:40 +01:00
Add Auto mode (network auto-detection) and Start with Windows
- Auto mode (Auto button or --auto): picks the profile matching the connected network instead of forcing one profile's SSID. Wi-Fi is matched by SSID; Ethernet profiles by whether their portal is reachable. Unknown networks are left alone. config.json is reloaded each check. - Watches for network changes every 5 seconds while waiting, so switching between Ethernet and Wi-Fi is handled right away. - Auto mode backs off instead of exiting after repeated errors. - Start with Windows checkbox (HKCU Run key, --auto --background). - --background flag to start without the log window. - Resolve the program directory for both script and PyInstaller builds, so startup launches find config.json. - Ignore Hyper-V/WSL vEthernet adapters when detecting Ethernet. - Fix crash when pressing Enter in the profile list. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
bbbb76d577
commit
53bf01733e
+283
-57
@@ -1,5 +1,8 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import argparse
|
||||
import winreg
|
||||
import requests
|
||||
import time
|
||||
import subprocess
|
||||
@@ -21,17 +24,66 @@ dns.resolver.default_resolver = dns.resolver.Resolver()
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
||||
|
||||
################################# REMOVE THIS BEFORE USING PYINSTALLER #################################
|
||||
# Change the current working directory to the script's directory
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# Change the current working directory to the program's directory.
|
||||
# Works for both the script and a PyInstaller build, and is required when launched at Windows startup,
|
||||
# where the working directory is not the program folder (so config.json and icon.ico would not be found).
|
||||
if getattr(sys, "frozen", False):
|
||||
script_dir = os.path.dirname(sys.executable)
|
||||
else:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
os.chdir(script_dir)
|
||||
################################# REMOVE THIS BEFORE USING PYINSTALLER #################################
|
||||
|
||||
# Program Information
|
||||
program_name = "HotspotAutoLogin"
|
||||
version = "v1.90"
|
||||
github_link = "https://github.com/denizsafak/HotspotAutoLogin"
|
||||
|
||||
# Command line arguments
|
||||
# --auto Skip profile selection and auto-detect which profile to use from the connected network
|
||||
# --background Don't open the log window at startup (used when starting with Windows)
|
||||
arg_parser = argparse.ArgumentParser(add_help=False)
|
||||
arg_parser.add_argument("--auto", action="store_true")
|
||||
arg_parser.add_argument("--background", action="store_true")
|
||||
args, _ = arg_parser.parse_known_args()
|
||||
auto_mode = args.auto
|
||||
|
||||
# Start with Windows (HKCU Run key, no admin rights needed)
|
||||
AUTOSTART_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
|
||||
|
||||
def get_autostart_command():
|
||||
if getattr(sys, "frozen", False):
|
||||
return f'"{sys.executable}" --auto --background'
|
||||
# Use pythonw.exe so no console window is shown at startup
|
||||
pythonw = os.path.join(os.path.dirname(sys.executable), "pythonw.exe")
|
||||
if not os.path.exists(pythonw):
|
||||
pythonw = sys.executable
|
||||
return f'"{pythonw}" "{os.path.abspath(__file__)}" --auto --background'
|
||||
|
||||
def is_autostart_enabled():
|
||||
try:
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, AUTOSTART_KEY) as key:
|
||||
winreg.QueryValueEx(key, program_name)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def set_autostart(enabled):
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, AUTOSTART_KEY, 0, winreg.KEY_SET_VALUE) as key:
|
||||
if enabled:
|
||||
winreg.SetValueEx(key, program_name, 0, winreg.REG_SZ, get_autostart_command())
|
||||
else:
|
||||
try:
|
||||
winreg.DeleteValue(key, program_name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
# If autostart is enabled, keep the registered path up to date in case the program folder was moved
|
||||
if is_autostart_enabled():
|
||||
try:
|
||||
set_autostart(True)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Function to center a window on the screen
|
||||
def center_window(window, width, height):
|
||||
screen_width = window.winfo_screenwidth()
|
||||
@@ -40,7 +92,7 @@ def center_window(window, width, height):
|
||||
y = (screen_height - height) // 2
|
||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||
|
||||
def run_profile():
|
||||
def run_profile(event=None):
|
||||
global selected_profile
|
||||
refresh_profile_details()
|
||||
selected_index = listbox.curselection()
|
||||
@@ -49,6 +101,24 @@ def run_profile():
|
||||
selected_profile = profiles[selected_index]
|
||||
root.destroy()
|
||||
|
||||
# Run in auto mode: the profile is picked automatically based on the connected network
|
||||
def run_auto():
|
||||
global auto_mode, selected_profile
|
||||
refresh_profile_details()
|
||||
if not profiles:
|
||||
messagebox.showerror("No Profiles", "Add at least one profile before using Auto mode.")
|
||||
return
|
||||
auto_mode = True
|
||||
selected_profile = None
|
||||
root.destroy()
|
||||
|
||||
def toggle_autostart():
|
||||
try:
|
||||
set_autostart(autostart_var.get())
|
||||
except OSError as e:
|
||||
autostart_var.set(is_autostart_enabled())
|
||||
messagebox.showerror("Start with Windows", f"Could not change the startup setting: {e}")
|
||||
|
||||
# Define a function to update the profile details
|
||||
def update_profile_details(event):
|
||||
global selected_profile
|
||||
@@ -107,6 +177,8 @@ with open("config.json", "r") as f:
|
||||
profiles = config.get("profiles", [])
|
||||
# Create the main window
|
||||
root = tk.Tk()
|
||||
if auto_mode:
|
||||
root.withdraw() # Started with --auto, the profile selection window is not needed
|
||||
root.title(f"Profile Selection - {program_name} {version}")
|
||||
root.iconbitmap("icon.ico")
|
||||
# Set a maximum size for the window
|
||||
@@ -552,6 +624,16 @@ github_button = tk.Button(
|
||||
)
|
||||
github_button.pack(side=tk.RIGHT, padx=(0, 5))
|
||||
|
||||
# Create the "Start with Windows" checkbox (starts in Auto mode in the background)
|
||||
autostart_var = tk.BooleanVar(value=is_autostart_enabled())
|
||||
autostart_check = tk.Checkbutton(
|
||||
profile_name_frame,
|
||||
text="Start with Windows (Auto)",
|
||||
variable=autostart_var,
|
||||
command=toggle_autostart,
|
||||
)
|
||||
autostart_check.pack(side=tk.RIGHT, padx=(0, 5))
|
||||
|
||||
# Create the "Config" button
|
||||
config_button = tk.Button(
|
||||
details_frame,
|
||||
@@ -568,6 +650,22 @@ config_button = tk.Button(
|
||||
)
|
||||
config_button.pack(side=tk.LEFT, fill="x", padx=(0, 2), pady=(10, 0), expand=False)
|
||||
|
||||
# Create the "Auto" button (detects the profile from the connected network)
|
||||
auto_button = tk.Button(
|
||||
details_frame,
|
||||
text="Auto",
|
||||
command=run_auto,
|
||||
padx=30,
|
||||
pady=10,
|
||||
fg="white",
|
||||
bg="lightskyblue4",
|
||||
activebackground="lightsteelblue4",
|
||||
activeforeground="white",
|
||||
relief=tk.RAISED,
|
||||
borderwidth=2,
|
||||
)
|
||||
auto_button.pack(side=tk.LEFT, fill="x", padx=2, pady=(10, 0), expand=False)
|
||||
|
||||
# Create the "Run" button
|
||||
run_button = tk.Button(
|
||||
details_frame,
|
||||
@@ -595,24 +693,34 @@ if profiles:
|
||||
listbox.select_set(0) # Highlight the first item in the list
|
||||
update_profile_details(None) # Update the profile details
|
||||
|
||||
root.mainloop()
|
||||
if auto_mode:
|
||||
root.destroy()
|
||||
else:
|
||||
root.mainloop()
|
||||
|
||||
# Load the given profile's data into the variables used by the network checker
|
||||
def apply_profile(profile):
|
||||
global selected_profile, payload, url, internet_check_url, ssid, check_every_second, dialog_geometry, headers, expected_ssid, expected_ssid_lower
|
||||
selected_profile = profile
|
||||
payload = profile.get('payload')
|
||||
url = profile.get('url', "")
|
||||
internet_check_url = profile.get('internet_check_url', "")
|
||||
ssid = profile.get('ssid', "") or "Ethernet"
|
||||
check_every_second = profile.get('check_every_second', 600)
|
||||
dialog_geometry = profile.get('dialog_geometry', {"width": 1024, "height": 500})
|
||||
headers = profile.get('headers', "")
|
||||
expected_ssid = ssid
|
||||
expected_ssid_lower = expected_ssid.lower()
|
||||
|
||||
# Access the selected profile's data after the window closes
|
||||
if selected_profile:
|
||||
payload = selected_profile.get('payload')
|
||||
url = selected_profile.get('url', "")
|
||||
internet_check_url = selected_profile.get('internet_check_url', "")
|
||||
ssid = selected_profile.get('ssid', "")
|
||||
check_every_second = selected_profile.get('check_every_second', "")
|
||||
dialog_geometry = selected_profile.get('dialog_geometry', "")
|
||||
headers = selected_profile.get('headers', "")
|
||||
if ssid:
|
||||
expected_ssid = ssid
|
||||
expected_ssid_lower = expected_ssid.lower()
|
||||
else:
|
||||
ssid = "Ethernet"
|
||||
expected_ssid = ssid
|
||||
expected_ssid_lower = expected_ssid.lower()
|
||||
if auto_mode:
|
||||
# The profile is chosen later, based on the connected network
|
||||
selected_profile = None
|
||||
expected_ssid = expected_ssid_lower = None
|
||||
check_every_second = 600
|
||||
dialog_geometry = profiles[0].get('dialog_geometry', {"width": 1024, "height": 500}) if profiles else {"width": 1024, "height": 500}
|
||||
elif selected_profile:
|
||||
apply_profile(selected_profile)
|
||||
|
||||
# Function to send the request
|
||||
def send_request():
|
||||
@@ -641,35 +749,96 @@ def is_internet_available():
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
# Function to get all currently connected networks (for Windows)
|
||||
# Returns (ethernet_connected, wifi_ssid, wifi_error)
|
||||
def get_connected_networks():
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
ethernet_connected = False
|
||||
try:
|
||||
ethernet_output = subprocess.check_output(["netsh", "interface", "show", "interface"], startupinfo=startupinfo).decode("utf-8", errors="replace")
|
||||
for line in ethernet_output.splitlines():
|
||||
# Columns: Admin State, State, Type, Interface Name
|
||||
parts = line.split(None, 3)
|
||||
if len(parts) == 4 and parts[1] == "Connected" and "Ethernet" in parts[3] and not parts[3].startswith("vEthernet"):
|
||||
ethernet_connected = True # vEthernet adapters (Hyper-V / WSL) are virtual and always "Connected"
|
||||
break
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
wifi_ssid = None
|
||||
wifi_error = None
|
||||
try:
|
||||
wifi_output = subprocess.check_output(["netsh", "wlan", "show", "interfaces"], startupinfo=startupinfo).decode("utf-8", errors="replace")
|
||||
match = re.search(r"^\s*SSID\s*:\s*(.+)$", wifi_output, re.MULTILINE)
|
||||
if match:
|
||||
wifi_ssid = match.group(1).strip()
|
||||
except subprocess.CalledProcessError as e:
|
||||
wifi_error = str(e)
|
||||
return ethernet_connected, wifi_ssid, wifi_error
|
||||
|
||||
# Function to get the currently connected SSID using system commands (for Windows)
|
||||
def get_connected_network():
|
||||
try:
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||
# Check if connected to Ethernet
|
||||
ethernet_output = subprocess.check_output(["netsh", "interface", "show", "interface"], startupinfo=startupinfo).decode("utf-8")
|
||||
ethernet_lines = ethernet_output.split("\n")
|
||||
for line in ethernet_lines:
|
||||
if "Connected" in line:
|
||||
if "Ethernet" in line:
|
||||
return "Ethernet"
|
||||
# If not connected to Ethernet, check Wi-Fi SSID
|
||||
try:
|
||||
wifi_output = subprocess.check_output(["netsh", "wlan", "show", "interfaces"], startupinfo=startupinfo).decode("utf-8")
|
||||
wifi_lines = wifi_output.split("\n")
|
||||
wifi_ssid = None
|
||||
for line in wifi_lines:
|
||||
if "SSID" in line:
|
||||
wifi_ssid = line.strip().split(": ")[1]
|
||||
break
|
||||
if wifi_ssid is not None:
|
||||
return wifi_ssid
|
||||
else:
|
||||
return False
|
||||
except subprocess.CalledProcessError as e:
|
||||
return "Error: " + str(e)
|
||||
except subprocess.CalledProcessError:
|
||||
ethernet_connected, wifi_ssid, wifi_error = get_connected_networks()
|
||||
if ethernet_connected:
|
||||
return "Ethernet"
|
||||
if wifi_error:
|
||||
return "Error: " + wifi_error
|
||||
return wifi_ssid or False
|
||||
|
||||
# Check whether the login portal's host can be reached from the current network
|
||||
def is_portal_reachable(portal_url, timeout=3):
|
||||
parsed = urlparse(portal_url)
|
||||
if not parsed.hostname:
|
||||
return False
|
||||
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||
try:
|
||||
with socket.create_connection((parsed.hostname, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# Reload profiles from config.json, so edits are picked up without restarting (auto mode)
|
||||
def load_profiles():
|
||||
global profiles
|
||||
try:
|
||||
with open("config.json", "r") as f:
|
||||
profiles = json.load(f).get("profiles", [])
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass # Keep using the last valid profiles
|
||||
return profiles
|
||||
|
||||
# Sleep for the given seconds, but check the connected networks every few seconds and return early
|
||||
# if they changed (e.g. switching between Ethernet and Wi-Fi), so the new network is handled right away (auto mode)
|
||||
NETWORK_POLL_SECONDS = 5
|
||||
def wait_or_network_change(seconds, networks):
|
||||
deadline = time.time() + seconds
|
||||
while True:
|
||||
remaining = deadline - time.time()
|
||||
if remaining <= 0:
|
||||
return False
|
||||
time.sleep(min(NETWORK_POLL_SECONDS, remaining))
|
||||
ethernet_connected, wifi_ssid, _ = get_connected_networks()
|
||||
if (ethernet_connected, wifi_ssid) != tuple(networks):
|
||||
add_to_log("Network change detected. Checking again now...")
|
||||
return True
|
||||
|
||||
# Find the profile that belongs to the currently connected network (auto mode)
|
||||
# Returns (profile, connected_network_name) or (None, None)
|
||||
def find_matching_profile(ethernet_connected, wifi_ssid):
|
||||
# Wi-Fi: match by SSID. If several profiles share the SSID, prefer one whose portal is reachable.
|
||||
if wifi_ssid:
|
||||
candidates = [p for p in profiles if p.get('ssid', "").strip().lower() == wifi_ssid.lower()]
|
||||
if len(candidates) > 1:
|
||||
reachable = [p for p in candidates if is_portal_reachable(p.get('url', ""))]
|
||||
candidates = reachable or candidates
|
||||
if candidates:
|
||||
return candidates[0], wifi_ssid
|
||||
# Ethernet: profiles have no SSID, so use the one whose login portal is reachable on this network
|
||||
if ethernet_connected:
|
||||
for p in profiles:
|
||||
if not p.get('ssid') and is_portal_reachable(p.get('url', "")):
|
||||
return p, "Ethernet"
|
||||
return None, None
|
||||
|
||||
# Is connected to wifi
|
||||
def is_connected_to_wifi():
|
||||
@@ -708,18 +877,25 @@ def update_window_geometry(event):
|
||||
x, y = event.x, event.y
|
||||
width, height = event.width, event.height
|
||||
|
||||
# Title of the log dialog, with the profile currently in use
|
||||
def get_log_title():
|
||||
if auto_mode:
|
||||
name = selected_profile['name'] if selected_profile else "waiting for a known network"
|
||||
return f"Log Messages (Auto: {name})"
|
||||
return f"Log Messages ({selected_profile['name']})"
|
||||
|
||||
# Function to show the log dialog with the currently selected profile name
|
||||
def show_log_dialog():
|
||||
global log_dialog, log_text, log_dialog_open, successful_logins_label
|
||||
if not log_dialog_open:
|
||||
log_dialog_open = True
|
||||
if log_dialog:
|
||||
log_dialog.title(f"Log Messages ({selected_profile['name']})") # Update title with profile name
|
||||
log_dialog.title(get_log_title()) # Update title with profile name
|
||||
log_dialog.deiconify()
|
||||
update_log()
|
||||
else:
|
||||
log_dialog = tk.Tk()
|
||||
log_dialog.title(f"Log Messages ({selected_profile['name']})") # Update title with profile name
|
||||
log_dialog.title(get_log_title()) # Update title with profile name
|
||||
log_text = tk.Text(log_dialog)
|
||||
log_frame = tk.Frame(log_dialog)
|
||||
log_frame.pack(fill=tk.BOTH, expand=True)
|
||||
@@ -778,6 +954,8 @@ def add_to_log(message, style=None):
|
||||
# Function to update the log
|
||||
def update_log():
|
||||
if log_text:
|
||||
if auto_mode and log_dialog:
|
||||
log_dialog.title(get_log_title()) # The profile can change in auto mode
|
||||
log_text.delete(1.0, tk.END)
|
||||
for message, style in get_last_log_messages():
|
||||
if style:
|
||||
@@ -820,10 +998,53 @@ request_success = False
|
||||
request_errorcount = 0
|
||||
sleepcount = check_every_second
|
||||
connected_ssid_lower = None
|
||||
last_auto_message = None
|
||||
def check_network_status():
|
||||
global running, errorcount, sleepcount, connected_ssid_lower, ssid, response, request_success, request_errorcount, successful_logins_count
|
||||
while running and errorcount < 10:
|
||||
connected_ssid = get_connected_network()
|
||||
global running, errorcount, sleepcount, connected_ssid_lower, ssid, response, request_success, request_errorcount, successful_logins_count, last_auto_message, selected_profile
|
||||
# In auto mode the program keeps running (e.g. started with Windows), so it never exits on errors
|
||||
while running and (auto_mode or errorcount < 10):
|
||||
if auto_mode:
|
||||
if errorcount >= 10:
|
||||
sleepcount = 300
|
||||
message = "Too many errors. Pausing for {} seconds before trying again...".format(str(sleepcount))
|
||||
add_to_log(message, "bold_red")
|
||||
save_to_file(message)
|
||||
errorcount = 0
|
||||
time.sleep(sleepcount)
|
||||
continue
|
||||
# Detect which profile belongs to the connected network
|
||||
load_profiles()
|
||||
ethernet_connected, wifi_ssid, wifi_error = get_connected_networks()
|
||||
detected_profile, connected_ssid = find_matching_profile(ethernet_connected, wifi_ssid)
|
||||
if detected_profile is None:
|
||||
sleepcount = 30
|
||||
if wifi_error and not ethernet_connected:
|
||||
message = "{}\nFailed to get SSID. If location services are disabled, please enable them. Checking again in {} seconds...".format(wifi_error, str(sleepcount))
|
||||
elif wifi_ssid or ethernet_connected:
|
||||
network = " and ".join(n for n in [wifi_ssid, "Ethernet" if ethernet_connected else None] if n)
|
||||
message = "Connected to {}, which does not match any profile. Waiting for a known network...".format(network)
|
||||
else:
|
||||
message = "Not connected to any network. Waiting for a known network..."
|
||||
# Only log when the situation changes, so the log isn't flooded while idle
|
||||
if message != last_auto_message:
|
||||
add_to_log(message, "red" if wifi_error else None)
|
||||
last_auto_message = message
|
||||
selected_profile = None # So the next known network is logged as a new detection
|
||||
wait_or_network_change(sleepcount, (ethernet_connected, wifi_ssid))
|
||||
continue
|
||||
last_auto_message = None
|
||||
if selected_profile is None or selected_profile.get('name') != detected_profile.get('name'):
|
||||
errorcount = 0
|
||||
request_success = False
|
||||
request_errorcount = 0
|
||||
apply_profile(detected_profile)
|
||||
message = "Detected network {}. Using profile '{}'.".format(connected_ssid, detected_profile.get('name'))
|
||||
add_to_log(message, "bold_green")
|
||||
save_to_file(message)
|
||||
else:
|
||||
apply_profile(detected_profile) # Pick up config.json edits
|
||||
else:
|
||||
connected_ssid = get_connected_network()
|
||||
if isinstance(connected_ssid, str) and "error" in connected_ssid.lower():
|
||||
sleepcount = 60
|
||||
message = "{}\nFailed to get SSID. If location services are disabled, please enable them. Retrying in {} seconds...".format(connected_ssid, str(sleepcount))
|
||||
@@ -834,7 +1055,8 @@ def check_network_status():
|
||||
if (connected_ssid):
|
||||
connected_ssid_lower = connected_ssid.lower()
|
||||
if (connected_ssid) and ((connected_ssid_lower == expected_ssid_lower)):
|
||||
if is_connected_to_wifi() and (connected_ssid == "Ethernet" and expected_ssid == "Ethernet"):
|
||||
# Not in auto mode: there, Wi-Fi may be a separate network the user wants to keep
|
||||
if not auto_mode and is_connected_to_wifi() and (connected_ssid == "Ethernet" and expected_ssid == "Ethernet"):
|
||||
message = "You are both connected to Wi-Fi and Ethernet. Disconnecting from Wi-Fi and continuing with Ethernet..."
|
||||
add_to_log(message)
|
||||
startupinfo = subprocess.STARTUPINFO()
|
||||
@@ -1000,7 +1222,10 @@ def check_network_status():
|
||||
message = "Something went wrong. Please check your Wi-Fi or Ethernet connection. Checking again in {} seconds... (Errors: {}/10)".format(str(sleepcount), errorcount)
|
||||
save_to_file(message)
|
||||
add_to_log(message, "red")
|
||||
time.sleep(sleepcount) # Sleep ... seconds before trying again
|
||||
if auto_mode:
|
||||
wait_or_network_change(sleepcount, (ethernet_connected, wifi_ssid))
|
||||
else:
|
||||
time.sleep(sleepcount) # Sleep ... seconds before trying again
|
||||
message = "Maximum error count reached. Exiting in 5 seconds..."
|
||||
add_to_log(message, "bold_red")
|
||||
save_to_file(message)
|
||||
@@ -1012,9 +1237,10 @@ if __name__ == '__main__':
|
||||
# Create a thread for the system tray icon
|
||||
tray_thread = threading.Thread(target=create_system_tray_icon)
|
||||
tray_thread.start()
|
||||
# Open Log Messages at startup
|
||||
open_log_messages_startup = threading.Thread(target=show_log_dialog)
|
||||
open_log_messages_startup.start()
|
||||
# Open Log Messages at startup (unless started in the background, e.g. with Windows)
|
||||
if not args.background:
|
||||
open_log_messages_startup = threading.Thread(target=show_log_dialog)
|
||||
open_log_messages_startup.start()
|
||||
# Start the network status checking in the main thread
|
||||
check_network_status()
|
||||
# Wait for all threads to finish
|
||||
|
||||
Reference in New Issue
Block a user