Add Android forensics, IOC threat-intel DB, Compose companion; scrub secrets from configs
This commit is contained in:
531
launcher_windows.py
Normal file
531
launcher_windows.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""AUTARCH Desktop Launcher — Windows.
|
||||
|
||||
Tkinter-based launcher equivalent to launcher_linux.py. Uses stdlib only (Tkinter +
|
||||
ctypes). UAC ('runas') plays the role of pkexec/sudo for privileged operations.
|
||||
|
||||
Run unprivileged for the web UI; relaunch elevated via the Elevate toggle when an
|
||||
operation needs Administrator. The web app itself does not need elevation for most
|
||||
features — only daemon-style privileged actions do.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import ctypes
|
||||
import signal
|
||||
import socket
|
||||
import threading
|
||||
import subprocess
|
||||
import configparser
|
||||
import webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, scrolledtext
|
||||
|
||||
|
||||
DIR = Path(__file__).parent
|
||||
CONF = DIR / 'autarch_settings.conf'
|
||||
LOGO_PNG = DIR / 'assets' / 'logo.png'
|
||||
ICON_ICO = DIR / 'autarch.ico'
|
||||
SPLASH_FLAG = DIR / 'data' / '.splash_accepted'
|
||||
|
||||
_web_proc: 'subprocess.Popen | None' = None
|
||||
|
||||
|
||||
# ── Process helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
def get_py() -> str:
|
||||
"""Return the Python interpreter to use for child processes."""
|
||||
venv_py = DIR / 'venv' / 'Scripts' / 'python.exe'
|
||||
return str(venv_py) if venv_py.exists() else sys.executable
|
||||
|
||||
|
||||
def is_admin() -> bool:
|
||||
try:
|
||||
return bool(ctypes.windll.shell32.IsUserAnAdmin())
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def relaunch_elevated() -> None:
|
||||
"""Restart this launcher with Administrator rights via UAC."""
|
||||
params = ' '.join(f'"{a}"' for a in sys.argv)
|
||||
ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, params, str(DIR), 1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def is_web_running() -> bool:
|
||||
if _web_proc is not None and _web_proc.poll() is None:
|
||||
return True
|
||||
# Also check whether something is listening on the configured port
|
||||
try:
|
||||
cfg = _read_config()
|
||||
port = int(cfg.get('web', 'port', fallback='8181'))
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(0.3)
|
||||
return s.connect_ex(('127.0.0.1', port)) == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def start_web() -> bool:
|
||||
global _web_proc
|
||||
if is_web_running():
|
||||
return True
|
||||
py = get_py()
|
||||
# CREATE_NO_WINDOW (0x08000000) hides the console for the child
|
||||
flags = 0x08000000 if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
|
||||
_web_proc = subprocess.Popen(
|
||||
[py, str(DIR / 'autarch_web.py')],
|
||||
cwd=str(DIR),
|
||||
creationflags=flags,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def stop_web() -> None:
|
||||
global _web_proc
|
||||
if _web_proc is not None:
|
||||
try:
|
||||
_web_proc.terminate()
|
||||
_web_proc.wait(timeout=5)
|
||||
except Exception:
|
||||
try:
|
||||
_web_proc.kill()
|
||||
except Exception:
|
||||
pass
|
||||
_web_proc = None
|
||||
# Fallback: kill anything still bound to the configured port
|
||||
cfg = _read_config()
|
||||
port = cfg.get('web', 'port', fallback='8181')
|
||||
subprocess.run(['taskkill', '/F', '/IM', 'python.exe', '/FI', f'WINDOWTITLE eq AUTARCH*'],
|
||||
capture_output=True, shell=False)
|
||||
|
||||
|
||||
def open_dashboard() -> None:
|
||||
cfg = _read_config()
|
||||
host = cfg.get('web', 'host', fallback='127.0.0.1')
|
||||
if host in ('0.0.0.0', ''):
|
||||
host = '127.0.0.1'
|
||||
port = cfg.get('web', 'port', fallback='8181')
|
||||
scheme = 'https' if cfg.get('web', 'https', fallback='true').lower() != 'false' else 'http'
|
||||
webbrowser.open(f'{scheme}://{host}:{port}/')
|
||||
|
||||
|
||||
def _read_config() -> configparser.ConfigParser:
|
||||
cfg = configparser.ConfigParser()
|
||||
if CONF.exists():
|
||||
try:
|
||||
cfg.read(str(CONF))
|
||||
except Exception:
|
||||
pass
|
||||
return cfg
|
||||
|
||||
|
||||
# ── EULA / Privacy text (kept in sync with launcher_linux.py) ─────────────────
|
||||
|
||||
EULA_TEXT = """AUTARCH — Autonomous Tactical Agent for Reconnaissance, Counterintelligence, and Hacking
|
||||
By darkHal Security Group & Setec Security Labs
|
||||
|
||||
END USER LICENSE AGREEMENT
|
||||
|
||||
1. DISCLAIMER OF WARRANTY
|
||||
This software is provided "AS IS" without warranty of any kind, express or implied. The authors make no guarantees regarding functionality, reliability, or fitness for any particular purpose. Use at your own risk.
|
||||
|
||||
2. LIMITATION OF LIABILITY
|
||||
In no event shall darkHal Security Group, Setec Security Labs, or any contributors be held liable for any damages whatsoever arising from the use of this software, including but not limited to direct, indirect, incidental, special, or consequential damages.
|
||||
|
||||
3. AUTHORIZED USE ONLY
|
||||
This software is designed for authorized security testing and research. You are solely responsible for ensuring you have proper authorization before testing any system. Unauthorized access to computer systems is illegal.
|
||||
|
||||
4. LICENSE
|
||||
Unless otherwise noted on a module, tool, addon, or extension:
|
||||
- This software is FREE for personal/home use
|
||||
- This software is FREE to give away (unmodified) for home use
|
||||
- Commercial use is NOT permitted without a commercial license
|
||||
- Government use is PROHIBITED — however, a single-day user license may be purchased for the deed to half of the United States of America, or $10,000 USD per minute of use, whichever is greater
|
||||
|
||||
5. NO RESPONSIBILITY
|
||||
We are not responsible for what you do with this software. If you break the law, that's on you. If you get caught, that's also on you. We told you not to do it.
|
||||
"""
|
||||
|
||||
PRIVACY_TEXT = """PRIVACY POLICY & ACKNOWLEDGEMENTS
|
||||
|
||||
COOKIES & DATA COLLECTION
|
||||
AUTARCH does not collect, transmit, or sell any user data. All data stays on your machine. There are no analytics, telemetry, tracking pixels, or phone-home features. We don't want your data. We have enough of our own problems.
|
||||
|
||||
This policy complies with GDPR (EU), CCPA (California), PIPEDA (Canada), LGPD (Brazil), POPIA (South Africa), and every other privacy regulation because the answer to "what data do you collect?" is "none."
|
||||
|
||||
THIRD-PARTY ACKNOWLEDGEMENTS
|
||||
All rights reserved (c) Setec Security Labs 2020-2026.
|
||||
|
||||
AUTARCH is built with the help of outstanding open-source projects. We are NOT affiliated with any of the following organizations. All trademarks belong to their respective owners.
|
||||
|
||||
PLEASE SUPPORT THESE PROJECTS
|
||||
If you find AUTARCH useful, consider donating to the open-source projects that make it possible. They do the hard work. We just glued it together.
|
||||
"""
|
||||
|
||||
|
||||
# ── Theme ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
BG = '#14171f'
|
||||
PANEL = '#1d2230'
|
||||
TEXT = '#e4e6f0'
|
||||
DIM = '#8b8fa8'
|
||||
ACCENT = '#3df0c0'
|
||||
ACCENT_HOT = '#ff3d81'
|
||||
WARN = '#ffb13d'
|
||||
DANGER = '#ff4d57'
|
||||
|
||||
|
||||
def _style_root(win: tk.Tk | tk.Toplevel) -> None:
|
||||
win.configure(bg=BG)
|
||||
if ICON_ICO.exists():
|
||||
try:
|
||||
win.iconbitmap(str(ICON_ICO))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ── Splash ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class Splash:
|
||||
def __init__(self, on_accept):
|
||||
self.on_accept = on_accept
|
||||
self.root = tk.Tk()
|
||||
self.root.title("AUTARCH")
|
||||
self.root.geometry('640x720')
|
||||
self.root.resizable(False, False)
|
||||
_style_root(self.root)
|
||||
self._build_eula()
|
||||
self.root.protocol('WM_DELETE_WINDOW', self._reject)
|
||||
self.root.mainloop()
|
||||
|
||||
def _build_eula(self):
|
||||
for w in self.root.winfo_children():
|
||||
w.destroy()
|
||||
tk.Label(self.root, text='AUTARCH', font=('Segoe UI', 28, 'bold'),
|
||||
fg=ACCENT, bg=BG).pack(pady=(24, 4))
|
||||
tk.Label(self.root, text='Security Platform', fg=DIM, bg=BG,
|
||||
font=('Segoe UI', 10)).pack(pady=(0, 12))
|
||||
|
||||
txt = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, font=('Consolas', 9),
|
||||
bg=PANEL, fg=TEXT, insertbackground=TEXT,
|
||||
borderwidth=0, padx=12, pady=8, height=22)
|
||||
txt.pack(fill='both', expand=True, padx=20)
|
||||
txt.insert('1.0', EULA_TEXT)
|
||||
txt.configure(state='disabled')
|
||||
|
||||
bar = tk.Frame(self.root, bg=BG)
|
||||
bar.pack(fill='x', pady=14, padx=20)
|
||||
tk.Button(bar, text='Screw This Shit', command=self._reject,
|
||||
bg=PANEL, fg=DANGER, activebackground=PANEL, borderwidth=0,
|
||||
padx=14, pady=6).pack(side='left')
|
||||
tk.Button(bar, text='I Accept', command=self._show_privacy,
|
||||
bg=ACCENT, fg='#04110d', activebackground=ACCENT, borderwidth=0,
|
||||
padx=14, pady=6, font=('Segoe UI', 10, 'bold')).pack(side='right')
|
||||
|
||||
def _show_privacy(self):
|
||||
for w in self.root.winfo_children():
|
||||
w.destroy()
|
||||
tk.Label(self.root, text='Privacy & Acknowledgements',
|
||||
font=('Segoe UI', 18, 'bold'), fg=ACCENT, bg=BG).pack(pady=(24, 4))
|
||||
tk.Label(self.root, text='Scroll to the bottom to continue', fg=WARN, bg=BG,
|
||||
font=('Segoe UI', 9)).pack(pady=(0, 12))
|
||||
|
||||
txt = scrolledtext.ScrolledText(self.root, wrap=tk.WORD, font=('Consolas', 9),
|
||||
bg=PANEL, fg=TEXT, insertbackground=TEXT,
|
||||
borderwidth=0, padx=12, pady=8, height=22)
|
||||
txt.pack(fill='both', expand=True, padx=20)
|
||||
txt.insert('1.0', PRIVACY_TEXT)
|
||||
txt.configure(state='disabled')
|
||||
|
||||
bar = tk.Frame(self.root, bg=BG)
|
||||
bar.pack(fill='x', pady=14, padx=20)
|
||||
|
||||
next_btn = tk.Button(bar, text='Next', state='disabled',
|
||||
bg=ACCENT, fg='#04110d', activebackground=ACCENT,
|
||||
borderwidth=0, padx=14, pady=6,
|
||||
font=('Segoe UI', 10, 'bold'),
|
||||
command=self._accept)
|
||||
next_btn.pack(side='right')
|
||||
|
||||
def watch():
|
||||
try:
|
||||
if txt.yview()[1] >= 0.98:
|
||||
next_btn.configure(state='normal')
|
||||
else:
|
||||
self.root.after(250, watch)
|
||||
except tk.TclError:
|
||||
pass
|
||||
self.root.after(250, watch)
|
||||
|
||||
def _accept(self):
|
||||
SPLASH_FLAG.parent.mkdir(parents=True, exist_ok=True)
|
||||
SPLASH_FLAG.write_text('accepted', encoding='utf-8')
|
||||
self.root.destroy()
|
||||
self.on_accept()
|
||||
|
||||
def _reject(self):
|
||||
if messagebox.askyesno(
|
||||
'AUTARCH',
|
||||
"Really? You're going to pass on this masterpiece?\n\nQuit now?"):
|
||||
self.root.destroy()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# ── Main launcher window ──────────────────────────────────────────────────────
|
||||
|
||||
class LauncherWindow:
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title('AUTARCH Launcher')
|
||||
self.root.geometry('560x520')
|
||||
self.root.minsize(520, 480)
|
||||
_style_root(self.root)
|
||||
|
||||
self.cfg = _read_config()
|
||||
|
||||
self._build_header()
|
||||
self._build_controls()
|
||||
self._build_settings()
|
||||
self._build_footer()
|
||||
|
||||
self._refresh_status()
|
||||
self.root.protocol('WM_DELETE_WINDOW', self._on_quit)
|
||||
self.root.after(2500, self._tick_status)
|
||||
|
||||
def run(self):
|
||||
self.root.mainloop()
|
||||
|
||||
def _build_header(self):
|
||||
head = tk.Frame(self.root, bg=BG)
|
||||
head.pack(fill='x', padx=20, pady=(18, 8))
|
||||
tk.Label(head, text='AUTARCH', font=('Segoe UI', 22, 'bold'),
|
||||
fg=ACCENT, bg=BG).pack(side='left')
|
||||
tk.Label(head, text='Security Platform', font=('Segoe UI', 10),
|
||||
fg=DIM, bg=BG).pack(side='left', padx=(12, 0), pady=(8, 0))
|
||||
|
||||
admin_badge_text = 'ADMIN' if is_admin() else 'USER'
|
||||
admin_color = ACCENT_HOT if is_admin() else DIM
|
||||
self.admin_lbl = tk.Label(head, text=admin_badge_text,
|
||||
font=('Consolas', 9, 'bold'),
|
||||
fg=admin_color, bg=BG,
|
||||
borderwidth=1, relief='solid',
|
||||
padx=6, pady=2)
|
||||
self.admin_lbl.pack(side='right')
|
||||
|
||||
def _build_controls(self):
|
||||
wrap = tk.LabelFrame(self.root, text=' Service ', bg=BG, fg=DIM,
|
||||
font=('Segoe UI', 9), borderwidth=1, relief='solid',
|
||||
labelanchor='nw')
|
||||
wrap.pack(fill='x', padx=20, pady=10)
|
||||
|
||||
self.status_lbl = tk.Label(wrap, text='Stopped', font=('Consolas', 10),
|
||||
fg=DIM, bg=BG)
|
||||
self.status_lbl.pack(pady=(10, 6))
|
||||
|
||||
grid = tk.Frame(wrap, bg=BG)
|
||||
grid.pack(pady=(0, 12))
|
||||
|
||||
self.btn_start = tk.Button(grid, text='Start Web', width=14,
|
||||
command=self._on_start,
|
||||
bg=ACCENT, fg='#04110d', borderwidth=0,
|
||||
font=('Segoe UI', 10, 'bold'),
|
||||
activebackground=ACCENT)
|
||||
self.btn_start.grid(row=0, column=0, padx=4, pady=4)
|
||||
self.btn_stop = tk.Button(grid, text='Stop Web', width=14,
|
||||
command=self._on_stop,
|
||||
bg=PANEL, fg=DANGER, borderwidth=0,
|
||||
font=('Segoe UI', 10, 'bold'),
|
||||
activebackground=PANEL)
|
||||
self.btn_stop.grid(row=0, column=1, padx=4, pady=4)
|
||||
self.btn_open = tk.Button(grid, text='Open Dashboard', width=18,
|
||||
command=open_dashboard,
|
||||
bg=PANEL, fg=ACCENT, borderwidth=0,
|
||||
font=('Segoe UI', 10),
|
||||
activebackground=PANEL)
|
||||
self.btn_open.grid(row=0, column=2, padx=4, pady=4)
|
||||
|
||||
if not is_admin():
|
||||
tk.Button(grid, text='Elevate (UAC)', width=14,
|
||||
command=relaunch_elevated,
|
||||
bg=PANEL, fg=WARN, borderwidth=0,
|
||||
font=('Segoe UI', 9),
|
||||
activebackground=PANEL).grid(row=1, column=0, padx=4, pady=(2, 0))
|
||||
|
||||
def _build_settings(self):
|
||||
nb = ttk.Notebook(self.root)
|
||||
nb.pack(fill='both', expand=True, padx=20, pady=(0, 10))
|
||||
|
||||
# Web settings tab
|
||||
web = tk.Frame(nb, bg=BG)
|
||||
nb.add(web, text='Web Server')
|
||||
|
||||
rows = [('web', 'host', 'Listen Host', '0.0.0.0'),
|
||||
('web', 'port', 'Listen Port', '8181'),
|
||||
('web', 'mcp_port', 'MCP Port', '8081'),
|
||||
('revshell', 'port', 'Reverse Shell Port', '4444')]
|
||||
self.entries: dict[tuple[str, str], tk.Entry] = {}
|
||||
for i, (sec, key, label, default) in enumerate(rows):
|
||||
tk.Label(web, text=label, fg=TEXT, bg=BG,
|
||||
font=('Segoe UI', 10)).grid(row=i, column=0, sticky='w',
|
||||
padx=12, pady=6)
|
||||
e = tk.Entry(web, bg=PANEL, fg=TEXT, insertbackground=TEXT,
|
||||
borderwidth=0, relief='flat',
|
||||
font=('Consolas', 10))
|
||||
e.insert(0, self.cfg.get(sec, key, fallback=default))
|
||||
e.grid(row=i, column=1, sticky='ew', padx=12, pady=6)
|
||||
self.entries[(sec, key)] = e
|
||||
|
||||
web.grid_columnconfigure(1, weight=1)
|
||||
|
||||
tk.Button(web, text='Save Settings', command=self._save_settings,
|
||||
bg=ACCENT, fg='#04110d', borderwidth=0,
|
||||
font=('Segoe UI', 9, 'bold'),
|
||||
activebackground=ACCENT).grid(row=len(rows), column=0, columnspan=2,
|
||||
pady=12)
|
||||
|
||||
# Logs tab
|
||||
logs = tk.Frame(nb, bg=BG)
|
||||
nb.add(logs, text='Logs')
|
||||
|
||||
self.log_view = scrolledtext.ScrolledText(logs, wrap=tk.WORD,
|
||||
font=('Consolas', 9),
|
||||
bg=PANEL, fg=TEXT,
|
||||
insertbackground=TEXT,
|
||||
borderwidth=0,
|
||||
padx=8, pady=8, height=14)
|
||||
self.log_view.pack(fill='both', expand=True, padx=8, pady=8)
|
||||
self.log_view.configure(state='disabled')
|
||||
|
||||
ctl = tk.Frame(logs, bg=BG)
|
||||
ctl.pack(fill='x', padx=8, pady=(0, 8))
|
||||
tk.Button(ctl, text='Refresh', command=self._refresh_logs,
|
||||
bg=PANEL, fg=TEXT, borderwidth=0,
|
||||
font=('Segoe UI', 9),
|
||||
activebackground=PANEL).pack(side='left')
|
||||
tk.Button(ctl, text='Open Log Folder', command=self._open_log_folder,
|
||||
bg=PANEL, fg=TEXT, borderwidth=0,
|
||||
font=('Segoe UI', 9),
|
||||
activebackground=PANEL).pack(side='left', padx=6)
|
||||
|
||||
# Info tab
|
||||
info = tk.Frame(nb, bg=BG)
|
||||
nb.add(info, text='Info')
|
||||
info_text = (
|
||||
f'AUTARCH Windows Launcher\n'
|
||||
f'Project: {DIR}\n'
|
||||
f'Python: {sys.executable}\n'
|
||||
f'Admin: {"yes" if is_admin() else "no"}\n\n'
|
||||
f'Linux equivalent: launcher_linux.py (GTK3 + pkexec daemon).\n'
|
||||
f'On Windows, privileged operations elevate per-process via UAC.'
|
||||
)
|
||||
tk.Label(info, text=info_text, justify='left', fg=DIM, bg=BG,
|
||||
font=('Consolas', 9)).pack(padx=14, pady=14, anchor='nw')
|
||||
|
||||
def _build_footer(self):
|
||||
foot = tk.Frame(self.root, bg=BG)
|
||||
foot.pack(fill='x', padx=20, pady=(0, 14))
|
||||
tk.Label(foot, text='ANARCHY IS LIFE / LIFE IS ANARCHY',
|
||||
font=('Consolas', 8), fg=DIM, bg=BG).pack(side='left')
|
||||
tk.Button(foot, text='Quit', command=self._on_quit,
|
||||
bg=PANEL, fg=TEXT, borderwidth=0,
|
||||
font=('Segoe UI', 9),
|
||||
activebackground=PANEL).pack(side='right')
|
||||
|
||||
def _save_settings(self):
|
||||
for (sec, key), entry in self.entries.items():
|
||||
if not self.cfg.has_section(sec):
|
||||
self.cfg.add_section(sec)
|
||||
self.cfg.set(sec, key, entry.get())
|
||||
with open(CONF, 'w', encoding='utf-8') as f:
|
||||
self.cfg.write(f)
|
||||
messagebox.showinfo('AUTARCH', 'Settings saved.')
|
||||
|
||||
def _refresh_logs(self):
|
||||
log_dir = DIR / 'data' / 'logs'
|
||||
latest = None
|
||||
if log_dir.exists():
|
||||
try:
|
||||
logs = sorted(log_dir.glob('*.log'),
|
||||
key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if logs:
|
||||
latest = logs[0]
|
||||
except Exception:
|
||||
pass
|
||||
self.log_view.configure(state='normal')
|
||||
self.log_view.delete('1.0', 'end')
|
||||
if latest:
|
||||
try:
|
||||
self.log_view.insert('1.0', latest.read_text(encoding='utf-8',
|
||||
errors='replace'))
|
||||
self.log_view.see('end')
|
||||
except Exception as e:
|
||||
self.log_view.insert('1.0', f'(failed to read log: {e})')
|
||||
else:
|
||||
self.log_view.insert('1.0', '(no logs yet — start the web server first)')
|
||||
self.log_view.configure(state='disabled')
|
||||
|
||||
def _open_log_folder(self):
|
||||
log_dir = DIR / 'data' / 'logs'
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
os.startfile(str(log_dir))
|
||||
|
||||
def _refresh_status(self):
|
||||
running = is_web_running()
|
||||
if running:
|
||||
self.status_lbl.configure(text='● Web server running',
|
||||
fg=ACCENT)
|
||||
else:
|
||||
self.status_lbl.configure(text='○ Stopped', fg=DIM)
|
||||
|
||||
def _tick_status(self):
|
||||
self._refresh_status()
|
||||
self.root.after(2500, self._tick_status)
|
||||
|
||||
def _on_start(self, *_):
|
||||
self.status_lbl.configure(text='Starting...', fg=WARN)
|
||||
|
||||
def go():
|
||||
start_web()
|
||||
self.root.after(400, self._refresh_status)
|
||||
threading.Thread(target=go, daemon=True).start()
|
||||
|
||||
def _on_stop(self, *_):
|
||||
self.status_lbl.configure(text='Stopping...', fg=WARN)
|
||||
|
||||
def go():
|
||||
stop_web()
|
||||
self.root.after(400, self._refresh_status)
|
||||
threading.Thread(target=go, daemon=True).start()
|
||||
|
||||
def _on_quit(self, *_):
|
||||
if is_web_running():
|
||||
if not messagebox.askyesno(
|
||||
'AUTARCH',
|
||||
'The web server is still running. Stop it and quit?'):
|
||||
return
|
||||
stop_web()
|
||||
try:
|
||||
self.root.destroy()
|
||||
finally:
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# ── Entry point ───────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
def launch():
|
||||
LauncherWindow().run()
|
||||
|
||||
if SPLASH_FLAG.exists():
|
||||
launch()
|
||||
else:
|
||||
Splash(on_accept=launch)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user