mirror of
https://github.com/Stone-Red-Code/HotspotAutoLogin.git
synced 2026-11-02 07:18:02 +01:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12c5808dd0 | ||
|
|
e9428675e9 | ||
|
|
53bf01733e | ||
|
|
bbbb76d577 | ||
|
|
8c7b735cb1 | ||
|
|
9b6d42aff5 | ||
|
|
337c84e9a4 |
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# macOS system files
|
||||||
|
.DS_Store
|
||||||
|
._*
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# Python artifacts
|
||||||
|
__pycache__/
|
||||||
|
**/__pycache__
|
||||||
|
*.py[cod]
|
||||||
|
*.so
|
||||||
|
*.exe
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# VS Code settings
|
||||||
|
.vscode/
|
||||||
|
# Runtime files
|
||||||
|
main/log.txt
|
||||||
|
main/session_state.json
|
||||||
|
main/last_known_directory.txt
|
||||||
@@ -26,14 +26,24 @@ HotspotAutoLogin is a script that designed to automate the login process for Wi-
|
|||||||
- Any important events or actions taken by the script are logged both in a log file (log.txt) and in the log window that can be accessed via the system tray icon.
|
- Any important events or actions taken by the script are logged both in a log file (log.txt) and in the log window that can be accessed via the system tray icon.
|
||||||
- The script reads its configuration from a config.json file. This file contains the necessary information, such as your login credentials, the URL of the authentication portal, the SSID (network name) to which you want to connect, and the frequency of network checks in seconds.
|
- The script reads its configuration from a config.json file. This file contains the necessary information, such as your login credentials, the URL of the authentication portal, the SSID (network name) to which you want to connect, and the frequency of network checks in seconds.
|
||||||
|
|
||||||
|
## `Auto Mode & Start with Windows`
|
||||||
|
- **Auto mode:** Click the **Auto** button in the profile selection window (or run with `--auto`). Instead of using a single profile, the program looks at the network you are actually connected to and picks the matching profile automatically:
|
||||||
|
- **Wi-Fi:** the profile whose `ssid` matches the connected Wi-Fi (case-insensitive).
|
||||||
|
- **Ethernet:** profiles without an `ssid` are Ethernet profiles. The one whose login `url` is reachable on the current network is used.
|
||||||
|
- If the connected network doesn't match any profile, nothing is sent and the program just waits for a known network. It never forces a connection to another network in this mode.
|
||||||
|
- `config.json` is re-read on every check, so edits apply without restarting.
|
||||||
|
- **Start with Windows:** Tick **Start with Windows (Auto)** in the profile selection window. The program will then start at sign-in in Auto mode, in the background (tray icon only). Untick it to disable. This uses the current user's `Run` registry key, so no admin rights are needed.
|
||||||
|
- Command line flags: `--auto` (skip profile selection, use Auto mode) and `--background` (don't open the log window at startup). E.g. `run.bat --auto`.
|
||||||
|
|
||||||
## `Usage`
|
## `Usage`
|
||||||
- Edit the config.json file with your specific information, including your login credentials, the portal URL, the SSID of the network you want to connect to, and the check interval in seconds. [Click to learn how to Configure the config.json](#how-to-configure-the-configjson)
|
- Edit the config.json file with your specific information, including your login credentials, the portal URL, the SSID of the network you want to connect to, and the check interval in seconds. [Click to learn how to Configure the config.json](#how-to-configure-the-configjson)
|
||||||
- When you run the script, a system tray icon will appear. Right-click on the icon to access options like showing the log or exiting the application.
|
- When you run the script, a system tray icon will appear. Right-click on the icon to access options like showing the log or exiting the application.
|
||||||
|
- To check the connection right away instead of waiting for the next check, click **Check Now** in the tray menu or in the log window. If the internet is down, it logs in immediately.
|
||||||
- You can view the log of the script's actions by clicking the "Show Log" option in the system tray menu.
|
- You can view the log of the script's actions by clicking the "Show Log" option in the system tray menu.
|
||||||
|
|
||||||
## `How to Configure the config.json?`
|
## `How to Configure the config.json?`
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> I recebtly, added "Add new" button to the program, so you can achieve the same thing without needing to edit the config.json.
|
> I recently, added "Add new" button to the program, so you can achieve the same thing without needing to edit the config.json.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -57,6 +67,8 @@ HotspotAutoLogin is a script that designed to automate the login process for Wi-
|
|||||||
|
|
||||||
`"check_every_second":` The frequency of the script for checking your internet connection status. For example "100", it will try to check the internet every 100 seconds.
|
`"check_every_second":` The frequency of the script for checking your internet connection status. For example "100", it will try to check the internet every 100 seconds.
|
||||||
|
|
||||||
|
`"session_hours":` *(optional)* If your network logs you out a fixed time after logging in (for example every 24 hours), enter that here, e.g. `24`. The program remembers when it last logged in (in `session_state.json`) and checks the connection every 5 seconds from 2 minutes before the expected expiry until 10 minutes after it, so you are logged back in within seconds instead of waiting for the next `check_every_second` check.
|
||||||
|
|
||||||
> ## Example useage:
|
> ## Example useage:
|
||||||
>
|
>
|
||||||
|
|
||||||
|
|||||||
+408
-56
@@ -1,5 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import winreg
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -14,24 +17,73 @@ from urllib.parse import urlparse
|
|||||||
from tkinter import Text, Scrollbar
|
from tkinter import Text, Scrollbar
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
dns.resolver.override_system_resolver(dns.resolver.Resolver())
|
dns.resolver.override_system_resolver(dns.resolver.Resolver())
|
||||||
dns.resolver.default_resolver = dns.resolver.Resolver()
|
dns.resolver.default_resolver = dns.resolver.Resolver()
|
||||||
from urllib3.exceptions import InsecureRequestWarning
|
from urllib3.exceptions import InsecureRequestWarning
|
||||||
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
||||||
|
|
||||||
################################# REMOVE THIS BEFORE USING PYINSTALLER #################################
|
# Change the current working directory to the program's directory.
|
||||||
# Change the current working directory to the script'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__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
os.chdir(script_dir)
|
os.chdir(script_dir)
|
||||||
################################# REMOVE THIS BEFORE USING PYINSTALLER #################################
|
|
||||||
|
|
||||||
# Program Information
|
# Program Information
|
||||||
program_name = "HotspotAutoLogin"
|
program_name = "HotspotAutoLogin"
|
||||||
version = "v1.90"
|
version = "v1.90"
|
||||||
github_link = "https://github.com/denizsafak/HotspotAutoLogin"
|
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
|
# Function to center a window on the screen
|
||||||
def center_window(window, width, height):
|
def center_window(window, width, height):
|
||||||
screen_width = window.winfo_screenwidth()
|
screen_width = window.winfo_screenwidth()
|
||||||
@@ -40,7 +92,7 @@ def center_window(window, width, height):
|
|||||||
y = (screen_height - height) // 2
|
y = (screen_height - height) // 2
|
||||||
window.geometry(f"{width}x{height}+{x}+{y}")
|
window.geometry(f"{width}x{height}+{x}+{y}")
|
||||||
|
|
||||||
def run_profile():
|
def run_profile(event=None):
|
||||||
global selected_profile
|
global selected_profile
|
||||||
refresh_profile_details()
|
refresh_profile_details()
|
||||||
selected_index = listbox.curselection()
|
selected_index = listbox.curselection()
|
||||||
@@ -49,6 +101,24 @@ def run_profile():
|
|||||||
selected_profile = profiles[selected_index]
|
selected_profile = profiles[selected_index]
|
||||||
root.destroy()
|
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
|
# Define a function to update the profile details
|
||||||
def update_profile_details(event):
|
def update_profile_details(event):
|
||||||
global selected_profile
|
global selected_profile
|
||||||
@@ -107,6 +177,8 @@ with open("config.json", "r") as f:
|
|||||||
profiles = config.get("profiles", [])
|
profiles = config.get("profiles", [])
|
||||||
# Create the main window
|
# Create the main window
|
||||||
root = tk.Tk()
|
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.title(f"Profile Selection - {program_name} {version}")
|
||||||
root.iconbitmap("icon.ico")
|
root.iconbitmap("icon.ico")
|
||||||
# Set a maximum size for the window
|
# Set a maximum size for the window
|
||||||
@@ -233,6 +305,17 @@ def add_new_profile():
|
|||||||
if not dialog_height:
|
if not dialog_height:
|
||||||
error_message("dialog_height cannot be empty.")
|
error_message("dialog_height cannot be empty.")
|
||||||
return
|
return
|
||||||
|
session_hours = session_hours_var.get().strip()
|
||||||
|
if session_hours:
|
||||||
|
try:
|
||||||
|
session_hours = float(session_hours)
|
||||||
|
if session_hours <= 0:
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
error_message("session_hours must be a positive number (or empty).")
|
||||||
|
return
|
||||||
|
if session_hours.is_integer():
|
||||||
|
session_hours = int(session_hours)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parsed_headers = json.loads(raw_headers)
|
parsed_headers = json.loads(raw_headers)
|
||||||
@@ -260,6 +343,8 @@ def add_new_profile():
|
|||||||
"check_every_second": int(check_every_second),
|
"check_every_second": int(check_every_second),
|
||||||
"dialog_geometry": {"width": int(dialog_width), "height": int(dialog_height)}
|
"dialog_geometry": {"width": int(dialog_width), "height": int(dialog_height)}
|
||||||
}
|
}
|
||||||
|
if session_hours:
|
||||||
|
new_profile["session_hours"] = session_hours
|
||||||
if not use_ethernet_var.get():
|
if not use_ethernet_var.get():
|
||||||
new_profile["ssid"] = ssid
|
new_profile["ssid"] = ssid
|
||||||
profiles.append(new_profile)
|
profiles.append(new_profile)
|
||||||
@@ -297,6 +382,7 @@ def add_new_profile():
|
|||||||
url_var = tk.StringVar()
|
url_var = tk.StringVar()
|
||||||
internet_check_url_var = tk.StringVar(value="8.8.8.8")
|
internet_check_url_var = tk.StringVar(value="8.8.8.8")
|
||||||
check_every_second_var = tk.StringVar(value="600")
|
check_every_second_var = tk.StringVar(value="600")
|
||||||
|
session_hours_var = tk.StringVar(value="")
|
||||||
dialog_width_var = tk.StringVar(value="1024")
|
dialog_width_var = tk.StringVar(value="1024")
|
||||||
dialog_height_var = tk.StringVar(value="500")
|
dialog_height_var = tk.StringVar(value="500")
|
||||||
use_ethernet_var = tk.BooleanVar(value=False)
|
use_ethernet_var = tk.BooleanVar(value=False)
|
||||||
@@ -363,6 +449,13 @@ def add_new_profile():
|
|||||||
check_entry = tk.Entry(check_frame, textvariable=check_every_second_var)
|
check_entry = tk.Entry(check_frame, textvariable=check_every_second_var)
|
||||||
check_entry.pack(side="left", fill="x", expand=True)
|
check_entry.pack(side="left", fill="x", expand=True)
|
||||||
check_entry.bind("<Button-3>", show_context_menu)
|
check_entry.bind("<Button-3>", show_context_menu)
|
||||||
|
# session_hours (optional)
|
||||||
|
session_frame = tk.Frame(container)
|
||||||
|
session_frame.pack(fill="x", pady=5)
|
||||||
|
tk.Label(session_frame, text="session_hours (optional):").pack(side="left")
|
||||||
|
session_entry = tk.Entry(session_frame, textvariable=session_hours_var)
|
||||||
|
session_entry.pack(side="left", fill="x", expand=True)
|
||||||
|
session_entry.bind("<Button-3>", show_context_menu)
|
||||||
# dialog_geometry width and height
|
# dialog_geometry width and height
|
||||||
dialog_geometry_frame = tk.Frame(container)
|
dialog_geometry_frame = tk.Frame(container)
|
||||||
dialog_geometry_frame.pack(fill="x", pady=5)
|
dialog_geometry_frame.pack(fill="x", pady=5)
|
||||||
@@ -552,6 +645,16 @@ github_button = tk.Button(
|
|||||||
)
|
)
|
||||||
github_button.pack(side=tk.RIGHT, padx=(0, 5))
|
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
|
# Create the "Config" button
|
||||||
config_button = tk.Button(
|
config_button = tk.Button(
|
||||||
details_frame,
|
details_frame,
|
||||||
@@ -568,6 +671,22 @@ config_button = tk.Button(
|
|||||||
)
|
)
|
||||||
config_button.pack(side=tk.LEFT, fill="x", padx=(0, 2), pady=(10, 0), expand=False)
|
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
|
# Create the "Run" button
|
||||||
run_button = tk.Button(
|
run_button = tk.Button(
|
||||||
details_frame,
|
details_frame,
|
||||||
@@ -595,24 +714,34 @@ if profiles:
|
|||||||
listbox.select_set(0) # Highlight the first item in the list
|
listbox.select_set(0) # Highlight the first item in the list
|
||||||
update_profile_details(None) # Update the profile details
|
update_profile_details(None) # Update the profile details
|
||||||
|
|
||||||
|
if auto_mode:
|
||||||
|
root.destroy()
|
||||||
|
else:
|
||||||
root.mainloop()
|
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
|
# Access the selected profile's data after the window closes
|
||||||
if selected_profile:
|
if auto_mode:
|
||||||
payload = selected_profile.get('payload')
|
# The profile is chosen later, based on the connected network
|
||||||
url = selected_profile.get('url', "")
|
selected_profile = None
|
||||||
internet_check_url = selected_profile.get('internet_check_url', "")
|
expected_ssid = expected_ssid_lower = None
|
||||||
ssid = selected_profile.get('ssid', "")
|
check_every_second = 600
|
||||||
check_every_second = selected_profile.get('check_every_second', "")
|
dialog_geometry = profiles[0].get('dialog_geometry', {"width": 1024, "height": 500}) if profiles else {"width": 1024, "height": 500}
|
||||||
dialog_geometry = selected_profile.get('dialog_geometry', "")
|
elif selected_profile:
|
||||||
headers = selected_profile.get('headers', "")
|
apply_profile(selected_profile)
|
||||||
if ssid:
|
|
||||||
expected_ssid = ssid
|
|
||||||
expected_ssid_lower = expected_ssid.lower()
|
|
||||||
else:
|
|
||||||
ssid = "Ethernet"
|
|
||||||
expected_ssid = ssid
|
|
||||||
expected_ssid_lower = expected_ssid.lower()
|
|
||||||
|
|
||||||
# Function to send the request
|
# Function to send the request
|
||||||
def send_request():
|
def send_request():
|
||||||
@@ -625,42 +754,124 @@ def send_request():
|
|||||||
# Function to check if internet is available
|
# Function to check if internet is available
|
||||||
def is_internet_available():
|
def is_internet_available():
|
||||||
try:
|
try:
|
||||||
#subprocess.run(['ping', internet_check_url, '-n', '3', '-l', '32', '-w', '20'], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)
|
parsed = urlparse(internet_check_url)
|
||||||
socket.create_connection((internet_check_url, 53), timeout=5)
|
if not parsed.scheme:
|
||||||
|
test_urls = [f"https://{internet_check_url}", f"http://{internet_check_url}"]
|
||||||
|
else:
|
||||||
|
test_urls = [internet_check_url]
|
||||||
|
for url in test_urls:
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
return True
|
return True
|
||||||
#except subprocess.CalledProcessError:
|
except requests.RequestException as e:
|
||||||
except OSError:
|
|
||||||
return False
|
return False
|
||||||
|
return False
|
||||||
|
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)
|
# Function to get the currently connected SSID using system commands (for Windows)
|
||||||
def get_connected_network():
|
def get_connected_network():
|
||||||
try:
|
ethernet_connected, wifi_ssid, wifi_error = get_connected_networks()
|
||||||
startupinfo = subprocess.STARTUPINFO()
|
if ethernet_connected:
|
||||||
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"
|
return "Ethernet"
|
||||||
# If not connected to Ethernet, check Wi-Fi SSID
|
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:
|
try:
|
||||||
wifi_output = subprocess.check_output(["netsh", "wlan", "show", "interfaces"], startupinfo=startupinfo).decode("utf-8")
|
with socket.create_connection((parsed.hostname, port), timeout=timeout):
|
||||||
wifi_lines = wifi_output.split("\n")
|
return True
|
||||||
wifi_ssid = None
|
except OSError:
|
||||||
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
|
return False
|
||||||
except subprocess.CalledProcessError as e:
|
|
||||||
return "Error: " + str(e)
|
# Reload profiles from config.json, so edits are picked up without restarting (auto mode)
|
||||||
except subprocess.CalledProcessError:
|
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
|
||||||
|
|
||||||
|
# "Check now" (tray menu / log window) sets this to end the current wait early
|
||||||
|
check_now_event = threading.Event()
|
||||||
|
|
||||||
|
def check_now(*_):
|
||||||
|
add_to_log("Checking now...")
|
||||||
|
check_now_event.set()
|
||||||
|
|
||||||
|
# Sleep for the given seconds, or until "Check now" is clicked
|
||||||
|
def wait_or_check_now(seconds):
|
||||||
|
return check_now_event.wait(seconds)
|
||||||
|
|
||||||
|
# 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
|
return False
|
||||||
|
if check_now_event.wait(min(NETWORK_POLL_SECONDS, remaining)):
|
||||||
|
return True # "Check now" was clicked
|
||||||
|
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
|
# Is connected to wifi
|
||||||
def is_connected_to_wifi():
|
def is_connected_to_wifi():
|
||||||
@@ -699,18 +910,25 @@ def update_window_geometry(event):
|
|||||||
x, y = event.x, event.y
|
x, y = event.x, event.y
|
||||||
width, height = event.width, event.height
|
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
|
# Function to show the log dialog with the currently selected profile name
|
||||||
def show_log_dialog():
|
def show_log_dialog():
|
||||||
global log_dialog, log_text, log_dialog_open, successful_logins_label
|
global log_dialog, log_text, log_dialog_open, successful_logins_label
|
||||||
if not log_dialog_open:
|
if not log_dialog_open:
|
||||||
log_dialog_open = True
|
log_dialog_open = True
|
||||||
if log_dialog:
|
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()
|
log_dialog.deiconify()
|
||||||
update_log()
|
update_log()
|
||||||
else:
|
else:
|
||||||
log_dialog = tk.Tk()
|
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_text = tk.Text(log_dialog)
|
||||||
log_frame = tk.Frame(log_dialog)
|
log_frame = tk.Frame(log_dialog)
|
||||||
log_frame.pack(fill=tk.BOTH, expand=True)
|
log_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
@@ -720,6 +938,21 @@ def show_log_dialog():
|
|||||||
# Create label for successful logins count
|
# Create label for successful logins count
|
||||||
successful_logins_label = tk.Label(log_dialog, text=f"Successful Logins: {successful_logins_count}", anchor="w", padx=5)
|
successful_logins_label = tk.Label(log_dialog, text=f"Successful Logins: {successful_logins_count}", anchor="w", padx=5)
|
||||||
successful_logins_label.place(x=0, rely=1.0, anchor=tk.SW, bordermode=tk.OUTSIDE)
|
successful_logins_label.place(x=0, rely=1.0, anchor=tk.SW, bordermode=tk.OUTSIDE)
|
||||||
|
# Create the "Check Now" button, to skip the current wait
|
||||||
|
check_now_button = tk.Button(
|
||||||
|
log_dialog,
|
||||||
|
text="Check Now",
|
||||||
|
command=check_now,
|
||||||
|
padx=5,
|
||||||
|
pady=0,
|
||||||
|
fg="white",
|
||||||
|
bg="#08872a",
|
||||||
|
activebackground="#076921",
|
||||||
|
activeforeground="white",
|
||||||
|
relief=tk.RAISED,
|
||||||
|
borderwidth=2,
|
||||||
|
)
|
||||||
|
check_now_button.place(relx=1.0, rely=1.0, x=-20, y=-2, anchor=tk.SE) # Left of the scrollbar area
|
||||||
# Define tags for different styles
|
# Define tags for different styles
|
||||||
log_text.tag_configure("green", foreground="green")
|
log_text.tag_configure("green", foreground="green")
|
||||||
log_text.tag_configure("red", foreground="red")
|
log_text.tag_configure("red", foreground="red")
|
||||||
@@ -769,6 +1002,8 @@ def add_to_log(message, style=None):
|
|||||||
# Function to update the log
|
# Function to update the log
|
||||||
def update_log():
|
def update_log():
|
||||||
if log_text:
|
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)
|
log_text.delete(1.0, tk.END)
|
||||||
for message, style in get_last_log_messages():
|
for message, style in get_last_log_messages():
|
||||||
if style:
|
if style:
|
||||||
@@ -794,11 +1029,67 @@ def save_to_file(message):
|
|||||||
with open("log.txt", "a", encoding="utf-8") as f:
|
with open("log.txt", "a", encoding="utf-8") as f:
|
||||||
f.write(formatted_message)
|
f.write(formatted_message)
|
||||||
|
|
||||||
|
# Session expiry tracking: many portals end the session a fixed time after login (e.g. 24 hours).
|
||||||
|
# If a profile has "session_hours", the time of the last successful login is remembered, and around the
|
||||||
|
# expected expiry the connection is checked every few seconds, so the program logs in again right away.
|
||||||
|
SESSION_STATE_FILE = "session_state.json"
|
||||||
|
SESSION_CHECK_BEFORE_SECONDS = 120 # Start checking quickly this long before the expected expiry
|
||||||
|
SESSION_CHECK_AFTER_SECONDS = 600 # Keep checking quickly this long after it (in case the portal's clock differs)
|
||||||
|
SESSION_FAST_CHECK_SECONDS = 5
|
||||||
|
|
||||||
|
def load_session_state():
|
||||||
|
try:
|
||||||
|
with open(SESSION_STATE_FILE, "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# Remember when the given profile last logged in successfully (survives restarts)
|
||||||
|
def save_last_login(profile_name):
|
||||||
|
state = load_session_state()
|
||||||
|
state[profile_name] = datetime.now().isoformat(timespec="seconds")
|
||||||
|
try:
|
||||||
|
with open(SESSION_STATE_FILE, "w") as f:
|
||||||
|
json.dump(state, f, indent=2)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Expected expiry of the current profile's session, or None if unknown
|
||||||
|
def get_session_expiry():
|
||||||
|
hours = selected_profile.get("session_hours") if selected_profile else None
|
||||||
|
if not hours:
|
||||||
|
return None
|
||||||
|
last_login = load_session_state().get(selected_profile["name"])
|
||||||
|
if not last_login:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(last_login) + timedelta(hours=float(hours))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Seconds until the next internet check while connected: the normal interval, shortened so that
|
||||||
|
# the check lands right before the expected session expiry, and every few seconds around it.
|
||||||
|
# Returns (seconds, expected_expiry or None, checking_quickly)
|
||||||
|
def get_check_interval():
|
||||||
|
expiry = get_session_expiry()
|
||||||
|
if expiry is None:
|
||||||
|
return check_every_second, None, False
|
||||||
|
now = datetime.now()
|
||||||
|
window_start = expiry - timedelta(seconds=SESSION_CHECK_BEFORE_SECONDS)
|
||||||
|
window_end = expiry + timedelta(seconds=SESSION_CHECK_AFTER_SECONDS)
|
||||||
|
if window_start <= now <= window_end:
|
||||||
|
return SESSION_FAST_CHECK_SECONDS, expiry, True
|
||||||
|
if now < window_start:
|
||||||
|
return max(1, min(check_every_second, int((window_start - now).total_seconds()) + 1)), expiry, False
|
||||||
|
# Still connected well after the expected expiry, so the session was renewed some other way
|
||||||
|
return check_every_second, None, False
|
||||||
|
|
||||||
# Create the system tray icon
|
# Create the system tray icon
|
||||||
def create_system_tray_icon():
|
def create_system_tray_icon():
|
||||||
image = Image.open("icon.ico")
|
image = Image.open("icon.ico")
|
||||||
menu = pystray.Menu(
|
menu = pystray.Menu(
|
||||||
pystray.MenuItem('Show Log', show_log_dialog, default=True),
|
pystray.MenuItem('Show Log', show_log_dialog, default=True),
|
||||||
|
pystray.MenuItem('Check Now', check_now),
|
||||||
pystray.MenuItem('Exit', exit_application)
|
pystray.MenuItem('Exit', exit_application)
|
||||||
)
|
)
|
||||||
icon = pystray.Icon("my_icon", image, "HotspotAutoLogin", menu)
|
icon = pystray.Icon("my_icon", image, "HotspotAutoLogin", menu)
|
||||||
@@ -811,9 +1102,54 @@ request_success = False
|
|||||||
request_errorcount = 0
|
request_errorcount = 0
|
||||||
sleepcount = check_every_second
|
sleepcount = check_every_second
|
||||||
connected_ssid_lower = None
|
connected_ssid_lower = None
|
||||||
|
last_auto_message = None
|
||||||
|
last_session_message = None
|
||||||
def check_network_status():
|
def check_network_status():
|
||||||
global running, errorcount, sleepcount, connected_ssid_lower, ssid, response, request_success, request_errorcount, successful_logins_count
|
global running, errorcount, sleepcount, connected_ssid_lower, ssid, response, request_success, request_errorcount, successful_logins_count, last_auto_message, last_session_message, selected_profile
|
||||||
while running and errorcount < 10:
|
# 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):
|
||||||
|
check_now_event.clear()
|
||||||
|
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()
|
connected_ssid = get_connected_network()
|
||||||
if isinstance(connected_ssid, str) and "error" in connected_ssid.lower():
|
if isinstance(connected_ssid, str) and "error" in connected_ssid.lower():
|
||||||
sleepcount = 60
|
sleepcount = 60
|
||||||
@@ -825,7 +1161,8 @@ def check_network_status():
|
|||||||
if (connected_ssid):
|
if (connected_ssid):
|
||||||
connected_ssid_lower = connected_ssid.lower()
|
connected_ssid_lower = connected_ssid.lower()
|
||||||
if (connected_ssid) and ((connected_ssid_lower == expected_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..."
|
message = "You are both connected to Wi-Fi and Ethernet. Disconnecting from Wi-Fi and continuing with Ethernet..."
|
||||||
add_to_log(message)
|
add_to_log(message)
|
||||||
startupinfo = subprocess.STARTUPINFO()
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
@@ -833,11 +1170,21 @@ def check_network_status():
|
|||||||
subprocess.check_output(['netsh', 'wlan', 'disconnect', 'interface=' + "Wi-Fi"], startupinfo=startupinfo).decode("utf-8")
|
subprocess.check_output(['netsh', 'wlan', 'disconnect', 'interface=' + "Wi-Fi"], startupinfo=startupinfo).decode("utf-8")
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
if (is_internet_available()):
|
if (is_internet_available()):
|
||||||
sleepcount = check_every_second
|
sleepcount, session_expiry, checking_quickly = get_check_interval()
|
||||||
request_success = False
|
request_success = False
|
||||||
request_errorcount = 0
|
request_errorcount = 0
|
||||||
message = "Connected to {} and internet connection is available. Checking again in {} seconds...".format(connected_ssid, str(sleepcount))
|
|
||||||
errorcount = 0
|
errorcount = 0
|
||||||
|
if checking_quickly:
|
||||||
|
# Log once instead of every few seconds, so the log isn't flooded
|
||||||
|
message = "Connected to {}. The session is expected to expire around {}, checking the connection every {} seconds...".format(connected_ssid, session_expiry.strftime("%H:%M:%S"), str(sleepcount))
|
||||||
|
if message != last_session_message:
|
||||||
|
add_to_log(message, "bold_green")
|
||||||
|
last_session_message = message
|
||||||
|
else:
|
||||||
|
last_session_message = None
|
||||||
|
message = "Connected to {} and internet connection is available. Checking again in {} seconds...".format(connected_ssid, str(sleepcount))
|
||||||
|
if session_expiry:
|
||||||
|
message += " (Session expires around {})".format(session_expiry.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
add_to_log(message, "bold_green")
|
add_to_log(message, "bold_green")
|
||||||
elif request_success and request_errorcount < 3:
|
elif request_success and request_errorcount < 3:
|
||||||
# If the request was successful but there is still no internet connection, wait for a few seconds and try again
|
# If the request was successful but there is still no internet connection, wait for a few seconds and try again
|
||||||
@@ -861,6 +1208,7 @@ def check_network_status():
|
|||||||
request_success = True
|
request_success = True
|
||||||
request_errorcount = 0
|
request_errorcount = 0
|
||||||
successful_logins_count += 1
|
successful_logins_count += 1
|
||||||
|
save_last_login(selected_profile['name'])
|
||||||
message = "Request was successful. Checking the internet connection in {} seconds...".format(str(sleepcount))
|
message = "Request was successful. Checking the internet connection in {} seconds...".format(str(sleepcount))
|
||||||
add_to_log(message, "green")
|
add_to_log(message, "green")
|
||||||
save_to_file(message)
|
save_to_file(message)
|
||||||
@@ -991,7 +1339,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)
|
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)
|
save_to_file(message)
|
||||||
add_to_log(message, "red")
|
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:
|
||||||
|
wait_or_check_now(sleepcount) # Sleep ... seconds before trying again
|
||||||
message = "Maximum error count reached. Exiting in 5 seconds..."
|
message = "Maximum error count reached. Exiting in 5 seconds..."
|
||||||
add_to_log(message, "bold_red")
|
add_to_log(message, "bold_red")
|
||||||
save_to_file(message)
|
save_to_file(message)
|
||||||
@@ -1003,7 +1354,8 @@ if __name__ == '__main__':
|
|||||||
# Create a thread for the system tray icon
|
# Create a thread for the system tray icon
|
||||||
tray_thread = threading.Thread(target=create_system_tray_icon)
|
tray_thread = threading.Thread(target=create_system_tray_icon)
|
||||||
tray_thread.start()
|
tray_thread.start()
|
||||||
# Open Log Messages at startup
|
# 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 = threading.Thread(target=show_log_dialog)
|
||||||
open_log_messages_startup.start()
|
open_log_messages_startup.start()
|
||||||
# Start the network status checking in the main thread
|
# Start the network status checking in the main thread
|
||||||
|
|||||||
Reference in New Issue
Block a user