computer-use-ootb-internal 0.0.109.post2__py3-none-any.whl → 0.0.111__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/__init__.py +1 -0
- computer_use_ootb_internal/app_teachmode.py +483 -482
- computer_use_ootb_internal/computer_use_demo/animation/test_animation.py +39 -39
- computer_use_ootb_internal/guard_service.py +436 -0
- computer_use_ootb_internal/service_manager.py +142 -0
- {computer_use_ootb_internal-0.0.109.post2.dist-info → computer_use_ootb_internal-0.0.111.dist-info}/METADATA +7 -8
- {computer_use_ootb_internal-0.0.109.post2.dist-info → computer_use_ootb_internal-0.0.111.dist-info}/RECORD +9 -6
- computer_use_ootb_internal-0.0.111.dist-info/entry_points.txt +4 -0
- computer_use_ootb_internal-0.0.109.post2.dist-info/entry_points.txt +0 -2
- {computer_use_ootb_internal-0.0.109.post2.dist-info → computer_use_ootb_internal-0.0.111.dist-info}/WHEEL +0 -0
@@ -1,40 +1,40 @@
|
|
1
|
-
"""
|
2
|
-
Test script to verify cursor animation is working
|
3
|
-
"""
|
4
|
-
import asyncio
|
5
|
-
import sys
|
6
|
-
import time
|
7
|
-
from pathlib import Path
|
8
|
-
from computer_use_ootb_internal.computer_use_demo.tools.computer import ComputerTool
|
9
|
-
|
10
|
-
async def test_animations():
|
11
|
-
|
12
|
-
# Initialize the computer tool
|
13
|
-
computer = ComputerTool()
|
14
|
-
|
15
|
-
# Test mouse move animation
|
16
|
-
print("Testing mouse move animation...")
|
17
|
-
await computer(action="mouse_move_windll", coordinate=(500, 500))
|
18
|
-
print("Waiting 2 seconds...")
|
19
|
-
await asyncio.sleep(2)
|
20
|
-
|
21
|
-
# Test click animation
|
22
|
-
print("Testing click animation...")
|
23
|
-
await computer(action="left_click_windll", coordinate=(700, 300))
|
24
|
-
print("Waiting 2 seconds...")
|
25
|
-
await asyncio.sleep(2)
|
26
|
-
|
27
|
-
# Test another move
|
28
|
-
print("Testing move and click sequence...")
|
29
|
-
await computer(action="mouse_move_windll", coordinate=(300, 300))
|
30
|
-
await asyncio.sleep(1)
|
31
|
-
await computer(action="left_click_windll", coordinate=(300, 300))
|
32
|
-
|
33
|
-
# Wait for animations to complete
|
34
|
-
print("Waiting for animations to complete...")
|
35
|
-
await asyncio.sleep(3)
|
36
|
-
|
37
|
-
print("Test completed")
|
38
|
-
|
39
|
-
if __name__ == "__main__":
|
1
|
+
"""
|
2
|
+
Test script to verify cursor animation is working
|
3
|
+
"""
|
4
|
+
import asyncio
|
5
|
+
import sys
|
6
|
+
import time
|
7
|
+
from pathlib import Path
|
8
|
+
from computer_use_ootb_internal.computer_use_demo.tools.computer import ComputerTool
|
9
|
+
|
10
|
+
async def test_animations():
|
11
|
+
|
12
|
+
# Initialize the computer tool
|
13
|
+
computer = ComputerTool()
|
14
|
+
|
15
|
+
# Test mouse move animation
|
16
|
+
print("Testing mouse move animation...")
|
17
|
+
await computer(action="mouse_move_windll", coordinate=(500, 500))
|
18
|
+
print("Waiting 2 seconds...")
|
19
|
+
await asyncio.sleep(2)
|
20
|
+
|
21
|
+
# Test click animation
|
22
|
+
print("Testing click animation...")
|
23
|
+
await computer(action="left_click_windll", coordinate=(700, 300))
|
24
|
+
print("Waiting 2 seconds...")
|
25
|
+
await asyncio.sleep(2)
|
26
|
+
|
27
|
+
# Test another move
|
28
|
+
print("Testing move and click sequence...")
|
29
|
+
await computer(action="mouse_move_windll", coordinate=(300, 300))
|
30
|
+
await asyncio.sleep(1)
|
31
|
+
await computer(action="left_click_windll", coordinate=(300, 300))
|
32
|
+
|
33
|
+
# Wait for animations to complete
|
34
|
+
print("Waiting for animations to complete...")
|
35
|
+
await asyncio.sleep(3)
|
36
|
+
|
37
|
+
print("Test completed")
|
38
|
+
|
39
|
+
if __name__ == "__main__":
|
40
40
|
asyncio.run(test_animations())
|
@@ -0,0 +1,436 @@
|
|
1
|
+
# src/computer_use_ootb_internal/guard_service.py
|
2
|
+
import sys
|
3
|
+
import os
|
4
|
+
import time
|
5
|
+
import logging
|
6
|
+
import subprocess
|
7
|
+
import pathlib
|
8
|
+
import ctypes
|
9
|
+
import requests # For server polling
|
10
|
+
import servicemanager # From pywin32
|
11
|
+
import win32serviceutil # From pywin32
|
12
|
+
import win32service # From pywin32
|
13
|
+
import win32event # From pywin32
|
14
|
+
import win32api # From pywin32
|
15
|
+
import win32process # From pywin32
|
16
|
+
import win32security # From pywin32
|
17
|
+
import win32profile # From pywin32
|
18
|
+
import win32ts # From pywin32 (Terminal Services API)
|
19
|
+
import win32con # Added import
|
20
|
+
import psutil # For process/user info
|
21
|
+
|
22
|
+
# --- Configuration ---
|
23
|
+
# Internal service name
|
24
|
+
_SERVICE_NAME = "OOTBGuardService"
|
25
|
+
# Display name in Windows Services MMC
|
26
|
+
_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
|
30
|
+
_PACKAGE_NAME = "computer-use-ootb-internal"
|
31
|
+
# Main module to start/stop for users
|
32
|
+
_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"
|
41
|
+
# --- End Configuration ---
|
42
|
+
|
43
|
+
# Ensure log directory exists
|
44
|
+
_LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
45
|
+
|
46
|
+
logging.basicConfig(
|
47
|
+
filename=_LOG_FILE,
|
48
|
+
level=logging.INFO,
|
49
|
+
format='%(asctime)s %(levelname)s %(message)s'
|
50
|
+
)
|
51
|
+
|
52
|
+
def get_python_executable():
|
53
|
+
"""Gets the quoted path to the current python executable."""
|
54
|
+
python_exe = sys.executable
|
55
|
+
if " " in python_exe and not python_exe.startswith('"'):
|
56
|
+
python_exe = f'"{python_exe}"'
|
57
|
+
return python_exe
|
58
|
+
|
59
|
+
def get_pip_executable():
|
60
|
+
"""Tries to locate the pip executable in the same environment."""
|
61
|
+
python_path = pathlib.Path(sys.executable)
|
62
|
+
pip_path = python_path.parent / "Scripts" / "pip.exe"
|
63
|
+
if pip_path.exists():
|
64
|
+
pip_exe = str(pip_path)
|
65
|
+
if " " in pip_exe and not pip_exe.startswith('"'):
|
66
|
+
pip_exe = f'"{pip_exe}"'
|
67
|
+
return pip_exe
|
68
|
+
else:
|
69
|
+
logging.warning("pip.exe not found in Scripts directory. Falling back to 'python -m pip'.")
|
70
|
+
return f"{get_python_executable()} -m pip"
|
71
|
+
|
72
|
+
def log_info(msg):
|
73
|
+
logging.info(msg)
|
74
|
+
try:
|
75
|
+
servicemanager.LogInfoMsg(str(msg)) # Also log to Windows Event Log Application channel
|
76
|
+
except Exception as e:
|
77
|
+
logging.warning(f"Could not write to Windows Event Log: {e}") # Avoid crashing service if event log fails
|
78
|
+
|
79
|
+
def log_error(msg, exc_info=False):
|
80
|
+
logging.error(msg, exc_info=exc_info)
|
81
|
+
try:
|
82
|
+
servicemanager.LogErrorMsg(str(msg))
|
83
|
+
except Exception as e:
|
84
|
+
logging.warning(f"Could not write error to Windows Event Log: {e}")
|
85
|
+
|
86
|
+
class GuardService(win32serviceutil.ServiceFramework):
|
87
|
+
_svc_name_ = _SERVICE_NAME
|
88
|
+
_svc_display_name_ = _SERVICE_DISPLAY_NAME
|
89
|
+
_svc_description_ = _SERVICE_DESCRIPTION
|
90
|
+
|
91
|
+
def __init__(self, args):
|
92
|
+
win32serviceutil.ServiceFramework.__init__(self, args)
|
93
|
+
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
|
94
|
+
self.is_running = True
|
95
|
+
self.python_exe = get_python_executable()
|
96
|
+
self.pip_command_base = get_pip_executable()
|
97
|
+
self.ootb_command = f"{self.python_exe} -m {_OOTB_MODULE}"
|
98
|
+
self.session = requests.Session() # Reuse session for polling
|
99
|
+
|
100
|
+
def SvcStop(self):
|
101
|
+
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
102
|
+
win32event.SetEvent(self.hWaitStop)
|
103
|
+
self.is_running = False
|
104
|
+
log_info(f"{_SERVICE_NAME} is stopping.")
|
105
|
+
|
106
|
+
def SvcDoRun(self):
|
107
|
+
servicemanager.LogMsg(servicemanager.EVENTLOG_INFORMATION_TYPE,
|
108
|
+
servicemanager.PYS_SERVICE_STARTED,
|
109
|
+
(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.")
|
113
|
+
|
114
|
+
|
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
|
+
|
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.")
|
129
|
+
|
130
|
+
|
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:
|
147
|
+
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
|
150
|
+
|
151
|
+
log_info(f"Processing Command ID {command_id}: action='{action}', target='{target}'")
|
152
|
+
status = "failed" # Default status
|
153
|
+
try:
|
154
|
+
if action == "update":
|
155
|
+
status = self.handle_update()
|
156
|
+
elif action == "stop_ootb":
|
157
|
+
status = self.handle_stop(target)
|
158
|
+
elif action == "start_ootb":
|
159
|
+
status = self.handle_start(target)
|
160
|
+
else:
|
161
|
+
log_error(f"Unknown action received: {action}")
|
162
|
+
status = "unknown_action"
|
163
|
+
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
|
+
|
171
|
+
except requests.exceptions.RequestException as e:
|
172
|
+
log_error(f"Failed to poll server: {e}")
|
173
|
+
except Exception as e:
|
174
|
+
log_error(f"Error processing server commands: {e}", exc_info=True)
|
175
|
+
|
176
|
+
|
177
|
+
def handle_update(self):
|
178
|
+
log_info("Executing OOTB update...")
|
179
|
+
if not self.pip_command_base:
|
180
|
+
log_error("Cannot update: pip command not found.")
|
181
|
+
return "failed_pip_not_found"
|
182
|
+
|
183
|
+
update_command = f"{self.pip_command_base} install --upgrade --no-cache-dir {_PACKAGE_NAME}"
|
184
|
+
log_info(f"Running update command: {update_command}")
|
185
|
+
try:
|
186
|
+
# Run update command
|
187
|
+
result = subprocess.run(update_command, shell=True, capture_output=True, text=True, check=True, timeout=300)
|
188
|
+
log_info(f"Update successful: \nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}")
|
189
|
+
return "success"
|
190
|
+
except subprocess.CalledProcessError as e:
|
191
|
+
log_error(f"Update failed (Exit Code {e.returncode}):\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}")
|
192
|
+
return f"failed_exit_{e.returncode}"
|
193
|
+
except subprocess.TimeoutExpired:
|
194
|
+
log_error(f"Update command timed out.")
|
195
|
+
return "failed_timeout"
|
196
|
+
except Exception as e:
|
197
|
+
log_error(f"Unexpected error during update: {e}", exc_info=True)
|
198
|
+
return "failed_exception"
|
199
|
+
|
200
|
+
|
201
|
+
def _get_ootb_processes(self, target_user="all_active"):
|
202
|
+
"""Finds OOTB processes, optionally filtering by username."""
|
203
|
+
ootb_procs = []
|
204
|
+
target_pid_list = []
|
205
|
+
try:
|
206
|
+
# Get a list of usernames we are interested in
|
207
|
+
target_users = set()
|
208
|
+
if target_user == "all_active":
|
209
|
+
for user_session in psutil.users():
|
210
|
+
# Normalize username (remove domain if present)
|
211
|
+
username = user_session.name.split('\\')[-1]
|
212
|
+
target_users.add(username.lower())
|
213
|
+
else:
|
214
|
+
target_users.add(target_user.lower())
|
215
|
+
|
216
|
+
log_info(f"Searching for OOTB processes for users: {target_users}")
|
217
|
+
|
218
|
+
for proc in psutil.process_iter(['pid', 'name', 'username', 'cmdline']):
|
219
|
+
try:
|
220
|
+
pinfo = proc.info
|
221
|
+
# Normalize process username
|
222
|
+
proc_username = pinfo['username']
|
223
|
+
if proc_username:
|
224
|
+
proc_username = proc_username.split('\\')[-1].lower()
|
225
|
+
|
226
|
+
# Check if process user is one of the targets
|
227
|
+
if proc_username in target_users:
|
228
|
+
# Check if command line matches our OOTB app pattern
|
229
|
+
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):
|
232
|
+
log_info(f"Found matching OOTB process: PID={pinfo['pid']}, User={pinfo['username']}, Cmd={cmdline}")
|
233
|
+
ootb_procs.append(proc)
|
234
|
+
target_pid_list.append(pinfo['pid'])
|
235
|
+
|
236
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
237
|
+
continue # Process might have died or we lack permissions
|
238
|
+
log_info(f"Found {len(ootb_procs)} OOTB process(es) matching criteria: {target_pid_list}")
|
239
|
+
except Exception as e:
|
240
|
+
log_error(f"Error enumerating processes: {e}", exc_info=True)
|
241
|
+
return ootb_procs
|
242
|
+
|
243
|
+
|
244
|
+
def handle_stop(self, target_user="all_active"):
|
245
|
+
log_info(f"Executing stop OOTB for target '{target_user}'...")
|
246
|
+
stopped_count = 0
|
247
|
+
procs_to_stop = self._get_ootb_processes(target_user)
|
248
|
+
|
249
|
+
if not procs_to_stop:
|
250
|
+
log_info("No running OOTB processes found for target.")
|
251
|
+
return "no_process_found"
|
252
|
+
|
253
|
+
for proc in procs_to_stop:
|
254
|
+
try:
|
255
|
+
username = proc.info.get('username', 'unknown_user')
|
256
|
+
log_info(f"Terminating process PID={proc.pid}, User={username}")
|
257
|
+
proc.terminate() # Ask nicely first
|
258
|
+
try:
|
259
|
+
proc.wait(timeout=3) # Wait a bit
|
260
|
+
log_info(f"Process PID={proc.pid} terminated successfully.")
|
261
|
+
stopped_count += 1
|
262
|
+
except psutil.TimeoutExpired:
|
263
|
+
log_warning(f"Process PID={proc.pid} did not terminate gracefully, killing.")
|
264
|
+
proc.kill()
|
265
|
+
stopped_count += 1
|
266
|
+
except psutil.NoSuchProcess:
|
267
|
+
log_info(f"Process PID={proc.pid} already terminated.")
|
268
|
+
stopped_count +=1 # Count it if it disappeared
|
269
|
+
except psutil.AccessDenied:
|
270
|
+
log_error(f"Access denied trying to terminate process PID={proc.pid}. Service might lack privileges?")
|
271
|
+
except Exception as e:
|
272
|
+
log_error(f"Error stopping process PID={proc.pid}: {e}", exc_info=True)
|
273
|
+
|
274
|
+
log_info(f"Finished stopping OOTB. Terminated {stopped_count} process(es).")
|
275
|
+
return f"success_stopped_{stopped_count}"
|
276
|
+
|
277
|
+
|
278
|
+
def handle_start(self, target_user="all_active"):
|
279
|
+
log_info(f"Executing start OOTB for target '{target_user}'...")
|
280
|
+
started_count = 0
|
281
|
+
target_users_started = set()
|
282
|
+
users_failed_to_start = set()
|
283
|
+
|
284
|
+
try:
|
285
|
+
sessions = win32ts.WTSEnumerateSessions(win32ts.WTS_CURRENT_SERVER_HANDLE)
|
286
|
+
active_sessions = {} # Store user: session_id for active sessions
|
287
|
+
|
288
|
+
for session in sessions:
|
289
|
+
# Look for Active sessions, potentially disconnected ones too?
|
290
|
+
# For now, only WTSActive
|
291
|
+
if session['State'] == win32ts.WTSActive:
|
292
|
+
try:
|
293
|
+
user = win32ts.WTSQuerySessionInformation(win32ts.WTS_CURRENT_SERVER_HANDLE, session['SessionId'], win32ts.WTSUserName)
|
294
|
+
if user: # Filter out system sessions etc.
|
295
|
+
normalized_user = user.lower()
|
296
|
+
active_sessions[normalized_user] = session['SessionId']
|
297
|
+
except Exception as query_err:
|
298
|
+
log_warning(f"Could not query session {session['SessionId']}: {query_err}")
|
299
|
+
|
300
|
+
log_info(f"Found active user sessions: {active_sessions}")
|
301
|
+
|
302
|
+
target_session_map = {} # user:session_id
|
303
|
+
if target_user == "all_active":
|
304
|
+
target_session_map = active_sessions
|
305
|
+
else:
|
306
|
+
normalized_target = target_user.lower()
|
307
|
+
if normalized_target in active_sessions:
|
308
|
+
target_session_map[normalized_target] = active_sessions[normalized_target]
|
309
|
+
else:
|
310
|
+
log_warning(f"Target user '{target_user}' not found in active sessions.")
|
311
|
+
return "failed_user_not_active"
|
312
|
+
|
313
|
+
if not target_session_map:
|
314
|
+
log_info("No target user sessions found to start OOTB in.")
|
315
|
+
return "failed_no_target_sessions"
|
316
|
+
|
317
|
+
# Check if OOTB is already running for the target users
|
318
|
+
running_procs = self._get_ootb_processes(target_user)
|
319
|
+
users_already_running = set()
|
320
|
+
for proc in running_procs:
|
321
|
+
try:
|
322
|
+
proc_username = proc.info.get('username')
|
323
|
+
if proc_username:
|
324
|
+
users_already_running.add(proc_username.split('\\')[-1].lower())
|
325
|
+
except Exception:
|
326
|
+
pass # Ignore errors getting username here
|
327
|
+
|
328
|
+
log_info(f"Users already running OOTB: {users_already_running}")
|
329
|
+
|
330
|
+
for user, session_id in target_session_map.items():
|
331
|
+
token = None # Ensure token is reset/defined
|
332
|
+
try:
|
333
|
+
if user in users_already_running:
|
334
|
+
log_info(f"OOTB already seems to be running for user '{user}'. Skipping start.")
|
335
|
+
continue
|
336
|
+
|
337
|
+
log_info(f"Attempting to start OOTB for user '{user}' in session {session_id}...")
|
338
|
+
|
339
|
+
# Get user token
|
340
|
+
token = win32ts.WTSQueryUserToken(session_id)
|
341
|
+
|
342
|
+
# Create environment block for the user
|
343
|
+
env = win32profile.CreateEnvironmentBlock(token, False)
|
344
|
+
|
345
|
+
# Create startup info
|
346
|
+
startup = win32process.STARTUPINFO()
|
347
|
+
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.
|
357
|
+
creation_flags = win32process.CREATE_NEW_CONSOLE | win32process.CREATE_UNICODE_ENVIRONMENT
|
358
|
+
|
359
|
+
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
|
370
|
+
)
|
371
|
+
log_info(f"Successfully started OOTB for user '{user}' (PID: {dwPid}).")
|
372
|
+
started_count += 1
|
373
|
+
target_users_started.add(user)
|
374
|
+
# Close handles immediately
|
375
|
+
win32api.CloseHandle(hProcess)
|
376
|
+
win32api.CloseHandle(hThread)
|
377
|
+
|
378
|
+
except Exception as proc_err:
|
379
|
+
log_error(f"Failed to start OOTB for user '{user}' in session {session_id}: {proc_err}", exc_info=True)
|
380
|
+
users_failed_to_start.add(user)
|
381
|
+
finally:
|
382
|
+
# Ensure token handle is always closed if obtained
|
383
|
+
if token:
|
384
|
+
try: win32api.CloseHandle(token)
|
385
|
+
except: pass
|
386
|
+
|
387
|
+
|
388
|
+
log_info(f"Finished starting OOTB. Started {started_count} new instance(s). Failed for users: {users_failed_to_start or 'None'}")
|
389
|
+
if users_failed_to_start:
|
390
|
+
return f"partial_success_started_{started_count}_failed_for_{len(users_failed_to_start)}"
|
391
|
+
elif started_count > 0:
|
392
|
+
return f"success_started_{started_count}"
|
393
|
+
else:
|
394
|
+
return "no_action_needed_already_running"
|
395
|
+
|
396
|
+
except Exception as e:
|
397
|
+
log_error(f"Error during start OOTB process: {e}", exc_info=True)
|
398
|
+
return "failed_exception"
|
399
|
+
|
400
|
+
|
401
|
+
# This block is essential for the service framework to handle
|
402
|
+
# command-line arguments like 'install', 'start', 'stop', 'remove', 'debug'.
|
403
|
+
if __name__ == '__main__':
|
404
|
+
# Add logic to allow debugging from command line easily
|
405
|
+
if len(sys.argv) > 1 and sys.argv[1] == 'debug':
|
406
|
+
log_info("Starting service in debug mode...")
|
407
|
+
service_instance = GuardService(sys.argv)
|
408
|
+
service_instance.is_running = True # Ensure loop runs
|
409
|
+
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?
|
414
|
+
except KeyboardInterrupt:
|
415
|
+
log_info("Debug mode interrupted by user.")
|
416
|
+
service_instance.SvcStop() # Attempt clean stop
|
417
|
+
log_info("Debug mode finished.")
|
418
|
+
elif len(sys.argv) == 1:
|
419
|
+
# Called without arguments, run as a service instance via SCM
|
420
|
+
try:
|
421
|
+
servicemanager.Initialize()
|
422
|
+
servicemanager.PrepareToHostSingle(GuardService)
|
423
|
+
servicemanager.StartServiceCtrlDispatcher()
|
424
|
+
except win32service.error as details:
|
425
|
+
import winerror
|
426
|
+
if details.winerror == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
|
427
|
+
print(f"Error: Cannot connect to Service Control Manager.")
|
428
|
+
print(f"Use 'python {os.path.basename(__file__)} install|start|stop|remove|debug'")
|
429
|
+
else:
|
430
|
+
print(f"Error preparing service: {details}")
|
431
|
+
except Exception as e:
|
432
|
+
print(f"Unexpected error initializing service: {e}")
|
433
|
+
|
434
|
+
else:
|
435
|
+
# Called with install/start/stop/remove args, let ServiceFramework handle them
|
436
|
+
win32serviceutil.HandleCommandLine(GuardService)
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# src/computer_use_ootb_internal/service_manager.py
|
2
|
+
import sys
|
3
|
+
import os
|
4
|
+
import inspect
|
5
|
+
import subprocess
|
6
|
+
import ctypes
|
7
|
+
import platform
|
8
|
+
import time
|
9
|
+
|
10
|
+
# Constants need to match guard_service.py
|
11
|
+
_SERVICE_NAME = "OOTBGuardService"
|
12
|
+
_SERVICE_DISPLAY_NAME = "OOTB Guard Service"
|
13
|
+
|
14
|
+
def is_admin():
|
15
|
+
"""Check if the script is running with administrative privileges."""
|
16
|
+
if platform.system() != "Windows":
|
17
|
+
return False # Only applicable on Windows
|
18
|
+
try:
|
19
|
+
return ctypes.windll.shell32.IsUserAnAdmin()
|
20
|
+
except:
|
21
|
+
return False
|
22
|
+
|
23
|
+
def get_service_module_path():
|
24
|
+
"""Gets the absolute path to the guard_service.py module."""
|
25
|
+
# Find the path relative to this script's location
|
26
|
+
# This assumes service_manager.py and guard_service.py are in the same installed package directory
|
27
|
+
try:
|
28
|
+
current_dir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
|
29
|
+
service_module = os.path.join(current_dir, "guard_service.py")
|
30
|
+
if not os.path.exists(service_module):
|
31
|
+
raise FileNotFoundError(f"guard_service.py not found adjacent to service_manager.py in {current_dir}")
|
32
|
+
return service_module
|
33
|
+
except Exception as e:
|
34
|
+
# Fallback if inspect fails (e.g., in some frozen environments)
|
35
|
+
# Try finding it relative to the script itself? Unreliable.
|
36
|
+
# Let's try sys.prefix - might work in standard venv/conda installs
|
37
|
+
try:
|
38
|
+
# sys.prefix points to the environment root (e.g., C:\path\to\env)
|
39
|
+
# Package likely installed in Lib\site-packages\<package_name>
|
40
|
+
# This depends heavily on installation layout
|
41
|
+
package_name = __name__.split('.')[0] # Should be 'computer_use_ootb_internal'
|
42
|
+
site_packages_path = os.path.join(sys.prefix, 'Lib', 'site-packages')
|
43
|
+
module_dir = os.path.join(site_packages_path, package_name)
|
44
|
+
service_module = os.path.join(module_dir, "guard_service.py")
|
45
|
+
if os.path.exists(service_module):
|
46
|
+
print(f"Warning: Found service module via sys.prefix fallback: {service_module}")
|
47
|
+
return service_module
|
48
|
+
else:
|
49
|
+
raise FileNotFoundError(f"guard_service.py not found via inspect or sys.prefix fallback (checked {module_dir})")
|
50
|
+
except Exception as fallback_e:
|
51
|
+
raise FileNotFoundError(f"Could not find guard_service.py using inspect ({e}) or sys.prefix ({fallback_e}). Check installation.")
|
52
|
+
|
53
|
+
|
54
|
+
def run_service_command(command_args, check_errors=True):
|
55
|
+
"""Runs the guard_service.py script with specified command-line args."""
|
56
|
+
if not is_admin():
|
57
|
+
print("Error: Administrative privileges are required to manage the service.", file=sys.stderr)
|
58
|
+
print("Please run this command from an Administrator Command Prompt or PowerShell.", file=sys.stderr)
|
59
|
+
return False
|
60
|
+
|
61
|
+
try:
|
62
|
+
python_exe = sys.executable # Use the same python that's running this script
|
63
|
+
service_script = get_service_module_path()
|
64
|
+
except FileNotFoundError as e:
|
65
|
+
print(f"Error: {e}", file=sys.stderr)
|
66
|
+
return False
|
67
|
+
|
68
|
+
# Quote paths if they contain spaces
|
69
|
+
if " " in python_exe and not python_exe.startswith('"'):
|
70
|
+
python_exe = f'"{python_exe}"'
|
71
|
+
if " " in service_script and not service_script.startswith('"'):
|
72
|
+
service_script = f'"{service_script}"'
|
73
|
+
|
74
|
+
# Construct command using list to avoid shell quoting issues
|
75
|
+
cmd = [sys.executable, get_service_module_path()] + command_args
|
76
|
+
print(f"Executing command: {' '.join(cmd)}")
|
77
|
+
|
78
|
+
try:
|
79
|
+
# Run the command. Use shell=False with list of args.
|
80
|
+
# Capture output to check for specific errors if needed, but print it too.
|
81
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=check_errors, encoding='utf-8')
|
82
|
+
if result.stdout:
|
83
|
+
print("Command STDOUT:")
|
84
|
+
print(result.stdout)
|
85
|
+
if result.stderr:
|
86
|
+
print("Command STDERR:")
|
87
|
+
print(result.stderr)
|
88
|
+
print(f"Command {' '.join(command_args)} executed successfully.")
|
89
|
+
return True
|
90
|
+
except FileNotFoundError as e:
|
91
|
+
print(f"Error: Could not find Python executable or service script during execution.", file=sys.stderr)
|
92
|
+
print(f" Details: {e}", file=sys.stderr)
|
93
|
+
return False
|
94
|
+
except subprocess.CalledProcessError as e:
|
95
|
+
print(f"Error executing service command {' '.join(command_args)} (Exit Code {e.returncode}).", file=sys.stderr)
|
96
|
+
if e.stdout:
|
97
|
+
print("Subprocess STDOUT:")
|
98
|
+
print(e.stdout)
|
99
|
+
if e.stderr:
|
100
|
+
print("Subprocess STDERR:")
|
101
|
+
print(e.stderr)
|
102
|
+
return False
|
103
|
+
except Exception as e:
|
104
|
+
print(f"An unexpected error occurred running service command: {e}", file=sys.stderr)
|
105
|
+
return False
|
106
|
+
|
107
|
+
|
108
|
+
def install_and_start():
|
109
|
+
"""Installs and starts the Guard Service."""
|
110
|
+
print(f"Attempting to install/update service: '{_SERVICE_NAME}' ('{_SERVICE_DISPLAY_NAME}')")
|
111
|
+
# Use 'update' which handles install or updates display name/path if needed
|
112
|
+
if run_service_command(['--startup', 'auto', 'update']): # Set startup to auto, then update
|
113
|
+
print(f"\nAttempting to start service: '{_SERVICE_NAME}' (waiting a few seconds first)")
|
114
|
+
time.sleep(3) # Give SCM time to register the update
|
115
|
+
if run_service_command(['start']):
|
116
|
+
print(f"\nService '{_SERVICE_NAME}' installed/updated and started successfully.")
|
117
|
+
else:
|
118
|
+
print(f"\nWarning: Service '{_SERVICE_NAME}' installed/updated but failed to start.", file=sys.stderr)
|
119
|
+
print(f" Check service logs ('C:\ProgramData\OOTBGuardService\guard.log') or Windows Event Viewer for details.", file=sys.stderr)
|
120
|
+
print(f" You may need to start it manually via 'services.msc' or `net start {_SERVICE_NAME}`.")
|
121
|
+
else:
|
122
|
+
print(f"\nService '{_SERVICE_NAME}' installation/update failed.", file=sys.stderr)
|
123
|
+
|
124
|
+
|
125
|
+
def stop_and_remove():
|
126
|
+
"""Stops and removes the Guard Service."""
|
127
|
+
print(f"Attempting to stop service: '{_SERVICE_NAME}' (will ignore errors if not running)")
|
128
|
+
# Run stop first, ignore errors (check_errors=False)
|
129
|
+
run_service_command(['stop'], check_errors=False)
|
130
|
+
time.sleep(2) # Give service time to stop
|
131
|
+
|
132
|
+
print(f"\nAttempting to remove service: '{_SERVICE_NAME}'")
|
133
|
+
if run_service_command(['remove']):
|
134
|
+
print(f"\nService '{_SERVICE_NAME}' stopped (if running) and removed successfully.")
|
135
|
+
else:
|
136
|
+
print(f"\nService '{_SERVICE_NAME}' removal failed.", file=sys.stderr)
|
137
|
+
print(f" Ensure the service was stopped first, or check permissions.", file=sys.stderr)
|
138
|
+
|
139
|
+
if __name__ == '__main__':
|
140
|
+
# Allow calling functions directly for testing if needed
|
141
|
+
print("This script provides service management commands.")
|
142
|
+
print("Use 'ootb-install-service' or 'ootb-remove-service' as Administrator.")
|