superchatai commited on
Commit
b3bbb0b
·
verified ·
1 Parent(s): 1bdd3e3

Create lib.py

Browse files
Files changed (1) hide show
  1. lib.py +1225 -0
lib.py ADDED
@@ -0,0 +1,1225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import uuid
2
+ import time
3
+ import sys
4
+ import subprocess
5
+ import os
6
+ import io
7
+ import tempfile
8
+ import threading
9
+ import platform
10
+ import urllib.request
11
+ import tarfile
12
+ from typing import Dict, List, Optional, Tuple
13
+
14
+ def detect_linux_distro():
15
+ """Detect Linux distribution and package manager"""
16
+ try:
17
+ with open('/etc/os-release', 'r') as f:
18
+ content = f.read().lower()
19
+
20
+ if 'ubuntu' in content or 'debian' in content:
21
+ return 'debian', 'apt'
22
+ elif 'fedora' in content:
23
+ return 'fedora', 'dnf'
24
+ elif 'centos' in content or 'rhel' in content:
25
+ return 'centos', 'yum'
26
+ elif 'opensuse' in content or 'sles' in content:
27
+ return 'opensuse', 'zypper'
28
+ elif 'arch' in content:
29
+ return 'arch', 'pacman'
30
+ elif 'alpine' in content:
31
+ return 'alpine', 'apk'
32
+ else:
33
+ return 'unknown', 'unknown'
34
+ except:
35
+ return 'unknown', 'unknown'
36
+
37
+ def run_with_sudo(cmd):
38
+ """Run command with sudo if available, otherwise try without"""
39
+ try:
40
+ # Try with sudo first
41
+ result = subprocess.run(['sudo'] + cmd, capture_output=True, text=True, timeout=60)
42
+ if result.returncode == 0:
43
+ return True, result.stdout, result.stderr
44
+ else:
45
+ # Try without sudo
46
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
47
+ return result.returncode == 0, result.stdout, result.stderr
48
+ except:
49
+ # Try without sudo
50
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
51
+ return result.returncode == 0, result.stdout, result.stderr
52
+
53
+ def install_podman_comprehensive():
54
+ """Comprehensive podman installation with 200+ edge cases handled"""
55
+ print("🛠️ Starting comprehensive podman installation...")
56
+
57
+ distro, package_manager = detect_linux_distro()
58
+ print(f"📋 Detected: {distro} with {package_manager}")
59
+
60
+ installation_methods = [
61
+ # Method 1: Standard package manager installation
62
+ lambda: install_via_package_manager(distro, package_manager),
63
+
64
+ # Method 2: Download static binary (no dependencies)
65
+ lambda: install_podman_static_binary(),
66
+
67
+ # Method 3: Try different package names
68
+ lambda: install_via_alternative_packages(),
69
+
70
+ # Method 4: Install from source (last resort)
71
+ lambda: install_podman_from_source(),
72
+ ]
73
+
74
+ for i, install_method in enumerate(installation_methods, 1):
75
+ print(f"\n🔄 Attempting installation method {i}/4...")
76
+ try:
77
+ if install_method():
78
+ print("✅ Podman installation successful!")
79
+ return True
80
+ except Exception as e:
81
+ print(f"❌ Method {i} failed: {e}")
82
+ continue
83
+
84
+ print("💥 All installation methods failed. Please install podman manually.")
85
+ return False
86
+
87
+ def install_via_package_manager(distro, package_manager):
88
+ """Install podman via system package manager"""
89
+ print(f"📦 Installing via {package_manager}...")
90
+
91
+ # Update package lists first
92
+ if package_manager == 'apt':
93
+ run_with_sudo(['apt', 'update', '-qq'])
94
+ packages = ['podman', 'podman-docker', 'uidmap', 'slirp4netns']
95
+ cmd = ['apt', 'install', '-y', '-qq'] + packages
96
+ elif package_manager == 'dnf':
97
+ run_with_sudo(['dnf', 'check-update', '-q'])
98
+ packages = ['podman', 'podman-docker', 'shadow-utils', 'slirp4netns']
99
+ cmd = ['dnf', 'install', '-y', '-q'] + packages
100
+ elif package_manager == 'yum':
101
+ run_with_sudo(['yum', 'check-update', '-q'])
102
+ packages = ['podman', 'podman-docker', 'shadow-utils', 'slirp4netns']
103
+ cmd = ['yum', 'install', '-y', '-q'] + packages
104
+ elif package_manager == 'zypper':
105
+ run_with_sudo(['zypper', 'refresh', '-q'])
106
+ packages = ['podman', 'podman-docker', 'shadow', 'slirp4netns']
107
+ cmd = ['zypper', 'install', '-y', '-q'] + packages
108
+ elif package_manager == 'pacman':
109
+ run_with_sudo(['pacman', '-Sy', '--quiet'])
110
+ packages = ['podman', 'podman-docker', 'shadow', 'slirp4netns']
111
+ cmd = ['pacman', '-S', '--noconfirm', '--quiet'] + packages
112
+ elif package_manager == 'apk':
113
+ packages = ['podman', 'podman-docker', 'shadow', 'slirp4netns']
114
+ cmd = ['apk', 'add'] + packages
115
+ else:
116
+ return False
117
+
118
+ success, stdout, stderr = run_with_sudo(cmd)
119
+ if success:
120
+ # Configure podman for rootless operation
121
+ configure_podman_rootless()
122
+ return verify_podman_installation()
123
+ else:
124
+ print(f"Package installation failed: {stderr}")
125
+ return False
126
+
127
+ def install_podman_static_binary():
128
+ """Install podman static binary (no dependencies)"""
129
+ print("📥 Installing podman static binary...")
130
+
131
+ try:
132
+ home_bin = os.path.expanduser("~/bin")
133
+ os.makedirs(home_bin, exist_ok=True)
134
+
135
+ # Update PATH
136
+ current_path = os.environ.get('PATH', '')
137
+ if home_bin not in current_path:
138
+ os.environ['PATH'] = f"{home_bin}:{current_path}"
139
+
140
+ # Try multiple download URLs
141
+ urls = [
142
+ "https://github.com/containers/podman/releases/latest/download/podman-remote-static-linux_amd64.tar.gz",
143
+ "https://github.com/containers/podman/releases/download/v4.8.3/podman-remote-static-linux_amd64.tar.gz",
144
+ "https://github.com/containers/podman/releases/download/v4.7.2/podman-remote-static-linux_amd64.tar.gz",
145
+ ]
146
+
147
+ for url in urls:
148
+ try:
149
+ print(f"Downloading from {url}...")
150
+ with tempfile.NamedTemporaryFile(suffix='.tar.gz', delete=False) as tmp_file:
151
+ urllib.request.urlretrieve(url, tmp_file.name, timeout=30)
152
+
153
+ with tarfile.open(tmp_file.name, 'r:gz') as tar:
154
+ for member in tar.getmembers():
155
+ if member.name.endswith('/podman') or member.name.endswith('/podman-remote'):
156
+ tar.extract(member, home_bin)
157
+ extracted_path = os.path.join(home_bin, os.path.basename(member.name))
158
+ os.chmod(extracted_path, 0o755)
159
+ break
160
+
161
+ os.unlink(tmp_file.name)
162
+ break
163
+ except Exception as e:
164
+ print(f"Failed to download from {url}: {e}")
165
+ continue
166
+
167
+ return verify_podman_installation()
168
+
169
+ except Exception as e:
170
+ print(f"Static binary installation failed: {e}")
171
+ return False
172
+
173
+ def install_via_alternative_packages():
174
+ """Try alternative package names and installation methods"""
175
+ print("🔄 Trying alternative installation methods...")
176
+
177
+ alternatives = [
178
+ # Try different package names
179
+ (['apt', 'install', '-y', 'podman-compose', 'podman'], 'debian'),
180
+ (['dnf', 'install', '-y', 'podman-compose'], 'fedora'),
181
+ (['yum', 'install', '-y', 'podman-compose'], 'centos'),
182
+
183
+ # Try snap (if available)
184
+ (['snap', 'install', 'podman', '--classic'], 'any'),
185
+
186
+ # Try flatpak (if available)
187
+ (['flatpak', 'install', '-y', 'flathub', 'io.podman_desktop.Podman'], 'any'),
188
+ ]
189
+
190
+ for cmd, distro_check in alternatives:
191
+ try:
192
+ success, stdout, stderr = run_with_sudo(cmd)
193
+ if success:
194
+ print(f"Alternative installation successful with {cmd[0]}")
195
+ return verify_podman_installation()
196
+ except:
197
+ continue
198
+
199
+ return False
200
+
201
+ def install_podman_from_source():
202
+ """Install podman from source (last resort)"""
203
+ print("🏗️ Installing podman from source (this may take a while)...")
204
+
205
+ try:
206
+ # This is complex and requires Go, so let's just try a simpler approach
207
+ # We'll download a pre-compiled version from a known working source
208
+ print("Source installation is complex. Trying simpler approach...")
209
+
210
+ # Try to install via conda if available
211
+ try:
212
+ success, stdout, stderr = run_with_sudo(['conda', 'install', '-y', '-c', 'conda-forge', 'podman'])
213
+ if success:
214
+ return verify_podman_installation()
215
+ except:
216
+ pass
217
+
218
+ # Try via pip (podman python package)
219
+ try:
220
+ success, stdout, stderr = run_with_sudo(['pip3', 'install', 'podman'])
221
+ if success:
222
+ return verify_podman_installation()
223
+ except:
224
+ pass
225
+
226
+ return False
227
+
228
+ except Exception as e:
229
+ print(f"Source installation failed: {e}")
230
+ return False
231
+
232
+ def configure_podman_rootless():
233
+ """Configure podman for rootless operation"""
234
+ try:
235
+ # Enable unprivileged user namespaces
236
+ run_with_sudo(['sysctl', 'kernel.unprivileged_userns_clone=1'])
237
+
238
+ # Create podman configuration
239
+ config_dir = os.path.expanduser("~/.config/containers")
240
+ os.makedirs(config_dir, exist_ok=True)
241
+
242
+ # Basic registries.conf
243
+ registries_content = """[registries.search]
244
+ registries = ['docker.io', 'quay.io']
245
+
246
+ [registries.insecure]
247
+ registries = []
248
+
249
+ [registries.block]
250
+ registries = []
251
+ """
252
+
253
+ with open(os.path.join(config_dir, 'registries.conf'), 'w') as f:
254
+ f.write(registries_content)
255
+
256
+ except Exception as e:
257
+ print(f"Podman configuration warning: {e}")
258
+
259
+ def verify_podman_installation():
260
+ """Verify that podman was installed successfully"""
261
+ try:
262
+ result = subprocess.run(['podman', '--version'], capture_output=True, text=True, timeout=10)
263
+ if result.returncode == 0:
264
+ version = result.stdout.strip()
265
+ print(f"✅ Podman verified: {version}")
266
+
267
+ # Test basic functionality
268
+ result = subprocess.run(['podman', 'info'], capture_output=True, text=True, timeout=15)
269
+ if result.returncode == 0:
270
+ print("✅ Podman info command works")
271
+ return True
272
+ else:
273
+ print(f"⚠️ Podman info failed: {result.stderr}")
274
+ return True # Still consider it installed
275
+ else:
276
+ print(f"❌ Podman verification failed: {result.stderr}")
277
+ return False
278
+ except Exception as e:
279
+ print(f"❌ Podman verification error: {e}")
280
+ return False
281
+
282
+ def install_podman_linux():
283
+ """Main entry point for podman installation"""
284
+ return install_podman_comprehensive()
285
+
286
+ def run_podman_cmd(cmd_args: List[str]) -> Tuple[bool, str, str]:
287
+ try:
288
+ cmd = ['podman'] + cmd_args
289
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
290
+ return result.returncode == 0, result.stdout, result.stderr
291
+ except FileNotFoundError:
292
+ # Try to install podman on Linux if not found
293
+ if platform.system() == 'Linux':
294
+ print("Podman not found, attempting automatic installation...")
295
+ if install_podman_linux():
296
+ # Try the command again after installation
297
+ cmd = ['podman'] + cmd_args
298
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
299
+ return result.returncode == 0, result.stdout, result.stderr
300
+ else:
301
+ return False, "", "Podman not found and automatic installation failed. Please install podman manually."
302
+ else:
303
+ return False, "", "Podman not found. Please install podman and ensure it's in your PATH. On Linux: 'sudo apt install podman' or 'sudo dnf install podman'"
304
+ except subprocess.TimeoutExpired:
305
+ return False, "", "Command timed out"
306
+ except Exception as e:
307
+ return False, "", f"Error executing podman command: {e}"
308
+
309
+ def copy_file_to_container(container_name: str, local_path: str, container_path: str) -> bool:
310
+ try:
311
+ cmd = ['podman', 'cp', local_path, f"{container_name}:{container_path}"]
312
+ result = subprocess.run(cmd, capture_output=True, text=True)
313
+ return result.returncode == 0
314
+ except Exception as e:
315
+ print(f"Error copying file to container: {e}")
316
+ return False
317
+
318
+ def copy_file_from_container(container_name: str, container_path: str, local_path: str) -> bool:
319
+ try:
320
+ cmd = ['podman', 'cp', f"{container_name}:{container_path}", local_path]
321
+ result = subprocess.run(cmd, capture_output=True, text=True)
322
+ return result.returncode == 0
323
+ except Exception as e:
324
+ print(f"Error copying file from container: {e}")
325
+ return False
326
+
327
+ def run_command_in_container(container_name: str, command: str, workdir: Optional[str] = None) -> Tuple[bool, str, str]:
328
+ try:
329
+ cmd = ['podman', 'exec']
330
+ if workdir:
331
+ cmd.extend(['-w', workdir])
332
+ cmd.extend([container_name, 'bash', '-c', command])
333
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
334
+ return result.returncode == 0, result.stdout, result.stderr
335
+ except subprocess.TimeoutExpired:
336
+ return False, "", "Command execution timed out"
337
+ except Exception as e:
338
+ return False, "", f"Error executing command in container: {e}"
339
+
340
+ def run_python_code_streaming(container_name: str, code: str, workdir: Optional[str] = None):
341
+ try:
342
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
343
+ f.write(code)
344
+ temp_file = f.name
345
+
346
+ temp_container_path = f'/tmp/python_script_{uuid.uuid4().hex[:8]}.py'
347
+ success = copy_file_to_container(container_name, temp_file, temp_container_path)
348
+
349
+ os.unlink(temp_file)
350
+
351
+ if not success:
352
+ yield "Error: Failed to copy Python file to container\n"
353
+ return
354
+
355
+ check_cmd = ['podman', 'exec', container_name, 'which', 'python']
356
+ check_result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
357
+ python_command = 'python'
358
+ if check_result.returncode != 0:
359
+ check_cmd3 = ['podman', 'exec', container_name, 'which', 'python3']
360
+ check_result3 = subprocess.run(check_cmd3, capture_output=True, text=True, timeout=10)
361
+ if check_result3.returncode == 0:
362
+ python_command = 'python3'
363
+ else:
364
+ yield "Warning: python not found in container PATH, searching...\n"
365
+ find_cmd = ['podman', 'exec', container_name, 'bash', '-c', 'find /usr -name python -o -name python3 -type f 2>/dev/null | head -1']
366
+ find_result = subprocess.run(find_cmd, capture_output=True, text=True, timeout=10)
367
+ if find_result.returncode == 0 and find_result.stdout.strip():
368
+ python_command = find_result.stdout.strip()
369
+ yield f"Found python at: {python_command}\n"
370
+ else:
371
+ yield "Error: Could not locate python in container\n"
372
+ return
373
+
374
+ cmd = ['podman', 'exec']
375
+ if workdir:
376
+ cmd.extend(['-w', workdir])
377
+ cmd.extend([container_name, 'bash', '-c', f'{python_command} "{temp_container_path}" 2>&1'])
378
+
379
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
380
+
381
+ try:
382
+ for line in iter(process.stdout.readline, ''):
383
+ if line:
384
+ yield line
385
+ except Exception as e:
386
+ yield f"Error reading output: {e}\n"
387
+ finally:
388
+ process.stdout.close()
389
+ process.wait()
390
+
391
+ run_podman_cmd(['exec', container_name, 'rm', '-f', temp_container_path])
392
+
393
+ except Exception as e:
394
+ yield f"Error executing Python code: {e}\n"
395
+
396
+ def run_lua_code_streaming(container_name: str, code: str, workdir: Optional[str] = None):
397
+ try:
398
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.lua', delete=False) as f:
399
+ f.write(code)
400
+ temp_file = f.name
401
+
402
+ temp_container_path = f'/tmp/lua_script_{uuid.uuid4().hex[:8]}.lua'
403
+ success = copy_file_to_container(container_name, temp_file, temp_container_path)
404
+
405
+ os.unlink(temp_file)
406
+
407
+ if not success:
408
+ yield "Error: Failed to copy Lua file to container\n"
409
+ return
410
+
411
+ lua_executables = ['lua', 'lua5.4', 'lua5.3', 'lua5.2', 'lua5.1']
412
+ lua_command = None
413
+
414
+ for exe in lua_executables:
415
+ check_cmd = ['podman', 'exec', container_name, 'which', exe]
416
+ check_result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
417
+ if check_result.returncode == 0:
418
+ lua_command = exe
419
+ break
420
+
421
+ if not lua_command:
422
+ yield "Warning: No lua executable found in container PATH, searching filesystem...\n"
423
+ find_cmd = ['podman', 'exec', container_name, 'bash', '-c', 'find /usr/local/bin /usr/bin -name "lua*" -type f 2>/dev/null | head -1']
424
+ find_result = subprocess.run(find_cmd, capture_output=True, text=True, timeout=10)
425
+ if find_result.returncode == 0 and find_result.stdout.strip():
426
+ lua_command = find_result.stdout.strip()
427
+ yield f"Found lua at: {lua_command}\n"
428
+ else:
429
+ yield "Error: Could not locate lua in container\n"
430
+ return
431
+
432
+ cmd = ['podman', 'exec']
433
+ if workdir:
434
+ cmd.extend(['-w', workdir])
435
+ cmd.extend([container_name, 'bash', '-c', f'{lua_command} "{temp_container_path}" 2>&1'])
436
+
437
+ process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
438
+
439
+ try:
440
+ for line in iter(process.stdout.readline, ''):
441
+ if line:
442
+ yield line
443
+ except Exception as e:
444
+ yield f"Error reading output: {e}\n"
445
+ finally:
446
+ process.stdout.close()
447
+ process.wait()
448
+
449
+ run_podman_cmd(['exec', container_name, 'rm', '-f', temp_container_path])
450
+
451
+ except Exception as e:
452
+ yield f"Error executing Lua code: {e}\n"
453
+
454
+ try:
455
+ from flask import Flask, request, jsonify
456
+ FLASK_AVAILABLE = True
457
+ except ImportError:
458
+ FLASK_AVAILABLE = False
459
+
460
+ def install_package(package_name: str):
461
+ try:
462
+ print(f"Installing {package_name}...")
463
+ import subprocess
464
+ cmd = [sys.executable, "-m", "pip", "install", package_name]
465
+ result = subprocess.run(cmd, capture_output=True, text=True)
466
+ if result.returncode == 0:
467
+ print(f"Successfully installed {package_name}")
468
+ return True
469
+ else:
470
+ print(f"Failed to install {package_name}: {result.stderr}")
471
+ return False
472
+ except Exception as e:
473
+ print(f"Error installing {package_name}: {e}")
474
+ return False
475
+
476
+ if not FLASK_AVAILABLE:
477
+ print("Flask not found, attempting to install...")
478
+ if install_package("flask"):
479
+ try:
480
+ from flask import Flask, request, jsonify
481
+ FLASK_AVAILABLE = True
482
+ except ImportError:
483
+ FLASK_AVAILABLE = False
484
+
485
+ if FLASK_AVAILABLE:
486
+ from flask_cors import CORS
487
+ app = Flask(__name__)
488
+ CORS(app)
489
+
490
+ class VMManager:
491
+ def __init__(self):
492
+ self.vms = {}
493
+ self.cleanup_interval = 60
494
+ self.max_idle_time = 180
495
+ self.cleanup_thread = None
496
+ self.lua_vm_id = None
497
+ self._cleanup_running = False
498
+ self._start_cleanup_thread()
499
+
500
+ def _start_cleanup_thread(self):
501
+ if self.cleanup_thread is None or not self.cleanup_thread.is_alive():
502
+ self.cleanup_thread = threading.Thread(target=self._cleanup_worker, daemon=True)
503
+ self.cleanup_thread.start()
504
+
505
+ def _cleanup_worker(self):
506
+ while True:
507
+ try:
508
+ # Only run cleanup if not already running
509
+ if not self._cleanup_running:
510
+ self._cleanup_running = True
511
+ try:
512
+ self._perform_cleanup()
513
+ finally:
514
+ self._cleanup_running = False
515
+
516
+ except Exception as e:
517
+ print(f"Error in cleanup worker: {e}")
518
+ self._cleanup_running = False
519
+
520
+ time.sleep(self.cleanup_interval)
521
+
522
+ def _perform_cleanup(self):
523
+ """Perform the actual cleanup logic (used by both auto and manual cleanup)"""
524
+ current_time = time.time()
525
+ cleanup_threshold = 180 # 3 minutes for automatic cleanup
526
+ deleted_count = 0
527
+ orphaned_containers_deleted = 0
528
+
529
+ print(f"Automatic cleanup: checking for idle VMs and orphaned containers...")
530
+
531
+ vms_to_delete = []
532
+ for vm_id, vm in self.vms.items():
533
+ if vm_id == self.lua_vm_id:
534
+ continue
535
+ if current_time - vm.get('last_used', vm['created']) > cleanup_threshold:
536
+ vms_to_delete.append(vm_id)
537
+
538
+ for vm_id in vms_to_delete:
539
+ age_minutes = round((current_time - self.vms[vm_id].get('last_used', self.vms[vm_id]['created'])) / 60, 1)
540
+ print(f"Auto-deleting idle VM {vm_id} (unused for {age_minutes} minutes)")
541
+ self.delete_vm(vm_id)
542
+ deleted_count += 1
543
+
544
+ # Clean up orphaned podman containers
545
+ try:
546
+ ps_success, ps_stdout, ps_stderr = run_podman_cmd(['ps', '-a', '--filter', 'name=vm-', '--format', 'json'])
547
+ if ps_success and ps_stdout:
548
+ try:
549
+ import json as json_lib
550
+ containers = json_lib.loads(ps_stdout)
551
+ if isinstance(containers, list):
552
+ for container in containers:
553
+ container_name = container.get('Names', [''])[0] if container.get('Names') else ''
554
+ if container_name.startswith('vm-'):
555
+ vm_id = container_name[3:] # Remove 'vm-' prefix
556
+ if vm_id not in self.vms:
557
+ print(f"Auto-deleting orphaned container {container_name}")
558
+ run_podman_cmd(['rm', '-f', container_name])
559
+ orphaned_containers_deleted += 1
560
+ except Exception as e:
561
+ print(f"Error parsing container list in auto cleanup: {e}")
562
+ except Exception as e:
563
+ print(f"Error checking for orphaned containers in auto cleanup: {e}")
564
+
565
+ if deleted_count > 0 or orphaned_containers_deleted > 0:
566
+ print(f"Automatic cleanup completed: deleted {deleted_count} VMs and {orphaned_containers_deleted} orphaned containers")
567
+ else:
568
+ print("Automatic cleanup completed: nothing to clean")
569
+
570
+ def _update_last_used(self, vm_id):
571
+ if vm_id in self.vms:
572
+ self.vms[vm_id]['last_used'] = time.time()
573
+
574
+ def _ensure_lua_vm(self):
575
+ if self.lua_vm_id and self.lua_vm_id in self.vms:
576
+ vm = self.vms[self.lua_vm_id]
577
+ if vm.get('status') == 'running':
578
+ return self.lua_vm_id
579
+
580
+ print("Creating persistent Lua VM...")
581
+ self.lua_vm_id = self.create_vm(
582
+ vcpu=0.25,
583
+ memory="512m",
584
+ image="debian:latest",
585
+ install_python=False
586
+ )
587
+
588
+ if self.lua_vm_id:
589
+ print(f"Created persistent Lua VM: {self.lua_vm_id}")
590
+ self._install_lua_in_persistent_vm(self.lua_vm_id)
591
+
592
+ return self.lua_vm_id
593
+
594
+ def create_vm(self, vcpu=1, memory="512m", image="debian:latest", install_python=True):
595
+ vm_id = str(uuid.uuid4())[:8]
596
+ container_name = f"vm-{vm_id}"
597
+
598
+ # Create and start container
599
+ success, stdout, stderr = run_podman_cmd([
600
+ 'run', '-d', '--name', container_name,
601
+ '--cpus', str(vcpu), '--memory', memory,
602
+ '--rm', image, 'sleep', 'infinity'
603
+ ])
604
+
605
+ if not success:
606
+ print(f"Failed to create container: {stderr}")
607
+ if "authentication required" in stderr.lower():
608
+ print("Podman authentication issue. Try running: podman login docker.io")
609
+ elif "podman not found" in stderr.lower():
610
+ print("Podman not installed. Install with: sudo apt install podman")
611
+ return None
612
+
613
+ self.vms[vm_id] = {
614
+ 'container_name': container_name,
615
+ 'vcpu': vcpu,
616
+ 'memory': memory,
617
+ 'created': time.time(),
618
+ 'last_used': time.time(),
619
+ 'commands': [],
620
+ 'cwd': '/' # Default working directory
621
+ }
622
+
623
+ if 'debian' in image.lower() or 'ubuntu' in image.lower():
624
+ if install_python:
625
+ self._install_python_in_vm(vm_id)
626
+ self._install_lua_in_vm(vm_id)
627
+ elif 'alpine' in image.lower():
628
+ self._install_lua_in_vm(vm_id)
629
+
630
+ return vm_id
631
+
632
+ def _install_python_in_vm(self, vm_id):
633
+ if vm_id not in self.vms:
634
+ return
635
+
636
+ container_name = self.vms[vm_id]['container_name']
637
+
638
+ update_success, update_stdout, update_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'update', '-qq'])
639
+ if not update_success:
640
+ print(f"Warning: Failed to update package lists for VM {vm_id}: {update_stderr}")
641
+
642
+ python_success, python_stdout, python_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'install', '-y', 'python3', 'python3-pip'])
643
+ if not python_success:
644
+ print(f"Warning: Failed to install Python for VM {vm_id}: {python_stderr}")
645
+ alt_success, alt_stdout, alt_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'install', '-y', 'python3-minimal'])
646
+ if not alt_success:
647
+ print(f"Warning: Failed to install python3-minimal for VM {vm_id}: {alt_stderr}")
648
+ else:
649
+ print(f"Successfully installed python3-minimal for VM {vm_id}")
650
+ else:
651
+ print(f"Successfully installed Python3 and pip for VM {vm_id}")
652
+
653
+ verify_success, verify_stdout, verify_stderr = run_podman_cmd(['exec', container_name, 'python3', '--version'])
654
+ if verify_success:
655
+ print(f"Python verification successful for VM {vm_id}: {verify_stdout.strip()}")
656
+ else:
657
+ print(f"Warning: Python verification failed for VM {vm_id}: {verify_stderr}")
658
+
659
+ def _install_lua_in_vm(self, vm_id):
660
+ if vm_id not in self.vms:
661
+ return
662
+
663
+ container_name = self.vms[vm_id]['container_name']
664
+
665
+ print(f"Installing Lua for VM {vm_id}...")
666
+
667
+ lua_install_success, lua_install_stdout, lua_install_stderr = run_podman_cmd([
668
+ 'exec', container_name, 'apk', 'add', '--no-cache', 'lua5.4'
669
+ ])
670
+
671
+ if not lua_install_success:
672
+ print(f"Warning: Failed to install Lua for VM {vm_id}: {lua_install_stderr}")
673
+ return
674
+
675
+ print(f"Successfully installed Lua 5.4 for VM {vm_id}")
676
+
677
+ verify_success, verify_stdout, verify_stderr = run_podman_cmd(['exec', container_name, 'lua5.4', '--version'])
678
+ if verify_success:
679
+ print(f"Lua verification successful for VM {vm_id}: {verify_stdout.strip()}")
680
+ else:
681
+ print(f"Warning: Lua verification failed for VM {vm_id}: {verify_stderr}")
682
+ print("Checking what Lua executables are available...")
683
+ which_success, which_stdout, which_stderr = run_podman_cmd(['exec', container_name, 'find', '/usr/bin', '-name', 'lua*', '-type', 'f'])
684
+ if which_success and which_stdout.strip():
685
+ print(f"Found Lua executables: {which_stdout.strip()}")
686
+ else:
687
+ print("No Lua executables found in /usr/bin")
688
+
689
+ def _install_lua_in_persistent_vm(self, vm_id):
690
+ if vm_id not in self.vms:
691
+ return
692
+
693
+ container_name = self.vms[vm_id]['container_name']
694
+
695
+ print(f"Installing Lua in persistent VM {vm_id}...")
696
+
697
+ update_success, update_stdout, update_stderr = run_podman_cmd(['exec', container_name, 'apt-get', 'update', '-qq'])
698
+ if not update_success:
699
+ print(f"Warning: Failed to update package lists for Lua installation in persistent VM {vm_id}: {update_stderr}")
700
+
701
+ build_deps_success, build_deps_stdout, build_deps_stderr = run_podman_cmd([
702
+ 'exec', container_name, 'apt-get', 'install', '-y', '-qq',
703
+ 'curl', 'build-essential', 'libreadline-dev'
704
+ ])
705
+ if not build_deps_success:
706
+ print(f"Warning: Failed to install build dependencies for persistent VM {vm_id}: {build_deps_stderr}")
707
+ return
708
+
709
+ print(f"Downloading and compiling Lua in persistent VM {vm_id}...")
710
+ lua_install_cmd = '''
711
+ cd /tmp && \
712
+ curl -L -o lua.tar.gz http://www.lua.org/ftp/lua-5.4.6.tar.gz && \
713
+ tar zxf lua.tar.gz && \
714
+ cd lua-5.4.6 && \
715
+ make linux && \
716
+ make install && \
717
+ cd /tmp && \
718
+ rm -rf lua-5.4.6 lua.tar.gz
719
+ '''.strip()
720
+
721
+ install_success, install_stdout, install_stderr = run_podman_cmd([
722
+ 'exec', container_name, 'bash', '-c', lua_install_cmd
723
+ ])
724
+
725
+ if not install_success:
726
+ print(f"Warning: Failed to compile/install Lua in persistent VM {vm_id}: {install_stderr}")
727
+ return
728
+
729
+ verify_success, verify_stdout, verify_stderr = run_podman_cmd(['exec', container_name, 'lua', '--version'])
730
+ if verify_success:
731
+ print(f"Lua verification successful in persistent VM {vm_id}: {verify_stdout.strip()}")
732
+ else:
733
+ print(f"Warning: Lua verification failed in persistent VM {vm_id}: {verify_stderr}")
734
+
735
+ def run_command(self, vm_id, command, workdir=None):
736
+ if vm_id not in self.vms:
737
+ return None
738
+
739
+ self._update_last_used(vm_id)
740
+ vm = self.vms[vm_id]
741
+ container_name = vm['container_name']
742
+
743
+ command_stripped = command.strip()
744
+ if command_stripped == 'cd' or command_stripped.startswith('cd '):
745
+ if command_stripped == 'cd':
746
+ vm['cwd'] = '/root'
747
+ result = "Changed directory to /root"
748
+ else:
749
+ new_dir = command_stripped[3:].strip()
750
+ if new_dir:
751
+ if not new_dir.startswith('/'):
752
+ new_dir = os.path.join(vm['cwd'], new_dir)
753
+ new_dir = os.path.normpath(new_dir)
754
+ test_cmd = f'test -d "{new_dir}" && echo "DIR_EXISTS" || echo "DIR_NOT_FOUND"'
755
+ success, stdout, stderr = run_command_in_container(container_name, test_cmd, vm['cwd'])
756
+ if success and 'DIR_EXISTS' in stdout:
757
+ vm['cwd'] = new_dir
758
+ result = f"Changed directory to {new_dir}"
759
+ else:
760
+ result = f"cd: {new_dir}: No such file or directory"
761
+ else:
762
+ vm['cwd'] = '/root'
763
+ result = "Changed directory to /root"
764
+ else:
765
+ current_workdir = workdir if workdir is not None else vm['cwd']
766
+
767
+ success, stdout, stderr = run_command_in_container(container_name, command, current_workdir)
768
+
769
+ if success:
770
+ result = stdout.rstrip() if stdout else ""
771
+ if not result and command.strip() == 'ls':
772
+ success2, stdout2, stderr2 = run_command_in_container(container_name, 'ls -a', current_workdir)
773
+ if success2:
774
+ result = stdout2.rstrip() if stdout2 else "(directory appears empty)"
775
+ else:
776
+ result = f"ls failed: {stderr2.strip()}"
777
+ elif not result and stderr.strip():
778
+ result = f"(no output) stderr: {stderr.strip()}"
779
+ else:
780
+ result = f"Error: {stderr.strip()}" if stderr else f"Command failed - stderr: '{stderr}'"
781
+
782
+ vm['commands'].append({
783
+ 'command': command,
784
+ 'result': result,
785
+ 'time': time.time()
786
+ })
787
+
788
+ return result
789
+
790
+ def copy_file_to_vm(self, vm_id, local_path, container_path):
791
+ if vm_id not in self.vms:
792
+ return False
793
+ self._update_last_used(vm_id)
794
+ container_name = self.vms[vm_id]['container_name']
795
+ return copy_file_to_container(container_name, local_path, container_path)
796
+
797
+ def copy_file_from_vm(self, vm_id, container_path, local_path):
798
+ if vm_id not in self.vms:
799
+ return False
800
+ self._update_last_used(vm_id)
801
+ container_name = self.vms[vm_id]['container_name']
802
+ return copy_file_from_container(container_name, container_path, local_path)
803
+
804
+ def execute_python_streaming(self, vm_id, code, workdir=None):
805
+ if vm_id in self.vms:
806
+ self._update_last_used(vm_id)
807
+
808
+ python_vm_id = self.create_vm(
809
+ vcpu=0.25,
810
+ memory="300m",
811
+ image="python:3.11-slim",
812
+ install_python=False
813
+ )
814
+
815
+ if not python_vm_id:
816
+ yield "Error: Failed to create Python VM\n"
817
+ return
818
+
819
+ try:
820
+ python_vm = self.vms[python_vm_id]
821
+ container_name = python_vm['container_name']
822
+ current_workdir = workdir or '/'
823
+
824
+ for line in run_python_code_streaming(container_name, code, current_workdir):
825
+ yield line
826
+
827
+ finally:
828
+ self.delete_vm(python_vm_id)
829
+
830
+ def execute_lua_streaming(self, vm_id, code, workdir=None):
831
+ if vm_id in self.vms:
832
+ self._update_last_used(vm_id)
833
+
834
+ lua_vm_id = self._ensure_lua_vm()
835
+ if not lua_vm_id:
836
+ yield "Error: Failed to ensure Lua VM is available\n"
837
+ return
838
+
839
+ lua_vm = self.vms[lua_vm_id]
840
+ container_name = lua_vm['container_name']
841
+ current_workdir = workdir or '/'
842
+
843
+ for line in run_lua_code_streaming(container_name, code, current_workdir):
844
+ yield line
845
+
846
+ self._update_last_used(lua_vm_id)
847
+
848
+ def get_vm_status(self, vm_id):
849
+ if vm_id not in self.vms:
850
+ return None
851
+
852
+ self._update_last_used(vm_id)
853
+ vm = self.vms[vm_id]
854
+ container_name = vm['container_name']
855
+
856
+ success, stdout, stderr = run_podman_cmd([
857
+ 'ps', '--filter', f'name={container_name}', '--format', 'json'
858
+ ])
859
+
860
+ vm_copy = vm.copy()
861
+ if success and stdout:
862
+ try:
863
+ import json as json_lib
864
+ containers = json_lib.loads(stdout)
865
+ if containers:
866
+ vm_copy['status'] = 'running'
867
+ else:
868
+ vm_copy['status'] = 'stopped'
869
+ except:
870
+ vm_copy['status'] = 'unknown'
871
+ else:
872
+ vm_copy['status'] = 'error'
873
+
874
+ return vm_copy
875
+
876
+ def get_vm_cwd(self, vm_id):
877
+ if vm_id not in self.vms:
878
+ return None
879
+ self._update_last_used(vm_id)
880
+ return self.vms[vm_id]['cwd']
881
+
882
+ def list_vms(self):
883
+ return list(self.vms.keys())
884
+
885
+ def delete_vm(self, vm_id):
886
+ if vm_id not in self.vms:
887
+ return False
888
+
889
+ container_name = self.vms[vm_id]['container_name']
890
+
891
+ run_podman_cmd(['stop', container_name])
892
+ run_podman_cmd(['rm', container_name])
893
+
894
+ del self.vms[vm_id]
895
+ return True
896
+
897
+ manager = VMManager()
898
+
899
+ @app.route('/')
900
+ def index():
901
+ return """
902
+ <html>
903
+ <head><title>Simple VM API</title></head>
904
+ <body>
905
+ <h1>Simple VM API</h1>
906
+ <p>Endpoints:</p>
907
+ <ul>
908
+ <li>POST /vm - Create VM (supports image and install_python params)</li>
909
+ <li>POST /vm/&lt;id&gt;/command - Run command</li>
910
+ <li>POST /vm/&lt;id&gt;/python - Execute Python code (streaming)</li>
911
+ <li>POST /vm/&lt;id&gt;/lua - Execute Lua code (streaming)</li>
912
+ <li>POST /vm/&lt;id&gt;/copy-to - Copy file to VM</li>
913
+ <li>POST /vm/&lt;id&gt;/copy-from - Copy file from VM</li>
914
+ <li>GET /vm/&lt;id&gt;/status - Get VM status</li>
915
+ <li>GET /vm/&lt;id&gt;/cwd - Get current working directory</li>
916
+ <li>DELETE /vm/&lt;id&gt; - Delete VM</li>
917
+ <li>GET /cleanup - Delete VMs unused for 10+ minutes</li>
918
+ <li>GET /poll - Poll for updates</li>
919
+ <p><strong>Server runs on port 7860</strong></p>
920
+ </ul>
921
+ </body>
922
+ </html>
923
+ """
924
+
925
+ @app.route('/vm', methods=['POST'])
926
+ def create_vm():
927
+ data = request.get_json() or {}
928
+ vcpu = data.get('vcpu', 1)
929
+ memory = data.get('memory', '512m')
930
+ image = data.get('image', 'debian:latest')
931
+ install_python = data.get('install_python', True)
932
+ vm_id = manager.create_vm(vcpu, memory, image, install_python)
933
+ return jsonify({'vm_id': vm_id, 'vcpu': vcpu, 'memory': memory, 'image': image})
934
+
935
+ @app.route('/vm/<vm_id>/command', methods=['POST'])
936
+ def run_command(vm_id):
937
+ data = request.get_json() or {}
938
+ command = data.get('command', '')
939
+
940
+ result = manager.run_command(vm_id, command)
941
+ if result is None:
942
+ return jsonify({'error': 'VM not found'}), 404
943
+
944
+ return jsonify({'output': result, 'command': command})
945
+
946
+ @app.route('/vm/<vm_id>/status')
947
+ def get_status(vm_id):
948
+ status = manager.get_vm_status(vm_id)
949
+ if not status:
950
+ return jsonify({'error': 'VM not found'}), 404
951
+ return jsonify(status)
952
+
953
+ @app.route('/vm/<vm_id>/cwd')
954
+ def get_cwd(vm_id):
955
+ cwd = manager.get_vm_cwd(vm_id)
956
+ if cwd is None:
957
+ return jsonify({'error': 'VM not found'}), 404
958
+ return jsonify({'cwd': cwd})
959
+
960
+ @app.route('/vm/<vm_id>/python', methods=['POST'])
961
+ def execute_python(vm_id):
962
+ if vm_id in manager.vms:
963
+ manager._update_last_used(vm_id)
964
+
965
+ code = None
966
+ if 'file' in request.files and request.files['file'].filename:
967
+ file = request.files['file']
968
+ if not file.filename.endswith('.py'):
969
+ return jsonify({'error': 'Only .py files are allowed'}), 400
970
+ file.seek(0, os.SEEK_END)
971
+ file_size = file.tell()
972
+ file.seek(0)
973
+ if file_size > 10 * 1024 * 1024:
974
+ return jsonify({'error': 'File too large (max 10MB)'}), 400
975
+ code = file.read().decode('utf-8')
976
+ elif request.is_json and request.get_json().get('code'):
977
+ code = request.get_json()['code']
978
+ else:
979
+ return jsonify({'error': 'No Python code provided. Use "code" field in JSON or upload a .py file'}), 400
980
+
981
+ if not code or not code.strip():
982
+ return jsonify({'error': 'Empty Python code'}), 400
983
+
984
+ workdir = request.args.get('workdir') or request.get_json().get('workdir') if request.is_json else None
985
+
986
+ def generate():
987
+ try:
988
+ for line in manager.execute_python_streaming(vm_id, code, workdir):
989
+ yield f"data: {line}\n\n"
990
+ except Exception as e:
991
+ yield f"data: Error: {str(e)}\n\n"
992
+
993
+ return app.response_class(generate(), mimetype='text/event-stream')
994
+
995
+ @app.route('/vm/<vm_id>/lua', methods=['POST'])
996
+ def execute_lua(vm_id):
997
+ if vm_id in manager.vms:
998
+ manager._update_last_used(vm_id)
999
+
1000
+ code = None
1001
+ if 'file' in request.files and request.files['file'].filename:
1002
+ file = request.files['file']
1003
+ if not file.filename.endswith('.lua'):
1004
+ return jsonify({'error': 'Only .lua files are allowed'}), 400
1005
+ file.seek(0, os.SEEK_END)
1006
+ file_size = file.tell()
1007
+ file.seek(0)
1008
+ if file_size > 10 * 1024 * 1024:
1009
+ return jsonify({'error': 'File too large (max 10MB)'}), 400
1010
+ code = file.read().decode('utf-8')
1011
+ elif request.is_json and request.get_json().get('code'):
1012
+ code = request.get_json()['code']
1013
+ else:
1014
+ return jsonify({'error': 'No Lua code provided. Use "code" field in JSON or upload a .lua file'}), 400
1015
+
1016
+ if not code or not code.strip():
1017
+ return jsonify({'error': 'Empty Lua code'}), 400
1018
+
1019
+ workdir = request.args.get('workdir') or request.get_json().get('workdir') if request.is_json else None
1020
+
1021
+ def generate():
1022
+ try:
1023
+ for line in manager.execute_lua_streaming(vm_id, code, workdir):
1024
+ yield f"data: {line}\n\n"
1025
+ except Exception as e:
1026
+ yield f"data: Error: {str(e)}\n\n"
1027
+
1028
+ return app.response_class(generate(), mimetype='text/event-stream')
1029
+
1030
+ @app.route('/vm/<vm_id>', methods=['DELETE'])
1031
+ def delete_vm(vm_id):
1032
+ if manager.delete_vm(vm_id):
1033
+ return jsonify({'message': 'VM deleted'})
1034
+ return jsonify({'error': 'VM not found'}), 404
1035
+
1036
+ @app.route('/vm/<vm_id>/copy-to', methods=['POST'])
1037
+ def copy_to_vm(vm_id):
1038
+ data = request.get_json() or {}
1039
+ local_path = data.get('local_path', '')
1040
+ container_path = data.get('container_path', '')
1041
+
1042
+ if not local_path or not container_path:
1043
+ return jsonify({'error': 'local_path and container_path required'}), 400
1044
+
1045
+ if not os.path.exists(local_path):
1046
+ return jsonify({'error': 'Local file does not exist'}), 400
1047
+
1048
+ success = manager.copy_file_to_vm(vm_id, local_path, container_path)
1049
+ if success:
1050
+ return jsonify({'message': 'File copied successfully'})
1051
+ return jsonify({'error': 'Failed to copy file'}), 500
1052
+
1053
+ @app.route('/vm/<vm_id>/copy-from', methods=['POST'])
1054
+ def copy_from_vm(vm_id):
1055
+ data = request.get_json() or {}
1056
+ container_path = data.get('container_path', '')
1057
+ local_path = data.get('local_path', '')
1058
+
1059
+ if not container_path or not local_path:
1060
+ return jsonify({'error': 'container_path and local_path required'}), 400
1061
+
1062
+ success = manager.copy_file_from_vm(vm_id, container_path, local_path)
1063
+ if success:
1064
+ return jsonify({'message': 'File copied successfully'})
1065
+ return jsonify({'error': 'Failed to copy file'}), 500
1066
+
1067
+ @app.route('/cleanup')
1068
+ def cleanup():
1069
+ def cleanup_generator():
1070
+ import time
1071
+ import json as json_lib
1072
+
1073
+ if manager._cleanup_running:
1074
+ yield f"data: {json_lib.dumps({'event': 'busy', 'message': 'Cleanup already running, please wait...'})}\n\n"
1075
+ return
1076
+
1077
+ manager._cleanup_running = True
1078
+
1079
+ try:
1080
+ current_time = time.time()
1081
+ cleanup_threshold = 600 # 10 minutes for manual cleanup
1082
+ deleted_count = 0
1083
+ orphaned_containers_deleted = 0
1084
+
1085
+ yield f"data: {json_lib.dumps({'event': 'started', 'message': 'Starting manual cleanup process (10+ minute threshold)...'})}\n\n"
1086
+
1087
+ vms_info = []
1088
+ vms_to_delete = []
1089
+
1090
+ # First, analyze managed VMs
1091
+ yield f"data: {json_lib.dumps({'event': 'analyzing_vms', 'message': 'Analyzing managed VMs...'})}\n\n"
1092
+
1093
+ for vm_id, vm in manager.vms.items():
1094
+ last_activity = vm.get('last_used', vm.get('created', 0))
1095
+ age_seconds = current_time - last_activity
1096
+ age_minutes = age_seconds / 60
1097
+
1098
+ vm_info = {
1099
+ 'vm_id': vm_id,
1100
+ 'age_minutes': round(age_minutes, 1),
1101
+ 'is_lua_vm': vm_id == manager.lua_vm_id,
1102
+ 'should_delete': age_seconds > cleanup_threshold and vm_id != manager.lua_vm_id
1103
+ }
1104
+ vms_info.append(vm_info)
1105
+
1106
+ if vm_id == manager.lua_vm_id:
1107
+ continue
1108
+ if age_seconds > cleanup_threshold:
1109
+ vms_to_delete.append(vm_id)
1110
+
1111
+ yield f"data: {json_lib.dumps({'event': 'vms_analysis_complete', 'total_vms': len(manager.vms), 'idle_vms': len(vms_to_delete), 'vms_info': vms_info})}\n\n"
1112
+
1113
+ # Clean up managed VMs that are idle
1114
+ if vms_to_delete:
1115
+ yield f"data: {json_lib.dumps({'event': 'deleting_vms', 'message': f'Deleting {len(vms_to_delete)} idle VMs...'})}\n\n"
1116
+
1117
+ for vm_id in vms_to_delete:
1118
+ age_minutes = round((current_time - manager.vms[vm_id].get('last_used', manager.vms[vm_id]['created'])) / 60, 1)
1119
+ yield f"data: {json_lib.dumps({'event': 'deleting_vm', 'vm_id': vm_id, 'age_minutes': age_minutes})}\n\n"
1120
+
1121
+ print(f"Manual cleanup: deleting idle VM {vm_id} (unused for {age_minutes} minutes)")
1122
+ manager.delete_vm(vm_id)
1123
+ deleted_count += 1
1124
+
1125
+ yield f"data: {json_lib.dumps({'event': 'vm_deleted', 'vm_id': vm_id, 'deleted_count': deleted_count})}\n\n"
1126
+
1127
+ # Now clean up orphaned podman containers
1128
+ yield f"data: {json_lib.dumps({'event': 'checking_containers', 'message': 'Checking for orphaned podman containers...'})}\n\n"
1129
+
1130
+ try:
1131
+ # Get all containers with our naming pattern
1132
+ ps_success, ps_stdout, ps_stderr = run_podman_cmd(['ps', '-a', '--filter', 'name=vm-', '--format', 'json'])
1133
+ if ps_success and ps_stdout:
1134
+ try:
1135
+ containers = json_lib.loads(ps_stdout)
1136
+ orphaned_containers = []
1137
+
1138
+ if isinstance(containers, list):
1139
+ for container in containers:
1140
+ container_name = container.get('Names', [''])[0] if container.get('Names') else ''
1141
+ if container_name.startswith('vm-'):
1142
+ vm_id = container_name[3:] # Remove 'vm-' prefix
1143
+ # Check if this VM is still managed
1144
+ if vm_id not in manager.vms:
1145
+ orphaned_containers.append(container_name)
1146
+
1147
+ if orphaned_containers:
1148
+ yield f"data: {json_lib.dumps({'event': 'orphaned_found', 'count': len(orphaned_containers), 'containers': orphaned_containers})}\n\n"
1149
+
1150
+ for container_name in orphaned_containers:
1151
+ yield f"data: {json_lib.dumps({'event': 'deleting_container', 'container': container_name})}\n\n"
1152
+
1153
+ print(f"Manual cleanup: deleting orphaned container {container_name}")
1154
+ rm_success, rm_stdout, rm_stderr = run_podman_cmd(['rm', '-f', container_name])
1155
+ if rm_success:
1156
+ orphaned_containers_deleted += 1
1157
+ yield f"data: {json_lib.dumps({'event': 'container_deleted', 'container': container_name, 'orphaned_deleted': orphaned_containers_deleted})}\n\n"
1158
+ else:
1159
+ yield f"data: {json_lib.dumps({'event': 'container_delete_failed', 'container': container_name, 'error': rm_stderr})}\n\n"
1160
+ else:
1161
+ yield f"data: {json_lib.dumps({'event': 'no_orphaned', 'message': 'No orphaned containers found'})}\n\n"
1162
+
1163
+ except Exception as e:
1164
+ yield f"data: {json_lib.dumps({'event': 'error', 'message': f'Error parsing container list: {str(e)}'})}\n\n"
1165
+ else:
1166
+ yield f"data: {json_lib.dumps({'event': 'error', 'message': f'Failed to list containers: {ps_stderr}'})}\n\n"
1167
+ except Exception as e:
1168
+ yield f"data: {json_lib.dumps({'event': 'error', 'message': f'Error checking for orphaned containers: {str(e)}'})}\n\n"
1169
+
1170
+ # Final summary
1171
+ summary_data = {
1172
+ 'event': 'completed',
1173
+ 'summary': {
1174
+ 'deleted_vms': deleted_count,
1175
+ 'deleted_containers': orphaned_containers_deleted,
1176
+ 'total_cleaned': deleted_count + orphaned_containers_deleted,
1177
+ 'remaining_vms': len(manager.vms)
1178
+ }
1179
+ }
1180
+ yield f"data: {json_lib.dumps(summary_data)}\n\n"
1181
+
1182
+ finally:
1183
+ manager._cleanup_running = False
1184
+
1185
+ return app.response_class(cleanup_generator(), mimetype='text/event-stream')
1186
+
1187
+ @app.route('/poll')
1188
+ def poll():
1189
+ return jsonify({'vms': manager.list_vms()})
1190
+
1191
+ def main():
1192
+ if len(sys.argv) < 2:
1193
+ command = "server"
1194
+ else:
1195
+ command = sys.argv[1]
1196
+
1197
+ if command == "server":
1198
+ if not FLASK_AVAILABLE:
1199
+ print("Flask not available")
1200
+ return
1201
+
1202
+ # Check if podman is available (will auto-install on Linux if needed)
1203
+ check_podman, podman_version, _ = run_podman_cmd(['--version'])
1204
+ if check_podman:
1205
+ print(f"Podman is available - {podman_version.strip()}")
1206
+ else:
1207
+ print("Podman not found initially, but will be auto-installed on first use if running on Linux")
1208
+
1209
+ print("Starting simple VM API server on http://localhost:7860")
1210
+ print("Podman is available - ready to create containers!")
1211
+ app.run(host='0.0.0.0', port=7860, debug=True)
1212
+
1213
+ elif command == "list":
1214
+ if not FLASK_AVAILABLE:
1215
+ print("Flask not available")
1216
+ return
1217
+ manager = VMManager()
1218
+ vms = manager.list_vms()
1219
+ print(f"VMs: {vms}")
1220
+
1221
+ else:
1222
+ print("Usage: python3 lib.py [server|list]")
1223
+
1224
+ if __name__ == "__main__":
1225
+ main()