computer-use-ootb-internal 0.0.111__py3-none-any.whl → 0.0.113__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.
@@ -8,6 +8,9 @@ import platform # Add platform import
8
8
  import pyautogui # Add pyautogui import
9
9
  import webbrowser # Add webbrowser import
10
10
  import os # Import os for path joining
11
+ import logging # Import logging
12
+ import importlib # For dynamic imports
13
+ import pkgutil # To find modules
11
14
  from fastapi import FastAPI, Request
12
15
  from fastapi.responses import JSONResponse
13
16
  from fastapi.middleware.cors import CORSMiddleware
@@ -90,68 +93,47 @@ class SharedState:
90
93
  shared_state = None
91
94
  rate_limiter = RateLimiter(interval_seconds=2)
92
95
 
93
- # Add the new prepare_environment function here
96
+ # Set up logging for this module
97
+ log = logging.getLogger(__name__)
98
+
94
99
  def prepare_environment(state):
95
- """Prepares the environment before starting the main processing loop, e.g., opening specific apps."""
96
- if platform.system() == "Windows":
97
- # Assuming Star Rail mode is indicated by user_id containing "star_rail"
98
- # You might need to adjust this condition based on the actual logic in run_teachmode_args
99
- is_star_rail = "star_rail" in state.user_id.lower() or \
100
- "star_rail" in state.trace_id.lower() or \
101
- "hero_case" in state.user_id.lower() or \
102
- "hero_case" in state.trace_id.lower()
103
-
104
- if is_star_rail:
105
- print("Star Rail mode detected on Windows. Opening Edge browser...")
106
- url = "https://sr.mihoyo.com/cloud/#/"
107
- browser_opened = False
108
- try:
109
- # Use only webbrowser.open
110
- print(f"Attempting to open {url} using webbrowser.open()...")
111
- if webbrowser.open(url):
112
- print(f"Successfully requested browser to open {url} via webbrowser.open().")
113
- browser_opened = True
114
- else:
115
- print("webbrowser.open() returned False, indicating potential failure.")
116
-
117
- if not browser_opened:
118
- print("ERROR: Failed to confirm browser opening via webbrowser.open().")
119
- # Still proceed to click attempt
120
-
121
- # Add pyautogui click after attempting to open the browser
122
- print("Proceeding with pyautogui actions...")
123
- time.sleep(5) # Wait time for the browser to load
124
-
125
- # Print detected screen size
126
- screen_width, screen_height = pyautogui.size()
127
- print(f"Detected screen size: {screen_width}x{screen_height}")
128
-
129
- click_x = int(screen_width * (1036 / 1280))
130
- click_y = int(screen_height * (500 / 720))
131
- print(f"Calculated click coordinates: ({click_x}, {click_y})")
132
-
133
- # Disable failsafe before clicking
134
- pyautogui.FAILSAFE = False
135
- print("PyAutoGUI failsafe temporarily disabled.")
136
-
137
- print(f"Clicking at coordinates: ({click_x}, {click_y})")
138
- pyautogui.click(click_x, click_y)
139
- time.sleep(2)
140
- pyautogui.click(click_x, click_y)
141
-
142
- # Re-enable failsafe (optional, as script might end anyway)
143
- # pyautogui.FAILSAFE = True
144
- # print("PyAutoGUI failsafe re-enabled.")
145
-
146
- except Exception as e:
147
- print(f"Error during environment preparation (browser/click): {e}")
148
- finally:
149
- # Ensure failsafe is re-enabled if an error occurs after disabling it
150
- pyautogui.FAILSAFE = True
151
- print("PyAutoGUI failsafe re-enabled.")
152
- else:
153
- # Placeholder for potential preparations on other OS or non-Star Rail modes
154
- print("Environment preparation: No specific actions required for this OS/mode.")
100
+ """Dynamically loads and runs preparation logic based on software name."""
101
+ # TODO: Replace hardcoded software name with value from shared_state when available
102
+ software_name = "star rail"
103
+ # Normalize the software name to be a valid Python module name
104
+ # Replace spaces/hyphens with underscores, convert to lowercase
105
+ module_name_base = software_name.replace(" ", "_").replace("-", "_").lower()
106
+ module_to_run = f"{module_name_base}_prepare"
107
+
108
+ log.info(f"Attempting preparation for software: '{software_name}' (Module: '{module_to_run}')")
109
+
110
+ try:
111
+ # Construct the full module path within the package
112
+ prep_package = "computer_use_ootb_internal.preparation"
113
+ full_module_path = f"{prep_package}.{module_to_run}"
114
+
115
+ # Dynamically import the module
116
+ # Check if module exists first using pkgutil to avoid import errors
117
+ # Note: pkgutil.find_loader might be deprecated, consider importlib.util.find_spec
118
+ loader = pkgutil.find_loader(full_module_path)
119
+ if loader is None:
120
+ log.warning(f"Preparation module '{full_module_path}' not found. Skipping preparation.")
121
+ return
122
+
123
+ prep_module = importlib.import_module(full_module_path)
124
+
125
+ # Check if the module has the expected function
126
+ if hasattr(prep_module, "run_preparation") and callable(prep_module.run_preparation):
127
+ log.info(f"Running preparation function from {full_module_path}...")
128
+ prep_module.run_preparation(state)
129
+ log.info(f"Preparation function from {full_module_path} completed.")
130
+ else:
131
+ log.warning(f"Module {full_module_path} found, but does not have a callable 'run_preparation' function. Skipping.")
132
+
133
+ except ModuleNotFoundError:
134
+ log.warning(f"Preparation module '{full_module_path}' not found. Skipping preparation.")
135
+ except Exception as e:
136
+ log.error(f"Error during dynamic preparation loading/execution for '{module_to_run}': {e}", exc_info=True)
155
137
 
156
138
 
157
139
  @app.post("/update_params")
@@ -178,7 +160,7 @@ async def update_parameters(request: Request):
178
160
 
179
161
  log_ootb_request(shared_state.server_url, "update_params", data)
180
162
 
181
- # Call the preparation function here, after parameters are updated
163
+ # Call the (now dynamic) preparation function here, after parameters are updated
182
164
  prepare_environment(shared_state)
183
165
 
184
166
  return JSONResponse(
@@ -6,7 +6,9 @@ import logging
6
6
  import subprocess
7
7
  import pathlib
8
8
  import ctypes
9
- import requests # For server polling
9
+ import threading # For running server thread
10
+ import queue # For queuing commands
11
+ import requests # Keep for status reporting back
10
12
  import servicemanager # From pywin32
11
13
  import win32serviceutil # From pywin32
12
14
  import win32service # From pywin32
@@ -16,48 +18,81 @@ import win32process # From pywin32
16
18
  import win32security # From pywin32
17
19
  import win32profile # From pywin32
18
20
  import win32ts # From pywin32 (Terminal Services API)
19
- import win32con # Added import
21
+ import win32con
20
22
  import psutil # For process/user info
23
+ from flask import Flask, request, jsonify # For embedded server
24
+ from waitress import serve # For serving Flask app
21
25
 
22
26
  # --- Configuration ---
23
- # Internal service name
24
27
  _SERVICE_NAME = "OOTBGuardService"
25
- # Display name in Windows Services MMC
26
28
  _SERVICE_DISPLAY_NAME = "OOTB Guard Service"
27
- # Description in Windows Services MMC
28
- _SERVICE_DESCRIPTION = "Background service for OOTB monitoring and remote management."
29
- # Package name for updates
29
+ _SERVICE_DESCRIPTION = "Background service for OOTB monitoring and remote management (Server POST mode)."
30
30
  _PACKAGE_NAME = "computer-use-ootb-internal"
31
- # Main module to start/stop for users
32
31
  _OOTB_MODULE = "computer_use_ootb_internal.app_teachmode"
33
- # Server endpoint to poll for commands (replace with your actual URL)
34
- _SERVER_COMMAND_URL = "https://52.160.105.102/api/guard/commands" # EXAMPLE URL - Verify protocol/port/path
35
- # How often to poll the server (in seconds)
36
- _POLLING_INTERVAL = 60
37
- # Placeholder for a machine identifier or API key for the server
38
- _MACHINE_ID = "YOUR_MACHINE_ID_OR_API_KEY" # EXAMPLE - Implement actual ID/auth
39
- # Log file location (consider using Windows Event Log instead for production)
40
- _LOG_FILE = pathlib.Path(os.environ['PROGRAMDATA']) / "OOTBGuardService" / "guard.log"
32
+ # --- Server POST Configuration ---
33
+ _LISTEN_HOST = "0.0.0.0" # Listen on all interfaces
34
+ _LISTEN_PORT = 7001 # Port for server to POST commands TO
35
+ # _SHARED_SECRET = "YOUR_SECRET_HERE" # !! REMOVED !! - No secret check implemented now
36
+ # --- End Server POST Configuration ---
37
+ _SERVER_STATUS_REPORT_URL = "http://52.160.105.102:7000/api/guard/status" # URL to POST status back TO
38
+ _LOG_FILE = pathlib.Path(os.environ['PROGRAMDATA']) / "OOTBGuardService" / "guard_post_mode.log" # Different log file
41
39
  # --- End Configuration ---
42
40
 
43
- # Ensure log directory exists
44
41
  _LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
45
-
46
42
  logging.basicConfig(
47
43
  filename=_LOG_FILE,
48
44
  level=logging.INFO,
49
- format='%(asctime)s %(levelname)s %(message)s'
45
+ format='%(asctime)s %(levelname)s:%(name)s:%(threadName)s: %(message)s'
50
46
  )
51
47
 
48
+ # --- Global service instance reference (needed for Flask routes) ---
49
+ _service_instance = None
50
+
51
+ # --- Flask App Definition ---
52
+ flask_app = Flask(__name__)
53
+
54
+ @flask_app.route('/command', methods=['POST'])
55
+ def receive_command():
56
+ global _service_instance
57
+ if not _service_instance:
58
+ logging.error("Received command but service instance is not set.")
59
+ return jsonify({"error": "Service not ready"}), 503
60
+
61
+ # --- Authentication REMOVED ---
62
+ # secret = request.headers.get('X-Guard-Secret')
63
+ # if not secret or secret != _SHARED_SECRET:
64
+ # logging.warning(f"Unauthorized command POST received (Invalid/Missing X-Guard-Secret). Remote Addr: {request.remote_addr}")
65
+ # return jsonify({"error": "Unauthorized"}), 403
66
+ # --- End Authentication REMOVED ---
67
+
68
+ if not request.is_json:
69
+ logging.warning("Received non-JSON command POST.")
70
+ return jsonify({"error": "Request must be JSON"}), 400
71
+
72
+ command = request.get_json()
73
+ logging.info(f"Received command via POST: {command}")
74
+
75
+ # Basic validation
76
+ action = command.get("action")
77
+ command_id = command.get("command_id", "N/A") # Use for status reporting
78
+ if not action:
79
+ logging.error(f"Received command POST with no action: {command}")
80
+ return jsonify({"error": "Missing 'action' in command"}), 400
81
+
82
+ # Queue the command for processing in a background thread
83
+ _service_instance.command_queue.put((command_id, command))
84
+ logging.info(f"Queued command {command_id} ({action}) for processing.")
85
+
86
+ return jsonify({"message": f"Command {command_id} received and queued"}), 202 # Accepted
87
+
88
+ # --- Helper Functions --- Only logging helpers needed adjustments
52
89
  def get_python_executable():
53
- """Gets the quoted path to the current python executable."""
54
90
  python_exe = sys.executable
55
91
  if " " in python_exe and not python_exe.startswith('"'):
56
92
  python_exe = f'"{python_exe}"'
57
93
  return python_exe
58
94
 
59
95
  def get_pip_executable():
60
- """Tries to locate the pip executable in the same environment."""
61
96
  python_path = pathlib.Path(sys.executable)
62
97
  pip_path = python_path.parent / "Scripts" / "pip.exe"
63
98
  if pip_path.exists():
@@ -70,16 +105,25 @@ def get_pip_executable():
70
105
  return f"{get_python_executable()} -m pip"
71
106
 
72
107
  def log_info(msg):
73
- logging.info(msg)
108
+ thread_name = threading.current_thread().name
109
+ full_msg = f"[{thread_name}] {msg}"
110
+ logging.info(full_msg)
74
111
  try:
75
- servicemanager.LogInfoMsg(str(msg)) # Also log to Windows Event Log Application channel
112
+ # Only log to event log from main service thread or known non-daemon threads if possible
113
+ # Trying from waitress/flask threads might cause issues.
114
+ # For simplicity, maybe remove event log integration or make it conditional.
115
+ if threading.current_thread().name in ["MainThread", "CommandProcessor"]: # Example condition
116
+ servicemanager.LogInfoMsg(str(full_msg))
76
117
  except Exception as e:
77
- logging.warning(f"Could not write to Windows Event Log: {e}") # Avoid crashing service if event log fails
118
+ logging.warning(f"Could not write info to Windows Event Log: {e}")
78
119
 
79
120
  def log_error(msg, exc_info=False):
80
- logging.error(msg, exc_info=exc_info)
121
+ thread_name = threading.current_thread().name
122
+ full_msg = f"[{thread_name}] {msg}"
123
+ logging.error(full_msg, exc_info=exc_info)
81
124
  try:
82
- servicemanager.LogErrorMsg(str(msg))
125
+ if threading.current_thread().name in ["MainThread", "CommandProcessor"]:
126
+ servicemanager.LogErrorMsg(str(full_msg))
83
127
  except Exception as e:
84
128
  logging.warning(f"Could not write error to Windows Event Log: {e}")
85
129
 
@@ -89,67 +133,92 @@ class GuardService(win32serviceutil.ServiceFramework):
89
133
  _svc_description_ = _SERVICE_DESCRIPTION
90
134
 
91
135
  def __init__(self, args):
136
+ global _service_instance
92
137
  win32serviceutil.ServiceFramework.__init__(self, args)
93
138
  self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
94
139
  self.is_running = True
140
+ self.server_thread = None
141
+ self.command_queue = queue.Queue()
142
+ self.command_processor_thread = None
143
+ self.session = requests.Session() # For status reporting
144
+
95
145
  self.python_exe = get_python_executable()
96
146
  self.pip_command_base = get_pip_executable()
97
147
  self.ootb_command = f"{self.python_exe} -m {_OOTB_MODULE}"
98
- self.session = requests.Session() # Reuse session for polling
148
+ _service_instance = self # Set global reference
99
149
 
100
150
  def SvcStop(self):
151
+ log_info(f"Service stop requested.")
101
152
  self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
102
- win32event.SetEvent(self.hWaitStop)
103
153
  self.is_running = False
104
- log_info(f"{_SERVICE_NAME} is stopping.")
154
+ # Signal the command processor thread to stop
155
+ self.command_queue.put(None) # Sentinel value
156
+ # Signal the main wait loop
157
+ win32event.SetEvent(self.hWaitStop)
158
+ # Stopping waitress gracefully from another thread is non-trivial.
159
+ # We rely on the SCM timeout / process termination for now.
160
+ log_info(f"{_SERVICE_NAME} SvcStop: Stop signaled. Server thread will be terminated by SCM.")
161
+
105
162
 
106
163
  def SvcDoRun(self):
107
164
  servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
108
165
  servicemanager.PYS_SERVICE_STARTED,
109
166
  (self._svc_name_, ''))
110
- self.main_loop()
111
- # If main_loop exits cleanly (shouldn't happen with while self.is_running)
112
- log_info(f"{_SERVICE_NAME} main loop exited.")
167
+ try:
168
+ log_info(f"{_SERVICE_NAME} starting.")
169
+ # Start the command processor thread
170
+ self.command_processor_thread = threading.Thread(
171
+ target=self.process_commands, name="CommandProcessor", daemon=True)
172
+ self.command_processor_thread.start()
173
+ log_info("Command processor thread started.")
174
+
175
+ # Start the Flask server (via Waitress) in a separate thread
176
+ self.server_thread = threading.Thread(
177
+ target=self.run_server, name="WebServerThread", daemon=True)
178
+ self.server_thread.start()
179
+ log_info(f"Web server thread started, listening on {_LISTEN_HOST}:{_LISTEN_PORT}.")
180
+
181
+ log_info(f"{_SERVICE_NAME} running. Waiting for stop signal.")
182
+ # Keep the main service thread alive waiting for stop signal
183
+ win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
184
+ log_info(f"{_SERVICE_NAME} received stop signal in main thread.")
113
185
 
186
+ except Exception as e:
187
+ log_error(f"Fatal error in SvcDoRun: {e}", exc_info=True)
188
+ self.SvcStop() # Signal stop if possible
189
+ finally:
190
+ log_info(f"{_SERVICE_NAME} SvcDoRun finished.")
114
191
 
115
- def main_loop(self):
116
- log_info(f"{_SERVICE_NAME} started. Polling {_SERVER_COMMAND_URL} every {_POLLING_INTERVAL}s.")
117
- while self.is_running:
118
- try:
119
- self.poll_server_for_commands()
120
- except Exception as e:
121
- log_error(f"Error in main loop polling cycle: {e}", exc_info=True)
122
192
 
123
- # Wait for stop event or timeout
124
- rc = win32event.WaitForSingleObject(self.hWaitStop, _POLLING_INTERVAL * 1000)
125
- if rc == win32event.WAIT_OBJECT_0:
126
- # Stop event signaled
127
- break
128
- log_info(f"{_SERVICE_NAME} is shutting down main loop.")
193
+ def run_server(self):
194
+ """Runs the Flask app using Waitress."""
195
+ log_info(f"Waitress server starting on {_LISTEN_HOST}:{_LISTEN_PORT}")
196
+ try:
197
+ serve(flask_app, host=_LISTEN_HOST, port=_LISTEN_PORT, threads=4)
198
+ log_info("Waitress server has stopped.") # Should only happen on shutdown
199
+ except Exception as e:
200
+ log_error(f"Web server thread encountered an error: {e}", exc_info=True)
201
+ # Consider signaling the main thread to stop if the web server fails critically
202
+ # For now, just log the error.
129
203
 
130
204
 
131
- def poll_server_for_commands(self):
132
- log_info(f"Polling server for commands...")
133
- try:
134
- headers = {'Authorization': f'Bearer {_MACHINE_ID}'} # Example auth
135
- # Add machine identifier if needed by server
136
- params = {'machine_id': os.getenv('COMPUTERNAME', 'unknown')}
137
- response = self.session.get(_SERVER_COMMAND_URL, headers=headers, params=params, timeout=30)
138
- response.raise_for_status() # Raise exception for bad status codes
139
-
140
- commands = response.json() # Expecting a list of command objects
141
- if not commands:
142
- # log_info("No commands received.") # Reduce log noise
143
- return
144
-
145
- log_info(f"Received {len(commands)} command(s). Processing...")
146
- for command in commands:
205
+ def process_commands(self):
206
+ """Worker thread to process commands from the queue."""
207
+ log_info("Command processor thread starting.")
208
+ while self.is_running:
209
+ try:
210
+ item = self.command_queue.get(block=True, timeout=1) # Add timeout to check is_running periodically
211
+ if item is None:
212
+ log_info("Command processor received stop signal.")
213
+ break # Exit loop
214
+
215
+ command_id, command = item
147
216
  action = command.get("action")
148
- target = command.get("target_user", "all_active") # Default to all
149
- command_id = command.get("command_id", "N/A") # Optional: for reporting status
217
+ target = command.get("target_user", "all_active")
218
+ status = "failed_unknown" # Default
219
+
220
+ log_info(f"Dequeued Command ID {command_id}: action='{action}', target='{target}'")
150
221
 
151
- log_info(f"Processing Command ID {command_id}: action='{action}', target='{target}'")
152
- status = "failed" # Default status
153
222
  try:
154
223
  if action == "update":
155
224
  status = self.handle_update()
@@ -158,21 +227,49 @@ class GuardService(win32serviceutil.ServiceFramework):
158
227
  elif action == "start_ootb":
159
228
  status = self.handle_start(target)
160
229
  else:
161
- log_error(f"Unknown action received: {action}")
162
- status = "unknown_action"
230
+ log_error(f"Unknown action in queue: {action}")
231
+ status = "failed_unknown_action"
163
232
  except Exception as handler_ex:
164
- log_error(f"Error executing action '{action}' for command {command_id}: {handler_ex}", exc_info=True)
165
- status = "execution_error"
166
-
167
- # TODO: Add mechanism to report command completion/failure back to server
168
- # Example: self.report_command_status(command_id, status)
169
- log_info(f"Finished processing Command ID {command_id}: Status='{status}'")
170
-
233
+ log_error(f"Exception processing Command ID {command_id} ({action}): {handler_ex}", exc_info=True)
234
+ status = "failed_exception"
235
+ finally:
236
+ self.report_command_status(command_id, status)
237
+ self.command_queue.task_done()
238
+
239
+ except queue.Empty:
240
+ # Timeout occurred, just loop again and check self.is_running
241
+ continue
242
+ except Exception as e:
243
+ log_error(f"Error in command processing loop: {e}", exc_info=True)
244
+ if self.is_running:
245
+ time.sleep(5)
246
+
247
+ log_info("Command processor thread finished.")
248
+
249
+
250
+ def report_command_status(self, command_id, status, details=""):
251
+ """Sends command status back to the server."""
252
+ if not _SERVER_STATUS_REPORT_URL:
253
+ log_warning("No server status report URL configured. Skipping report.")
254
+ return
255
+
256
+ payload = {
257
+ "command_id": command_id,
258
+ "status": status,
259
+ "details": details,
260
+ "machine_id": os.getenv('COMPUTERNAME', 'unknown_guard')
261
+ }
262
+ log_info(f"Reporting status for command {command_id}: {status}")
263
+ try:
264
+ response = self.session.post(_SERVER_STATUS_REPORT_URL, json=payload, timeout=15)
265
+ response.raise_for_status()
266
+ log_info(f"Status report for command {command_id} accepted by server.")
171
267
  except requests.exceptions.RequestException as e:
172
- log_error(f"Failed to poll server: {e}")
268
+ log_error(f"Failed to report status for command {command_id}: {e}")
173
269
  except Exception as e:
174
- log_error(f"Error processing server commands: {e}", exc_info=True)
270
+ log_error(f"Unexpected error reporting status for command {command_id}: {e}", exc_info=True)
175
271
 
272
+ # --- Command Handlers --- Copying full implementation from previous version
176
273
 
177
274
  def handle_update(self):
178
275
  log_info("Executing OOTB update...")
@@ -183,8 +280,7 @@ class GuardService(win32serviceutil.ServiceFramework):
183
280
  update_command = f"{self.pip_command_base} install --upgrade --no-cache-dir {_PACKAGE_NAME}"
184
281
  log_info(f"Running update command: {update_command}")
185
282
  try:
186
- # Run update command
187
- result = subprocess.run(update_command, shell=True, capture_output=True, text=True, check=True, timeout=300)
283
+ result = subprocess.run(update_command, shell=True, capture_output=True, text=True, check=True, timeout=300, encoding='utf-8')
188
284
  log_info(f"Update successful: \nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
189
285
  return "success"
190
286
  except subprocess.CalledProcessError as e:
@@ -199,15 +295,12 @@ class GuardService(win32serviceutil.ServiceFramework):
199
295
 
200
296
 
201
297
  def _get_ootb_processes(self, target_user="all_active"):
202
- """Finds OOTB processes, optionally filtering by username."""
203
298
  ootb_procs = []
204
299
  target_pid_list = []
205
300
  try:
206
- # Get a list of usernames we are interested in
207
301
  target_users = set()
208
302
  if target_user == "all_active":
209
303
  for user_session in psutil.users():
210
- # Normalize username (remove domain if present)
211
304
  username = user_session.name.split('\\')[-1]
212
305
  target_users.add(username.lower())
213
306
  else:
@@ -215,26 +308,25 @@ class GuardService(win32serviceutil.ServiceFramework):
215
308
 
216
309
  log_info(f"Searching for OOTB processes for users: {target_users}")
217
310
 
218
- for proc in psutil.process_iter(['pid', 'name', 'username', 'cmdline']):
311
+ python_exe_path = self.python_exe.strip('"') # Get unquoted path for comparison
312
+
313
+ for proc in psutil.process_iter(['pid', 'name', 'username', 'cmdline', 'exe']):
219
314
  try:
220
315
  pinfo = proc.info
221
- # Normalize process username
222
316
  proc_username = pinfo['username']
223
317
  if proc_username:
224
318
  proc_username = proc_username.split('\\')[-1].lower()
225
319
 
226
- # Check if process user is one of the targets
227
320
  if proc_username in target_users:
228
- # Check if command line matches our OOTB app pattern
229
321
  cmdline = ' '.join(pinfo['cmdline']) if pinfo['cmdline'] else ''
230
- # Simple check: does it contain python executable and the module name?
231
- if (self.python_exe.strip('"') in cmdline) and (_OOTB_MODULE in cmdline):
322
+ # Check if the process executable matches our python path AND module is in cmdline
323
+ if pinfo['exe'] and pinfo['exe'] == python_exe_path and _OOTB_MODULE in cmdline:
232
324
  log_info(f"Found matching OOTB process: PID={pinfo['pid']}, User={pinfo['username']}, Cmd={cmdline}")
233
325
  ootb_procs.append(proc)
234
326
  target_pid_list.append(pinfo['pid'])
235
327
 
236
328
  except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
237
- continue # Process might have died or we lack permissions
329
+ continue
238
330
  log_info(f"Found {len(ootb_procs)} OOTB process(es) matching criteria: {target_pid_list}")
239
331
  except Exception as e:
240
332
  log_error(f"Error enumerating processes: {e}", exc_info=True)
@@ -254,9 +346,9 @@ class GuardService(win32serviceutil.ServiceFramework):
254
346
  try:
255
347
  username = proc.info.get('username', 'unknown_user')
256
348
  log_info(f"Terminating process PID={proc.pid}, User={username}")
257
- proc.terminate() # Ask nicely first
349
+ proc.terminate()
258
350
  try:
259
- proc.wait(timeout=3) # Wait a bit
351
+ proc.wait(timeout=3)
260
352
  log_info(f"Process PID={proc.pid} terminated successfully.")
261
353
  stopped_count += 1
262
354
  except psutil.TimeoutExpired:
@@ -265,7 +357,7 @@ class GuardService(win32serviceutil.ServiceFramework):
265
357
  stopped_count += 1
266
358
  except psutil.NoSuchProcess:
267
359
  log_info(f"Process PID={proc.pid} already terminated.")
268
- stopped_count +=1 # Count it if it disappeared
360
+ stopped_count +=1
269
361
  except psutil.AccessDenied:
270
362
  log_error(f"Access denied trying to terminate process PID={proc.pid}. Service might lack privileges?")
271
363
  except Exception as e:
@@ -283,15 +375,13 @@ class GuardService(win32serviceutil.ServiceFramework):
283
375
 
284
376
  try:
285
377
  sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
286
- active_sessions = {} # Store user: session_id for active sessions
378
+ active_sessions = {}
287
379
 
288
380
  for session in sessions:
289
- # Look for Active sessions, potentially disconnected ones too?
290
- # For now, only WTSActive
291
381
  if session['State'] == win32ts.WTSActive:
292
382
  try:
293
383
  user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
294
- if user: # Filter out system sessions etc.
384
+ if user:
295
385
  normalized_user = user.lower()
296
386
  active_sessions[normalized_user] = session['SessionId']
297
387
  except Exception as query_err:
@@ -299,7 +389,7 @@ class GuardService(win32serviceutil.ServiceFramework):
299
389
 
300
390
  log_info(f"Found active user sessions: {active_sessions}")
301
391
 
302
- target_session_map = {} # user:session_id
392
+ target_session_map = {}
303
393
  if target_user == "all_active":
304
394
  target_session_map = active_sessions
305
395
  else:
@@ -314,7 +404,6 @@ class GuardService(win32serviceutil.ServiceFramework):
314
404
  log_info("No target user sessions found to start OOTB in.")
315
405
  return "failed_no_target_sessions"
316
406
 
317
- # Check if OOTB is already running for the target users
318
407
  running_procs = self._get_ootb_processes(target_user)
319
408
  users_already_running = set()
320
409
  for proc in running_procs:
@@ -323,55 +412,33 @@ class GuardService(win32serviceutil.ServiceFramework):
323
412
  if proc_username:
324
413
  users_already_running.add(proc_username.split('\\')[-1].lower())
325
414
  except Exception:
326
- pass # Ignore errors getting username here
415
+ pass
327
416
 
328
417
  log_info(f"Users already running OOTB: {users_already_running}")
329
418
 
330
419
  for user, session_id in target_session_map.items():
331
- token = None # Ensure token is reset/defined
420
+ token = None
332
421
  try:
333
422
  if user in users_already_running:
334
423
  log_info(f"OOTB already seems to be running for user '{user}'. Skipping start.")
335
424
  continue
336
425
 
337
426
  log_info(f"Attempting to start OOTB for user '{user}' in session {session_id}...")
338
-
339
- # Get user token
340
427
  token = win32ts.WTSQueryUserToken(session_id)
341
-
342
- # Create environment block for the user
343
428
  env = win32profile.CreateEnvironmentBlock(token, False)
344
-
345
- # Create startup info
346
429
  startup = win32process.STARTUPINFO()
347
430
  startup.dwFlags = win32process.STARTF_USESHOWWINDOW
348
- # Attempt to show window on user's desktop
349
- # Requires Service to have "Allow service to interact with desktop" checked
350
- # AND Interactive Services Detection service running (often disabled now).
351
- # May be better to run hidden (SW_HIDE) or default.
352
- startup.wShowWindow = win32con.SW_SHOW
353
- startup.lpDesktop = 'winsta0\\default' # Try targeting default interactive desktop
354
-
355
- # Create process as user
356
- # Needs SeAssignPrimaryTokenPrivilege, SeIncreaseQuotaPrivilege for service account.
431
+ startup.wShowWindow = win32con.SW_SHOW # Try showing the window
432
+ startup.lpDesktop = 'winsta0\\default'
357
433
  creation_flags = win32process.CREATE_NEW_CONSOLE | win32process.CREATE_UNICODE_ENVIRONMENT
358
434
 
359
435
  hProcess, hThread, dwPid, dwTid = win32process.CreateProcessAsUser(
360
- token, # User token
361
- self.python_exe, # Application name (python executable)
362
- self.ootb_command, # Command line
363
- None, # Process attributes
364
- None, # Thread attributes
365
- False, # Inherit handles
366
- creation_flags, # Creation flags
367
- env, # Environment
368
- None, # Current directory (use default)
369
- startup # Startup info
436
+ token, self.python_exe, self.ootb_command,
437
+ None, None, False, creation_flags, env, None, startup
370
438
  )
371
439
  log_info(f"Successfully started OOTB for user '{user}' (PID: {dwPid}).")
372
440
  started_count += 1
373
441
  target_users_started.add(user)
374
- # Close handles immediately
375
442
  win32api.CloseHandle(hProcess)
376
443
  win32api.CloseHandle(hThread)
377
444
 
@@ -379,12 +446,10 @@ class GuardService(win32serviceutil.ServiceFramework):
379
446
  log_error(f"Failed to start OOTB for user '{user}' in session {session_id}: {proc_err}", exc_info=True)
380
447
  users_failed_to_start.add(user)
381
448
  finally:
382
- # Ensure token handle is always closed if obtained
383
449
  if token:
384
450
  try: win32api.CloseHandle(token)
385
451
  except: pass
386
452
 
387
-
388
453
  log_info(f"Finished starting OOTB. Started {started_count} new instance(s). Failed for users: {users_failed_to_start or 'None'}")
389
454
  if users_failed_to_start:
390
455
  return f"partial_success_started_{started_count}_failed_for_{len(users_failed_to_start)}"
@@ -397,26 +462,20 @@ class GuardService(win32serviceutil.ServiceFramework):
397
462
  log_error(f"Error during start OOTB process: {e}", exc_info=True)
398
463
  return "failed_exception"
399
464
 
400
-
401
- # This block is essential for the service framework to handle
402
- # command-line arguments like 'install', 'start', 'stop', 'remove', 'debug'.
465
+ # --- Main Execution Block ---
403
466
  if __name__ == '__main__':
404
- # Add logic to allow debugging from command line easily
405
467
  if len(sys.argv) > 1 and sys.argv[1] == 'debug':
406
468
  log_info("Starting service in debug mode...")
407
- service_instance = GuardService(sys.argv)
408
- service_instance.is_running = True # Ensure loop runs
469
+ print(f"Running Flask server via Waitress on {_LISTEN_HOST}:{_LISTEN_PORT} for debugging...")
470
+ print("Service logic (command processing) will NOT run in this mode.")
471
+ print("Use this primarily to test the '/command' endpoint receiving POSTs.")
472
+ print("Press Ctrl+C to stop.")
409
473
  try:
410
- # Run the main loop directly for debugging
411
- service_instance.main_loop()
412
- # Simulate stop signal for clean exit in debug
413
- # service_instance.SvcStop() # Or let it run until Ctrl+C?
474
+ serve(flask_app, host=_LISTEN_HOST, port=_LISTEN_PORT, threads=1)
414
475
  except KeyboardInterrupt:
415
- log_info("Debug mode interrupted by user.")
416
- service_instance.SvcStop() # Attempt clean stop
417
- log_info("Debug mode finished.")
476
+ print("\nDebug server stopped.")
477
+
418
478
  elif len(sys.argv) == 1:
419
- # Called without arguments, run as a service instance via SCM
420
479
  try:
421
480
  servicemanager.Initialize()
422
481
  servicemanager.PrepareToHostSingle(GuardService)
@@ -424,13 +483,11 @@ if __name__ == '__main__':
424
483
  except win32service.error as details:
425
484
  import winerror
426
485
  if details.winerror == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
427
- print(f"Error: Cannot connect to Service Control Manager.")
486
+ print(f"Error: Not started by SCM.")
428
487
  print(f"Use 'python {os.path.basename(__file__)} install|start|stop|remove|debug'")
429
488
  else:
430
489
  print(f"Error preparing service: {details}")
431
490
  except Exception as e:
432
491
  print(f"Unexpected error initializing service: {e}")
433
-
434
492
  else:
435
- # Called with install/start/stop/remove args, let ServiceFramework handle them
436
493
  win32serviceutil.HandleCommandLine(GuardService)
@@ -0,0 +1 @@
1
+ # This file makes the 'preparation' directory a Python package
@@ -0,0 +1,65 @@
1
+ # src/computer_use_ootb_internal/preparation/star_rail_prepare.py
2
+ import time
3
+ import platform
4
+ import pyautogui
5
+ import webbrowser
6
+ import logging # Use logging instead of print for better practice
7
+
8
+ # Set up logging for this module if needed, or rely on root logger
9
+ log = logging.getLogger(__name__)
10
+
11
+ def run_preparation(state):
12
+ """
13
+ Performs environment preparation specific to Star Rail on Windows.
14
+ Opens the specified URL in Edge and performs initial clicks.
15
+ """
16
+ if platform.system() != "Windows":
17
+ log.info("Star Rail preparation skipped: Not running on Windows.")
18
+ return
19
+
20
+ log.info("Star Rail preparation: Starting environment setup on Windows...")
21
+ url = "https://sr.mihoyo.com/cloud/#/" # Consider making this configurable later
22
+ browser_opened = False
23
+ try:
24
+ # Use only webbrowser.open
25
+ log.info(f"Attempting to open {url} using webbrowser.open()...")
26
+ if webbrowser.open(url):
27
+ log.info(f"Successfully requested browser to open {url} via webbrowser.open().")
28
+ browser_opened = True
29
+ else:
30
+ log.warning("webbrowser.open() returned False, indicating potential failure.")
31
+
32
+ if not browser_opened:
33
+ log.error("Failed to confirm browser opening via webbrowser.open(). Will still attempt clicks.")
34
+
35
+ # Add pyautogui click after attempting to open the browser
36
+ log.info("Proceeding with pyautogui actions...")
37
+ time.sleep(5) # Wait time for the browser to load
38
+
39
+ # Get screen size
40
+ screen_width, screen_height = pyautogui.size()
41
+ log.info(f"Detected screen size: {screen_width}x{screen_height}")
42
+
43
+ # Calculate click coordinates based on a reference resolution (e.g., 1280x720)
44
+ # TODO: Make these coordinates more robust or configurable
45
+ click_x = int(screen_width * (1036 / 1280))
46
+ click_y = int(screen_height * (500 / 720))
47
+ log.info(f"Calculated click coordinates: ({click_x}, {click_y})")
48
+
49
+ # Disable failsafe before clicking
50
+ pyautogui.FAILSAFE = False
51
+ log.info("PyAutoGUI failsafe temporarily disabled.")
52
+
53
+ log.info(f"Clicking at coordinates: ({click_x}, {click_y})")
54
+ pyautogui.click(click_x, click_y)
55
+ time.sleep(2)
56
+ pyautogui.click(click_x, click_y) # Double click?
57
+
58
+ log.info("Star Rail preparation clicks completed.")
59
+
60
+ except Exception as e:
61
+ log.error(f"Error during Star Rail preparation (browser/click): {e}", exc_info=True)
62
+ finally:
63
+ # Ensure failsafe is re-enabled
64
+ pyautogui.FAILSAFE = True
65
+ log.info("PyAutoGUI failsafe re-enabled.")
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: computer-use-ootb-internal
3
- Version: 0.0.111
3
+ Version: 0.0.113
4
4
  Summary: Computer Use OOTB
5
5
  Author-email: Siyuan Hu <siyuan.hu.sg@gmail.com>
6
6
  Requires-Python: >=3.11
7
7
  Requires-Dist: anthropic[bedrock,vertex]>=0.37.1
8
8
  Requires-Dist: boto3>=1.28.57
9
+ Requires-Dist: flask>=2.0
9
10
  Requires-Dist: google-auth<3,>=2
10
11
  Requires-Dist: gradio>=5.6.0
11
12
  Requires-Dist: jsonschema==4.22.0
@@ -21,6 +22,7 @@ Requires-Dist: screeninfo
21
22
  Requires-Dist: streamlit>=1.38.0
22
23
  Requires-Dist: textdistance
23
24
  Requires-Dist: uiautomation; sys_platform == 'win32'
25
+ Requires-Dist: waitress>=2.0
24
26
  Provides-Extra: dev
25
27
  Requires-Dist: pre-commit>=3.8.0; extra == 'dev'
26
28
  Requires-Dist: pytest-asyncio>=0.23.6; extra == 'dev'
@@ -1,9 +1,9 @@
1
1
  computer_use_ootb_internal/README.md,sha256=FxpW95lyub2iX73ZDfK6ML7SdEKg060H5I6Grub7li4,31
2
2
  computer_use_ootb_internal/__init__.py,sha256=Nqnn8clbgv-5l0PgxcTOldg8mkMKrFn4TvPL-rYUUGg,1
3
- computer_use_ootb_internal/app_teachmode.py,sha256=rNJ8o6EHhQLsFwHgDSUHghFaKjheGW957NHPfRXsewY,18836
3
+ computer_use_ootb_internal/app_teachmode.py,sha256=U0vHAEHeDMDioYatLQkzesT1Hy0aI6kKjhAlV86zxfc,17902
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=bhxgDD2bYZ7g12gf-2o_7o0irEUovc5bFXLOpTnX2Gw,20570
6
+ computer_use_ootb_internal/guard_service.py,sha256=HU9_nM2Jw38Us7vo49qSc-jBeMkCZFoXiILBx1VvOoc,22574
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
9
  computer_use_ootb_internal/service_manager.py,sha256=sesbSUBBqm6W77Dykaw-HGARU-yNdK9DqOHPiR2TbkE,6920
@@ -32,7 +32,9 @@ computer_use_ootb_internal/computer_use_demo/tools/computer_marbot.py,sha256=zZu
32
32
  computer_use_ootb_internal/computer_use_demo/tools/edit.py,sha256=b0PwUitxckHCQqFP3ZwlthWdqNkn7WETeTHeB6-o98c,11486
33
33
  computer_use_ootb_internal/computer_use_demo/tools/run.py,sha256=xhXdnBK1di9muaO44CEirL9hpGy3NmKbjfMpyeVmn8Y,1595
34
34
  computer_use_ootb_internal/computer_use_demo/tools/screen_capture.py,sha256=L8qfvtUkPPQGt92N-2Zfw5ZTDBzLsDps39uMnX3_uSA,6857
35
- computer_use_ootb_internal-0.0.111.dist-info/METADATA,sha256=Ybw5i4bp5sY5a5bT6SWWymP-jXV87juXL5YQxFM5pvY,993
36
- computer_use_ootb_internal-0.0.111.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- computer_use_ootb_internal-0.0.111.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
38
- computer_use_ootb_internal-0.0.111.dist-info/RECORD,,
35
+ computer_use_ootb_internal/preparation/__init__.py,sha256=AgtGHcBpiTkxJjF0xwcs3yyQ6SyUvhL3G0vD2XO-zJw,63
36
+ computer_use_ootb_internal/preparation/star_rail_prepare.py,sha256=s1VWszcTnJAKxqCHFlaOEwPkqVSrkiFx_yKpWSnSbHs,2649
37
+ computer_use_ootb_internal-0.0.113.dist-info/METADATA,sha256=NHF7noSgn8aC5JpMIfId8afNaTZI_XssG0HNCW2nPPI,1048
38
+ computer_use_ootb_internal-0.0.113.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
39
+ computer_use_ootb_internal-0.0.113.dist-info/entry_points.txt,sha256=bXfyAU_qq-G1EiEgAQEioXvgEdRCFxaTooqdDD9Y4OA,258
40
+ computer_use_ootb_internal-0.0.113.dist-info/RECORD,,