-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlauncher.py
More file actions
335 lines (299 loc) · 14.6 KB
/
launcher.py
File metadata and controls
335 lines (299 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
import os
import sys
import json
import subprocess
import threading
import platform
import webbrowser
import urllib.parse
import shutil
import pkgutil
from tkinter import messagebox
import tkinter as tk
from tkinter import ttk, messagebox
def _base_dir():
if getattr(sys, 'frozen', False):
# When frozen (PyInstaller), executables live in this directory
return os.path.dirname(sys.executable)
# Running from source
return os.path.dirname(os.path.abspath(__file__))
BASE_DIR = _base_dir()
SERVER_CONFIG_PATH = os.path.join(BASE_DIR, 'server_config.json')
DEFAULT_SERVER_CFG = {
'bind': '0.0.0.0',
'port': 8765,
'token': 'CHANGE_ME_TOKEN',
'mjpeg_fps': 6,
'backup_retention': 5,
}
def load_server_config():
cfg = DEFAULT_SERVER_CFG.copy()
try:
if os.path.exists(SERVER_CONFIG_PATH):
with open(SERVER_CONFIG_PATH, 'r', encoding='utf-8') as f:
user_cfg = json.load(f)
if isinstance(user_cfg, dict):
cfg.update(user_cfg)
except Exception:
pass
return cfg
def save_server_config(cfg):
try:
with open(SERVER_CONFIG_PATH, 'w', encoding='utf-8') as f:
json.dump(cfg, f, indent=2)
return True
except Exception as e:
messagebox.showerror('Error', f'Failed to save server_config.json: {e}')
return False
def start_process(args):
try:
creationflags = 0
if platform.system() == 'Windows':
# Open new console window for each process (Windows only)
creationflags = subprocess.CREATE_NEW_CONSOLE
proc = subprocess.Popen(args, cwd=BASE_DIR, creationflags=creationflags)
return proc
except Exception as e:
messagebox.showerror('Error', f'Failed to launch: {" ".join(args)}\n\n{e}')
return None
class LauncherApp(tk.Tk):
def __init__(self):
super().__init__()
self.title('Image Detection Bot Launcher')
self.geometry('520x420')
self.resizable(False, False)
# In dev, use current Python. In frozen, avoid using sys.executable (points to this EXE).
self.is_frozen = getattr(sys, 'frozen', False)
if self.is_frozen:
# Try to locate a system Python for non-bundled fallbacks (rare). Prefer py launcher.
py_cmd = shutil.which('py')
if py_cmd:
self.python_exe = py_cmd
else:
self.python_exe = shutil.which('python') or 'python'
else:
self.python_exe = sys.executable
self.server_cfg = load_server_config()
self.var_launch_gui = tk.BooleanVar(value=True)
self.var_launch_web = tk.BooleanVar(value=False)
self.var_use_last = tk.BooleanVar(value=True)
self.proc_web = None
self.proc_gui = None
self._build_ui()
self._refresh_fields()
def _build_ui(self):
frm = ttk.Frame(self, padding=12)
frm.pack(fill=tk.BOTH, expand=True)
# Header
ttk.Label(frm, text='Choose components to start:').grid(row=0, column=0, sticky='w')
ttk.Checkbutton(frm, text='Launch GUI', variable=self.var_launch_gui).grid(row=1, column=0, sticky='w')
ttk.Checkbutton(frm, text='Launch Web Server', variable=self.var_launch_web, command=self._refresh_fields).grid(row=2, column=0, sticky='w')
ttk.Separator(frm).grid(row=3, column=0, sticky='ew', pady=(10, 10))
# Server settings section
self.grp = ttk.LabelFrame(frm, text='Web Server Settings')
self.grp.grid(row=4, column=0, sticky='ew')
# Use last vs edit
ttk.Radiobutton(self.grp, text='Use last settings (server_config.json)', value=True, variable=self.var_use_last, command=self._refresh_fields).grid(row=0, column=0, columnspan=2, sticky='w', pady=(6, 2))
ttk.Radiobutton(self.grp, text='Edit and save new settings', value=False, variable=self.var_use_last, command=self._refresh_fields).grid(row=1, column=0, columnspan=2, sticky='w')
# Fields
self.entry_bind = self._add_field(self.grp, 'Bind', self.server_cfg.get('bind'))
self.entry_port = self._add_field(self.grp, 'Port', str(self.server_cfg.get('port')))
self.entry_token = self._add_field(self.grp, 'Token', self.server_cfg.get('token'))
self.entry_fps = self._add_field(self.grp, 'MJPEG FPS', str(self.server_cfg.get('mjpeg_fps')))
self.entry_ret = self._add_field(self.grp, 'Backup Retention', str(self.server_cfg.get('backup_retention')))
# Spacer
ttk.Separator(frm).grid(row=5, column=0, sticky='ew', pady=(10, 10))
# Buttons
btns = ttk.Frame(frm)
btns.grid(row=6, column=0, sticky='ew')
ttk.Button(btns, text='Start', command=self.on_start).pack(side=tk.LEFT)
ttk.Button(btns, text='Open Web Portal', command=self.on_open_portal).pack(side=tk.LEFT, padx=(8,0))
ttk.Button(btns, text='Stop GUI', command=self.on_stop_gui).pack(side=tk.LEFT, padx=(8,0))
ttk.Button(btns, text='Stop Web Server', command=self.on_stop_web).pack(side=tk.LEFT, padx=(8,0))
ttk.Button(btns, text='Exit', command=self.destroy).pack(side=tk.RIGHT)
# Status
self.lbl_status = ttk.Label(frm, text='Ready.')
self.lbl_status.grid(row=7, column=0, sticky='w', pady=(12, 0))
def _add_field(self, parent, label, value):
row = len(parent.grid_slaves()) + 1
ttk.Label(parent, text=label).grid(row=row, column=0, sticky='w', padx=(6, 6), pady=(4, 2))
entry = ttk.Entry(parent)
entry.insert(0, value if value is not None else '')
entry.grid(row=row, column=1, sticky='ew', pady=(4, 2))
parent.columnconfigure(1, weight=1)
return entry
def _refresh_fields(self):
# Enable/disable server config fields based on choices
editing = self.var_launch_web.get() and (not self.var_use_last.get())
state = 'normal' if editing else 'disabled'
for entry in (self.entry_bind, self.entry_port, self.entry_token, self.entry_fps, self.entry_ret):
try:
entry.configure(state=state)
except Exception:
pass
def on_start(self):
# Optionally save server config
if self.var_launch_web.get():
if not self.var_use_last.get():
try:
cfg = {
'bind': self.entry_bind.get().strip() or DEFAULT_SERVER_CFG['bind'],
'port': int(self.entry_port.get().strip() or DEFAULT_SERVER_CFG['port']),
'token': self.entry_token.get().strip() or DEFAULT_SERVER_CFG['token'],
'mjpeg_fps': int(self.entry_fps.get().strip() or DEFAULT_SERVER_CFG['mjpeg_fps']),
'backup_retention': int(self.entry_ret.get().strip() or DEFAULT_SERVER_CFG['backup_retention']),
}
except Exception:
messagebox.showerror('Error', 'Please enter valid numeric values for Port / MJPEG FPS / Backup Retention.')
return
if not save_server_config(cfg):
return
# Launch selected components in background threads
if not (self.var_launch_web.get() or self.var_launch_gui.get()):
messagebox.showinfo('Info', 'No components selected.')
return
def run_all():
ok_all = True
try:
# Preflight: if running from source (not frozen) or bundled EXEs missing, check deps
need_web = bool(self.var_launch_web.get())
need_gui = bool(self.var_launch_gui.get())
exe_web = os.path.join(BASE_DIR, 'WebServer.exe')
exe_gui = os.path.join(BASE_DIR, 'ImageDetectionBot.exe')
if (need_web and not os.path.exists(exe_web)) or (need_gui and not os.path.exists(exe_gui)):
if not self.check_and_install_deps(need_web=need_web, need_gui=need_gui):
self.lbl_status.configure(text='Launch cancelled (dependencies missing).')
return
# Prefer built executables if present; fall back to Python scripts
if self.var_launch_web.get():
if os.path.exists(exe_web):
self.proc_web = start_process([exe_web])
else:
if self.is_frozen:
messagebox.showerror('Error', 'Bundled WebServer.exe not found. Please rebuild or run from source.')
ok_all = False
else:
self.proc_web = start_process([self.python_exe, 'web_server.py'])
ok_all = ok_all and (self.proc_web is not None)
if self.var_launch_gui.get():
if os.path.exists(exe_gui):
self.proc_gui = start_process([exe_gui])
else:
if self.is_frozen:
messagebox.showerror('Error', 'Bundled ImageDetectionBot.exe not found. Please rebuild or run from source.')
ok_all = False
else:
self.proc_gui = start_process([self.python_exe, 'bot_gui.py'])
ok_all = ok_all and (self.proc_gui is not None)
finally:
self.lbl_status.configure(text=('Launched successfully.' if ok_all else 'Launched with errors.'))
# Run launch flow in a background thread so UI stays responsive
threading.Thread(target=run_all, daemon=True).start()
def check_and_install_deps(self, need_web: bool = True, need_gui: bool = True) -> bool:
"""Check required Python packages and offer to install them automatically.
Returns True if deps are satisfied or successfully installed; False otherwise.
"""
# When running bundled EXEs, deps are included; skip checks in that case
if self.is_frozen:
return True
required = set()
if need_gui:
required.update(['PyQt6', 'pyautogui', 'opencv-python', 'numpy', 'Pillow', 'darkdetect'])
if need_web:
# mss is optional but recommended for MJPEG capture; pyautogui used by server controls
required.update(['mss', 'pyautogui'])
missing = []
for pkg in sorted(required):
try:
__import__(pkg if pkg != 'opencv-python' else 'cv2')
except Exception:
missing.append(pkg)
if not missing:
return True
msg = 'Missing packages:\n- ' + '\n- '.join(missing) + '\n\nInstall them now?'
if not messagebox.askyesno('Install Dependencies', msg):
return False
# Ensure pip exists
try:
subprocess.run([self.python_exe, '-m', 'pip', '--version'], cwd=BASE_DIR, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except Exception:
try:
subprocess.run([self.python_exe, '-m', 'ensurepip', '--upgrade'], cwd=BASE_DIR, check=True)
except Exception as e:
messagebox.showerror('Error', f'pip is not available and ensurepip failed: {e}')
return False
# Prefer requirements.txt if present
req_path = os.path.join(BASE_DIR, 'requirements.txt')
try:
if os.path.isfile(req_path):
proc = subprocess.run([self.python_exe, '-m', 'pip', 'install', '-r', 'requirements.txt'], cwd=BASE_DIR)
if proc.returncode != 0:
raise RuntimeError('pip install -r requirements.txt failed')
else:
# Install missing individually
proc = subprocess.run([self.python_exe, '-m', 'pip', 'install'] + missing, cwd=BASE_DIR)
if proc.returncode != 0:
raise RuntimeError('pip install missing packages failed')
except Exception as e:
messagebox.showerror('Error', f'Automatic install failed: {e}')
return False
messagebox.showinfo('Done', 'Dependencies installed successfully.')
return True
def on_open_portal(self):
# Open the web dashboard in the default browser; prefer localhost for browser access
cfg = load_server_config()
port = cfg.get('port', 8765)
tok = (cfg.get('token') or '').strip()
q = f'?token={urllib.parse.quote(tok)}' if tok else ''
url = f'http://localhost:{port}/{q}'
try:
webbrowser.open_new_tab(url)
self.lbl_status.configure(text=f'Opened {url}')
except Exception as e:
messagebox.showerror('Error', f'Failed to open web portal: {e}')
def on_stop_gui(self):
try:
# First try via process handle
if self.proc_gui and (self.proc_gui.poll() is None):
self.proc_gui.terminate()
try:
self.proc_gui.wait(timeout=3)
except Exception:
pass
# If still running or no handle, fallback to image name kill (Windows)
self.proc_gui = None
if platform.system() == 'Windows':
exe_gui = os.path.join(BASE_DIR, 'ImageDetectionBot.exe')
img = 'ImageDetectionBot.exe'
if os.path.exists(os.path.join(BASE_DIR, 'ImageDetectionBotConsole.exe')):
img = 'ImageDetectionBotConsole.exe'
try:
subprocess.run(['taskkill', '/IM', img, '/F'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
# best effort
pass
self.lbl_status.configure(text='GUI stopped.')
except Exception as e:
messagebox.showerror('Error', f'Failed to stop GUI: {e}')
def on_stop_web(self):
try:
# First try via process handle
if self.proc_web and (self.proc_web.poll() is None):
self.proc_web.terminate()
try:
self.proc_web.wait(timeout=3)
except Exception:
pass
self.proc_web = None
# Fallback: kill by image name on Windows
if platform.system() == 'Windows':
try:
subprocess.run(['taskkill', '/IM', 'WebServer.exe', '/F'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except Exception:
pass
self.lbl_status.configure(text='Web server stopped.')
except Exception as e:
messagebox.showerror('Error', f'Failed to stop web server: {e}')
if __name__ == '__main__':
app = LauncherApp()
app.mainloop()