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.
- computer_use_ootb_internal/app_teachmode.py +44 -62
- computer_use_ootb_internal/guard_service.py +201 -144
- computer_use_ootb_internal/preparation/__init__.py +1 -0
- computer_use_ootb_internal/preparation/star_rail_prepare.py +65 -0
- {computer_use_ootb_internal-0.0.111.dist-info → computer_use_ootb_internal-0.0.113.dist-info}/METADATA +3 -1
- {computer_use_ootb_internal-0.0.111.dist-info → computer_use_ootb_internal-0.0.113.dist-info}/RECORD +8 -6
- {computer_use_ootb_internal-0.0.111.dist-info → computer_use_ootb_internal-0.0.113.dist-info}/WHEEL +0 -0
- {computer_use_ootb_internal-0.0.111.dist-info → computer_use_ootb_internal-0.0.113.dist-info}/entry_points.txt +0 -0
@@ -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
|
-
#
|
96
|
+
# Set up logging for this module
|
97
|
+
log = logging.getLogger(__name__)
|
98
|
+
|
94
99
|
def prepare_environment(state):
|
95
|
-
"""
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
# 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
|
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
|
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
|
-
|
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
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
#
|
38
|
-
|
39
|
-
|
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
|
-
|
108
|
+
thread_name = threading.current_thread().name
|
109
|
+
full_msg = f"[{thread_name}] {msg}"
|
110
|
+
logging.info(full_msg)
|
74
111
|
try:
|
75
|
-
|
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}")
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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")
|
149
|
-
|
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
|
162
|
-
status = "
|
230
|
+
log_error(f"Unknown action in queue: {action}")
|
231
|
+
status = "failed_unknown_action"
|
163
232
|
except Exception as handler_ex:
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
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
|
268
|
+
log_error(f"Failed to report status for command {command_id}: {e}")
|
173
269
|
except Exception as e:
|
174
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
231
|
-
if
|
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
|
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()
|
349
|
+
proc.terminate()
|
258
350
|
try:
|
259
|
-
proc.wait(timeout=3)
|
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
|
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 = {}
|
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:
|
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 = {}
|
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
|
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
|
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
|
-
|
349
|
-
|
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,
|
361
|
-
|
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
|
-
|
408
|
-
|
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
|
-
|
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
|
-
|
416
|
-
|
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:
|
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.
|
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'
|
{computer_use_ootb_internal-0.0.111.dist-info → computer_use_ootb_internal-0.0.113.dist-info}/RECORD
RENAMED
@@ -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=
|
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=
|
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
|
36
|
-
computer_use_ootb_internal
|
37
|
-
computer_use_ootb_internal-0.0.
|
38
|
-
computer_use_ootb_internal-0.0.
|
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,,
|
{computer_use_ootb_internal-0.0.111.dist-info → computer_use_ootb_internal-0.0.113.dist-info}/WHEEL
RENAMED
File without changes
|
File without changes
|