iflow-mcp_bethington-cheat-engine-server-python 0.1.0__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.
Files changed (40) hide show
  1. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/METADATA +16 -0
  2. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/RECORD +40 -0
  3. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/WHEEL +5 -0
  4. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. iflow_mcp_bethington_cheat_engine_server_python-0.1.0.dist-info/top_level.txt +1 -0
  7. server/cheatengine/__init__.py +19 -0
  8. server/cheatengine/ce_bridge.py +1670 -0
  9. server/cheatengine/lua_interface.py +460 -0
  10. server/cheatengine/table_parser.py +1221 -0
  11. server/config/__init__.py +20 -0
  12. server/config/settings.py +347 -0
  13. server/config/whitelist.py +378 -0
  14. server/gui_automation/__init__.py +43 -0
  15. server/gui_automation/core/__init__.py +8 -0
  16. server/gui_automation/core/integration.py +951 -0
  17. server/gui_automation/demos/__init__.py +8 -0
  18. server/gui_automation/demos/basic_demo.py +754 -0
  19. server/gui_automation/demos/notepad_demo.py +460 -0
  20. server/gui_automation/demos/simple_demo.py +319 -0
  21. server/gui_automation/tools/__init__.py +8 -0
  22. server/gui_automation/tools/mcp_tools.py +974 -0
  23. server/main.py +519 -0
  24. server/memory/__init__.py +0 -0
  25. server/memory/analyzer.py +0 -0
  26. server/memory/reader.py +0 -0
  27. server/memory/scanner.py +0 -0
  28. server/memory/symbols.py +0 -0
  29. server/process/__init__.py +16 -0
  30. server/process/launcher.py +608 -0
  31. server/process/manager.py +185 -0
  32. server/process/monitors.py +202 -0
  33. server/process/permissions.py +131 -0
  34. server/process_whitelist.json +119 -0
  35. server/pyautogui/__init__.py +0 -0
  36. server/utils/__init__.py +37 -0
  37. server/utils/data_types.py +368 -0
  38. server/utils/formatters.py +430 -0
  39. server/utils/validators.py +340 -0
  40. server/window_automation/__init__.py +59 -0
@@ -0,0 +1,608 @@
1
+ """
2
+ Application Launcher Module
3
+ Handles execution and termination of whitelisted applications
4
+ """
5
+
6
+ import os
7
+ import sys
8
+ import subprocess
9
+ import logging
10
+ import time
11
+ import json
12
+ from typing import Dict, List, Optional, Any, Tuple
13
+ from pathlib import Path
14
+ from dataclasses import dataclass
15
+ from datetime import datetime
16
+
17
+ import psutil
18
+
19
+ # Import whitelist functionality
20
+ from server.config.whitelist import ProcessWhitelist
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ @dataclass
25
+ class LaunchedApplication:
26
+ """Information about a launched application"""
27
+ process_name: str
28
+ pid: int
29
+ exe_path: str
30
+ launch_time: datetime
31
+ command_line: List[str]
32
+ working_directory: str
33
+ status: str = "running" # running, terminated, crashed
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ """Convert to dictionary for serialization"""
37
+ return {
38
+ 'process_name': self.process_name,
39
+ 'pid': self.pid,
40
+ 'exe_path': self.exe_path,
41
+ 'launch_time': self.launch_time.isoformat(),
42
+ 'command_line': self.command_line,
43
+ 'working_directory': self.working_directory,
44
+ 'status': self.status
45
+ }
46
+
47
+ class ApplicationLauncher:
48
+ """Manages launching and termination of whitelisted applications"""
49
+
50
+ def __init__(self, whitelist: ProcessWhitelist):
51
+ self.whitelist = whitelist
52
+ self.launched_applications: Dict[int, LaunchedApplication] = {}
53
+ self._session_file = None
54
+
55
+ # Common Windows application paths
56
+ self.system_paths = [
57
+ os.environ.get('WINDIR', 'C:\\Windows'),
58
+ os.path.join(os.environ.get('WINDIR', 'C:\\Windows'), 'System32'),
59
+ os.path.join(os.environ.get('WINDIR', 'C:\\Windows'), 'SysWOW64'),
60
+ os.environ.get('PROGRAMFILES', 'C:\\Program Files'),
61
+ os.environ.get('PROGRAMFILES(X86)', 'C:\\Program Files (x86)'),
62
+ ]
63
+
64
+ # Add common application directories
65
+ self.common_app_paths = [
66
+ os.path.join(os.environ.get('PROGRAMFILES', 'C:\\Program Files'), 'Windows NT', 'Accessories'),
67
+ os.path.join(os.environ.get('PROGRAMFILES', 'C:\\Program Files'), 'Microsoft VS Code'),
68
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Microsoft VS Code'),
69
+ ]
70
+
71
+ logger.info("Application launcher initialized")
72
+
73
+ def set_session_file(self, session_file_path: str):
74
+ """Set the session file for persistence"""
75
+ self._session_file = session_file_path
76
+ self._load_session()
77
+
78
+ def _load_session(self):
79
+ """Load previously launched applications from session file"""
80
+ if not self._session_file or not os.path.exists(self._session_file):
81
+ return
82
+
83
+ try:
84
+ with open(self._session_file, 'r') as f:
85
+ session_data = json.load(f)
86
+
87
+ for app_data in session_data.get('launched_applications', []):
88
+ try:
89
+ # Check if process is still running
90
+ pid = app_data['pid']
91
+ if psutil.pid_exists(pid):
92
+ app = LaunchedApplication(
93
+ process_name=app_data['process_name'],
94
+ pid=pid,
95
+ exe_path=app_data['exe_path'],
96
+ launch_time=datetime.fromisoformat(app_data['launch_time']),
97
+ command_line=app_data['command_line'],
98
+ working_directory=app_data['working_directory'],
99
+ status='running'
100
+ )
101
+ self.launched_applications[pid] = app
102
+ logger.info(f"Restored session for PID {pid}: {app.process_name}")
103
+ else:
104
+ logger.info(f"Process {pid} ({app_data['process_name']}) no longer running")
105
+ except Exception as e:
106
+ logger.warning(f"Failed to restore session entry: {e}")
107
+
108
+ except Exception as e:
109
+ logger.error(f"Failed to load session file: {e}")
110
+
111
+ def _save_session(self):
112
+ """Save current launched applications to session file"""
113
+ if not self._session_file:
114
+ return
115
+
116
+ try:
117
+ # Update status of all applications
118
+ self._update_application_status()
119
+
120
+ session_data = {
121
+ 'last_updated': datetime.now().isoformat(),
122
+ 'launched_applications': [app.to_dict() for app in self.launched_applications.values()]
123
+ }
124
+
125
+ # Ensure directory exists
126
+ os.makedirs(os.path.dirname(self._session_file), exist_ok=True)
127
+
128
+ with open(self._session_file, 'w') as f:
129
+ json.dump(session_data, f, indent=2)
130
+
131
+ except Exception as e:
132
+ logger.error(f"Failed to save session file: {e}")
133
+
134
+ def find_executable(self, process_name: str) -> Optional[str]:
135
+ """Find the full path to an executable"""
136
+
137
+ # If it's already a full path and exists
138
+ if os.path.isabs(process_name) and os.path.exists(process_name):
139
+ return process_name
140
+
141
+ # Try just the name in PATH
142
+ try:
143
+ result = subprocess.run(['where', process_name],
144
+ capture_output=True, text=True, shell=True)
145
+ if result.returncode == 0:
146
+ paths = result.stdout.strip().split('\n')
147
+ if paths and paths[0].strip():
148
+ return paths[0].strip()
149
+ except:
150
+ pass
151
+
152
+ # Search in common system paths
153
+ for search_path in self.system_paths + self.common_app_paths:
154
+ if os.path.exists(search_path):
155
+ full_path = os.path.join(search_path, process_name)
156
+ if os.path.exists(full_path):
157
+ return full_path
158
+
159
+ # Search in PATH environment variable
160
+ path_env = os.environ.get('PATH', '')
161
+ for path_dir in path_env.split(os.pathsep):
162
+ if os.path.exists(path_dir):
163
+ full_path = os.path.join(path_dir, process_name)
164
+ if os.path.exists(full_path):
165
+ return full_path
166
+
167
+ return None
168
+
169
+ def _find_running_process(self, process_name: str) -> Optional[int]:
170
+ """Find a running process by name
171
+
172
+ Args:
173
+ process_name: Name of the process to find
174
+
175
+ Returns:
176
+ PID of the running process, or None if not found
177
+ """
178
+ try:
179
+ # Remove .exe extension for comparison
180
+ base_name = process_name.lower().replace('.exe', '')
181
+
182
+ # Map common launcher names to actual app names
183
+ app_mapping = {
184
+ 'calc': ['calculatorapp', 'calculator'],
185
+ 'notepad': ['notepad'], # Notepad.exe vs notepad.exe
186
+ 'mspaint': ['paint', 'mspaint', 'paintdotnet'],
187
+ 'wordpad': ['wordpad'],
188
+ 'dbengine-x86_64': ['dbengine-x86_64', 'dbengine'],
189
+ }
190
+
191
+ # Get possible actual process names
192
+ possible_names = [base_name]
193
+ if base_name in app_mapping:
194
+ possible_names.extend(app_mapping[base_name])
195
+
196
+ for proc in psutil.process_iter(['pid', 'name', 'create_time']):
197
+ try:
198
+ proc_name = proc.info['name'].lower().replace('.exe', '')
199
+
200
+ # Check if this process matches any of our possible names
201
+ for possible_name in possible_names:
202
+ if (possible_name in proc_name or proc_name in possible_name):
203
+ pid = proc.info['pid']
204
+
205
+ # Make sure it's not one we already launched
206
+ if pid not in self.launched_applications:
207
+ # Check if this is a recently created process (within last 10 seconds)
208
+ process_age = time.time() - proc.info['create_time']
209
+ if process_age <= 10: # Recently created
210
+ logger.info(f"Found recently created process: PID {pid} ({proc.info['name']}) for {process_name}")
211
+ return pid
212
+
213
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
214
+ continue
215
+
216
+ except Exception as e:
217
+ logger.warning(f"Error finding running process {process_name}: {e}")
218
+
219
+ return None
220
+
221
+ def get_whitelisted_applications(self) -> List[Dict[str, Any]]:
222
+ """Get list of all whitelisted applications"""
223
+ if not self.whitelist.is_enabled():
224
+ return []
225
+
226
+ applications = []
227
+ for entry in self.whitelist.entries:
228
+ if entry.enabled:
229
+ app_info = {
230
+ 'process_name': entry.process_name,
231
+ 'description': entry.description,
232
+ 'category': entry.category,
233
+ 'exact_match': entry.exact_match,
234
+ 'executable_path': self.find_executable(entry.process_name) if entry.exact_match else None
235
+ }
236
+ applications.append(app_info)
237
+
238
+ return applications
239
+
240
+ def launch_application(self, process_name: str, arguments: Optional[List[str]] = None,
241
+ working_directory: Optional[str] = None) -> Tuple[bool, str, Optional[int]]:
242
+ """Launch a whitelisted application
243
+
244
+ Args:
245
+ process_name: Name of the process to launch
246
+ arguments: Optional command line arguments
247
+ working_directory: Optional working directory
248
+
249
+ Returns:
250
+ Tuple of (success, message, pid)
251
+ """
252
+
253
+ # Check if process is whitelisted
254
+ # For whitelist checking, use just the filename (not full path)
255
+ check_name = os.path.basename(process_name) if os.path.isabs(process_name) else process_name
256
+ if self.whitelist.is_enabled() and not self.whitelist.is_allowed(check_name):
257
+ return False, f"Process '{check_name}' is not whitelisted", None
258
+
259
+ # Find executable
260
+ exe_path = self.find_executable(process_name)
261
+ if not exe_path:
262
+ return False, f"Could not find executable for '{process_name}'", None
263
+
264
+ try:
265
+ # Prepare command line
266
+ cmd_line = [exe_path]
267
+ if arguments:
268
+ cmd_line.extend(arguments)
269
+
270
+ # Set working directory
271
+ if not working_directory:
272
+ # Use the directory containing the executable, or current directory as fallback
273
+ if os.path.dirname(exe_path):
274
+ working_directory = os.path.dirname(exe_path)
275
+ else:
276
+ working_directory = os.getcwd()
277
+
278
+ logger.info(f"Launching application: {' '.join(cmd_line)}")
279
+ logger.info(f"Working directory: {working_directory}")
280
+
281
+ # Launch the process - use startupinfo to detach properly on Windows
282
+ if sys.platform == 'win32':
283
+ # Windows constants for subprocess
284
+ SW_NORMAL = 1
285
+ STARTF_USESHOWWINDOW = 1
286
+ CREATE_NEW_PROCESS_GROUP = 0x00000200
287
+
288
+ startupinfo = subprocess.STARTUPINFO()
289
+ startupinfo.dwFlags |= STARTF_USESHOWWINDOW
290
+ startupinfo.wShowWindow = SW_NORMAL
291
+
292
+ try:
293
+ process = subprocess.Popen(
294
+ cmd_line,
295
+ cwd=working_directory,
296
+ startupinfo=startupinfo,
297
+ creationflags=CREATE_NEW_PROCESS_GROUP
298
+ )
299
+ except OSError as e:
300
+ if e.winerror == 740: # ERROR_ELEVATION_REQUIRED
301
+ # Try launching with elevated permissions using shell execute
302
+ logger.info(f"Application requires elevation, attempting elevated launch...")
303
+ import ctypes
304
+ try:
305
+ # Use ShellExecuteW with 'runas' verb for elevation
306
+ result = ctypes.windll.shell32.ShellExecuteW(
307
+ None,
308
+ "runas", # This triggers UAC prompt
309
+ exe_path,
310
+ " ".join(arguments) if arguments else None,
311
+ working_directory,
312
+ SW_NORMAL
313
+ )
314
+
315
+ if result > 32: # Success
316
+ # ShellExecute doesn't return a process object, so we need to find the process
317
+ logger.info("Elevated launch initiated, waiting for process to start...")
318
+ time.sleep(2) # Give time for UAC and process startup
319
+
320
+ # Try to find the process that was just launched
321
+ actual_pid = self._find_running_process(process_name)
322
+ if actual_pid:
323
+ app = LaunchedApplication(
324
+ process_name=process_name,
325
+ pid=actual_pid,
326
+ exe_path=exe_path,
327
+ launch_time=datetime.now(),
328
+ command_line=cmd_line,
329
+ working_directory=working_directory,
330
+ status='running'
331
+ )
332
+
333
+ self.launched_applications[actual_pid] = app
334
+ self._save_session()
335
+
336
+ logger.info(f"Successfully launched {process_name} with elevated privileges (PID: {actual_pid})")
337
+ return True, f"Successfully launched {process_name} with elevated privileges (PID: {actual_pid})", actual_pid
338
+ else:
339
+ # Process might still be starting or user cancelled UAC
340
+ logger.warning("Elevated launch initiated but process not found yet (user may have cancelled UAC)")
341
+ return False, "Elevated launch initiated but process not detected (UAC may have been cancelled)", None
342
+ else:
343
+ logger.error(f"ShellExecute failed with result: {result}")
344
+ return False, f"Failed to launch with elevation (error code: {result})", None
345
+
346
+ except Exception as shell_error:
347
+ logger.error(f"Failed to launch with elevation: {shell_error}")
348
+ raise e # Re-raise original error
349
+ else:
350
+ raise # Re-raise if not elevation error
351
+ else:
352
+ process = subprocess.Popen(
353
+ cmd_line,
354
+ cwd=working_directory
355
+ )
356
+
357
+ # Wait a moment for the process to start
358
+ time.sleep(0.5)
359
+
360
+ # Check if process started successfully
361
+ process_poll = process.poll()
362
+ if process_poll is None:
363
+ # Process is still running - traditional application
364
+ app = LaunchedApplication(
365
+ process_name=process_name,
366
+ pid=process.pid,
367
+ exe_path=exe_path,
368
+ launch_time=datetime.now(),
369
+ command_line=cmd_line,
370
+ working_directory=working_directory,
371
+ status='running'
372
+ )
373
+
374
+ self.launched_applications[process.pid] = app
375
+ self._save_session()
376
+
377
+ logger.info(f"Successfully launched {process_name} with PID {process.pid}")
378
+ return True, f"Successfully launched {process_name} (PID: {process.pid})", process.pid
379
+ elif process_poll == 0:
380
+ # Process exited with code 0 (success) - likely a launcher that spawned the real app
381
+ logger.info(f"Launcher process for {process_name} exited successfully (code 0)")
382
+
383
+ # Wait a bit more for the actual application to start
384
+ time.sleep(1.5)
385
+
386
+ # Try to find the actual running process by name
387
+ actual_pid = self._find_running_process(process_name)
388
+ if actual_pid:
389
+ # Found the actual running process
390
+ app = LaunchedApplication(
391
+ process_name=process_name,
392
+ pid=actual_pid,
393
+ exe_path=exe_path,
394
+ launch_time=datetime.now(),
395
+ command_line=cmd_line,
396
+ working_directory=working_directory,
397
+ status='running'
398
+ )
399
+
400
+ self.launched_applications[actual_pid] = app
401
+ self._save_session()
402
+
403
+ logger.info(f"Successfully launched {process_name} with actual PID {actual_pid} (launcher PID {process.pid} exited)")
404
+ return True, f"Successfully launched {process_name} (PID: {actual_pid})", actual_pid
405
+ else:
406
+ # Launcher succeeded but we can't find the running process
407
+ # This might be a UWP app or service - consider it successful anyway
408
+ logger.info(f"Launcher for {process_name} succeeded but actual process not found (may be UWP/service)")
409
+
410
+ # Create a record with the launcher PID for tracking purposes
411
+ app = LaunchedApplication(
412
+ process_name=process_name,
413
+ pid=process.pid,
414
+ exe_path=exe_path,
415
+ launch_time=datetime.now(),
416
+ command_line=cmd_line,
417
+ working_directory=working_directory,
418
+ status='launched' # Special status for launcher-spawned apps
419
+ )
420
+
421
+ self.launched_applications[process.pid] = app
422
+ self._save_session()
423
+
424
+ return True, f"Successfully launched {process_name} via launcher (launcher PID: {process.pid})", process.pid
425
+ else:
426
+ # Process terminated with non-zero exit code - actual failure
427
+ stdout, stderr = process.communicate()
428
+ error_msg = f"Process failed with exit code: {process_poll}"
429
+ if stderr:
430
+ error_msg += f"\\nError: {stderr.decode('utf-8', errors='ignore')}"
431
+
432
+ logger.error(f"Failed to launch {process_name}: {error_msg}")
433
+ return False, error_msg, None
434
+
435
+ except Exception as e:
436
+ logger.error(f"Failed to launch {process_name}: {e}")
437
+ return False, f"Failed to launch application: {str(e)}", None
438
+
439
+ def terminate_application(self, pid: int, force: bool = False) -> Tuple[bool, str]:
440
+ """Terminate a launched application
441
+
442
+ Args:
443
+ pid: Process ID to terminate
444
+ force: Whether to force termination (kill vs terminate)
445
+
446
+ Returns:
447
+ Tuple of (success, message)
448
+ """
449
+
450
+ try:
451
+ # Check if we launched this process
452
+ if pid not in self.launched_applications:
453
+ # Check if process exists anyway
454
+ if not psutil.pid_exists(pid):
455
+ return False, f"Process {pid} does not exist"
456
+
457
+ # Allow termination of any process if it exists
458
+ logger.warning(f"Terminating process {pid} that was not launched by this session")
459
+
460
+ # Get process object
461
+ try:
462
+ process = psutil.Process(pid)
463
+ process_name = process.name()
464
+ except psutil.NoSuchProcess:
465
+ # Remove from our tracking if it's already gone
466
+ if pid in self.launched_applications:
467
+ self.launched_applications[pid].status = 'terminated'
468
+ self._save_session()
469
+ return True, f"Process {pid} was already terminated"
470
+
471
+ logger.info(f"Terminating process {pid} ({process_name}), force={force}")
472
+
473
+ if force:
474
+ # Force kill the process
475
+ process.kill()
476
+ message = f"Force killed process {pid} ({process_name})"
477
+ else:
478
+ # Try graceful termination first
479
+ process.terminate()
480
+
481
+ # Wait for process to terminate gracefully
482
+ try:
483
+ process.wait(timeout=5) # Wait up to 5 seconds
484
+ message = f"Gracefully terminated process {pid} ({process_name})"
485
+ except psutil.TimeoutExpired:
486
+ # Force kill if graceful termination failed
487
+ logger.warning(f"Graceful termination timeout, force killing {pid}")
488
+ process.kill()
489
+ message = f"Force killed process {pid} ({process_name}) after timeout"
490
+
491
+ # Update our tracking
492
+ if pid in self.launched_applications:
493
+ self.launched_applications[pid].status = 'terminated'
494
+ self._save_session()
495
+
496
+ logger.info(message)
497
+ return True, message
498
+
499
+ except psutil.NoSuchProcess:
500
+ if pid in self.launched_applications:
501
+ self.launched_applications[pid].status = 'terminated'
502
+ self._save_session()
503
+ return True, f"Process {pid} was already terminated"
504
+ except psutil.AccessDenied:
505
+ return False, f"Access denied when trying to terminate process {pid}"
506
+ except Exception as e:
507
+ logger.error(f"Failed to terminate process {pid}: {e}")
508
+ return False, f"Failed to terminate process: {str(e)}"
509
+
510
+ def get_launched_applications(self) -> List[Dict[str, Any]]:
511
+ """Get list of applications launched in this session"""
512
+ self._update_application_status()
513
+ return [app.to_dict() for app in self.launched_applications.values()]
514
+
515
+ def get_application_by_pid(self, pid: int) -> Optional[Dict[str, Any]]:
516
+ """Get information about a specific launched application"""
517
+ if pid in self.launched_applications:
518
+ self._update_application_status()
519
+ return self.launched_applications[pid].to_dict()
520
+ return None
521
+
522
+ def _update_application_status(self):
523
+ """Update the status of all tracked applications"""
524
+ # Create a list of items to avoid dictionary modification during iteration
525
+ items_to_update = list(self.launched_applications.items())
526
+
527
+ for pid, app in items_to_update:
528
+ # Check if this PID is still in our dictionary (might have been moved)
529
+ if pid not in self.launched_applications:
530
+ continue
531
+
532
+ if app.status in ['running', 'launched']:
533
+ if not psutil.pid_exists(pid):
534
+ # For apps where the original PID no longer exists, try to find the actual process
535
+ # This handles both launcher-spawned apps and apps that immediately spawn new processes
536
+ actual_pid = self._find_running_process(app.process_name)
537
+ if actual_pid and actual_pid != pid:
538
+ # Update to the actual running process
539
+ app.pid = actual_pid
540
+ app.status = 'running'
541
+ # Move the entry to the new PID
542
+ del self.launched_applications[pid]
543
+ self.launched_applications[actual_pid] = app
544
+ logger.info(f"Updated {app.process_name} from original PID {pid} to actual PID {actual_pid}")
545
+ else:
546
+ app.status = 'terminated'
547
+ else:
548
+ try:
549
+ process = psutil.Process(pid)
550
+ if process.status() in [psutil.STATUS_ZOMBIE, psutil.STATUS_DEAD]:
551
+ app.status = 'terminated'
552
+ elif app.status == 'launched':
553
+ # Launcher process still exists, upgrade to running
554
+ app.status = 'running'
555
+ except psutil.NoSuchProcess:
556
+ app.status = 'terminated'
557
+ except Exception:
558
+ pass # Keep current status if we can't determine
559
+
560
+ def cleanup_terminated_applications(self) -> int:
561
+ """Remove terminated applications from tracking
562
+
563
+ Returns:
564
+ Number of applications removed
565
+ """
566
+ self._update_application_status()
567
+
568
+ terminated_pids = [pid for pid, app in self.launched_applications.items()
569
+ if app.status == 'terminated']
570
+
571
+ for pid in terminated_pids:
572
+ del self.launched_applications[pid]
573
+
574
+ if terminated_pids:
575
+ self._save_session()
576
+ logger.info(f"Cleaned up {len(terminated_pids)} terminated applications")
577
+
578
+ return len(terminated_pids)
579
+
580
+ def terminate_all_launched_applications(self, force: bool = False) -> Dict[str, Any]:
581
+ """Terminate all applications launched in this session
582
+
583
+ Args:
584
+ force: Whether to force termination
585
+
586
+ Returns:
587
+ Dictionary with termination results
588
+ """
589
+ self._update_application_status()
590
+
591
+ running_apps = [app for app in self.launched_applications.values()
592
+ if app.status in ['running', 'launched']]
593
+
594
+ if not running_apps:
595
+ return {'terminated': 0, 'failed': 0, 'messages': ['No running applications to terminate']}
596
+
597
+ results = {'terminated': 0, 'failed': 0, 'messages': []}
598
+
599
+ for app in running_apps:
600
+ success, message = self.terminate_application(app.pid, force)
601
+ if success:
602
+ results['terminated'] += 1
603
+ else:
604
+ results['failed'] += 1
605
+ results['messages'].append(f"PID {app.pid} ({app.process_name}): {message}")
606
+
607
+ logger.info(f"Bulk termination complete: {results['terminated']} terminated, {results['failed']} failed")
608
+ return results