cua-computer 0.2.10__py3-none-any.whl → 0.2.11__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/computer.py +19 -3
- computer/interface/factory.py +6 -3
- computer/interface/windows.py +687 -0
- computer/providers/base.py +1 -0
- computer/providers/factory.py +22 -0
- computer/providers/winsandbox/__init__.py +11 -0
- computer/providers/winsandbox/provider.py +468 -0
- computer/providers/winsandbox/setup_script.ps1 +124 -0
- computer/ui/__main__.py +15 -0
- computer/ui/gradio/app.py +70 -10
- {cua_computer-0.2.10.dist-info → cua_computer-0.2.11.dist-info}/METADATA +1 -1
- {cua_computer-0.2.10.dist-info → cua_computer-0.2.11.dist-info}/RECORD +14 -9
- {cua_computer-0.2.10.dist-info → cua_computer-0.2.11.dist-info}/WHEEL +0 -0
- {cua_computer-0.2.10.dist-info → cua_computer-0.2.11.dist-info}/entry_points.txt +0 -0
computer/providers/base.py
CHANGED
computer/providers/factory.py
CHANGED
@@ -112,5 +112,27 @@ class VMProviderFactory:
|
|
112
112
|
"The CloudProvider is not fully implemented yet. "
|
113
113
|
"Please use LUME or LUMIER provider instead."
|
114
114
|
) from e
|
115
|
+
elif provider_type == VMProviderType.WINSANDBOX:
|
116
|
+
try:
|
117
|
+
from .winsandbox import WinSandboxProvider, HAS_WINSANDBOX
|
118
|
+
if not HAS_WINSANDBOX:
|
119
|
+
raise ImportError(
|
120
|
+
"pywinsandbox is required for WinSandboxProvider. "
|
121
|
+
"Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
|
122
|
+
)
|
123
|
+
return WinSandboxProvider(
|
124
|
+
port=port,
|
125
|
+
host=host,
|
126
|
+
storage=storage,
|
127
|
+
verbose=verbose,
|
128
|
+
ephemeral=ephemeral,
|
129
|
+
**kwargs
|
130
|
+
)
|
131
|
+
except ImportError as e:
|
132
|
+
logger.error(f"Failed to import WinSandboxProvider: {e}")
|
133
|
+
raise ImportError(
|
134
|
+
"pywinsandbox is required for WinSandboxProvider. "
|
135
|
+
"Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
|
136
|
+
) from e
|
115
137
|
else:
|
116
138
|
raise ValueError(f"Unsupported provider type: {provider_type}")
|
@@ -0,0 +1,468 @@
|
|
1
|
+
"""Windows Sandbox VM provider implementation using pywinsandbox."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import asyncio
|
5
|
+
import logging
|
6
|
+
import time
|
7
|
+
from typing import Dict, Any, Optional, List
|
8
|
+
|
9
|
+
from ..base import BaseVMProvider, VMProviderType
|
10
|
+
|
11
|
+
# Setup logging
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
try:
|
15
|
+
import winsandbox
|
16
|
+
HAS_WINSANDBOX = True
|
17
|
+
except ImportError:
|
18
|
+
HAS_WINSANDBOX = False
|
19
|
+
|
20
|
+
|
21
|
+
class WinSandboxProvider(BaseVMProvider):
|
22
|
+
"""Windows Sandbox VM provider implementation using pywinsandbox.
|
23
|
+
|
24
|
+
This provider uses Windows Sandbox to create isolated Windows environments.
|
25
|
+
Storage is always ephemeral with Windows Sandbox.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(
|
29
|
+
self,
|
30
|
+
port: int = 7777,
|
31
|
+
host: str = "localhost",
|
32
|
+
storage: Optional[str] = None,
|
33
|
+
verbose: bool = False,
|
34
|
+
ephemeral: bool = True, # Windows Sandbox is always ephemeral
|
35
|
+
memory_mb: int = 4096,
|
36
|
+
networking: bool = True,
|
37
|
+
**kwargs
|
38
|
+
):
|
39
|
+
"""Initialize the Windows Sandbox provider.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
port: Port for the computer server (default: 7777)
|
43
|
+
host: Host to use for connections (default: localhost)
|
44
|
+
storage: Storage path (ignored - Windows Sandbox is always ephemeral)
|
45
|
+
verbose: Enable verbose logging
|
46
|
+
ephemeral: Always True for Windows Sandbox
|
47
|
+
memory_mb: Memory allocation in MB (default: 4096)
|
48
|
+
networking: Enable networking in sandbox (default: True)
|
49
|
+
"""
|
50
|
+
if not HAS_WINSANDBOX:
|
51
|
+
raise ImportError(
|
52
|
+
"pywinsandbox is required for WinSandboxProvider. "
|
53
|
+
"Please install it with 'pip install pywinsandbox'"
|
54
|
+
)
|
55
|
+
|
56
|
+
self.host = host
|
57
|
+
self.port = port
|
58
|
+
self.verbose = verbose
|
59
|
+
self.memory_mb = memory_mb
|
60
|
+
self.networking = networking
|
61
|
+
|
62
|
+
# Windows Sandbox is always ephemeral
|
63
|
+
if not ephemeral:
|
64
|
+
logger.warning("Windows Sandbox storage is always ephemeral. Ignoring ephemeral=False.")
|
65
|
+
self.ephemeral = True
|
66
|
+
|
67
|
+
# Storage is always ephemeral for Windows Sandbox
|
68
|
+
if storage and storage != "ephemeral":
|
69
|
+
logger.warning("Windows Sandbox does not support persistent storage. Using ephemeral storage.")
|
70
|
+
self.storage = "ephemeral"
|
71
|
+
|
72
|
+
self.logger = logging.getLogger(__name__)
|
73
|
+
|
74
|
+
# Track active sandboxes
|
75
|
+
self._active_sandboxes: Dict[str, Any] = {}
|
76
|
+
|
77
|
+
@property
|
78
|
+
def provider_type(self) -> VMProviderType:
|
79
|
+
"""Get the provider type."""
|
80
|
+
return VMProviderType.WINSANDBOX
|
81
|
+
|
82
|
+
async def __aenter__(self):
|
83
|
+
"""Enter async context manager."""
|
84
|
+
# Verify Windows Sandbox is available
|
85
|
+
if not HAS_WINSANDBOX:
|
86
|
+
raise ImportError("pywinsandbox is not available")
|
87
|
+
|
88
|
+
return self
|
89
|
+
|
90
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
91
|
+
"""Exit async context manager."""
|
92
|
+
# Clean up any active sandboxes
|
93
|
+
for name, sandbox in self._active_sandboxes.items():
|
94
|
+
try:
|
95
|
+
sandbox.shutdown()
|
96
|
+
self.logger.info(f"Terminated sandbox: {name}")
|
97
|
+
except Exception as e:
|
98
|
+
self.logger.error(f"Error terminating sandbox {name}: {e}")
|
99
|
+
|
100
|
+
self._active_sandboxes.clear()
|
101
|
+
|
102
|
+
async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
103
|
+
"""Get VM information by name.
|
104
|
+
|
105
|
+
Args:
|
106
|
+
name: Name of the VM to get information for
|
107
|
+
storage: Ignored for Windows Sandbox (always ephemeral)
|
108
|
+
|
109
|
+
Returns:
|
110
|
+
Dictionary with VM information including status, IP address, etc.
|
111
|
+
"""
|
112
|
+
if name not in self._active_sandboxes:
|
113
|
+
return {
|
114
|
+
"name": name,
|
115
|
+
"status": "stopped",
|
116
|
+
"ip_address": None,
|
117
|
+
"storage": "ephemeral"
|
118
|
+
}
|
119
|
+
|
120
|
+
sandbox = self._active_sandboxes[name]
|
121
|
+
|
122
|
+
# Check if sandbox is still running
|
123
|
+
try:
|
124
|
+
# Try to ping the sandbox to see if it's responsive
|
125
|
+
try:
|
126
|
+
sandbox.rpyc.modules.os.getcwd()
|
127
|
+
sandbox_responsive = True
|
128
|
+
except Exception:
|
129
|
+
sandbox_responsive = False
|
130
|
+
|
131
|
+
if not sandbox_responsive:
|
132
|
+
return {
|
133
|
+
"name": name,
|
134
|
+
"status": "starting",
|
135
|
+
"ip_address": None,
|
136
|
+
"storage": "ephemeral",
|
137
|
+
"memory_mb": self.memory_mb,
|
138
|
+
"networking": self.networking
|
139
|
+
}
|
140
|
+
|
141
|
+
# Check for computer server address file
|
142
|
+
server_address_file = r"C:\Users\WDAGUtilityAccount\Desktop\shared_windows_sandbox_dir\server_address"
|
143
|
+
|
144
|
+
try:
|
145
|
+
# Check if the server address file exists
|
146
|
+
file_exists = sandbox.rpyc.modules.os.path.exists(server_address_file)
|
147
|
+
|
148
|
+
if file_exists:
|
149
|
+
# Read the server address file
|
150
|
+
with sandbox.rpyc.builtin.open(server_address_file, 'r') as f:
|
151
|
+
server_address = f.read().strip()
|
152
|
+
|
153
|
+
if server_address and ':' in server_address:
|
154
|
+
# Parse IP:port from the file
|
155
|
+
ip_address, port = server_address.split(':', 1)
|
156
|
+
|
157
|
+
# Verify the server is actually responding
|
158
|
+
try:
|
159
|
+
import socket
|
160
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
161
|
+
sock.settimeout(3)
|
162
|
+
result = sock.connect_ex((ip_address, int(port)))
|
163
|
+
sock.close()
|
164
|
+
|
165
|
+
if result == 0:
|
166
|
+
# Server is responding
|
167
|
+
status = "running"
|
168
|
+
self.logger.debug(f"Computer server found at {ip_address}:{port}")
|
169
|
+
else:
|
170
|
+
# Server file exists but not responding
|
171
|
+
status = "starting"
|
172
|
+
ip_address = None
|
173
|
+
except Exception as e:
|
174
|
+
self.logger.debug(f"Error checking server connectivity: {e}")
|
175
|
+
status = "starting"
|
176
|
+
ip_address = None
|
177
|
+
else:
|
178
|
+
# File exists but doesn't contain valid address
|
179
|
+
status = "starting"
|
180
|
+
ip_address = None
|
181
|
+
else:
|
182
|
+
# Server address file doesn't exist yet
|
183
|
+
status = "starting"
|
184
|
+
ip_address = None
|
185
|
+
|
186
|
+
except Exception as e:
|
187
|
+
self.logger.debug(f"Error checking server address file: {e}")
|
188
|
+
status = "starting"
|
189
|
+
ip_address = None
|
190
|
+
|
191
|
+
except Exception as e:
|
192
|
+
self.logger.error(f"Error checking sandbox status: {e}")
|
193
|
+
status = "error"
|
194
|
+
ip_address = None
|
195
|
+
|
196
|
+
return {
|
197
|
+
"name": name,
|
198
|
+
"status": status,
|
199
|
+
"ip_address": ip_address,
|
200
|
+
"storage": "ephemeral",
|
201
|
+
"memory_mb": self.memory_mb,
|
202
|
+
"networking": self.networking
|
203
|
+
}
|
204
|
+
|
205
|
+
async def list_vms(self) -> List[Dict[str, Any]]:
|
206
|
+
"""List all available VMs."""
|
207
|
+
vms = []
|
208
|
+
for name in self._active_sandboxes.keys():
|
209
|
+
vm_info = await self.get_vm(name)
|
210
|
+
vms.append(vm_info)
|
211
|
+
return vms
|
212
|
+
|
213
|
+
async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
214
|
+
"""Run a VM with the given options.
|
215
|
+
|
216
|
+
Args:
|
217
|
+
image: Image name (ignored for Windows Sandbox - always uses host Windows)
|
218
|
+
name: Name of the VM to run
|
219
|
+
run_opts: Dictionary of run options (memory, cpu, etc.)
|
220
|
+
storage: Ignored for Windows Sandbox (always ephemeral)
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
Dictionary with VM run status and information
|
224
|
+
"""
|
225
|
+
if name in self._active_sandboxes:
|
226
|
+
return {
|
227
|
+
"success": False,
|
228
|
+
"error": f"Sandbox {name} is already running"
|
229
|
+
}
|
230
|
+
|
231
|
+
try:
|
232
|
+
# Extract options from run_opts
|
233
|
+
memory_mb = run_opts.get("memory_mb", self.memory_mb)
|
234
|
+
if isinstance(memory_mb, str):
|
235
|
+
# Convert memory string like "4GB" to MB
|
236
|
+
if memory_mb.upper().endswith("GB"):
|
237
|
+
memory_mb = int(float(memory_mb[:-2]) * 1024)
|
238
|
+
elif memory_mb.upper().endswith("MB"):
|
239
|
+
memory_mb = int(memory_mb[:-2])
|
240
|
+
else:
|
241
|
+
memory_mb = self.memory_mb
|
242
|
+
|
243
|
+
networking = run_opts.get("networking", self.networking)
|
244
|
+
|
245
|
+
# Create folder mappers if shared directories are specified
|
246
|
+
folder_mappers = []
|
247
|
+
shared_directories = run_opts.get("shared_directories", [])
|
248
|
+
for shared_dir in shared_directories:
|
249
|
+
if isinstance(shared_dir, dict):
|
250
|
+
host_path = shared_dir.get("hostPath", "")
|
251
|
+
elif isinstance(shared_dir, str):
|
252
|
+
host_path = shared_dir
|
253
|
+
else:
|
254
|
+
continue
|
255
|
+
|
256
|
+
if host_path and os.path.exists(host_path):
|
257
|
+
folder_mappers.append(winsandbox.FolderMapper(host_path))
|
258
|
+
|
259
|
+
self.logger.info(f"Creating Windows Sandbox: {name}")
|
260
|
+
self.logger.info(f"Memory: {memory_mb}MB, Networking: {networking}")
|
261
|
+
if folder_mappers:
|
262
|
+
self.logger.info(f"Shared directories: {len(folder_mappers)}")
|
263
|
+
|
264
|
+
# Create the sandbox without logon script
|
265
|
+
sandbox = winsandbox.new_sandbox(
|
266
|
+
memory_mb=str(memory_mb),
|
267
|
+
networking=networking,
|
268
|
+
folder_mappers=folder_mappers
|
269
|
+
)
|
270
|
+
|
271
|
+
# Store the sandbox
|
272
|
+
self._active_sandboxes[name] = sandbox
|
273
|
+
|
274
|
+
self.logger.info(f"Windows Sandbox {name} created successfully")
|
275
|
+
|
276
|
+
# Setup the computer server in the sandbox
|
277
|
+
await self._setup_computer_server(sandbox, name)
|
278
|
+
|
279
|
+
return {
|
280
|
+
"success": True,
|
281
|
+
"name": name,
|
282
|
+
"status": "starting",
|
283
|
+
"memory_mb": memory_mb,
|
284
|
+
"networking": networking,
|
285
|
+
"storage": "ephemeral"
|
286
|
+
}
|
287
|
+
|
288
|
+
except Exception as e:
|
289
|
+
self.logger.error(f"Failed to create Windows Sandbox {name}: {e}")
|
290
|
+
# stack trace
|
291
|
+
import traceback
|
292
|
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
293
|
+
return {
|
294
|
+
"success": False,
|
295
|
+
"error": f"Failed to create sandbox: {str(e)}"
|
296
|
+
}
|
297
|
+
|
298
|
+
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
299
|
+
"""Stop a running VM.
|
300
|
+
|
301
|
+
Args:
|
302
|
+
name: Name of the VM to stop
|
303
|
+
storage: Ignored for Windows Sandbox
|
304
|
+
|
305
|
+
Returns:
|
306
|
+
Dictionary with stop status and information
|
307
|
+
"""
|
308
|
+
if name not in self._active_sandboxes:
|
309
|
+
return {
|
310
|
+
"success": False,
|
311
|
+
"error": f"Sandbox {name} is not running"
|
312
|
+
}
|
313
|
+
|
314
|
+
try:
|
315
|
+
sandbox = self._active_sandboxes[name]
|
316
|
+
|
317
|
+
# Terminate the sandbox
|
318
|
+
sandbox.shutdown()
|
319
|
+
|
320
|
+
# Remove from active sandboxes
|
321
|
+
del self._active_sandboxes[name]
|
322
|
+
|
323
|
+
self.logger.info(f"Windows Sandbox {name} stopped successfully")
|
324
|
+
|
325
|
+
return {
|
326
|
+
"success": True,
|
327
|
+
"name": name,
|
328
|
+
"status": "stopped"
|
329
|
+
}
|
330
|
+
|
331
|
+
except Exception as e:
|
332
|
+
self.logger.error(f"Failed to stop Windows Sandbox {name}: {e}")
|
333
|
+
return {
|
334
|
+
"success": False,
|
335
|
+
"error": f"Failed to stop sandbox: {str(e)}"
|
336
|
+
}
|
337
|
+
|
338
|
+
async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
339
|
+
"""Update VM configuration.
|
340
|
+
|
341
|
+
Note: Windows Sandbox does not support runtime configuration updates.
|
342
|
+
The sandbox must be stopped and restarted with new configuration.
|
343
|
+
|
344
|
+
Args:
|
345
|
+
name: Name of the VM to update
|
346
|
+
update_opts: Dictionary of update options
|
347
|
+
storage: Ignored for Windows Sandbox
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
Dictionary with update status and information
|
351
|
+
"""
|
352
|
+
return {
|
353
|
+
"success": False,
|
354
|
+
"error": "Windows Sandbox does not support runtime configuration updates. "
|
355
|
+
"Please stop and restart the sandbox with new configuration."
|
356
|
+
}
|
357
|
+
|
358
|
+
async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
|
359
|
+
"""Get the IP address of a VM, waiting indefinitely until it's available.
|
360
|
+
|
361
|
+
Args:
|
362
|
+
name: Name of the VM to get the IP for
|
363
|
+
storage: Ignored for Windows Sandbox
|
364
|
+
retry_delay: Delay between retries in seconds (default: 2)
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
IP address of the VM when it becomes available
|
368
|
+
"""
|
369
|
+
total_attempts = 0
|
370
|
+
|
371
|
+
# Loop indefinitely until we get a valid IP
|
372
|
+
while True:
|
373
|
+
total_attempts += 1
|
374
|
+
|
375
|
+
# Log retry message but not on first attempt
|
376
|
+
if total_attempts > 1:
|
377
|
+
self.logger.info(f"Waiting for Windows Sandbox {name} IP address (attempt {total_attempts})...")
|
378
|
+
|
379
|
+
try:
|
380
|
+
# Get VM information
|
381
|
+
vm_info = await self.get_vm(name, storage=storage)
|
382
|
+
|
383
|
+
# Check if we got a valid IP
|
384
|
+
ip = vm_info.get("ip_address", None)
|
385
|
+
if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
|
386
|
+
self.logger.info(f"Got valid Windows Sandbox IP address: {ip}")
|
387
|
+
return ip
|
388
|
+
|
389
|
+
# Check the VM status
|
390
|
+
status = vm_info.get("status", "unknown")
|
391
|
+
|
392
|
+
# If VM is not running yet, log and wait
|
393
|
+
if status != "running":
|
394
|
+
self.logger.info(f"Windows Sandbox is not running yet (status: {status}). Waiting...")
|
395
|
+
# If VM is running but no IP yet, wait and retry
|
396
|
+
else:
|
397
|
+
self.logger.info("Windows Sandbox is running but no valid IP address yet. Waiting...")
|
398
|
+
|
399
|
+
except Exception as e:
|
400
|
+
self.logger.warning(f"Error getting Windows Sandbox {name} IP: {e}, continuing to wait...")
|
401
|
+
|
402
|
+
# Wait before next retry
|
403
|
+
await asyncio.sleep(retry_delay)
|
404
|
+
|
405
|
+
# Add progress log every 10 attempts
|
406
|
+
if total_attempts % 10 == 0:
|
407
|
+
self.logger.info(f"Still waiting for Windows Sandbox {name} IP after {total_attempts} attempts...")
|
408
|
+
|
409
|
+
async def _setup_computer_server(self, sandbox, name: str, visible: bool = False):
|
410
|
+
"""Setup the computer server in the Windows Sandbox using RPyC.
|
411
|
+
|
412
|
+
Args:
|
413
|
+
sandbox: The Windows Sandbox instance
|
414
|
+
name: Name of the sandbox
|
415
|
+
visible: Whether the opened process should be visible (default: False)
|
416
|
+
"""
|
417
|
+
try:
|
418
|
+
self.logger.info(f"Setting up computer server in sandbox {name}...")
|
419
|
+
|
420
|
+
# Read the PowerShell setup script
|
421
|
+
script_path = os.path.join(os.path.dirname(__file__), "setup_script.ps1")
|
422
|
+
with open(script_path, 'r', encoding='utf-8') as f:
|
423
|
+
setup_script_content = f.read()
|
424
|
+
|
425
|
+
# Write the setup script to the sandbox using RPyC
|
426
|
+
script_dest_path = r"C:\Users\WDAGUtilityAccount\setup_cua.ps1"
|
427
|
+
|
428
|
+
self.logger.info(f"Writing setup script to {script_dest_path}")
|
429
|
+
with sandbox.rpyc.builtin.open(script_dest_path, 'w') as f:
|
430
|
+
f.write(setup_script_content)
|
431
|
+
|
432
|
+
# Execute the PowerShell script in the background
|
433
|
+
self.logger.info("Executing setup script in sandbox...")
|
434
|
+
|
435
|
+
# Use subprocess to run PowerShell script
|
436
|
+
import subprocess
|
437
|
+
powershell_cmd = [
|
438
|
+
"powershell.exe",
|
439
|
+
"-ExecutionPolicy", "Bypass",
|
440
|
+
"-NoExit", # Keep window open after script completes
|
441
|
+
"-File", script_dest_path
|
442
|
+
]
|
443
|
+
|
444
|
+
# Set creation flags based on visibility preference
|
445
|
+
if visible:
|
446
|
+
# CREATE_NEW_CONSOLE - creates a new console window (visible)
|
447
|
+
creation_flags = 0x00000010
|
448
|
+
else:
|
449
|
+
creation_flags = 0x08000000 # CREATE_NO_WINDOW
|
450
|
+
|
451
|
+
# Start the process using RPyC
|
452
|
+
process = sandbox.rpyc.modules.subprocess.Popen(
|
453
|
+
powershell_cmd,
|
454
|
+
creationflags=creation_flags,
|
455
|
+
shell=False
|
456
|
+
)
|
457
|
+
|
458
|
+
# # Sleep for 30 seconds
|
459
|
+
# await asyncio.sleep(30)
|
460
|
+
|
461
|
+
ip = await self.get_ip(name)
|
462
|
+
self.logger.info(f"Sandbox IP: {ip}")
|
463
|
+
self.logger.info(f"Setup script started in background in sandbox {name} with PID: {process.pid}")
|
464
|
+
|
465
|
+
except Exception as e:
|
466
|
+
self.logger.error(f"Failed to setup computer server in sandbox {name}: {e}")
|
467
|
+
import traceback
|
468
|
+
self.logger.error(f"Stack trace: {traceback.format_exc()}")
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# Setup script for Windows Sandbox CUA Computer provider
|
2
|
+
# This script runs when the sandbox starts
|
3
|
+
|
4
|
+
Write-Host "Starting CUA Computer setup in Windows Sandbox..."
|
5
|
+
|
6
|
+
# Function to find the mapped Python installation from pywinsandbox
|
7
|
+
function Find-MappedPython {
|
8
|
+
Write-Host "Looking for mapped Python installation from pywinsandbox..."
|
9
|
+
|
10
|
+
# pywinsandbox maps the host Python installation to the sandbox
|
11
|
+
# Look for mapped shared folders on the desktop (common pywinsandbox pattern)
|
12
|
+
$desktopPath = "C:\Users\WDAGUtilityAccount\Desktop"
|
13
|
+
$sharedFolders = Get-ChildItem -Path $desktopPath -Directory -ErrorAction SilentlyContinue
|
14
|
+
|
15
|
+
foreach ($folder in $sharedFolders) {
|
16
|
+
# Look for Python executables in shared folders
|
17
|
+
$pythonPaths = @(
|
18
|
+
"$($folder.FullName)\python.exe",
|
19
|
+
"$($folder.FullName)\Scripts\python.exe",
|
20
|
+
"$($folder.FullName)\bin\python.exe"
|
21
|
+
)
|
22
|
+
|
23
|
+
foreach ($pythonPath in $pythonPaths) {
|
24
|
+
if (Test-Path $pythonPath) {
|
25
|
+
try {
|
26
|
+
$version = & $pythonPath --version 2>&1
|
27
|
+
if ($version -match "Python") {
|
28
|
+
Write-Host "Found mapped Python: $pythonPath - $version"
|
29
|
+
return $pythonPath
|
30
|
+
}
|
31
|
+
} catch {
|
32
|
+
continue
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
# Also check subdirectories that might contain Python
|
38
|
+
$subDirs = Get-ChildItem -Path $folder.FullName -Directory -ErrorAction SilentlyContinue
|
39
|
+
foreach ($subDir in $subDirs) {
|
40
|
+
$pythonPath = "$($subDir.FullName)\python.exe"
|
41
|
+
if (Test-Path $pythonPath) {
|
42
|
+
try {
|
43
|
+
$version = & $pythonPath --version 2>&1
|
44
|
+
if ($version -match "Python") {
|
45
|
+
Write-Host "Found mapped Python in subdirectory: $pythonPath - $version"
|
46
|
+
return $pythonPath
|
47
|
+
}
|
48
|
+
} catch {
|
49
|
+
continue
|
50
|
+
}
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
# Fallback: try common Python commands that might be available
|
56
|
+
$pythonCommands = @("python", "py", "python3")
|
57
|
+
foreach ($cmd in $pythonCommands) {
|
58
|
+
try {
|
59
|
+
$version = & $cmd --version 2>&1
|
60
|
+
if ($version -match "Python") {
|
61
|
+
Write-Host "Found Python via command '$cmd': $version"
|
62
|
+
return $cmd
|
63
|
+
}
|
64
|
+
} catch {
|
65
|
+
continue
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
throw "Could not find any Python installation (mapped or otherwise)"
|
70
|
+
}
|
71
|
+
|
72
|
+
try {
|
73
|
+
# Step 1: Find the mapped Python installation
|
74
|
+
Write-Host "Step 1: Finding mapped Python installation..."
|
75
|
+
$pythonExe = Find-MappedPython
|
76
|
+
Write-Host "Using Python: $pythonExe"
|
77
|
+
|
78
|
+
# Verify Python works and show version
|
79
|
+
$pythonVersion = & $pythonExe --version 2>&1
|
80
|
+
Write-Host "Python version: $pythonVersion"
|
81
|
+
|
82
|
+
# Step 2: Install cua-computer-server directly
|
83
|
+
Write-Host "Step 2: Installing cua-computer-server..."
|
84
|
+
|
85
|
+
Write-Host "Upgrading pip..."
|
86
|
+
& $pythonExe -m pip install --upgrade pip --quiet
|
87
|
+
|
88
|
+
Write-Host "Installing cua-computer-server..."
|
89
|
+
& $pythonExe -m pip install cua-computer-server --quiet
|
90
|
+
|
91
|
+
Write-Host "cua-computer-server installation completed."
|
92
|
+
|
93
|
+
# Step 3: Start computer server in background
|
94
|
+
Write-Host "Step 3: Starting computer server in background..."
|
95
|
+
Write-Host "Starting computer server with: $pythonExe"
|
96
|
+
|
97
|
+
# Start the computer server in the background
|
98
|
+
$serverProcess = Start-Process -FilePath $pythonExe -ArgumentList "-m", "computer_server.main" -WindowStyle Hidden -PassThru
|
99
|
+
Write-Host "Computer server started in background with PID: $($serverProcess.Id)"
|
100
|
+
|
101
|
+
# Give it a moment to start
|
102
|
+
Start-Sleep -Seconds 3
|
103
|
+
|
104
|
+
# Check if the process is still running
|
105
|
+
if (Get-Process -Id $serverProcess.Id -ErrorAction SilentlyContinue) {
|
106
|
+
Write-Host "Computer server is running successfully in background"
|
107
|
+
} else {
|
108
|
+
throw "Computer server failed to start or exited immediately"
|
109
|
+
}
|
110
|
+
|
111
|
+
} catch {
|
112
|
+
Write-Error "Setup failed: $_"
|
113
|
+
Write-Host "Error details: $($_.Exception.Message)"
|
114
|
+
Write-Host "Stack trace: $($_.ScriptStackTrace)"
|
115
|
+
Write-Host ""
|
116
|
+
Write-Host "Press any key to close this window..."
|
117
|
+
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
118
|
+
exit 1
|
119
|
+
}
|
120
|
+
|
121
|
+
Write-Host ""
|
122
|
+
Write-Host "Setup completed successfully!"
|
123
|
+
Write-Host "Press any key to close this window..."
|
124
|
+
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
|
computer/ui/__main__.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
"""
|
2
|
+
Main entry point for computer.ui module.
|
3
|
+
|
4
|
+
This allows running the computer UI with:
|
5
|
+
python -m computer.ui
|
6
|
+
|
7
|
+
Instead of:
|
8
|
+
python -m computer.ui.gradio.app
|
9
|
+
"""
|
10
|
+
|
11
|
+
from .gradio.app import create_gradio_ui
|
12
|
+
|
13
|
+
if __name__ == "__main__":
|
14
|
+
app = create_gradio_ui()
|
15
|
+
app.launch()
|