computer-use-ootb-internal 0.0.118__py3-none-any.whl → 0.0.120__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"
@@ -93,16 +94,33 @@ def get_python_executable():
93
94
  return python_exe
94
95
 
95
96
  def get_pip_executable():
96
- python_path = pathlib.Path(sys.executable)
97
- pip_path = python_path.parent / "Scripts" / "pip.exe"
98
- if pip_path.exists():
99
- pip_exe = str(pip_path)
100
- if " " in pip_exe and not pip_exe.startswith('"'):
101
- pip_exe = f'"{pip_exe}"'
102
- return pip_exe
103
- else:
104
- logging.warning("pip.exe not found in Scripts directory. Falling back to 'python -m pip'.")
105
- return f"{get_python_executable()} -m pip"
97
+ """Tries to locate the pip executable in the same environment."""
98
+ try:
99
+ current_python = sys.executable
100
+ log_info(f"get_pip_executable: sys.executable = {current_python}")
101
+ python_path = pathlib.Path(current_python)
102
+ # Common location is ../Scripts/pip.exe relative to python.exe
103
+ pip_path = python_path.parent / "Scripts" / "pip.exe"
104
+ log_info(f"get_pip_executable: Checking for pip at {pip_path}")
105
+
106
+ if pip_path.exists():
107
+ log_info(f"get_pip_executable: pip.exe found at {pip_path}")
108
+ # Quote if necessary
109
+ pip_exe = str(pip_path)
110
+ if " " in pip_exe and not pip_exe.startswith('"'):
111
+ pip_exe = f'"{pip_exe}"'
112
+ return pip_exe
113
+ else:
114
+ log_error(f"get_pip_executable: pip.exe NOT found at {pip_path}. Falling back to 'python -m pip'.")
115
+ # Fallback is intended here
116
+ pass # Explicitly pass to reach the fallback return outside the else
117
+
118
+ except Exception as e:
119
+ log_error(f"get_pip_executable: Error determining pip path: {e}", exc_info=True)
120
+ log_error("get_pip_executable: Falling back to 'python -m pip' due to error.")
121
+
122
+ # Fallback return statement if 'exists' is false or an exception occurred
123
+ return f"{get_python_executable()} -m pip"
106
124
 
107
125
  def log_info(msg):
108
126
  thread_name = threading.current_thread().name
@@ -127,6 +145,95 @@ def log_error(msg, exc_info=False):
127
145
  except Exception as e:
128
146
  logging.warning(f"Could not write error to Windows Event Log: {e}")
129
147
 
148
+ # --- PowerShell Task Scheduler Helpers ---
149
+
150
+ _TASK_NAME_PREFIX = "OOTB_UserLogon_"
151
+
152
+ def run_powershell_command(command, log_output=True):
153
+ """Executes a PowerShell command and handles output/errors. Returns True on success."""
154
+ # Use log_info from the service instance if available, otherwise use root logger
155
+ logger = _service_instance.log_info if _service_instance else logging.info
156
+ error_logger = _service_instance.log_error if _service_instance else logging.error
157
+ logger(f"Executing PowerShell: {command}")
158
+ try:
159
+ # Using encoding important for non-ASCII usernames/paths
160
+ result = subprocess.run(
161
+ ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command],
162
+ capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore'
163
+ )
164
+ if log_output and result.stdout:
165
+ logger(f"PowerShell STDOUT:\n{result.stdout.strip()}")
166
+ if log_output and result.stderr:
167
+ logger(f"PowerShell STDERR:\n{result.stderr.strip()}") # Log stderr as info
168
+ return True
169
+ except FileNotFoundError:
170
+ error_logger("'powershell.exe' not found. Cannot manage scheduled tasks.")
171
+ return False
172
+ except subprocess.CalledProcessError as e:
173
+ error_logger(f"PowerShell command failed (Exit Code {e.returncode}):")
174
+ error_logger(f" Command: {e.cmd}")
175
+ if e.stdout: error_logger(f" STDOUT: {e.stdout.strip()}")
176
+ if e.stderr: error_logger(f" STDERR: {e.stderr.strip()}")
177
+ return False
178
+ except Exception as e:
179
+ error_logger(f"Unexpected error running PowerShell: {e}", exc_info=True)
180
+ return False
181
+
182
+ def create_or_update_logon_task(username, task_command, python_executable):
183
+ """Creates or updates a scheduled task to run a command at user logon."""
184
+ logger = _service_instance.log_info if _service_instance else logging.info
185
+ error_logger = _service_instance.log_error if _service_instance else logging.error
186
+ task_name = f"{_TASK_NAME_PREFIX}{username}"
187
+ # Escape single quotes in paths and commands for PowerShell
188
+ safe_python_exe = python_executable.replace("'", "''")
189
+ # Ensure task_command is just the arguments, not the python exe itself
190
+ command_parts = task_command.split(' ', 1)
191
+ if len(command_parts) > 1 and command_parts[0] == python_executable:
192
+ safe_task_command_args = command_parts[1].replace("'", "''")
193
+ else: # Fallback if task_command doesn't start with python_exe
194
+ safe_task_command_args = task_command.replace(python_executable, "").strip().replace("'", "''")
195
+
196
+ safe_task_name = task_name.replace("'", "''")
197
+ safe_username = username.replace("'", "''") # Handle usernames with quotes?
198
+
199
+ action = f"$Action = New-ScheduledTaskAction -Execute '{safe_python_exe}' -Argument '{safe_task_command_args}'"
200
+ trigger = f"$Trigger = New-ScheduledTaskTrigger -AtLogOn -User '{safe_username}'"
201
+ principal = f"$Principal = New-ScheduledTaskPrincipal -UserId '{safe_username}' -LogonType Interactive"
202
+ settings = "$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -DontStopOnIdleEnd -ExecutionTimeLimit ([System.TimeSpan]::Zero) -RunOnlyIfNetworkAvailable:$false"
203
+
204
+ command = f"""
205
+ try {{
206
+ {action}
207
+ {trigger}
208
+ {principal}
209
+ {settings}
210
+ Register-ScheduledTask -TaskName '{safe_task_name}' -Action $Action -Trigger $Trigger -Principal $Principal -Settings $Settings -Force -ErrorAction Stop
211
+ Write-Host "Scheduled task '{safe_task_name}' registered/updated successfully."
212
+ }} catch {{
213
+ Write-Error "Failed to register/update scheduled task '{safe_task_name}': $_"
214
+ exit 1 # Indicate failure
215
+ }}
216
+ """
217
+ success = run_powershell_command(command)
218
+ if success:
219
+ logger(f"Successfully created/updated scheduled task '{task_name}' for user '{username}'.")
220
+ else:
221
+ error_logger(f"Failed to create/update scheduled task '{task_name}' for user '{username}'.")
222
+ return success
223
+
224
+
225
+ def remove_logon_task(username):
226
+ """Removes the logon scheduled task for a user."""
227
+ logger = _service_instance.log_info if _service_instance else logging.info
228
+ task_name = f"{_TASK_NAME_PREFIX}{username}"
229
+ safe_task_name = task_name.replace("'", "''")
230
+ command = f"Unregister-ScheduledTask -TaskName '{safe_task_name}' -Confirm:$false -ErrorAction SilentlyContinue"
231
+ run_powershell_command(command, log_output=False)
232
+ logger(f"Attempted removal of scheduled task '{task_name}' for user '{username}'.")
233
+ return True
234
+
235
+ # --- End PowerShell Task Scheduler Helpers ---
236
+
130
237
  class GuardService(win32serviceutil.ServiceFramework):
131
238
  _svc_name_ = _SERVICE_NAME
132
239
  _svc_display_name_ = _SERVICE_DISPLAY_NAME
@@ -250,7 +357,7 @@ class GuardService(win32serviceutil.ServiceFramework):
250
357
  def report_command_status(self, command_id, status, details=""):
251
358
  """Sends command status back to the server."""
252
359
  if not _SERVER_STATUS_REPORT_URL:
253
- log_warning("No server status report URL configured. Skipping report.")
360
+ log_error("No server status report URL configured. Skipping report.")
254
361
  return
255
362
 
256
363
  payload = {
@@ -335,147 +442,306 @@ class GuardService(win32serviceutil.ServiceFramework):
335
442
 
336
443
  def handle_stop(self, target_user="all_active"):
337
444
  log_info(f"Executing stop OOTB for target '{target_user}'...")
338
- stopped_count = 0
339
- procs_to_stop = self._get_ootb_processes(target_user)
445
+ stop_results = {} # Track results per user {username: (task_status, immediate_status)}
446
+ failed_users = set()
447
+
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
452
+ try:
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
+ log_error(f"Could not query session {session['SessionId']} during stop: {query_err}")
462
+ except Exception as user_enum_err:
463
+ log_error(f"Error enumerating users/sessions during stop: {user_enum_err}", exc_info=True)
464
+ return "failed_user_enumeration"
465
+
466
+ 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
+ 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
+ log_info(f"Stop targeting specific user: {normalized_target}")
478
+
479
+ if not target_users_normalized:
480
+ log_info("No target users identified for stop.")
481
+ return "failed_no_target_users" # Or success if none were targeted?
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
340
488
 
341
- if not procs_to_stop:
342
- log_info("No running OOTB processes found for target.")
343
- return "no_process_found"
489
+ log_info(f"Processing stop for user '{user}'...")
344
490
 
345
- for proc in procs_to_stop:
491
+ # 1. Always try to remove the scheduled task
492
+ try:
493
+ # remove_logon_task always returns True for now, just logs attempt
494
+ remove_logon_task(user)
495
+ task_removed_status = "task_removed_attempted"
496
+ except Exception as task_err:
497
+ 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
+ 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
+ log_info(f"No running OOTB processes found for active user '{user}'.")
513
+ immediate_stop_status = "stop_skipped_not_running"
514
+ else:
515
+ 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
+ log_info(f"Terminating process PID={pid}, User={username}")
521
+ proc.terminate()
522
+ try:
523
+ proc.wait(timeout=3)
524
+ log_info(f"Process PID={pid} terminated successfully.")
525
+ stopped_count += 1
526
+ except psutil.TimeoutExpired:
527
+ log_error(f"Process PID={pid} did not terminate gracefully, killing.")
528
+ proc.kill()
529
+ stopped_count += 1
530
+ except psutil.NoSuchProcess:
531
+ 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
+ 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
+ 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
+ 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
+ log_info(f"Finished stopping OOTB. Overall Status: {final_status}. Results: {stop_results}")
346
563
  try:
347
- username = proc.info.get('username', 'unknown_user')
348
- log_info(f"Terminating process PID={proc.pid}, User={username}")
349
- proc.terminate()
350
- try:
351
- proc.wait(timeout=3)
352
- log_info(f"Process PID={proc.pid} terminated successfully.")
353
- stopped_count += 1
354
- except psutil.TimeoutExpired:
355
- log_warning(f"Process PID={proc.pid} did not terminate gracefully, killing.")
356
- proc.kill()
357
- stopped_count += 1
358
- except psutil.NoSuchProcess:
359
- log_info(f"Process PID={proc.pid} already terminated.")
360
- stopped_count +=1
361
- except psutil.AccessDenied:
362
- log_error(f"Access denied trying to terminate process PID={proc.pid}. Service might lack privileges?")
363
- except Exception as e:
364
- log_error(f"Error stopping process PID={proc.pid}: {e}", exc_info=True)
564
+ details = json.dumps(stop_results)
565
+ except Exception:
566
+ details = str(stop_results) # Fallback
567
+ return f"{final_status}::{details}" # Use :: as separator
365
568
 
366
- log_info(f"Finished stopping OOTB. Terminated {stopped_count} process(es).")
367
- return f"success_stopped_{stopped_count}"
569
+ except Exception as e:
570
+ log_error(f"Error during combined stop OOTB process: {e}", exc_info=True)
571
+ return "failed_exception"
368
572
 
369
573
 
370
574
  def handle_start(self, target_user="all_active"):
371
575
  log_info(f"Executing start OOTB for target '{target_user}'...")
372
- started_count = 0
373
- target_users_started = set()
374
- users_failed_to_start = set()
576
+ start_results = {} # Track results per user {username: (task_status, immediate_status)}
577
+ failed_users = set()
375
578
 
376
579
  try:
377
- sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
378
- active_sessions = {}
379
-
380
- for session in sessions:
381
- if session['State'] == win32ts.WTSActive:
382
- try:
383
- user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
384
- if user:
385
- normalized_user = user.lower()
386
- active_sessions[normalized_user] = session['SessionId']
387
- except Exception as query_err:
388
- log_warning(f"Could not query session {session['SessionId']}: {query_err}")
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
+ log_error(f"Could not query session {session['SessionId']}: {query_err}")
598
+ except Exception as user_enum_err:
599
+ log_error(f"Error enumerating users/sessions: {user_enum_err}", exc_info=True)
600
+ return "failed_user_enumeration"
389
601
 
390
602
  log_info(f"Found active user sessions: {active_sessions}")
391
603
 
392
- target_session_map = {}
604
+ target_users_normalized = set()
393
605
  if target_user == "all_active":
394
- target_session_map = active_sessions
606
+ # If targeting all_active, only target those CURRENTLY active
607
+ target_users_normalized = set(active_sessions.keys())
608
+ log_info(f"Targeting all active users: {target_users_normalized}")
395
609
  else:
396
- normalized_target = target_user.lower()
397
- if normalized_target in active_sessions:
398
- target_session_map[normalized_target] = active_sessions[normalized_target]
399
- else:
400
- log_warning(f"Target user '{target_user}' not found in active sessions.")
401
- 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
+ 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
+ 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
+ log_error(f"Error checking existing processes: {e}")
643
+ log_info(f"Users currently running OOTB: {running_procs_by_user}")
402
644
 
403
- if not target_session_map:
404
- log_info("No target user sessions found to start OOTB in.")
405
- 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
406
650
 
407
- running_procs = self._get_ootb_processes(target_user)
408
- users_already_running = set()
409
- for proc in running_procs:
410
- try:
411
- proc_username = proc.info.get('username')
412
- if proc_username:
413
- users_already_running.add(proc_username.split('\\')[-1].lower())
414
- except Exception:
415
- pass
651
+ log_info(f"Processing start for user '{user}'...")
416
652
 
417
- 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 = 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
+ 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
+ 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
+ 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
+ log_info(f"Immediate start succeeded for user '{user}' (PID {dwPid}).")
696
+ immediate_start_status = "start_success"
697
+ else:
698
+ 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
+ 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
+ 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
+ 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"
418
734
 
419
- for user, session_id in target_session_map.items():
420
- token = None
421
- try:
422
- if user in users_already_running:
423
- log_info(f"OOTB already seems to be running for user '{user}'. Skipping start.")
424
- continue
425
-
426
- log_info(f"Attempting to start OOTB for user '{user}' in session {session_id}...")
427
- token = win32ts.WTSQueryUserToken(session_id)
428
- env = win32profile.CreateEnvironmentBlock(token, False)
429
- startup = win32process.STARTUPINFO()
430
- # Simplify startup flags: Run hidden, don't explicitly set desktop
431
- startup.dwFlags = win32process.STARTF_USESHOWWINDOW
432
- startup.wShowWindow = win32con.SW_HIDE
433
- # startup.lpDesktop = 'winsta0\\default' # Removed
434
-
435
- creation_flags = win32process.CREATE_NEW_CONSOLE | win32process.CREATE_UNICODE_ENVIRONMENT
436
- # Define cwd as user's profile directory if possible
437
- user_profile_dir = win32profile.GetUserProfileDirectory(token)
438
-
439
- hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
440
- token, self.python_exe, self.ootb_command,
441
- None, None, False, creation_flags, env,
442
- user_profile_dir, # Set current directory
443
- startup
444
- )
445
- log_info(f"CreateProcessAsUser call succeeded for user '{user}' (PID: {dwPid}).")
446
-
447
- # Add post-start check
448
- time.sleep(1) # Small delay
449
- if psutil.pid_exists(dwPid):
450
- log_info(f"Process PID {dwPid} confirmed to exist shortly after creation.")
451
- started_count += 1
452
- target_users_started.add(user)
453
- else:
454
- log_warning(f"Process PID {dwPid} reported by CreateProcessAsUser does NOT exist shortly after creation. It likely exited immediately.")
455
- users_failed_to_start.add(user)
456
- # Attempt to get exit code? Difficult without waiting.
457
-
458
- win32api.CloseHandle(hProcess)
459
- win32api.CloseHandle(hThread)
460
-
461
- except Exception as proc_err:
462
- log_error(f"Failed to start OOTB for user '{user}' in session {session_id}: {proc_err}", exc_info=True)
463
- users_failed_to_start.add(user)
464
- finally:
465
- if token:
466
- try: win32api.CloseHandle(token)
467
- except: pass
468
-
469
- log_info(f"Finished starting OOTB. Started {started_count} new instance(s). Failed for users: {users_failed_to_start or 'None'}")
470
- if users_failed_to_start:
471
- return f"partial_success_started_{started_count}_failed_for_{len(users_failed_to_start)}"
472
- elif started_count > 0:
473
- return f"success_started_{started_count}"
474
- else:
475
- return "no_action_needed_already_running"
735
+ 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}"
476
742
 
477
743
  except Exception as e:
478
- log_error(f"Error during start OOTB process: {e}", exc_info=True)
744
+ log_error(f"Error during combined start OOTB process: {e}", exc_info=True)
479
745
  return "failed_exception"
480
746
 
481
747
  # --- Main Execution Block ---
@@ -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.118
3
+ Version: 0.0.120
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=z9GQdPJCd_Ds1xwCUn7hJBqPuto_FLhE1F5xqrvgh98,23496
6
+ computer_use_ootb_internal/guard_service.py,sha256=1CiueJvILdqPB1Ncxq9lUIkhUk7yZ-AgVBEwnbmkCNU,39691
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.118.dist-info/METADATA,sha256=hXxBaos02qZPHK7UQIFaRPJsCjTCmhZuL2Z7WMPRMA0,1048
38
- computer_use_ootb_internal-0.0.118.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
- computer_use_ootb_internal-0.0.118.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
40
- computer_use_ootb_internal-0.0.118.dist-info/RECORD,,
37
+ computer_use_ootb_internal-0.0.120.dist-info/METADATA,sha256=51EX1JLqwmtsEHPfkcQ4Ie2exLtQdAMh6YTigZuhxhU,1048
38
+ computer_use_ootb_internal-0.0.120.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ computer_use_ootb_internal-0.0.120.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
40
+ computer_use_ootb_internal-0.0.120.dist-info/RECORD,,