computer-use-ootb-internal 0.0.119__py3-none-any.whl → 0.0.121__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ import win32con
22
22
  import psutil # For process/user info
23
23
  from flask import Flask, request, jsonify # For embedded server
24
24
  from waitress import serve # For serving Flask app
25
+ import json # Needed for status reporting
25
26
 
26
27
  # --- Configuration ---
27
28
  _SERVICE_NAME = "OOTBGuardService"
@@ -86,68 +87,158 @@ def receive_command():
86
87
  return jsonify({"message": f"Command {command_id} received and queued"}), 202 # Accepted
87
88
 
88
89
  # --- Helper Functions --- Only logging helpers needed adjustments
89
- def get_python_executable():
90
- python_exe = sys.executable
91
- if " " in python_exe and not python_exe.startswith('"'):
92
- python_exe = f'"{python_exe}"'
93
- return python_exe
94
-
95
- def get_pip_executable():
96
- """Tries to locate the pip executable in the same environment."""
97
- try:
98
- current_python = sys.executable
99
- log_info(f"get_pip_executable: sys.executable = {current_python}")
100
- python_path = pathlib.Path(current_python)
101
- # Common location is ../Scripts/pip.exe relative to python.exe
102
- pip_path = python_path.parent / "Scripts" / "pip.exe"
103
- log_info(f"get_pip_executable: Checking for pip at {pip_path}")
104
-
105
- if pip_path.exists():
106
- log_info(f"get_pip_executable: pip.exe found at {pip_path}")
107
- # Quote if necessary
108
- pip_exe = str(pip_path)
109
- if " " in pip_exe and not pip_exe.startswith('"'):
110
- pip_exe = f'"{pip_exe}"'
111
- return pip_exe
112
- else:
113
- log_error(f"get_pip_executable: pip.exe NOT found at {pip_path}. Falling back to 'python -m pip'.")
114
- # Fallback is intended here
115
- pass # Explicitly pass to reach the fallback return outside the else
116
-
117
- except Exception as e:
118
- log_error(f"get_pip_executable: Error determining pip path: {e}", exc_info=True)
119
- log_error("get_pip_executable: Falling back to 'python -m pip' due to error.")
120
-
121
- # Fallback return statement if 'exists' is false or an exception occurred
122
- return f"{get_python_executable()} -m pip"
123
-
124
- def log_info(msg):
125
- thread_name = threading.current_thread().name
126
- full_msg = f"[{thread_name}] {msg}"
127
- logging.info(full_msg)
128
- try:
129
- # Only log to event log from main service thread or known non-daemon threads if possible
130
- # Trying from waitress/flask threads might cause issues.
131
- # For simplicity, maybe remove event log integration or make it conditional.
132
- if threading.current_thread().name in ["MainThread", "CommandProcessor"]: # Example condition
133
- servicemanager.LogInfoMsg(str(full_msg))
134
- except Exception as e:
135
- logging.warning(f"Could not write info to Windows Event Log: {e}")
136
-
137
- def log_error(msg, exc_info=False):
138
- thread_name = threading.current_thread().name
139
- full_msg = f"[{thread_name}] {msg}"
140
- logging.error(full_msg, exc_info=exc_info)
141
- try:
142
- if threading.current_thread().name in ["MainThread", "CommandProcessor"]:
143
- servicemanager.LogErrorMsg(str(full_msg))
144
- except Exception as e:
145
- logging.warning(f"Could not write error to Windows Event Log: {e}")
90
+ # Move these inside the class later
91
+ # def get_python_executable(): ...
92
+ # def get_pip_executable(): ...
93
+
94
+ # Define loggers at module level for use before instance exists?
95
+ # Or handle carefully within instance methods.
96
+
97
+ # --- PowerShell Task Scheduler Helpers --- (These will become methods) ---
98
+
99
+ # _TASK_NAME_PREFIX = "OOTB_UserLogon_" # Move to class
100
+
101
+ # def run_powershell_command(command, log_output=True): ...
102
+ # def create_or_update_logon_task(username, task_command, python_executable): ...
103
+ # def remove_logon_task(username): ...
104
+
105
+ # --- End PowerShell Task Scheduler Helpers ---
146
106
 
147
107
  class GuardService(win32serviceutil.ServiceFramework):
148
108
  _svc_name_ = _SERVICE_NAME
149
109
  _svc_display_name_ = _SERVICE_DISPLAY_NAME
150
110
  _svc_description_ = _SERVICE_DESCRIPTION
111
+ _task_name_prefix = "OOTB_UserLogon_" # Class attribute for task prefix
112
+
113
+ # --- Instance Logging Methods ---
114
+ def log_info(self, msg):
115
+ thread_name = threading.current_thread().name
116
+ full_msg = f"[{thread_name}] {msg}"
117
+ logging.info(full_msg)
118
+ try:
119
+ if threading.current_thread().name in ["MainThread", "CommandProcessor"]:
120
+ servicemanager.LogInfoMsg(str(full_msg))
121
+ except Exception as e:
122
+ # Log only to file if event log fails
123
+ logging.warning(f"(Instance) Could not write info to Windows Event Log: {e}")
124
+
125
+ def log_error(self, msg, exc_info=False):
126
+ thread_name = threading.current_thread().name
127
+ full_msg = f"[{thread_name}] {msg}"
128
+ logging.error(full_msg, exc_info=exc_info)
129
+ try:
130
+ if threading.current_thread().name in ["MainThread", "CommandProcessor"]:
131
+ servicemanager.LogErrorMsg(str(full_msg))
132
+ except Exception as e:
133
+ logging.warning(f"(Instance) Could not write error to Windows Event Log: {e}")
134
+ # --- End Instance Logging ---
135
+
136
+ # --- Instance Helper Methods (Moved from module level) ---
137
+ def get_python_executable(self):
138
+ python_exe = sys.executable
139
+ if " " in python_exe and not python_exe.startswith('"'):
140
+ python_exe = f'"{python_exe}"'
141
+ return python_exe
142
+
143
+ def get_pip_executable(self):
144
+ """Tries to locate the pip executable in the same environment."""
145
+ try:
146
+ current_python = sys.executable
147
+ self.log_info(f"get_pip_executable: sys.executable = {current_python}")
148
+ python_path = pathlib.Path(current_python)
149
+ pip_path = python_path.parent / "Scripts" / "pip.exe"
150
+ self.log_info(f"get_pip_executable: Checking for pip at {pip_path}")
151
+
152
+ if pip_path.exists():
153
+ self.log_info(f"get_pip_executable: pip.exe found at {pip_path}")
154
+ pip_exe = str(pip_path)
155
+ if " " in pip_exe and not pip_exe.startswith('"'):
156
+ pip_exe = f'"{pip_exe}"'
157
+ return pip_exe
158
+ else:
159
+ self.log_error(f"get_pip_executable: pip.exe NOT found at {pip_path}. Falling back to 'python -m pip'.")
160
+ pass
161
+
162
+ except Exception as e:
163
+ self.log_error(f"get_pip_executable: Error determining pip path: {e}", exc_info=True)
164
+ self.log_error("get_pip_executable: Falling back to 'python -m pip' due to error.")
165
+
166
+ return f"{self.get_python_executable()} -m pip"
167
+
168
+ # --- PowerShell Methods (Moved from module level) ---
169
+ def run_powershell_command(self, command, log_output=True):
170
+ """Executes a PowerShell command and handles output/errors. Returns True on success."""
171
+ self.log_info(f"Executing PowerShell: {command}")
172
+ try:
173
+ result = subprocess.run(
174
+ ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command],
175
+ capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore'
176
+ )
177
+ if log_output and result.stdout:
178
+ self.log_info(f"PowerShell STDOUT:\n{result.stdout.strip()}")
179
+ if log_output and result.stderr:
180
+ self.log_info(f"PowerShell STDERR:\n{result.stderr.strip()}")
181
+ return True
182
+ except FileNotFoundError:
183
+ self.log_error("'powershell.exe' not found. Cannot manage scheduled tasks.")
184
+ return False
185
+ except subprocess.CalledProcessError as e:
186
+ self.log_error(f"PowerShell command failed (Exit Code {e.returncode}):")
187
+ self.log_error(f" Command: {e.cmd}")
188
+ if e.stdout: self.log_error(f" STDOUT: {e.stdout.strip()}")
189
+ if e.stderr: self.log_error(f" STDERR: {e.stderr.strip()}")
190
+ return False
191
+ except Exception as e:
192
+ self.log_error(f"Unexpected error running PowerShell: {e}", exc_info=True)
193
+ return False
194
+
195
+ def create_or_update_logon_task(self, username, task_command, python_executable):
196
+ """Creates or updates a scheduled task to run a command at user logon."""
197
+ task_name = f"{self._task_name_prefix}{username}"
198
+ safe_python_exe = python_executable.replace("'", "''")
199
+ command_parts = task_command.split(' ', 1)
200
+ if len(command_parts) > 1 and command_parts[0] == python_executable:
201
+ safe_task_command_args = command_parts[1].replace("'", "''")
202
+ else:
203
+ safe_task_command_args = task_command.replace(python_executable, "").strip().replace("'", "''")
204
+
205
+ safe_task_name = task_name.replace("'", "''")
206
+ safe_username = username.replace("'", "''")
207
+
208
+ action = f"$Action = New-ScheduledTaskAction -Execute '{safe_python_exe}' -Argument '{safe_task_command_args}'"
209
+ trigger = f"$Trigger = New-ScheduledTaskTrigger -AtLogOn -User '{safe_username}'"
210
+ principal = f"$Principal = New-ScheduledTaskPrincipal -UserId '{safe_username}' -LogonType Interactive"
211
+ settings = "$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -ExecutionTimeLimit ([System.TimeSpan]::Zero) -RunOnlyIfNetworkAvailable:$false"
212
+
213
+ command = f"""
214
+ try {{
215
+ {action}
216
+ {trigger}
217
+ {principal}
218
+ {settings}
219
+ Register-ScheduledTask -TaskName '{safe_task_name}' -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings -Force -ErrorAction Stop
220
+ Write-Host "Scheduled task '{safe_task_name}' registered/updated successfully."
221
+ }} catch {{
222
+ Write-Error "Failed to register/update scheduled task '{safe_task_name}': $_"
223
+ exit 1
224
+ }}
225
+ """
226
+ success = self.run_powershell_command(command)
227
+ if success:
228
+ self.log_info(f"Successfully created/updated scheduled task '{task_name}' for user '{username}'.")
229
+ else:
230
+ self.log_error(f"Failed to create/update scheduled task '{task_name}' for user '{username}'.")
231
+ return success
232
+
233
+ def remove_logon_task(self, username):
234
+ """Removes the logon scheduled task for a user."""
235
+ task_name = f"{self._task_name_prefix}{username}"
236
+ safe_task_name = task_name.replace("'", "''")
237
+ command = f"Unregister-ScheduledTask -TaskName '{safe_task_name}' -Confirm:$false -ErrorAction SilentlyContinue"
238
+ self.run_powershell_command(command, log_output=False)
239
+ self.log_info(f"Attempted removal of scheduled task '{task_name}' for user '{username}'.")
240
+ return True
241
+ # --- End Instance Helper Methods ---
151
242
 
152
243
  def __init__(self, args):
153
244
  global _service_instance
@@ -157,15 +248,16 @@ class GuardService(win32serviceutil.ServiceFramework):
157
248
  self.server_thread = None
158
249
  self.command_queue = queue.Queue()
159
250
  self.command_processor_thread = None
160
- self.session = requests.Session() # For status reporting
251
+ self.session = requests.Session()
161
252
 
162
- self.python_exe = get_python_executable()
163
- self.pip_command_base = get_pip_executable()
253
+ # Initialize paths using instance methods
254
+ self.python_exe = self.get_python_executable()
255
+ self.pip_command_base = self.get_pip_executable()
164
256
  self.ootb_command = f"{self.python_exe} -m {_OOTB_MODULE}"
165
- _service_instance = self # Set global reference
257
+ _service_instance = self
166
258
 
167
259
  def SvcStop(self):
168
- log_info(f"Service stop requested.")
260
+ self.log_info(f"Service stop requested.")
169
261
  self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
170
262
  self.is_running = False
171
263
  # Signal the command processor thread to stop
@@ -174,7 +266,7 @@ class GuardService(win32serviceutil.ServiceFramework):
174
266
  win32event.SetEvent(self.hWaitStop)
175
267
  # Stopping waitress gracefully from another thread is non-trivial.
176
268
  # We rely on the SCM timeout / process termination for now.
177
- log_info(f"{_SERVICE_NAME} SvcStop: Stop signaled. Server thread will be terminated by SCM.")
269
+ self.log_info(f"{_SERVICE_NAME} SvcStop: Stop signaled. Server thread will be terminated by SCM.")
178
270
 
179
271
 
180
272
  def SvcDoRun(self):
@@ -182,51 +274,51 @@ class GuardService(win32serviceutil.ServiceFramework):
182
274
  servicemanager.PYS_SERVICE_STARTED,
183
275
  (self._svc_name_, ''))
184
276
  try:
185
- log_info(f"{_SERVICE_NAME} starting.")
277
+ self.log_info(f"{_SERVICE_NAME} starting.")
186
278
  # Start the command processor thread
187
279
  self.command_processor_thread = threading.Thread(
188
280
  target=self.process_commands, name="CommandProcessor", daemon=True)
189
281
  self.command_processor_thread.start()
190
- log_info("Command processor thread started.")
282
+ self.log_info("Command processor thread started.")
191
283
 
192
284
  # Start the Flask server (via Waitress) in a separate thread
193
285
  self.server_thread = threading.Thread(
194
286
  target=self.run_server, name="WebServerThread", daemon=True)
195
287
  self.server_thread.start()
196
- log_info(f"Web server thread started, listening on {_LISTEN_HOST}:{_LISTEN_PORT}.")
288
+ self.log_info(f"Web server thread started, listening on {_LISTEN_HOST}:{_LISTEN_PORT}.")
197
289
 
198
- log_info(f"{_SERVICE_NAME} running. Waiting for stop signal.")
290
+ self.log_info(f"{_SERVICE_NAME} running. Waiting for stop signal.")
199
291
  # Keep the main service thread alive waiting for stop signal
200
292
  win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
201
- log_info(f"{_SERVICE_NAME} received stop signal in main thread.")
293
+ self.log_info(f"{_SERVICE_NAME} received stop signal in main thread.")
202
294
 
203
295
  except Exception as e:
204
- log_error(f"Fatal error in SvcDoRun: {e}", exc_info=True)
296
+ self.log_error(f"Fatal error in SvcDoRun: {e}", exc_info=True)
205
297
  self.SvcStop() # Signal stop if possible
206
298
  finally:
207
- log_info(f"{_SERVICE_NAME} SvcDoRun finished.")
299
+ self.log_info(f"{_SERVICE_NAME} SvcDoRun finished.")
208
300
 
209
301
 
210
302
  def run_server(self):
211
303
  """Runs the Flask app using Waitress."""
212
- log_info(f"Waitress server starting on {_LISTEN_HOST}:{_LISTEN_PORT}")
304
+ self.log_info(f"Waitress server starting on {_LISTEN_HOST}:{_LISTEN_PORT}")
213
305
  try:
214
306
  serve(flask_app, host=_LISTEN_HOST, port=_LISTEN_PORT, threads=4)
215
- log_info("Waitress server has stopped.") # Should only happen on shutdown
307
+ self.log_info("Waitress server has stopped.") # Should only happen on shutdown
216
308
  except Exception as e:
217
- log_error(f"Web server thread encountered an error: {e}", exc_info=True)
309
+ self.log_error(f"Web server thread encountered an error: {e}", exc_info=True)
218
310
  # Consider signaling the main thread to stop if the web server fails critically
219
311
  # For now, just log the error.
220
312
 
221
313
 
222
314
  def process_commands(self):
223
315
  """Worker thread to process commands from the queue."""
224
- log_info("Command processor thread starting.")
316
+ self.log_info("Command processor thread starting.")
225
317
  while self.is_running:
226
318
  try:
227
319
  item = self.command_queue.get(block=True, timeout=1) # Add timeout to check is_running periodically
228
320
  if item is None:
229
- log_info("Command processor received stop signal.")
321
+ self.log_info("Command processor received stop signal.")
230
322
  break # Exit loop
231
323
 
232
324
  command_id, command = item
@@ -234,7 +326,7 @@ class GuardService(win32serviceutil.ServiceFramework):
234
326
  target = command.get("target_user", "all_active")
235
327
  status = "failed_unknown" # Default
236
328
 
237
- log_info(f"Dequeued Command ID {command_id}: action='{action}', target='{target}'")
329
+ self.log_info(f"Dequeued Command ID {command_id}: action='{action}', target='{target}'")
238
330
 
239
331
  try:
240
332
  if action == "update":
@@ -244,10 +336,10 @@ class GuardService(win32serviceutil.ServiceFramework):
244
336
  elif action == "start_ootb":
245
337
  status = self.handle_start(target)
246
338
  else:
247
- log_error(f"Unknown action in queue: {action}")
339
+ self.log_error(f"Unknown action in queue: {action}")
248
340
  status = "failed_unknown_action"
249
341
  except Exception as handler_ex:
250
- log_error(f"Exception processing Command ID {command_id} ({action}): {handler_ex}", exc_info=True)
342
+ self.log_error(f"Exception processing Command ID {command_id} ({action}): {handler_ex}", exc_info=True)
251
343
  status = "failed_exception"
252
344
  finally:
253
345
  self.report_command_status(command_id, status)
@@ -257,17 +349,17 @@ class GuardService(win32serviceutil.ServiceFramework):
257
349
  # Timeout occurred, just loop again and check self.is_running
258
350
  continue
259
351
  except Exception as e:
260
- log_error(f"Error in command processing loop: {e}", exc_info=True)
352
+ self.log_error(f"Error in command processing loop: {e}", exc_info=True)
261
353
  if self.is_running:
262
354
  time.sleep(5)
263
355
 
264
- log_info("Command processor thread finished.")
356
+ self.log_info("Command processor thread finished.")
265
357
 
266
358
 
267
359
  def report_command_status(self, command_id, status, details=""):
268
360
  """Sends command status back to the server."""
269
361
  if not _SERVER_STATUS_REPORT_URL:
270
- log_error("No server status report URL configured. Skipping report.")
362
+ self.log_error("No server status report URL configured. Skipping report.")
271
363
  return
272
364
 
273
365
  payload = {
@@ -276,41 +368,40 @@ class GuardService(win32serviceutil.ServiceFramework):
276
368
  "details": details,
277
369
  "machine_id": os.getenv('COMPUTERNAME', 'unknown_guard')
278
370
  }
279
- log_info(f"Reporting status for command {command_id}: {status}")
371
+ self.log_info(f"Reporting status for command {command_id}: {status}")
280
372
  try:
281
373
  response = self.session.post(_SERVER_STATUS_REPORT_URL, json=payload, timeout=15)
282
374
  response.raise_for_status()
283
- log_info(f"Status report for command {command_id} accepted by server.")
375
+ self.log_info(f"Status report for command {command_id} accepted by server.")
284
376
  except requests.exceptions.RequestException as e:
285
- log_error(f"Failed to report status for command {command_id}: {e}")
377
+ self.log_error(f"Failed to report status for command {command_id}: {e}")
286
378
  except Exception as e:
287
- log_error(f"Unexpected error reporting status for command {command_id}: {e}", exc_info=True)
379
+ self.log_error(f"Unexpected error reporting status for command {command_id}: {e}", exc_info=True)
288
380
 
289
- # --- Command Handlers --- Copying full implementation from previous version
381
+ # --- Command Handlers --- Now call self. for helpers
290
382
 
291
383
  def handle_update(self):
292
- log_info("Executing OOTB update...")
384
+ self.log_info("Executing OOTB update...")
293
385
  if not self.pip_command_base:
294
- log_error("Cannot update: pip command not found.")
386
+ self.log_error("Cannot update: pip command not found.")
295
387
  return "failed_pip_not_found"
296
388
 
297
389
  update_command = f"{self.pip_command_base} install --upgrade --no-cache-dir {_PACKAGE_NAME}"
298
- log_info(f"Running update command: {update_command}")
390
+ self.log_info(f"Running update command: {update_command}")
299
391
  try:
300
392
  result = subprocess.run(update_command, shell=True, capture_output=True, text=True, check=True, timeout=300, encoding='utf-8')
301
- log_info(f"Update successful: \nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
393
+ self.log_info(f"Update successful: \nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
302
394
  return "success"
303
395
  except subprocess.CalledProcessError as e:
304
- log_error(f"Update failed (Exit Code {e.returncode}):\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}")
396
+ self.log_error(f"Update failed (Exit Code {e.returncode}):\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}")
305
397
  return f"failed_exit_{e.returncode}"
306
398
  except subprocess.TimeoutExpired:
307
- log_error(f"Update command timed out.")
399
+ self.log_error(f"Update command timed out.")
308
400
  return "failed_timeout"
309
401
  except Exception as e:
310
- log_error(f"Unexpected error during update: {e}", exc_info=True)
402
+ self.log_error(f"Unexpected error during update: {e}", exc_info=True)
311
403
  return "failed_exception"
312
404
 
313
-
314
405
  def _get_ootb_processes(self, target_user="all_active"):
315
406
  ootb_procs = []
316
407
  target_pid_list = []
@@ -323,7 +414,7 @@ class GuardService(win32serviceutil.ServiceFramework):
323
414
  else:
324
415
  target_users.add(target_user.lower())
325
416
 
326
- log_info(f"Searching for OOTB processes for users: {target_users}")
417
+ self.log_info(f"Searching for OOTB processes for users: {target_users}")
327
418
 
328
419
  python_exe_path = self.python_exe.strip('"') # Get unquoted path for comparison
329
420
 
@@ -338,167 +429,325 @@ class GuardService(win32serviceutil.ServiceFramework):
338
429
  cmdline = ' '.join(pinfo['cmdline']) if pinfo['cmdline'] else ''
339
430
  # Check if the process executable matches our python path AND module is in cmdline
340
431
  if pinfo['exe'] and pinfo['exe'] == python_exe_path and _OOTB_MODULE in cmdline:
341
- log_info(f"Found matching OOTB process: PID={pinfo['pid']}, User={pinfo['username']}, Cmd={cmdline}")
432
+ self.log_info(f"Found matching OOTB process: PID={pinfo['pid']}, User={pinfo['username']}, Cmd={cmdline}")
342
433
  ootb_procs.append(proc)
343
434
  target_pid_list.append(pinfo['pid'])
344
435
 
345
436
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
346
437
  continue
347
- log_info(f"Found {len(ootb_procs)} OOTB process(es) matching criteria: {target_pid_list}")
438
+ self.log_info(f"Found {len(ootb_procs)} OOTB process(es) matching criteria: {target_pid_list}")
348
439
  except Exception as e:
349
- log_error(f"Error enumerating processes: {e}", exc_info=True)
440
+ self.log_error(f"Error enumerating processes: {e}", exc_info=True)
350
441
  return ootb_procs
351
442
 
352
-
353
443
  def handle_stop(self, target_user="all_active"):
354
- log_info(f"Executing stop OOTB for target '{target_user}'...")
355
- stopped_count = 0
356
- procs_to_stop = self._get_ootb_processes(target_user)
444
+ self.log_info(f"Executing stop OOTB for target '{target_user}'...")
445
+ stop_results = {} # Track results per user {username: (task_status, immediate_status)}
446
+ failed_users = set()
357
447
 
358
- if not procs_to_stop:
359
- log_info("No running OOTB processes found for target.")
360
- return "no_process_found"
361
-
362
- for proc in procs_to_stop:
448
+ try:
449
+ # --- Get target users and active sessions ---
450
+ active_sessions = {} # user_lower: session_id
451
+ # No need for all_system_users for stop, we only care about active or the specific target
363
452
  try:
364
- username = proc.info.get('username', 'unknown_user')
365
- log_info(f"Terminating process PID={proc.pid}, User={username}")
366
- proc.terminate()
367
- try:
368
- proc.wait(timeout=3)
369
- log_info(f"Process PID={proc.pid} terminated successfully.")
370
- stopped_count += 1
371
- except psutil.TimeoutExpired:
372
- log_error(f"Process PID={proc.pid} did not terminate gracefully, killing.")
373
- proc.kill()
374
- stopped_count += 1
375
- except psutil.NoSuchProcess:
376
- log_info(f"Process PID={proc.pid} already terminated.")
377
- stopped_count +=1
378
- except psutil.AccessDenied:
379
- log_error(f"Access denied trying to terminate process PID={proc.pid}. Service might lack privileges?")
380
- except Exception as e:
381
- log_error(f"Error stopping process PID={proc.pid}: {e}", exc_info=True)
453
+ sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
454
+ for session in sessions:
455
+ if session['State'] == win32ts.WTSActive:
456
+ try:
457
+ user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
458
+ if user:
459
+ active_sessions[user.lower()] = session['SessionId']
460
+ except Exception as query_err:
461
+ self.log_error(f"Could not query session {session['SessionId']} during stop: {query_err}")
462
+ except Exception as user_enum_err:
463
+ self.log_error(f"Error enumerating users/sessions during stop: {user_enum_err}", exc_info=True)
464
+ return "failed_user_enumeration"
465
+
466
+ self.log_info(f"Stop target: '{target_user}'. Active sessions: {active_sessions}")
467
+
468
+ target_users_normalized = set()
469
+ if target_user == "all_active":
470
+ # Target only currently active users for stop all
471
+ target_users_normalized = set(active_sessions.keys())
472
+ self.log_info(f"Stop targeting all active users: {target_users_normalized}")
473
+ else:
474
+ # Target the specific user, regardless of active status (for task removal)
475
+ normalized_target = target_user.lower()
476
+ target_users_normalized.add(normalized_target)
477
+ self.log_info(f"Stop targeting specific user: {normalized_target}")
382
478
 
383
- log_info(f"Finished stopping OOTB. Terminated {stopped_count} process(es).")
384
- return f"success_stopped_{stopped_count}"
479
+ if not target_users_normalized:
480
+ self.log_info("No target users identified for stop.")
481
+ return "failed_no_target_users" # Or success if none were targeted?
385
482
 
483
+ # --- Process each target user ---
484
+ for user in target_users_normalized:
485
+ task_removed_status = "task_unknown"
486
+ immediate_stop_status = "stop_not_attempted"
487
+ stopped_count = 0
386
488
 
387
- def handle_start(self, target_user="all_active"):
388
- log_info(f"Executing start OOTB for target '{target_user}'...")
389
- started_count = 0
390
- target_users_started = set()
391
- users_failed_to_start = set()
489
+ self.log_info(f"Processing stop for user '{user}'...")
392
490
 
393
- try:
394
- sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
395
- active_sessions = {}
491
+ # 1. Always try to remove the scheduled task
492
+ try:
493
+ # remove_logon_task always returns True for now, just logs attempt
494
+ self.remove_logon_task(user)
495
+ task_removed_status = "task_removed_attempted"
496
+ except Exception as task_err:
497
+ self.log_error(f"Exception removing scheduled task for {user}: {task_err}", exc_info=True)
498
+ task_removed_status = "task_exception"
499
+ failed_users.add(user)
500
+ # Continue to try and stop process if active
501
+
502
+ # 2. If user is active, try to terminate process
503
+ is_active = user in active_sessions
504
+
505
+ if is_active:
506
+ immediate_stop_status = "stop_attempted"
507
+ self.log_info(f"User '{user}' is active. Attempting to terminate OOTB process(es)...")
508
+ # Pass the specific username to _get_ootb_processes
509
+ procs_to_stop = self._get_ootb_processes(user)
510
+
511
+ if not procs_to_stop:
512
+ self.log_info(f"No running OOTB processes found for active user '{user}'.")
513
+ immediate_stop_status = "stop_skipped_not_running"
514
+ else:
515
+ self.log_info(f"Found {len(procs_to_stop)} process(es) for user '{user}' to stop.")
516
+ for proc in procs_to_stop:
517
+ try:
518
+ pid = proc.pid # Get pid before potential termination
519
+ username = proc.info.get('username', 'unknown_user')
520
+ self.log_info(f"Terminating process PID={pid}, User={username}")
521
+ proc.terminate()
522
+ try:
523
+ proc.wait(timeout=3)
524
+ self.log_info(f"Process PID={pid} terminated successfully.")
525
+ stopped_count += 1
526
+ except psutil.TimeoutExpired:
527
+ self.log_error(f"Process PID={pid} did not terminate gracefully, killing.")
528
+ proc.kill()
529
+ stopped_count += 1
530
+ except psutil.NoSuchProcess:
531
+ self.log_info(f"Process PID={pid} already terminated.")
532
+ # Don't increment stopped_count here as we didn't stop it now
533
+ except psutil.AccessDenied:
534
+ self.log_error(f"Access denied trying to terminate process PID={pid}.")
535
+ failed_users.add(user) # Mark user as failed if stop fails
536
+ except Exception as e:
537
+ self.log_error(f"Error stopping process PID={pid}: {e}", exc_info=True)
538
+ failed_users.add(user) # Mark user as failed
539
+
540
+ # Determine status based on how many were found vs stopped
541
+ if user in failed_users:
542
+ immediate_stop_status = f"stop_errors_terminated_{stopped_count}_of_{len(procs_to_stop)}"
543
+ elif stopped_count == len(procs_to_stop):
544
+ immediate_stop_status = f"stop_success_terminated_{stopped_count}"
545
+ else: # Should ideally not happen if NoSuchProcess doesn't count
546
+ immediate_stop_status = f"stop_partial_terminated_{stopped_count}_of_{len(procs_to_stop)}"
547
+
548
+ else: # User not active
549
+ self.log_info(f"User '{user}' is not active. Skipping immediate process stop (task removal attempted).")
550
+ immediate_stop_status = "stop_skipped_inactive"
551
+
552
+ # Record final results for this user
553
+ stop_results[user] = (task_removed_status, immediate_stop_status)
554
+
555
+
556
+ # --- Consolidate status ---
557
+ total_processed = len(target_users_normalized)
558
+ final_status = "partial_success" if failed_users else "success"
559
+ if not stop_results: final_status = "no_targets_processed"
560
+ if len(failed_users) == total_processed and total_processed > 0 : final_status = "failed"
561
+
562
+ self.log_info(f"Finished stopping OOTB. Overall Status: {final_status}. Results: {stop_results}")
563
+ try:
564
+ details = json.dumps(stop_results)
565
+ except Exception:
566
+ details = str(stop_results) # Fallback
567
+ return f"{final_status}::{details}" # Use :: as separator
568
+
569
+ except Exception as e:
570
+ self.log_error(f"Error during combined stop OOTB process: {e}", exc_info=True)
571
+ return "failed_exception"
396
572
 
397
- for session in sessions:
398
- if session['State'] == win32ts.WTSActive:
399
- try:
400
- user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
401
- if user:
402
- normalized_user = user.lower()
403
- active_sessions[normalized_user] = session['SessionId']
404
- except Exception as query_err:
405
- log_error(f"Could not query session {session['SessionId']}: {query_err}")
406
573
 
407
- log_info(f"Found active user sessions: {active_sessions}")
574
+ def handle_start(self, target_user="all_active"):
575
+ self.log_info(f"Executing start OOTB for target '{target_user}'...")
576
+ start_results = {} # Track results per user {username: (task_status, immediate_status)}
577
+ failed_users = set()
408
578
 
409
- target_session_map = {}
579
+ try:
580
+ # --- Get target users and active sessions ---
581
+ active_sessions = {} # user_lower: session_id
582
+ all_system_users = set() # user_lower
583
+ try:
584
+ # Use psutil for system user list, WTS for active sessions/IDs
585
+ for user_session in psutil.users():
586
+ username_lower = user_session.name.split('\\')[-1].lower()
587
+ all_system_users.add(username_lower)
588
+
589
+ sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
590
+ for session in sessions:
591
+ if session['State'] == win32ts.WTSActive:
592
+ try:
593
+ user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
594
+ if user:
595
+ active_sessions[user.lower()] = session['SessionId']
596
+ except Exception as query_err:
597
+ self.log_error(f"Could not query session {session['SessionId']}: {query_err}")
598
+ except Exception as user_enum_err:
599
+ self.log_error(f"Error enumerating users/sessions: {user_enum_err}", exc_info=True)
600
+ return "failed_user_enumeration"
601
+
602
+ self.log_info(f"Found active user sessions: {active_sessions}")
603
+
604
+ target_users_normalized = set()
410
605
  if target_user == "all_active":
411
- target_session_map = active_sessions
606
+ # If targeting all_active, only target those CURRENTLY active
607
+ target_users_normalized = set(active_sessions.keys())
608
+ self.log_info(f"Targeting all active users: {target_users_normalized}")
412
609
  else:
413
- normalized_target = target_user.lower()
414
- if normalized_target in active_sessions:
415
- target_session_map[normalized_target] = active_sessions[normalized_target]
416
- else:
417
- log_error(f"Target user '{target_user}' not found in active sessions.")
418
- return "failed_user_not_active"
610
+ normalized_target = target_user.lower()
611
+ # Check if the target user actually exists on the system, even if inactive
612
+ # This check might be complex/unreliable. Rely on task scheduler potentially failing?
613
+ # Let's assume admin provides a valid username for specific targeting.
614
+ # if normalized_target in all_system_users: # Removing this check, assume valid user input
615
+ target_users_normalized.add(normalized_target)
616
+ self.log_info(f"Targeting specific user: {normalized_target}")
617
+ # else:
618
+ # log_error(f"Target user '{target_user}' does not appear to exist on this system based on psutil.")
619
+ # return "failed_user_does_not_exist"
620
+
621
+ if not target_users_normalized:
622
+ self.log_info("No target users identified (or none active for 'all_active').")
623
+ # If target was specific user but they weren't found active, still try task?
624
+ # Let's proceed to task creation anyway for specific user case.
625
+ if target_user != "all_active": target_users_normalized.add(target_user.lower())
626
+ if not target_users_normalized:
627
+ return "failed_no_target_users"
628
+
629
+ # --- Check existing processes ---
630
+ # This check is only relevant for immediate start attempt
631
+ running_procs_by_user = {} # user_lower: count
632
+ try:
633
+ current_running = self._get_ootb_processes("all_active") # Check all
634
+ for proc in current_running:
635
+ try:
636
+ proc_username = proc.info.get('username')
637
+ if proc_username:
638
+ user_lower = proc_username.split('\\')[-1].lower()
639
+ running_procs_by_user[user_lower] = running_procs_by_user.get(user_lower, 0) + 1
640
+ except Exception: pass
641
+ except Exception as e:
642
+ self.log_error(f"Error checking existing processes: {e}")
643
+ self.log_info(f"Users currently running OOTB: {running_procs_by_user}")
419
644
 
420
- if not target_session_map:
421
- log_info("No target user sessions found to start OOTB in.")
422
- return "failed_no_target_sessions"
645
+ # --- Process each target user ---
646
+ for user in target_users_normalized:
647
+ task_created_status = "task_unknown"
648
+ immediate_start_status = "start_not_attempted"
649
+ token = None # Ensure token is reset/defined
423
650
 
424
- running_procs = self._get_ootb_processes(target_user)
425
- users_already_running = set()
426
- for proc in running_procs:
427
- try:
428
- proc_username = proc.info.get('username')
429
- if proc_username:
430
- users_already_running.add(proc_username.split('\\')[-1].lower())
431
- except Exception:
432
- pass
651
+ self.log_info(f"Processing start for user '{user}'...")
433
652
 
434
- log_info(f"Users already running OOTB: {users_already_running}")
653
+ # 1. Always try to create/update the scheduled task
654
+ try:
655
+ task_created = self.create_or_update_logon_task(user, self.ootb_command, self.python_exe)
656
+ task_created_status = "task_success" if task_created else "task_failed"
657
+ except Exception as task_err:
658
+ self.log_error(f"Exception creating/updating scheduled task for {user}: {task_err}", exc_info=True)
659
+ task_created_status = "task_exception"
660
+ failed_users.add(user)
661
+ # Continue to potentially try immediate start IF user is active?
662
+ # Or maybe skip if task creation failed badly?
663
+ # Let's skip immediate start if task creation had exception.
664
+ start_results[user] = (task_created_status, immediate_start_status)
665
+ continue
666
+
667
+ # 2. If user is active AND not already running, try immediate start
668
+ is_active = user in active_sessions
669
+ is_running = running_procs_by_user.get(user, 0) > 0
670
+
671
+ if is_active:
672
+ if not is_running:
673
+ immediate_start_status = "start_attempted"
674
+ self.log_info(f"User '{user}' is active and not running OOTB. Attempting immediate start...")
675
+ try:
676
+ session_id = active_sessions[user]
677
+ token = win32ts.WTSQueryUserToken(session_id)
678
+ env = win32profile.CreateEnvironmentBlock(token, False)
679
+ startup = win32process.STARTUPINFO()
680
+ startup.dwFlags = win32process.STARTF_USESHOWWINDOW
681
+ startup.wShowWindow = win32con.SW_HIDE
682
+ creation_flags = win32process.CREATE_NEW_CONSOLE | win32process.CREATE_UNICODE_ENVIRONMENT
683
+ user_profile_dir = win32profile.GetUserProfileDirectory(token)
684
+
685
+ hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
686
+ token, self.python_exe, self.ootb_command,
687
+ None, None, False, creation_flags, env, user_profile_dir, startup
688
+ )
689
+ self.log_info(f"CreateProcessAsUser call succeeded for user '{user}' (PID: {dwPid}). Checking existence...")
690
+ win32api.CloseHandle(hProcess)
691
+ win32api.CloseHandle(hThread)
692
+
693
+ time.sleep(1)
694
+ if psutil.pid_exists(dwPid):
695
+ self.log_info(f"Immediate start succeeded for user '{user}' (PID {dwPid}).")
696
+ immediate_start_status = "start_success"
697
+ else:
698
+ self.log_error(f"Immediate start failed for user '{user}': Process {dwPid} exited immediately.")
699
+ immediate_start_status = "start_failed_exited"
700
+ failed_users.add(user)
701
+
702
+ except Exception as proc_err:
703
+ self.log_error(f"Exception during immediate start for user '{user}': {proc_err}", exc_info=True)
704
+ immediate_start_status = "start_failed_exception"
705
+ failed_users.add(user)
706
+ finally:
707
+ if token:
708
+ try: win32api.CloseHandle(token)
709
+ except: pass
710
+ else: # User is active but already running
711
+ self.log_info(f"User '{user}' is active but OOTB is already running. Skipping immediate start.")
712
+ immediate_start_status = "start_skipped_already_running"
713
+ else: # User is not active
714
+ self.log_info(f"User '{user}' is not active. Skipping immediate start (task created/updated).")
715
+ immediate_start_status = "start_skipped_inactive"
716
+
717
+ # Record final results for this user
718
+ start_results[user] = (task_created_status, immediate_start_status)
719
+
720
+
721
+ # --- Consolidate status ---
722
+ total_processed = len(target_users_normalized)
723
+ final_status = "partial_success" if failed_users else "success"
724
+ if not start_results: final_status = "no_targets_processed"
725
+ # If all processed users failed in some way (either task or start)
726
+ if len(failed_users) == total_processed and total_processed > 0: final_status = "failed"
727
+ # Special case: target was specific user who wasn't found active
728
+ elif total_processed == 1 and target_user != "all_active" and target_user.lower() not in active_sessions:
729
+ user_key = target_user.lower()
730
+ if user_key in start_results and start_results[user_key][0] == "task_success":
731
+ final_status = "success_task_only_user_inactive"
732
+ else:
733
+ final_status = "failed_task_user_inactive"
435
734
 
436
- for user, session_id in target_session_map.items():
437
- token = None
438
- try:
439
- if user in users_already_running:
440
- log_info(f"OOTB already seems to be running for user '{user}'. Skipping start.")
441
- continue
442
-
443
- log_info(f"Attempting to start OOTB for user '{user}' in session {session_id}...")
444
- token = win32ts.WTSQueryUserToken(session_id)
445
- env = win32profile.CreateEnvironmentBlock(token, False)
446
- startup = win32process.STARTUPINFO()
447
- # Simplify startup flags: Run hidden, don't explicitly set desktop
448
- startup.dwFlags = win32process.STARTF_USESHOWWINDOW
449
- startup.wShowWindow = win32con.SW_HIDE
450
- # startup.lpDesktop = 'winsta0\\default' # Removed
451
-
452
- creation_flags = win32process.CREATE_NEW_CONSOLE | win32process.CREATE_UNICODE_ENVIRONMENT
453
- # Define cwd as user's profile directory if possible
454
- user_profile_dir = win32profile.GetUserProfileDirectory(token)
455
-
456
- hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
457
- token, self.python_exe, self.ootb_command,
458
- None, None, False, creation_flags, env,
459
- user_profile_dir, # Set current directory
460
- startup
461
- )
462
- log_info(f"CreateProcessAsUser call succeeded for user '{user}' (PID: {dwPid}).")
463
-
464
- # Add post-start check
465
- time.sleep(1) # Small delay
466
- if psutil.pid_exists(dwPid):
467
- log_info(f"Process PID {dwPid} confirmed to exist shortly after creation.")
468
- started_count += 1
469
- target_users_started.add(user)
470
- else:
471
- log_error(f"Process PID {dwPid} reported by CreateProcessAsUser does NOT exist shortly after creation. It likely exited immediately.")
472
- users_failed_to_start.add(user)
473
- # Attempt to get exit code? Difficult without waiting.
474
-
475
- win32api.CloseHandle(hProcess)
476
- win32api.CloseHandle(hThread)
477
-
478
- except Exception as proc_err:
479
- log_error(f"Failed to start OOTB for user '{user}' in session {session_id}: {proc_err}", exc_info=True)
480
- users_failed_to_start.add(user)
481
- finally:
482
- if token:
483
- try: win32api.CloseHandle(token)
484
- except: pass
485
-
486
- log_info(f"Finished starting OOTB. Started {started_count} new instance(s). Failed for users: {users_failed_to_start or 'None'}")
487
- if users_failed_to_start:
488
- return f"partial_success_started_{started_count}_failed_for_{len(users_failed_to_start)}"
489
- elif started_count > 0:
490
- return f"success_started_{started_count}"
491
- else:
492
- return "no_action_needed_already_running"
735
+ self.log_info(f"Finished starting OOTB. Overall Status: {final_status}. Results: {start_results}")
736
+ # Return detailed results as a JSON string for easier parsing/logging server-side
737
+ try:
738
+ details = json.dumps(start_results)
739
+ except Exception:
740
+ details = str(start_results) # Fallback
741
+ return f"{final_status}::{details}"
493
742
 
494
743
  except Exception as e:
495
- log_error(f"Error during start OOTB process: {e}", exc_info=True)
744
+ self.log_error(f"Error during combined start OOTB process: {e}", exc_info=True)
496
745
  return "failed_exception"
497
746
 
498
747
  # --- Main Execution Block ---
499
748
  if __name__ == '__main__':
500
749
  if len(sys.argv) > 1 and sys.argv[1] == 'debug':
501
- log_info("Starting service in debug mode...")
750
+ self.log_info("Starting service in debug mode...")
502
751
  print(f"Running Flask server via Waitress on {_LISTEN_HOST}:{_LISTEN_PORT} for debugging...")
503
752
  print("Service logic (command processing) will NOT run in this mode.")
504
753
  print("Use this primarily to test the '/command' endpoint receiving POSTs.")
@@ -10,6 +10,7 @@ import time
10
10
  # Constants need to match guard_service.py
11
11
  _SERVICE_NAME = "OOTBGuardService"
12
12
  _SERVICE_DISPLAY_NAME = "OOTB Guard Service"
13
+ _TASK_NAME_PREFIX = "OOTB_UserLogon_" # Must match guard_service.py
13
14
 
14
15
  def is_admin():
15
16
  """Check if the script is running with administrative privileges."""
@@ -104,6 +105,41 @@ def run_service_command(command_args, check_errors=True):
104
105
  print(f"An unexpected error occurred running service command: {e}", file=sys.stderr)
105
106
  return False
106
107
 
108
+ # --- Add cleanup helpers ---
109
+ def _run_powershell_cleanup_command(command):
110
+ """Executes a PowerShell command specifically for cleanup, ignoring most errors."""
111
+ if platform.system() != "Windows": return True # Skip on non-windows
112
+ print(f"Executing PowerShell Cleanup: {command}")
113
+ try:
114
+ # Use check=False, don't capture output unless needed for debug
115
+ subprocess.run(
116
+ ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command],
117
+ check=False, # Don't throw error if command fails (e.g., no tasks found)
118
+ stdout=subprocess.DEVNULL, # Suppress stdout
119
+ stderr=subprocess.DEVNULL # Suppress stderr
120
+ )
121
+ return True # Assume success for cleanup flow
122
+ except Exception as e:
123
+ print(f"Warning: PowerShell cleanup command failed: {e}", file=sys.stderr)
124
+ return False # Indicate potential issue
125
+
126
+ def _cleanup_scheduled_tasks():
127
+ """Removes all OOTB user logon scheduled tasks."""
128
+ print("Attempting to remove any existing OOTB user logon scheduled tasks...")
129
+ # Use -like operator and wildcard
130
+ # Use try-catch within PowerShell for robustness
131
+ command = f"""
132
+ $tasks = Get-ScheduledTask | Where-Object {{ $_.TaskName -like '{_TASK_NAME_PREFIX}*' }}
133
+ if ($tasks) {{
134
+ Write-Host "Found $($tasks.Count) OOTB logon tasks to remove."
135
+ $tasks | Unregister-ScheduledTask -Confirm:$false -ErrorAction SilentlyContinue
136
+ Write-Host "OOTB logon task removal attempted."
137
+ }} else {{
138
+ Write-Host "No OOTB logon tasks found to remove."
139
+ }}
140
+ """
141
+ _run_powershell_cleanup_command(command)
142
+ # --- End cleanup helpers ---
107
143
 
108
144
  def install_and_start():
109
145
  """Installs and starts the Guard Service."""
@@ -133,17 +169,24 @@ def install_and_start():
133
169
 
134
170
 
135
171
  def stop_and_remove():
136
- """Stops and removes the Guard Service."""
172
+ """Stops and removes the Guard Service and associated scheduled tasks."""
137
173
  print(f"Attempting to stop service: '{_SERVICE_NAME}' (will ignore errors if not running)")
138
174
  # Run stop first, ignore errors (check_errors=False)
139
175
  run_service_command(['stop'], check_errors=False)
140
176
  time.sleep(2) # Give service time to stop
141
177
 
142
178
  print(f"\nAttempting to remove service: '{_SERVICE_NAME}'")
143
- if run_service_command(['remove']):
144
- print(f"\nService '{_SERVICE_NAME}' stopped (if running) and removed successfully.")
179
+ remove_success = run_service_command(['remove']) # Check if removal command itself failed
180
+
181
+ # Always attempt task cleanup, even if service removal had issues
182
+ _cleanup_scheduled_tasks()
183
+
184
+ if remove_success:
185
+ print(f"\nService '{_SERVICE_NAME}' stopped (if running) and removed successfully. Associated logon tasks cleanup attempted.")
145
186
  else:
146
- print(f"\nService '{_SERVICE_NAME}' removal failed.", file=sys.stderr)
187
+ print(f"\nService '{_SERVICE_NAME}' removal command failed.", file=sys.stderr)
188
+ # Make sure to mention cleanup was still attempted
189
+ print(f" Associated logon tasks cleanup attempted.", file=sys.stderr)
147
190
  print(f" Ensure the service was stopped first, or check permissions.", file=sys.stderr)
148
191
 
149
192
  if __name__ == '__main__':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computer-use-ootb-internal
3
- Version: 0.0.119
3
+ Version: 0.0.121
4
4
  Summary: Computer Use OOTB
5
5
  Author-email: Siyuan Hu <siyuan.hu.sg@gmail.com>
6
6
  Requires-Python: >=3.11
@@ -3,10 +3,10 @@ computer_use_ootb_internal/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4Tv
3
3
  computer_use_ootb_internal/app_teachmode.py,sha256=PClMS7X6zRGzY7YPOV6Zxkfv5BajLVqmBiv-mOBVxkw,22164
4
4
  computer_use_ootb_internal/app_teachmode_gradio.py,sha256=cmFpBrkdlZxOQADWveVdIaaNqaBD8IVs-xNLJogU7F8,7909
5
5
  computer_use_ootb_internal/dependency_check.py,sha256=y8RMEP6RXQzTgU1MS_1piBLtz4J-Hfn9RjUZg59dyvo,1333
6
- computer_use_ootb_internal/guard_service.py,sha256=d0tZgzjmS8UQGHx2Yf5HuMtRxdJIgy2zLpUTFQF5JF8,24378
6
+ computer_use_ootb_internal/guard_service.py,sha256=rDAoNd2g14bq9bZ57A1MgmBSlrjrzcXUEw9m-dm2XBU,40119
7
7
  computer_use_ootb_internal/requirements-lite.txt,sha256=5DAHomz4A_P2BmTIXNkNqkHbnIF0AyZ4_1XAlb1LaYs,290
8
8
  computer_use_ootb_internal/run_teachmode_ootb_args.py,sha256=7Dj0iY4GG7P03tRKYJ2x9Yvt-PE-b7uyjCAed3SaF3Y,7086
9
- computer_use_ootb_internal/service_manager.py,sha256=xbLIxQ4jzMr8AdZdxKw-QcAGm2Y86vudDzSf3hGjOM4,7595
9
+ computer_use_ootb_internal/service_manager.py,sha256=SD8jzfn0VVXBOr_nP6zmBWSC2TzrU_sp2e5JJkSlQFU,9734
10
10
  computer_use_ootb_internal/computer_use_demo/animation/click_animation.py,sha256=tP1gsayFy-CKk10UlrE9RlexwlHWiHQUe5Ogg4vQvSg,3234
11
11
  computer_use_ootb_internal/computer_use_demo/animation/icons8-select-cursor-transparent-96.gif,sha256=4LfwsfFQnREXrNRs32aJU2jO65JXianJoL_8q7-8elg,30966
12
12
  computer_use_ootb_internal/computer_use_demo/animation/test_animation.py,sha256=2R1u98OLKYalSZ5nt5vvyZ71FL5R5vLv-n8zM8jVdV8,1183
@@ -34,7 +34,7 @@ computer_use_ootb_internal/computer_use_demo/tools/run.py,sha256=xhXdnBK1di9muaO
34
34
  computer_use_ootb_internal/computer_use_demo/tools/screen_capture.py,sha256=L8qfvtUkPPQGt92N-2Zfw5ZTDBzLsDps39uMnX3_uSA,6857
35
35
  computer_use_ootb_internal/preparation/__init__.py,sha256=AgtGHcBpiTkxJjF0xwcs3yyQ6SyUvhL3G0vD2XO-zJw,63
36
36
  computer_use_ootb_internal/preparation/star_rail_prepare.py,sha256=s1VWszcTnJAKxqCHFlaOEwPkqVSrkiFx_yKpWSnSbHs,2649
37
- computer_use_ootb_internal-0.0.119.dist-info/METADATA,sha256=9FKAgkb-tPT3TpODW9nONQUvFwGnX7NwXOJvBKZGAAk,1048
38
- computer_use_ootb_internal-0.0.119.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
- computer_use_ootb_internal-0.0.119.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
40
- computer_use_ootb_internal-0.0.119.dist-info/RECORD,,
37
+ computer_use_ootb_internal-0.0.121.dist-info/METADATA,sha256=nFXcxghQx78Ukkgbzl6-RNyU4yEhdzNKSb_HAlr86DU,1048
38
+ computer_use_ootb_internal-0.0.121.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ computer_use_ootb_internal-0.0.121.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
40
+ computer_use_ootb_internal-0.0.121.dist-info/RECORD,,