computer-use-ootb-internal 0.0.169__tar.gz → 0.0.170__tar.gz

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.
Files changed (42) hide show
  1. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/PKG-INFO +1 -1
  2. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/pyproject.toml +1 -1
  3. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/app_teachmode.py +59 -0
  4. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/executor/teachmode_executor.py +5 -45
  5. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/computer.py +1 -1
  6. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/guard_service.py +110 -186
  7. computer_use_ootb_internal-0.0.170/src/computer_use_ootb_internal/launch_ootb_elevated.ps1 +63 -0
  8. computer_use_ootb_internal-0.0.170/src/computer_use_ootb_internal/service_manager.py +354 -0
  9. computer_use_ootb_internal-0.0.169/src/computer_use_ootb_internal/service_manager.py +0 -195
  10. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/.gitignore +0 -0
  11. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/README.md +0 -0
  12. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/__init__.py +0 -0
  13. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/animation/click_animation.py +0 -0
  14. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/animation/icons8-select-cursor-transparent-96.gif +0 -0
  15. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/animation/test_animation.py +0 -0
  16. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/simple_parser/__init__.py +0 -0
  17. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/simple_parser/gui_capture.py +0 -0
  18. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/simple_parser/utils.py +0 -0
  19. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/uia_tools/__init__.py +0 -0
  20. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/uia_tools/screenshot_cli.py +0 -0
  21. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/gui_parser/uia_tools/screenshot_service.py +0 -0
  22. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/llm_utils.py +0 -0
  23. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/oai.py +0 -0
  24. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/run_litellm.py +0 -0
  25. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/gui_agent/llm_utils/run_llm.py +0 -0
  26. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/__init__.py +0 -0
  27. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/aws_request.py +0 -0
  28. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/base.py +0 -0
  29. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/bash.py +0 -0
  30. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/collection.py +0 -0
  31. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/colorful_text.py +0 -0
  32. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/computer_marbot.py +0 -0
  33. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/edit.py +0 -0
  34. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/run.py +0 -0
  35. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/computer_use_demo/tools/screen_capture.py +0 -0
  36. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/dependency_check.py +0 -0
  37. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/preparation/__init__.py +0 -0
  38. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/preparation/star_rail_prepare.py +0 -0
  39. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/requirements-lite.txt +0 -0
  40. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/run_teachmode_ootb_args.py +0 -0
  41. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/signal_connection.py +0 -0
  42. {computer_use_ootb_internal-0.0.169 → computer_use_ootb_internal-0.0.170}/src/computer_use_ootb_internal/test_click_0425.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computer-use-ootb-internal
3
- Version: 0.0.169
3
+ Version: 0.0.170
4
4
  Summary: Computer Use OOTB
5
5
  Author-email: Siyuan Hu <siyuan.hu.sg@gmail.com>
6
6
  Requires-Python: >=3.11
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "computer-use-ootb-internal"
7
- version = "0.0.169"
7
+ version = "0.0.170"
8
8
  description = "Computer Use OOTB"
9
9
  authors = [{ name = "Siyuan Hu", email = "siyuan.hu.sg@gmail.com" }]
10
10
  requires-python = ">=3.11"
@@ -19,7 +19,10 @@ from fastapi.responses import JSONResponse
19
19
  from fastapi.middleware.cors import CORSMiddleware
20
20
  from computer_use_ootb_internal.computer_use_demo.tools.computer import get_screen_details
21
21
  from computer_use_ootb_internal.run_teachmode_ootb_args import simple_teachmode_sampling_loop
22
+ from computer_use_ootb_internal.computer_use_demo.executor.teachmode_executor import TeachmodeExecutor
22
23
  import uvicorn # Assuming uvicorn is used to run FastAPI
24
+ import concurrent.futures
25
+ import asyncio
23
26
 
24
27
  # --- App Logging Setup ---
25
28
  try:
@@ -395,6 +398,62 @@ async def get_status(request: Request):
395
398
  status_code=200
396
399
  )
397
400
 
401
+ @app.post("/exec_computer_tool")
402
+ async def exec_computer_tool(request: Request):
403
+ logging.info("Received request to /exec_computer_tool")
404
+ try:
405
+ data = await request.json()
406
+
407
+ # Extract parameters from the request
408
+ selected_screen = data.get('selected_screen', 0)
409
+ full_screen_game_mode = data.get('full_screen_game_mode', 0)
410
+ response = data.get('response', {})
411
+
412
+ logging.info(f"Executing TeachmodeExecutor with: screen={selected_screen}, mode={full_screen_game_mode}, response={response}")
413
+
414
+ # Create TeachmodeExecutor in a separate process to avoid event loop conflicts
415
+ # Since TeachmodeExecutor uses asyncio.run() internally, we need to run it in a way
416
+ # that doesn't conflict with FastAPI's event loop
417
+
418
+ def run_executor():
419
+ executor = TeachmodeExecutor(
420
+ selected_screen=selected_screen,
421
+ full_screen_game_mode=full_screen_game_mode
422
+ )
423
+
424
+ results = []
425
+ try:
426
+ for action_result in executor(response):
427
+ results.append(action_result)
428
+ except Exception as exec_error:
429
+ logging.error(f"Error executing action: {exec_error}", exc_info=True)
430
+ return {"error": str(exec_error)}
431
+
432
+ return results
433
+
434
+ # Execute in a thread pool to avoid blocking the event loop
435
+ with concurrent.futures.ThreadPoolExecutor() as pool:
436
+ results = await asyncio.get_event_loop().run_in_executor(pool, run_executor)
437
+
438
+ if isinstance(results, dict) and "error" in results:
439
+ return JSONResponse(
440
+ content={"status": "error", "message": results["error"]},
441
+ status_code=500
442
+ )
443
+
444
+ logging.info(f"Action results: {results}")
445
+
446
+ return JSONResponse(
447
+ content={"status": "success", "results": results},
448
+ status_code=200
449
+ )
450
+ except Exception as e:
451
+ logging.error("Error processing /exec_computer_tool:", exc_info=True)
452
+ return JSONResponse(
453
+ content={"status": "error", "message": f"Internal server error: {str(e)}"},
454
+ status_code=500
455
+ )
456
+
398
457
  def process_input():
399
458
  global shared_state
400
459
  logging.info("process_input thread started.")
@@ -115,20 +115,21 @@ class TeachmodeExecutor:
115
115
  print(f"Error parsing action output: {e}")
116
116
  return None
117
117
 
118
- def _parse_actor_output(self, output_text: str | dict) -> Union[List[Dict[str, Any]], None]:
118
+ def _parse_actor_output(self, action_item: str | dict) -> Union[List[Dict[str, Any]], None]:
119
119
  try:
120
120
  # refine key: value pairs, mapping to the Anthropic's format
121
121
  refined_output = []
122
122
 
123
- action_item = output_text
124
-
123
+ if type(action_item) == str:
124
+ action_item = ast.literal_eval(action_item)
125
+
125
126
  print("[_parse_actor_output] Action Item:", action_item)
126
127
 
127
128
  # sometime showui returns lower case action names
128
129
  action_item["action"] = action_item["action"].upper()
129
130
 
130
131
  if action_item["action"] not in self.supported_action_type:
131
- raise ValueError(f"Action {action_item['action']} not supported. Check the output from ShowUI: {output_text}")
132
+ raise ValueError(f"Action {action_item['action']} not supported. Check the output from Actor: {action_item}")
132
133
  # continue
133
134
 
134
135
  elif action_item["action"] == "CLICK": # 1. click -> mouse_move + left_click
@@ -314,44 +315,3 @@ class TeachmodeExecutor:
314
315
  return bbox
315
316
 
316
317
 
317
-
318
- # def _make_api_tool_result(
319
- # result: ToolResult, tool_use_id: str
320
- # ) -> BetaToolResultBlockParam:
321
- # """Convert an agent ToolResult to an API ToolResultBlockParam."""
322
- # tool_result_content: list[BetaTextBlockParam | BetaImageBlockParam] | str = []
323
- # is_error = False
324
- # if result.error:
325
- # is_error = True
326
- # tool_result_content = _maybe_prepend_system_tool_result(result, result.error)
327
- # else:
328
- # if result.output:
329
- # tool_result_content.append(
330
- # {
331
- # "type": "text",
332
- # "text": _maybe_prepend_system_tool_result(result, result.output),
333
- # }
334
- # )
335
- # if result.base64_image:
336
- # tool_result_content.append(
337
- # {
338
- # "type": "image",
339
- # "source": {
340
- # "type": "base64",
341
- # "media_type": "image/png",
342
- # "data": result.base64_image,
343
- # },
344
- # }
345
- # )
346
- # return {
347
- # "type": "tool_result",
348
- # "content": tool_result_content,
349
- # "tool_use_id": tool_use_id,
350
- # "is_error": is_error,
351
- # }
352
-
353
-
354
- # def _maybe_prepend_system_tool_result(result: ToolResult, result_text: str):
355
- # if result.system:
356
- # result_text = f"<system>{result.system}</system>\n{result_text}"
357
- # return result_text
@@ -384,7 +384,7 @@ class ComputerTool(BaseAnthropicTool):
384
384
  show_click(x, y)
385
385
  time.sleep(0.25)
386
386
  self.marbot_auto_gui.click(x=x, y=y)
387
- self.marbot_auto_gui.click(x=x, y=y)
387
+ # self.marbot_auto_gui.click(x=x, y=y)
388
388
  return ToolResult(output=f"Left click", action_base_type="click")
389
389
 
390
390
  elif action == "mouse_move_windll":
@@ -23,6 +23,7 @@ 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
25
  import json # Needed for status reporting
26
+ import shutil # Added for shutil.which
26
27
 
27
28
  # --- Configuration ---
28
29
  _SERVICE_NAME = "OOTBGuardService"
@@ -143,7 +144,6 @@ class GuardService(win32serviceutil.ServiceFramework):
143
144
  _svc_name_ = _SERVICE_NAME
144
145
  _svc_display_name_ = _SERVICE_DISPLAY_NAME
145
146
  _svc_description_ = _SERVICE_DESCRIPTION
146
- _task_name_prefix = "OOTB_UserLogon_" # Class attribute for task prefix
147
147
 
148
148
  # --- Instance Logging Methods ---
149
149
  def log_info(self, msg):
@@ -708,19 +708,9 @@ class GuardService(win32serviceutil.ServiceFramework):
708
708
 
709
709
 
710
710
  try:
711
- # 1. Ensure scheduled task exists (still useful fallback/persistence)
712
- try:
713
- task_created = self.create_or_update_logon_task(user)
714
- task_created_status = "task_success" if task_created else "task_failed"
715
- except Exception as task_err:
716
- self.log_error(f"Internal trigger: Exception creating/updating task for {user}: {task_err}", exc_info=True)
717
- task_created_status = "task_exception"
718
- # Don't necessarily fail the whole operation yet
719
-
720
711
  # 2. Check if user is active
721
712
  active_sessions = {} # Re-check active sessions specifically for this user
722
713
  session_id = None
723
- token = None
724
714
  is_active = False
725
715
  try:
726
716
  sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
@@ -741,7 +731,7 @@ class GuardService(win32serviceutil.ServiceFramework):
741
731
  if not is_active:
742
732
  self.log_info(f"Internal trigger: User '{user}' is not active. Skipping immediate start.")
743
733
  immediate_start_status = "start_skipped_inactive"
744
- final_status = task_created_status # Status depends only on task creation
734
+ final_status = "skipped_inactive" # Simplified status
745
735
  return final_status # Exit early if inactive
746
736
 
747
737
  # 3. Check if already running for this active user
@@ -758,80 +748,100 @@ class GuardService(win32serviceutil.ServiceFramework):
758
748
  self.log_error(f"Internal trigger: Error checking existing processes for {user}: {e}")
759
749
  # Continue and attempt start despite error?
760
750
 
761
- # 4. Attempt immediate start (User is active and not running)
762
- immediate_start_status = "start_attempted"
763
- self.log_info(f"Internal trigger: User '{user}' is active and not running. Attempting immediate start via PowerShell elevation...")
764
-
751
+ # 4. Attempt immediate start using CreateProcessAsUser to run helper script
752
+ immediate_start_status = "start_attempted_helper_script"
753
+ self.log_info(f"Internal trigger: User '{user}' is active and not running. Attempting start via helper script...")
754
+
765
755
  if not self.target_executable_path:
766
756
  self.log_error("Internal trigger: Cannot start process - target executable path not found.")
767
757
  final_status = "failed_exe_not_found"
768
- return final_status # Exit early if executable missing
758
+ return final_status
769
759
 
770
760
  try:
771
- token = win32ts.WTSQueryUserToken(session_id)
772
- env = win32profile.CreateEnvironmentBlock(token, False)
773
- startup = win32process.STARTUPINFO()
774
- # Use CREATE_NEW_CONSOLE to make the PS window visible and potentially the UAC prompt
775
- creation_flags = win32con.CREATE_NEW_CONSOLE
776
-
777
- # Command to run PowerShell, which then elevates the target executable
761
+ # Find helper script path
762
+ helper_script_path = self._find_helper_script("launch_ootb_elevated.ps1")
763
+ if not helper_script_path:
764
+ self.log_error("Internal trigger: Cannot start process - helper script not found.")
765
+ final_status = "failed_helper_script_not_found"
766
+ return final_status
767
+
768
+ # Get PowerShell executable path
769
+ powershell_executable = self._find_powershell_executable()
770
+ if not powershell_executable:
771
+ self.log_error("Internal trigger: Cannot start process - powershell.exe not found.")
772
+ final_status = "failed_powershell_not_found"
773
+ return final_status
774
+
775
+ # Prepare arguments for the helper script (paths need quoting if they contain spaces)
778
776
  target_exe_unquoted = self.target_executable_path.strip('"')
779
- target_exe_for_ps = target_exe_unquoted.replace("'", "''") # Escape single quotes for PS
780
- target_exe_dir_for_ps = os.path.dirname(target_exe_unquoted).replace("'", "''")
781
-
782
- # Build the second argument for cmd.exe /K, as used in the successful test command
783
- # Format: "<target_exe>" --port <port> --target_user ''<user>''
784
- cmd_k_second_arg_content = f'""{target_exe_for_ps}"" --port {calculated_port} --target_user ''{user}'''
785
- # Escape single quotes within this second arg for the outer PowerShell command string
786
- cmd_k_second_arg_for_ps_command = cmd_k_second_arg_content.replace("'", "''")
787
-
788
- # Build the full Start-Process command string matching the working test command
789
- start_process_cmd = (
790
- f"Start-Process -FilePath cmd.exe "
791
- f"-ArgumentList @('/K', '{cmd_k_second_arg_for_ps_command}') "
792
- f"-WorkingDirectory '{target_exe_dir_for_ps}' "
793
- f"-Verb RunAs"
777
+ target_exe_dir = os.path.dirname(target_exe_unquoted) # Use directory of unquoted path
778
+
779
+ # Quote paths/args for command line - IMPORTANT: Use double quotes for PowerShell args
780
+ helper_script_arg = f'"{helper_script_path}"' if " " in helper_script_path else helper_script_path
781
+ target_exe_arg = f'"{target_exe_unquoted}"' if " " in target_exe_unquoted else target_exe_unquoted
782
+ working_dir_arg = f'"{target_exe_dir}"' if " " in target_exe_dir else target_exe_dir
783
+ # Username might need quoting if it contains special characters, double quote should be safe
784
+ target_user_arg = f'"{user}"'
785
+
786
+ # Construct the command line to execute powershell with the helper script and args
787
+ lpCommandLine = (
788
+ f'"{powershell_executable}" -NoProfile -ExecutionPolicy Bypass -NoExit '
789
+ f'-File {helper_script_arg} '
790
+ f'-TargetExePath {target_exe_arg} '
791
+ f'-Port {calculated_port} '
792
+ f'-TargetUser {target_user_arg} '
793
+ f'-WorkingDirectory {working_dir_arg}'
794
794
  )
795
795
 
796
- # Launch powershell.exe directly, tell it to run the Start-Process command, and keep window open
797
- # Use outer single quotes for -Command argument value to simplify inner quoting
798
- # No need to escape inner double quotes when using outer single quotes
799
- # Escape any literal single quotes within start_process_cmd itself (shouldn't be needed here but good practice)
800
- start_process_cmd_escaped_for_single_quotes = start_process_cmd.replace("'", "''")
801
- lpCommandLine = f'powershell.exe -NoProfile -ExecutionPolicy Bypass -NoExit -Command \'{start_process_cmd_escaped_for_single_quotes}\''
802
-
803
- lpApplicationName = None # Must be None if using lpCommandLine
804
- cwd = os.path.dirname(target_exe_unquoted) if os.path.dirname(target_exe_unquoted) else None
805
-
806
- # Log details before call
807
- self.log_info(f"Internal trigger: Launching PowerShell directly for elevation:")
796
+ # Use CreateProcessAsUser to launch this command in the user's session
797
+ token = win32ts.WTSQueryUserToken(session_id)
798
+ env = win32profile.CreateEnvironmentBlock(token, False)
799
+ startup = win32process.STARTUPINFO()
800
+ # Ensure the window is visible on the user's desktop
801
+ startup.lpDesktop = "winsta0\\default"
802
+ creation_flags = win32con.CREATE_NEW_CONSOLE | win32con.NORMAL_PRIORITY_CLASS
803
+ lpApplicationName = None # Must be None when using lpCommandLine
804
+ # Working directory for powershell itself? Let it default or use script dir?
805
+ # Let's use the script's directory as CWD for powershell.exe
806
+ cwd = os.path.dirname(helper_script_path) # Use directory of unquoted path
807
+
808
+ self.log_info(f"Internal trigger: Launching helper script via CreateProcessAsUser:")
808
809
  self.log_info(f" lpCommandLine: {lpCommandLine}")
809
- self.log_info(f" lpCurrentDirectory: {cwd if cwd else 'Default'}")
810
-
810
+ self.log_info(f" lpCurrentDirectory: {cwd}")
811
+
812
+ # Execute the command in the user's session
811
813
  hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
812
814
  token, lpApplicationName, lpCommandLine, None, None, False,
813
815
  creation_flags, env, cwd, startup
814
816
  )
815
- # We get the PID of cmd.exe/powershell.exe here, not the final elevated process.
816
- # We can't easily track the elevated process PID across the UAC boundary.
817
- self.log_info(f"Internal trigger: CreateProcessAsUser call to initiate elevation succeeded for user '{user}' (Launcher PID: {dwPid}). UAC prompt expected if needed.")
817
+ self.log_info(f"Internal trigger: CreateProcessAsUser call for helper script succeeded (Launcher PID: {dwPid}). Helper script should now run and trigger elevation.")
818
818
  win32api.CloseHandle(hProcess)
819
819
  win32api.CloseHandle(hThread)
820
820
 
821
- # Assume success if the call didn't raise an exception.
822
- # We cannot easily verify if the *elevated* process actually started.
823
- immediate_start_status = "start_elevation_initiated"
824
- final_status = "success_elevation_initiated" # Report success based on initiating the elevation
825
-
826
- except Exception as proc_err:
827
- self.log_error(f"Internal trigger: Exception during CreateProcessAsUser for elevation attempt (user '{user}'): {proc_err}", exc_info=True)
828
- immediate_start_status = "start_failed_exception"
829
- final_status = "failed_start_exception"
821
+ # Assume success if CreateProcessAsUser succeeded.
822
+ # The actual success depends on the user interacting with UAC triggered by the helper.
823
+ immediate_start_status = "start_success_helper_launched"
824
+ final_status = "success_helper_launched"
825
+
826
+ except FileNotFoundError as fnf_err:
827
+ # This might now be powershell.exe OR the helper script
828
+ self.log_error(f"Internal trigger: File not found during helper script launch: {fnf_err}")
829
+ immediate_start_status = "start_failed_file_not_found"
830
+ final_status = "failed_file_not_found"
831
+ except Exception as launch_err:
832
+ self.log_error(f"Internal trigger: Exception during CreateProcessAsUser for helper script (user '{user}'): {launch_err}", exc_info=True)
833
+ immediate_start_status = "start_failed_launch_exception"
834
+ final_status = "failed_launch_exception"
830
835
  finally:
831
- if token: win32api.CloseHandle(token)
832
-
836
+ # Ensure token exists and is not None before attempting to close
837
+ if 'token' in locals() and token is not None:
838
+ try:
839
+ win32api.CloseHandle(token)
840
+ except: # Ignore errors closing handle
841
+ pass
842
+
833
843
  # Combine results (mostly determined by start attempt now)
834
- # Example: final_status = f"{task_created_status}_{immediate_start_status}"
844
+ # Status is now primarily determined by the helper script launch attempt
835
845
  return final_status
836
846
 
837
847
  except Exception as e:
@@ -839,133 +849,47 @@ class GuardService(win32serviceutil.ServiceFramework):
839
849
  return "failed_trigger_exception"
840
850
 
841
851
 
842
- def create_or_update_logon_task(self, username):
843
- """Creates/updates task to trigger the internal signal script on session connect."""
844
- if not self.signal_script_path:
845
- self.log_error(f"Cannot create task for {username}: Signal script path is not set.")
846
- return False
847
- if not sys.executable:
848
- self.log_error(f"Cannot create task for {username}: sys.executable is not found.")
849
- return False
850
-
851
- # Use the python executable that the service itself is running under
852
- python_exe = sys.executable
853
- if ' ' in python_exe and not python_exe.startswith('"'):
854
- python_exe = f'"{python_exe}"'
855
-
856
- task_name = f"OOTB_UserConnect_{username}"
857
- # Action: Run python.exe with the signal script and username argument
858
- action_executable = python_exe
859
- # Ensure script path is quoted if needed
860
- script_arg = self.signal_script_path # Should be quoted already by _find_signal_script
861
- # Username might need quoting if it contains spaces, though unlikely
862
- user_arg = username # Keep simple for now
863
- action_arguments = f'{script_arg} "{user_arg}"' # Pass username as quoted arg
864
- safe_action_executable = action_executable.replace("'", "''") # Escape for PS
865
- safe_action_arguments = action_arguments.replace("'", "''") # Escape for PS
866
-
867
- # Working directory for the script (likely its own directory)
868
- try:
869
- script_dir = os.path.dirname(self.signal_script_path.strip('"'))
870
- if not script_dir: script_dir = "."
871
- safe_working_directory = script_dir.replace("'", "''")
872
- working_directory_setting = f"$action.WorkingDirectory = '{safe_working_directory}'"
873
- except Exception as e:
874
- self.log_error(f"Error determining working directory for signal script task: {e}. WD will not be set.")
875
- working_directory_setting = "# Could not set WorkingDirectory"
876
-
877
- # PowerShell command construction
878
- ps_command = f"""
879
- $taskName = "{task_name}"
880
- $principal = New-ScheduledTaskPrincipal -UserId "{username}" -LogonType Interactive
881
-
882
- # Action: Run python signal script
883
- $action = New-ScheduledTaskAction -Execute '{safe_action_executable}' -Argument '{safe_action_arguments}'
884
- {working_directory_setting}
885
-
886
- # Trigger: On session connect (Event ID 21)
887
- $logName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
888
- $source = 'Microsoft-Windows-TerminalServices-LocalSessionManager'
889
- $eventIDs = @(21, 25)
890
- $trigger = New-ScheduledTaskTrigger -Event -LogName $logName -Source $source -EventId $eventIDs[0]
891
- # Optional Delay: -Delay 'PT15S'
892
- # $trigger.Delay = 'PT15S'
893
-
894
- $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Days 9999) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1)
895
- $description = "Triggers OOTB Guard Service for user {username} upon session connect via internal signal." # Updated description
896
-
897
- # Unregister existing task first (force)
898
- Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
899
-
900
- # Register the new task
901
- Register-ScheduledTask -TaskName $taskName -Principal $principal -Action $action -Trigger $trigger -Settings $settings -Description $description -Force
902
- """
903
- self.log_info(f"Attempting to create/update task '{task_name}' for user '{username}' to run signal script.")
904
- try:
905
- success = self.run_powershell_command(ps_command)
906
- if success:
907
- self.log_info(f"Successfully ran PowerShell command to create/update task '{task_name}'.")
908
- return True
909
- else:
910
- self.log_error(f"PowerShell command failed to create/update task '{task_name}'. See previous logs.")
911
- return False
912
- except Exception as e:
913
- self.log_error(f"Failed to create/update scheduled task '{task_name}' for user '{username}': {e}", exc_info=True)
914
- return False
915
-
916
- def run_powershell_command(self, command, log_output=True):
917
- """Executes a PowerShell command and handles output/errors. Returns True on success."""
918
- self.log_info(f"Executing PowerShell: {command}")
919
- try:
920
- result = subprocess.run(
921
- ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", command],
922
- capture_output=True, text=True, check=True, encoding='utf-8', errors='ignore'
923
- )
924
- if log_output and result.stdout:
925
- self.log_info(f"PowerShell STDOUT:\n{result.stdout.strip()}")
926
- if log_output and result.stderr:
927
- # Log stderr as info, as some commands write status here (like unregister task not found)
928
- self.log_info(f"PowerShell STDERR:\n{result.stderr.strip()}")
929
- return True
930
- except FileNotFoundError:
931
- self.log_error("'powershell.exe' not found. Cannot manage scheduled tasks.")
932
- return False
933
- except subprocess.CalledProcessError as e:
934
- # Log error but still return False, handled by caller
935
- self.log_error(f"PowerShell command failed (Exit Code {e.returncode}):")
936
- self.log_error(f" Command: {e.cmd}")
937
- if e.stdout: self.log_error(f" STDOUT: {e.stdout.strip()}")
938
- if e.stderr: self.log_error(f" STDERR: {e.stderr.strip()}")
939
- return False
940
- except Exception as e:
941
- self.log_error(f"Unexpected error running PowerShell: {e}", exc_info=True)
942
- return False
943
-
944
- def remove_logon_task(self, username):
945
- """Removes the logon scheduled task for a user."""
946
- task_name = f"{self._task_name_prefix}{username}"
947
- safe_task_name = task_name.replace("'", "''")
948
- command = f"Unregister-ScheduledTask -TaskName '{safe_task_name}' -Confirm:$false -ErrorAction SilentlyContinue"
949
- self.run_powershell_command(command, log_output=False)
950
- self.log_info(f"Attempted removal of scheduled task '{task_name}' for user '{username}'.")
951
- return True
952
-
953
852
  def _find_signal_script(self):
954
853
  """Finds the signal_connection.py script relative to this service file."""
854
+ return self._find_helper_script("signal_connection.py") # Reuse helper finding logic
855
+
856
+ def _find_helper_script(self, script_name):
857
+ """Finds a helper script relative to this service file."""
955
858
  try:
859
+ # Use __file__ which should be reliable when run as a service via pythonservice.exe
956
860
  base_dir = os.path.dirname(os.path.abspath(__file__))
957
- script_path = os.path.join(base_dir, "signal_connection.py")
861
+ script_path = os.path.join(base_dir, script_name)
958
862
  if os.path.exists(script_path):
959
- self.log_info(f"Found signal script at: {script_path}")
960
- # Quote if needed?
961
- if " " in script_path and not script_path.startswith('"'):
962
- return f'"{script_path}"'
863
+ self.log_info(f"Found helper script '{script_name}' at: {script_path}")
864
+ # Return unquoted path for potential quoting later
963
865
  return script_path
964
866
  else:
965
- self.log_error(f"Signal script signal_connection.py not found near {base_dir}")
867
+ self.log_error(f"Helper script '{script_name}' not found near {base_dir}")
966
868
  return None
967
869
  except Exception as e:
968
- self.log_error(f"Error finding signal script: {e}")
870
+ self.log_error(f"Error finding helper script '{script_name}': {e}")
871
+ return None
872
+
873
+ def _find_powershell_executable(self):
874
+ """Finds powershell.exe, preferring the system path."""
875
+ try:
876
+ # Check System32 first
877
+ system32_path = os.path.join(os.environ.get('SystemRoot', 'C:\\Windows'), 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe')
878
+ if os.path.exists(system32_path):
879
+ self.log_info(f"Found powershell.exe at: {system32_path}")
880
+ return system32_path
881
+ else:
882
+ # Fallback to checking PATH using shutil.which (requires Python 3.3+)
883
+ # Make sure to import shutil at the top of the file if not already present
884
+ powershell_path = shutil.which("powershell.exe")
885
+ if powershell_path:
886
+ self.log_info(f"Found powershell.exe via PATH: {powershell_path}")
887
+ return powershell_path
888
+ else:
889
+ self.log_error("powershell.exe not found in System32 or PATH.")
890
+ return None
891
+ except Exception as e:
892
+ self.log_error(f"Error finding powershell.exe: {e}")
969
893
  return None
970
894
 
971
895
  # --- Main Execution Block ---
@@ -0,0 +1,63 @@
1
+ # launch_ootb_elevated.ps1
2
+ param(
3
+ [Parameter(Mandatory=$true)]
4
+ [string]$TargetExePath,
5
+
6
+ [Parameter(Mandatory=$true)]
7
+ [string]$Port,
8
+
9
+ [Parameter(Mandatory=$true)]
10
+ [string]$TargetUser,
11
+
12
+ [Parameter(Mandatory=$true)]
13
+ [string]$WorkingDirectory
14
+ )
15
+
16
+ try {
17
+ Write-Host "--- OOTB Elevation Helper ---"
18
+ Write-Host "Timestamp: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
19
+ Write-Host "Received parameters:"
20
+ Write-Host " Target Exe Path : $TargetExePath"
21
+ Write-Host " Port : $Port"
22
+ Write-Host " Target User : $TargetUser"
23
+ Write-Host " Working Dir : $WorkingDirectory"
24
+ Write-Host ""
25
+
26
+ # Validate paths
27
+ if (-not (Test-Path -Path $TargetExePath -PathType Leaf)) {
28
+ throw "Target executable not found at '$TargetExePath'"
29
+ }
30
+ if (-not (Test-Path -Path $WorkingDirectory -PathType Container)) {
31
+ throw "Working directory not found at '$WorkingDirectory'"
32
+ }
33
+
34
+ # Construct the argument list string for the target executable
35
+ # Ensure target user is single-quoted for the argument parser
36
+ $argumentList = "--port $Port --target_user '$TargetUser'"
37
+
38
+ Write-Host "Constructed ArgumentList for Target Exe: $argumentList"
39
+ Write-Host "Executing elevated process..."
40
+ Write-Host "Command: Start-Process -FilePath `$TargetExePath` -ArgumentList `$argumentList` -WorkingDirectory `$WorkingDirectory` -Verb RunAs"
41
+ Write-Host "--- Waiting for UAC prompt if necessary ---"
42
+
43
+ # Execute the command to launch the target exe elevated directly
44
+ Start-Process -FilePath $TargetExePath -ArgumentList $argumentList -WorkingDirectory $WorkingDirectory -Verb RunAs
45
+
46
+ Write-Host "--- OOTB Elevation Helper: Start-Process command executed. ---"
47
+ # The calling powershell window (started by CreateProcessAsUser with -NoExit) will remain open.
48
+
49
+ } catch {
50
+ Write-Error "--- OOTB Elevation Helper Error ---"
51
+ Write-Error "Error launching elevated process: $($_.Exception.Message)"
52
+ Write-Error "Script Parameters:"
53
+ Write-Error " Target Exe Path : $TargetExePath"
54
+ Write-Error " Port : $Port"
55
+ Write-Error " Target User : $TargetUser"
56
+ Write-Error " Working Dir : $WorkingDirectory"
57
+ Write-Host "Press Enter to exit..."
58
+ Read-Host # Keep window open on error
59
+ Exit 1
60
+ }
61
+
62
+ # Exit gracefully if successful
63
+ Exit 0