Last active
December 8, 2024 04:10
-
-
Save enowdev/db957647c797e3e28cff22c171e60f0d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#virtual_system.py | |
import platform | |
import uuid | |
import random | |
import os | |
import subprocess | |
import psutil | |
import stat | |
import json | |
import hashlib | |
from datetime import datetime, timedelta | |
import ctypes | |
import tempfile | |
import shutil | |
import signal | |
import threading | |
import queue | |
import io | |
import sys | |
import time | |
import magic # pip install python-magic untuk Linux, python-magic-bin untuk Windows | |
class VirtualSystem: | |
def __init__(self, test_os=None): | |
self.os_type = test_os.lower() if test_os else platform.system().lower() | |
self._load_spoof_databases() | |
self._init_special_handlers() | |
# Database untuk spoofing | |
self.cpu_models = [ | |
"Intel(R) Core(TM) i5-9600K CPU @ 3.70GHz", | |
"Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz", | |
"AMD Ryzen 5 3600 6-Core Processor", | |
"AMD Ryzen 7 5800X 8-Core Processor", | |
"Intel(R) Core(TM) i9-11900K CPU @ 3.50GHz" | |
] | |
self.gpu_models = [ | |
"NVIDIA GeForce RTX 3060", | |
"NVIDIA GeForce GTX 1660", | |
"AMD Radeon RX 6700 XT", | |
"Intel(R) UHD Graphics 630", | |
"NVIDIA GeForce RTX 3070" | |
] | |
self.ram_sizes = [8192, 16384, 32768] # MB | |
self.screen_resolutions = ["1920x1080", "2560x1440", "3840x2160"] | |
self.windows_versions = [ | |
"Windows 10 Pro 21H2", | |
"Windows 10 Home 21H2", | |
"Windows 11 Pro 22H2", | |
"Windows 11 Home 22H2" | |
] | |
self.linux_distributions = [ | |
"Ubuntu 22.04 LTS", | |
"Fedora 37", | |
"Linux Mint 21.1", | |
"Manjaro 22.0", | |
"Pop!_OS 22.04" | |
] | |
def _load_spoof_databases(self): | |
"""Load all spoofing databases""" | |
# ... existing database code ... | |
# Tambahan database untuk browser fingerprinting | |
self.browser_profiles = { | |
"chrome": { | |
"user_agents": [ | |
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", | |
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" | |
], | |
"plugins": ["PDF Viewer", "Chrome PDF Viewer", "Chromium PDF Viewer"], | |
"webgl_vendors": ["Google Inc.", "Intel Inc.", "NVIDIA Corporation"], | |
"webgl_renderers": [ | |
"ANGLE (Intel, Intel(R) UHD Graphics Direct3D11 vs_5_0 ps_5_0)", | |
"ANGLE (NVIDIA, NVIDIA GeForce RTX 3060 Direct3D11 vs_5_0 ps_5_0)" | |
] | |
} | |
} | |
# Database untuk anti-debug detection | |
self.process_blacklist = [ | |
"ida64.exe", "x64dbg.exe", "ollydbg.exe", "windbg.exe", | |
"gdb", "lldb", "strace", "ltrace", "radare2" | |
] | |
# Database untuk virtual environment detection | |
self.vm_artifacts = [ | |
"VMware", "VirtualBox", "QEMU", "Virtual", "vbox", | |
"KVM", "Xen", "innotek", "VBOX" | |
] | |
def _init_special_handlers(self): | |
"""Initialize special handlers for different types of protection""" | |
self.special_handlers = { | |
"browser": self._handle_browser_app, | |
"game": self._handle_game_app, | |
"security": self._handle_security_app, | |
"drm": self._handle_drm_app, | |
"cursor_ide": self._handle_cursor_ide | |
} | |
# Update Cursor version database | |
self.cursor_profiles = { | |
"versions": { | |
"0.43.5": { | |
"vscode": "1.93.1", | |
"electron": "30.5.1", | |
"chromium": "124.0.6367.243", | |
"nodejs": "20.16.0", | |
"v8": "12.4.254.20-electron.0", | |
"commit": "2eaa79a1b14ccff5d1c78a2c358a08be16a8e5a0", | |
"date": "2024-11-27T09:11:51.854Z", | |
"features": [ | |
"basic-completion", | |
"file-search", | |
"chat", | |
"copilot", | |
"workspace", | |
"terminal", | |
"git", | |
"debugging", | |
"multi-cursor", | |
"refactoring" | |
] | |
} | |
}, | |
"settings": { | |
"editor.fontSize": [12, 13, 14, 15, 16], | |
"editor.fontFamily": [ | |
"JetBrains Mono", | |
"Fira Code", | |
"Source Code Pro", | |
"Consolas" | |
], | |
"workbench.colorTheme": [ | |
"Default Dark+", | |
"Default Light+", | |
"Monokai", | |
"Solarized Dark" | |
] | |
} | |
} | |
def _handle_browser_app(self, env_config): | |
"""Handle browser-specific protections""" | |
browser_profile = random.choice(list(self.browser_profiles.values())) | |
env_config.update({ | |
"SPOOF_USER_AGENT": random.choice(browser_profile["user_agents"]), | |
"SPOOF_PLUGINS": json.dumps(browser_profile["plugins"]), | |
"SPOOF_WEBGL_VENDOR": random.choice(browser_profile["webgl_vendors"]), | |
"SPOOF_WEBGL_RENDERER": random.choice(browser_profile["webgl_renderers"]), | |
"SPOOF_SCREEN_RES": random.choice(self.screen_resolutions), | |
"SPOOF_TIMEZONE": f"GMT{random.choice(['+','-'])}{random.randint(0,12)}", | |
"SPOOF_LANGUAGE": random.choice(["en-US", "en-GB", "es-ES", "fr-FR"]), | |
"DISABLE_WEBRTC": "1", | |
"SPOOF_FONTS": "1", | |
"SPOOF_CANVAS": "1" | |
}) | |
return env_config | |
def _handle_game_app(self, env_config): | |
"""Handle game-specific protections""" | |
env_config.update({ | |
"SPOOF_STEAM_ID": str(random.randint(10000000, 99999999)), | |
"SPOOF_DISCORD_ID": str(random.randint(100000000000000000, 999999999999999999)), | |
"SPOOF_GAME_HWID": hashlib.md5(os.urandom(32)).hexdigest(), | |
"DISABLE_GAME_ANALYTICS": "1", | |
"SPOOF_GAME_RESOLUTION": random.choice(self.screen_resolutions), | |
"SPOOF_INPUT_DEVICES": "1", | |
"SPOOF_GAME_PATH": "C:\\Games" if self.os_type == "windows" else "/home/user/Games" | |
}) | |
return env_config | |
def _handle_security_app(self, env_config): | |
"""Handle security software protections""" | |
env_config.update({ | |
"DISABLE_DEBUGGER_CHECK": "1", | |
"SPOOF_PROCESS_LIST": "1", | |
"HIDE_VM_ARTIFACTS": "1", | |
"DISABLE_MEMORY_SCAN": "1", | |
"SPOOF_REGISTRY": "1" if self.os_type == "windows" else "0", | |
"HIDE_ANALYSIS_TOOLS": "1", | |
"SPOOF_RUNNING_TIME": str(random.randint(100000, 999999)) | |
}) | |
return env_config | |
def _handle_drm_app(self, env_config): | |
"""Handle DRM protections""" | |
env_config.update({ | |
"SPOOF_LICENSE_PATH": "1", | |
"SPOOF_ACTIVATION": "1", | |
"SPOOF_HARDWARE_HASH": hashlib.sha256(os.urandom(32)).hexdigest(), | |
"DISABLE_INTEGRITY_CHECK": "1", | |
"SPOOF_INSTALL_DATE": self._generate_install_date(), | |
"SPOOF_PRODUCT_KEY": self._generate_windows_product_id() if self.os_type == "windows" else "" | |
}) | |
return env_config | |
def _handle_cursor_ide(self, env_config): | |
"""Enhanced Cursor handling for Windows""" | |
try: | |
if self.os_type == "windows": | |
# Generate Windows identity | |
windows_identity = self.generate_windows_identity() | |
# Create Cursor folders and settings | |
cursor_paths = self.create_cursor_folders(windows_identity) | |
# Update environment with Windows identity | |
env_config.update(windows_identity) | |
# Update Cursor-specific environment | |
env_config.update({ | |
"CURSOR_VERSION": "0.43.5", | |
"CURSOR_INSTALL_PATH": cursor_paths["main"], | |
"CURSOR_USER_DATA": cursor_paths["user_data"], | |
"CURSOR_CACHE": cursor_paths["cache"], | |
"CURSOR_EXTENSIONS": cursor_paths["extensions"], | |
"CURSOR_WORKSPACE": cursor_paths["workspace"], | |
# Windows-specific paths | |
"LOCALAPPDATA": windows_identity["LOCAL_APP_DATA"], | |
"APPDATA": windows_identity["ROAMING_APP_DATA"], | |
"TEMP": windows_identity["TEMP"], | |
"TMP": windows_identity["TEMP"], | |
# Additional Windows environment | |
"PROCESSOR_IDENTIFIER": f"Intel64 Family 6 Model {random.randint(60,90)} Stepping {random.randint(1,9)}", | |
"PROCESSOR_LEVEL": "6", | |
"PROCESSOR_REVISION": f"{random.randint(1000,9999)}", | |
"OS": "Windows_NT", | |
"PATHEXT": ".COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC;.PY;.PYW", | |
"COMPUTERNAME": windows_identity["COMPUTER_NAME"], | |
"USERDOMAIN": windows_identity["USER_DOMAIN"], | |
"USERNAME": windows_identity["USERNAME"], | |
"USERPROFILE": windows_identity["USER_PROFILE"], | |
"HOMEDRIVE": windows_identity["SYSTEM_DRIVE"], | |
"HOMEPATH": f"\\Users\\{windows_identity['USERNAME']}" | |
}) | |
return env_config | |
except Exception as e: | |
raise Exception(f"Error handling Cursor for Windows: {str(e)}") | |
def generate_new_identity(self): | |
"""Generate complete virtual system identity""" | |
try: | |
# Generate basic identifiers | |
new_hostname = self._generate_hostname() | |
new_mac = self._generate_mac() | |
new_uuid = str(uuid.uuid4()) | |
# Generate OS profile | |
if self.os_type == "linux": | |
os_info = { | |
"name": "Linux", | |
"arch": "x64", | |
"kernel": f"6.{random.randint(1,12)}.{random.randint(1,20)}-arch1-1", | |
"distro": random.choice([ | |
"EndeavourOS Linux", | |
"Arch Linux", | |
"Manjaro Linux", | |
"Fedora Linux" | |
]) | |
} | |
else: | |
os_info = { | |
"name": "Windows", | |
"arch": "x64", | |
"version": "10", | |
"build": f"19{random.randint(0,9)}{random.randint(0,9)}2" | |
} | |
# Generate hardware profile | |
hardware = { | |
"cpu": { | |
"model": random.choice([ | |
"AMD Ryzen 9 7950X 16-Core", | |
"AMD Ryzen 7 7700X 8-Core", | |
"Intel Core i9-13900K", | |
"Intel Core i7-13700K" | |
]), | |
"cores": random.choice([8, 12, 16, 24, 32]), | |
"threads": lambda cores: cores * 2 | |
}, | |
"gpu": random.choice([ | |
"NVIDIA GeForce RTX 4090", | |
"NVIDIA GeForce RTX 4080", | |
"AMD Radeon RX 7900 XTX", | |
"AMD Radeon RX 7900 XT" | |
]), | |
"ram": random.choice([16384, 32768, 65536]), # MB | |
"screen": random.choice([ | |
"2560x1440", | |
"3440x1440", | |
"3840x2160" | |
]) | |
} | |
# Set CPU threads based on cores | |
hardware["cpu"]["threads"] = hardware["cpu"]["cores"] * 2 | |
# Generate complete system profile | |
system_profile = { | |
"HOSTNAME": new_hostname, | |
"MAC_ADDRESS": new_mac, | |
"SYSTEM_UUID": new_uuid, | |
"HARDWARE_PROFILE": json.dumps(hardware), | |
"OS_INFO": json.dumps(os_info), | |
"SYSTEM_LOCALE": "en_US.UTF-8", | |
"TIMEZONE": "UTC", | |
"USERNAME": f"user_{new_hostname.lower()}", | |
"HOME_PATH": f"/home/user_{new_hostname.lower()}" if self.os_type == "linux" else f"C:\\Users\\user_{new_hostname.lower()}" | |
} | |
return new_hostname, new_mac, system_profile | |
except Exception as e: | |
raise Exception(f"Error generating identity: {str(e)}") | |
def _generate_hostname(self): | |
"""Generate random hostname""" | |
prefixes = ["DESKTOP", "LAPTOP", "PC", "WORKSTATION"] | |
suffix = ''.join(random.choices('0123456789ABCDEF', k=8)) | |
return f"{random.choice(prefixes)}-{suffix}" | |
def _generate_mac(self): | |
"""Generate random MAC address""" | |
mac = [random.randint(0x00, 0xff) for _ in range(6)] | |
return ':'.join(map(lambda x: f"{x:02x}", mac)) | |
def _generate_disk_serial(self): | |
"""Generate random disk serial""" | |
return ''.join(random.choices('0123456789ABCDEF', k=16)) | |
def _generate_motherboard_info(self): | |
"""Generate random motherboard information""" | |
manufacturers = ["ASUSTeK", "MSI", "Gigabyte", "ASRock"] | |
models = ["B550", "Z590", "X570", "B660"] | |
return { | |
"manufacturer": random.choice(manufacturers), | |
"model": f"{random.choice(models)}-{random.choice(['PRO','GAMING','PLUS','MAX'])}", | |
"serial": ''.join(random.choices('0123456789ABCDEF', k=12)) | |
} | |
def _generate_install_date(self): | |
"""Generate random installation date""" | |
from datetime import datetime, timedelta | |
start = datetime(2021, 1, 1) | |
days = random.randint(0, 700) | |
install_date = start + timedelta(days=days) | |
return install_date.strftime("%Y-%m-%d") | |
def _generate_windows_product_id(self): | |
"""Generate random Windows product ID""" | |
return f"{random.randint(10000,99999)}-{random.randint(10000,99999)}-{random.randint(10000,99999)}-{random.randint(10000,99999)}" | |
def _generate_machine_id(self): | |
"""Generate random machine ID for Linux""" | |
return ''.join(random.choices('0123456789abcdef', k=32)) | |
def create_preload_library(self): | |
"""Create preload library with Cursor specific hooks""" | |
if self.os_type != 'linux': | |
return None | |
code = """ | |
#define _GNU_SOURCE | |
#include <stdio.h> | |
#include <dlfcn.h> | |
#include <string.h> | |
#include <unistd.h> | |
#include <sys/utsname.h> | |
#include <sys/types.h> | |
#include <sys/socket.h> | |
#include <netinet/in.h> | |
#include <arpa/inet.h> | |
#include <stdlib.h> | |
#include <time.h> | |
#include <json-c/json.h> | |
#include <curl/curl.h> | |
#include <sqlite3.h> | |
// Cursor specific structures | |
typedef struct { | |
char* session_id; | |
char* version; | |
char* model; | |
char* workspace; | |
} CursorConfig; | |
// Global variables | |
static CursorConfig cursor_config; | |
static char* spoofed_home = NULL; | |
static char* spoofed_hostname = NULL; | |
// Original function pointers | |
static int (*real_uname)(struct utsname *buf) = NULL; | |
static FILE* (*real_fopen)(const char *pathname, const char *mode) = NULL; | |
static int (*real_sqlite3_open)(const char *filename, sqlite3 **ppDb) = NULL; | |
static CURLcode (*real_curl_easy_perform)(CURL *curl) = NULL; | |
// Cursor version detection and spoofing | |
const char* get_cursor_version() { | |
static const char* versions[] = { | |
"0.1.3", | |
"0.1.4", | |
"0.1.5", | |
"0.2.0", | |
"0.2.1" | |
}; | |
static int version_idx = -1; | |
if (version_idx == -1) { | |
srand(time(NULL)); | |
version_idx = rand() % (sizeof(versions) / sizeof(versions[0])); | |
} | |
return versions[version_idx]; | |
} | |
// Hook for file operations | |
FILE* fopen(const char *pathname, const char *mode) { | |
if (!real_fopen) real_fopen = dlsym(RTLD_NEXT, "fopen"); | |
// Intercept Cursor config files | |
if (strstr(pathname, ".cursor/config.json") || | |
strstr(pathname, "cursor-settings.json")) { | |
// Create temp config with spoofed values | |
char temp_path[1024]; | |
snprintf(temp_path, sizeof(temp_path), "/tmp/cursor_config_%d.json", getpid()); | |
FILE* temp = real_fopen(temp_path, "w"); | |
if (temp) { | |
fprintf(temp, "{\\"version\\":\\"%s\\",\\"telemetry\\":false,\\"ai\\":{\\"model\\":\\"%s\\"},\\"workspace\\":\\"%s\\"}", | |
get_cursor_version(), | |
getenv("CURSOR_MODEL") ? getenv("CURSOR_MODEL") : "gpt-3.5-turbo", | |
getenv("CURSOR_WORKSPACE") ? getenv("CURSOR_WORKSPACE") : "~/Projects" | |
); | |
fclose(temp); | |
return real_fopen(temp_path, mode); | |
} | |
} | |
return real_fopen(pathname, mode); | |
} | |
// Hook for SQLite (Cursor history and cache) | |
int sqlite3_open(const char *filename, sqlite3 **ppDb) { | |
if (!real_sqlite3_open) real_sqlite3_open = dlsym(RTLD_NEXT, "sqlite3_open"); | |
// Intercept Cursor databases | |
if (strstr(filename, ".cursor/") || strstr(filename, "cursor.db")) { | |
char temp_db[1024]; | |
snprintf(temp_db, sizeof(temp_db), "/tmp/cursor_db_%d.sqlite", getpid()); | |
return real_sqlite3_open(temp_db, ppDb); | |
} | |
return real_sqlite3_open(filename, ppDb); | |
} | |
// Hook for network requests | |
CURLcode curl_easy_perform(CURL *curl) { | |
if (!real_curl_easy_perform) | |
real_curl_easy_perform = dlsym(RTLD_NEXT, "curl_easy_perform"); | |
char* url = NULL; | |
curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &url); | |
// Block telemetry and tracking requests | |
if (url && (strstr(url, "telemetry") || | |
strstr(url, "crash-report") || | |
strstr(url, "analytics"))) { | |
return CURLE_OK; // Fake success | |
} | |
// Modify headers for AI requests | |
if (url && strstr(url, "api.openai.com")) { | |
struct curl_slist *headers = NULL; | |
headers = curl_slist_append(headers, "X-Cursor-Version: " get_cursor_version()); | |
headers = curl_slist_append(headers, "X-Cursor-Client: vscode"); | |
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); | |
} | |
return real_curl_easy_perform(curl); | |
} | |
// System information hooks | |
int uname(struct utsname *buf) { | |
if (!real_uname) real_uname = dlsym(RTLD_NEXT, "uname"); | |
int result = real_uname(buf); | |
if (result == 0 && getenv("SPOOFED_HOSTNAME")) { | |
strncpy(buf->nodename, getenv("SPOOFED_HOSTNAME"), sizeof(buf->nodename) - 1); | |
} | |
return result; | |
} | |
// Initialize hooks | |
__attribute__((constructor)) | |
static void init(void) { | |
// Load environment variables | |
cursor_config.session_id = getenv("CURSOR_AI_SESSION"); | |
cursor_config.version = (char*)get_cursor_version(); | |
cursor_config.model = getenv("CURSOR_MODEL"); | |
cursor_config.workspace = getenv("CURSOR_WORKSPACE"); | |
spoofed_hostname = getenv("HOSTNAME"); | |
spoofed_home = getenv("HOME"); | |
} | |
""" | |
# Compile preload library | |
try: | |
import tempfile | |
with tempfile.NamedTemporaryFile(suffix='.c', delete=False) as f: | |
f.write(code.encode()) | |
c_file = f.name | |
# Compile with required libraries | |
compile_cmd = f"gcc -shared -fPIC {c_file} -o {c_file}.so -ldl -ljson-c -lcurl -lsqlite3" | |
os.system(compile_cmd) | |
return f"{c_file}.so" | |
except Exception as e: | |
raise Exception(f"Error creating preload library: {str(e)}") | |
def __del__(self): | |
"""Cleanup when object is destroyed""" | |
try: | |
# Stop all running processes | |
if hasattr(self, '_processes'): | |
for pid in list(self._processes.keys()): | |
self.stop_application(pid) | |
# Remove temporary directory | |
if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir): | |
import shutil | |
shutil.rmtree(self.temp_dir) | |
except: | |
pass | |
def run_application(self, app_path): | |
try: | |
# Detect required protections | |
protections = self._detect_app_type(app_path) | |
# Generate base identity | |
hostname, mac, base_config = self.generate_new_identity() | |
# Apply special handlers based on detected protections | |
for protection in protections: | |
if protection in self.special_handlers: | |
base_config = self.special_handlers[protection](base_config) | |
# Add anti-detection measures | |
base_config.update({ | |
"HIDE_VIRTUAL_ENVIRONMENT": "1", | |
"RANDOMIZE_TIMING": "1", | |
"SPOOF_PROCESS_TREE": "1", | |
"DISABLE_TELEMETRY": "1" | |
}) | |
# Run the application with modified config | |
process = None | |
extracted_path = None | |
try: | |
if not os.path.exists(app_path): | |
raise Exception("File tidak ditemukan") | |
# Create preload library | |
preload_lib = self.create_preload_library() | |
# Set environment | |
env = os.environ.copy() | |
env.update(base_config) | |
if self.os_type == 'linux': | |
env["LD_PRELOAD"] = preload_lib | |
# Extract dan jalankan aplikasi | |
extracted_path = tempfile.mkdtemp(prefix="virtual_sys_") | |
if app_path.endswith('.AppImage'): | |
cmd = [app_path, "--appimage-extract-and-run"] | |
else: | |
cmd = [app_path] | |
process = subprocess.Popen( | |
cmd, | |
env=env, | |
cwd=extracted_path | |
) | |
process.wait() | |
return process.pid, hostname, mac, base_config | |
except Exception as e: | |
if process: | |
try: | |
process.kill() | |
except: | |
pass | |
if extracted_path and os.path.exists(extracted_path): | |
try: | |
import shutil | |
shutil.rmtree(extracted_path) | |
except: | |
pass | |
raise e | |
except Exception as e: | |
raise Exception(f"Error running application with protections: {str(e)}") | |
def check_application(self, app_path, test_os=None): | |
"""Check application compatibility without running it""" | |
try: | |
if not os.path.exists(app_path): | |
return { | |
"status": False, | |
"message": "File tidak ditemukan", | |
"platform": None | |
} | |
# Gunakan test_os jika ada | |
current_os = test_os.lower() if test_os else self.os_type | |
# Gunakan python-magic untuk deteksi tipe file | |
mime = magic.Magic() | |
file_type = mime.from_file(app_path) | |
result = { | |
"status": True, | |
"file_type": file_type, | |
"platform": None, | |
"message": "", | |
"can_run": False, | |
"simulated": bool(test_os) # Tandai jika ini simulasi | |
} | |
# Deteksi platform berdasarkan tipe file | |
if "PE32" in file_type or "PE32+" in file_type: # Windows Executable | |
result["platform"] = "windows" | |
result["message"] = "Aplikasi Windows (.exe)" | |
result["can_run"] = (current_os == "windows") | |
elif "ELF" in file_type: # Linux Executable | |
result["platform"] = "linux" | |
result["message"] = "Aplikasi Linux (ELF)" | |
result["can_run"] = (current_os == "linux") | |
elif "AppImage" in file_type or app_path.endswith('.AppImage'): | |
result["platform"] = "linux" | |
result["message"] = "Aplikasi Linux (AppImage)" | |
result["can_run"] = (current_os == "linux") | |
else: | |
result["status"] = False | |
result["message"] = f"Tipe file tidak dikenali: {file_type}" | |
# Tambahan informasi | |
result["file_size"] = os.path.getsize(app_path) | |
result["current_os"] = current_os | |
result["actual_os"] = platform.system().lower() | |
# Add protection information | |
protections = self._detect_app_type(app_path) | |
result["detected_protections"] = list(protections) | |
result["protection_level"] = len(protections) | |
if not result["can_run"]: | |
if result["platform"] == "windows" and current_os == "linux": | |
result["message"] += "\nDisarankan: Gunakan Windows atau Wine" | |
elif result["platform"] == "linux" and current_os == "windows": | |
result["message"] += "\nDisarankan: Gunakan Linux atau WSL" | |
if result["simulated"]: | |
result["message"] = f"[SIMULASI {current_os.upper()}] " + result["message"] | |
return result | |
except Exception as e: | |
return { | |
"status": False, | |
"message": f"Error saat mengecek aplikasi: {str(e)}", | |
"platform": None | |
} | |
def find_electron(self): | |
"""Find electron executable""" | |
possible_paths = [ | |
'/usr/bin/electron', | |
'/usr/local/bin/electron', | |
'/opt/electron/electron' | |
] | |
# Try which command first | |
try: | |
electron_path = subprocess.check_output(['which', 'electron'], | |
text=True).strip() | |
if electron_path: | |
return electron_path | |
except: | |
pass | |
# Check possible paths | |
for path in possible_paths: | |
if os.path.exists(path): | |
return path | |
return None | |
def make_executable(self, path): | |
try: | |
current_permissions = os.stat(path).st_mode | |
os.chmod(path, current_permissions | stat.S_IEXEC) | |
return True | |
except Exception as e: | |
raise Exception(f"Gagal mengatur permission eksekusi: {str(e)}") | |
def set_virtual_os(self, os_name): | |
if os_name.lower() in self.available_os: | |
self.current_os = os_name.lower() | |
return True | |
return False | |
def get_available_os(self): | |
return self.available_os | |
def kill_process(self, pid): | |
try: | |
process = psutil.Process(pid) | |
process.terminate() | |
return True | |
except: | |
return False | |
def _detect_app_type(self, app_path): | |
"""Detect application type and required protections""" | |
try: | |
protections = set() | |
# Read file headers and characteristics | |
with open(app_path, 'rb') as f: | |
header = f.read(4096) | |
# Check for Cursor IDE | |
if b"cursor" in header.lower() or "cursor" in app_path.lower(): | |
protections.add("cursor_ide") | |
# Existing checks... | |
if b"chrome" in header.lower() or b"firefox" in header.lower(): | |
protections.add("browser") | |
if any(x in header.lower() for x in [b"unreal", b"unity", b"gamemaker"]): | |
protections.add("game") | |
if any(x in header.lower() for x in [b"antivirus", b"security", b"protect"]): | |
protections.add("security") | |
if any(x in header.lower() for x in [b"denuvo", b"drm", b"license"]): | |
protections.add("drm") | |
return protections | |
except Exception: | |
return set() | |
def generate_windows_identity(self): | |
"""Generate detailed Windows-specific identity""" | |
win_versions = { | |
"10": { | |
"builds": ["19044", "19045", "22621", "22631"], | |
"editions": ["Pro", "Enterprise", "Education"], | |
"install_dates": [ | |
"2023-01-15", "2023-06-22", "2023-09-30", | |
"2024-01-05", "2024-02-15" | |
] | |
}, | |
"11": { | |
"builds": ["22621", "22631", "23621", "23631"], | |
"editions": ["Pro", "Enterprise"], | |
"install_dates": [ | |
"2023-08-15", "2023-11-22", "2024-01-30", | |
"2024-02-28" | |
] | |
} | |
} | |
# Select Windows version | |
win_ver = random.choice(list(win_versions.keys())) | |
ver_info = win_versions[win_ver] | |
# Generate Windows-specific IDs | |
machine_guid = str(uuid.uuid4()).upper() | |
product_id = '-'.join([ | |
str(random.randint(10000, 99999)) for _ in range(4) | |
]) | |
# Generate installation paths | |
system_drive = "C:" | |
windows_path = f"{system_drive}\\Windows" | |
program_files = f"{system_drive}\\Program Files" | |
program_files_x86 = f"{system_drive}\\Program Files (x86)" | |
# Generate user profile | |
username = f"User{random.randint(100,999)}" | |
user_profile = f"{system_drive}\\Users\\{username}" | |
# Generate Windows identity | |
windows_identity = { | |
"OS_NAME": f"Windows {win_ver}", | |
"OS_VERSION": win_ver, | |
"OS_BUILD": random.choice(ver_info["builds"]), | |
"OS_EDITION": random.choice(ver_info["editions"]), | |
"INSTALL_DATE": random.choice(ver_info["install_dates"]), | |
"MACHINE_GUID": machine_guid, | |
"PRODUCT_ID": product_id, | |
"SYSTEM_DRIVE": system_drive, | |
"WINDOWS_PATH": windows_path, | |
"PROGRAM_FILES": program_files, | |
"PROGRAM_FILES_X86": program_files_x86, | |
"USERNAME": username, | |
"USER_PROFILE": user_profile, | |
"APP_DATA": f"{user_profile}\\AppData", | |
"LOCAL_APP_DATA": f"{user_profile}\\AppData\\Local", | |
"ROAMING_APP_DATA": f"{user_profile}\\AppData\\Roaming", | |
"TEMP": f"{user_profile}\\AppData\\Local\\Temp", | |
"COMPUTER_NAME": self._generate_hostname(), | |
"USER_DOMAIN": self._generate_hostname().split('-')[0], | |
"PROCESSOR_ARCHITECTURE": "AMD64", | |
"NUMBER_OF_PROCESSORS": str(random.choice([8, 12, 16, 24, 32])), | |
"SYSTEM_TYPE": "x64-based PC" | |
} | |
return windows_identity | |
def create_cursor_folders(self, windows_identity): | |
"""Create Cursor folders with proper permissions""" | |
try: | |
# Get correct AppData paths from environment | |
local_appdata = os.getenv('LOCALAPPDATA') | |
roaming_appdata = os.getenv('APPDATA') | |
if not local_appdata or not roaming_appdata: | |
# Fallback paths if environment variables not available | |
user_profile = os.path.expanduser('~') | |
local_appdata = os.path.join(user_profile, 'AppData', 'Local') | |
roaming_appdata = os.path.join(user_profile, 'AppData', 'Roaming') | |
# Create base directories first | |
cursor_paths = { | |
"main": os.path.join(local_appdata, 'Programs', 'Cursor'), | |
"user_data": os.path.join(roaming_appdata, 'Cursor', 'User Data'), | |
"chat_storage": os.path.join(roaming_appdata, 'Cursor', 'User Data', 'Chat Storage'), | |
"workspace": os.path.join(local_appdata, 'Cursor', 'Workspaces') | |
} | |
# Create directories with proper permissions | |
for path in cursor_paths.values(): | |
try: | |
if not os.path.exists(path): | |
os.makedirs(path, exist_ok=True) | |
# Set proper permissions (read/write for current user) | |
import stat | |
os.chmod(path, stat.S_IRWXU) | |
except Exception as e: | |
print(f"Warning: Could not create directory {path}: {str(e)}") | |
continue | |
# Try to create minimal settings file | |
settings_file = os.path.join(cursor_paths["user_data"], "settings.json") | |
try: | |
if not os.path.exists(settings_file): | |
with open(settings_file, 'w') as f: | |
json.dump({ | |
"telemetry.telemetryLevel": "off", | |
"telemetry.enableTelemetry": False | |
}, f, indent=2) | |
except Exception as e: | |
print(f"Warning: Could not create settings file: {str(e)}") | |
return cursor_paths | |
except Exception as e: | |
print(f"Error creating Cursor folders: {str(e)}") | |
# Return default paths even if creation fails | |
return { | |
"main": os.path.expanduser("~/Cursor"), | |
"user_data": os.path.expanduser("~/Cursor/User Data"), | |
"chat_storage": os.path.expanduser("~/Cursor/User Data/Chat Storage"), | |
"workspace": os.path.expanduser("~/Cursor/Workspaces") | |
} | |
#controller.py | |
from virtual_system import VirtualSystem | |
import os | |
import psutil | |
class Controller: | |
def __init__(self): | |
self.virtual_system = VirtualSystem() | |
self.current_spoof_data = None | |
def start_application(self, app_path): | |
"""Alias untuk run_application untuk kompatibilitas""" | |
return self.run_application(app_path) | |
def run_application(self, app_path): | |
result = self.virtual_system.run_application(app_path) | |
if isinstance(result, tuple) and len(result) == 4: | |
pid, hostname, mac, spoof_data = result | |
self.current_spoof_data = spoof_data | |
return pid, hostname, mac, spoof_data | |
return result | |
def cleanup_processes(self): | |
"""Cleanup any zombie processes""" | |
for pid in list(self.running_processes.keys()): | |
try: | |
process = psutil.Process(pid) | |
if not process.is_running(): | |
del self.running_processes[pid] | |
except (psutil.NoSuchProcess, psutil.AccessDenied): | |
del self.running_processes[pid] | |
def stop_application(self, pid): | |
"""Stop running application""" | |
try: | |
if pid in self.running_processes: | |
process = psutil.Process(pid) | |
process.terminate() | |
try: | |
process.wait(timeout=5) | |
except psutil.TimeoutExpired: | |
process.kill() | |
del self.running_processes[pid] | |
return True | |
return False | |
except (psutil.NoSuchProcess, psutil.AccessDenied): | |
if pid in self.running_processes: | |
del self.running_processes[pid] | |
return False | |
def __del__(self): | |
"""Cleanup on object destruction""" | |
self.cleanup_processes() | |
def check_application(self, app_path, test_os=None): | |
# Buat instance baru jika menggunakan test_os | |
if test_os: | |
vs = VirtualSystem(test_os=test_os) | |
return vs.check_application(app_path, test_os) | |
return self.virtual_system.check_application(app_path) | |
def get_current_spoof_data(self): | |
return self.current_spoof_data | |
#gui.py | |
import sys | |
import os | |
import json | |
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, | |
QPushButton, QLabel, QFileDialog, QMessageBox, QDialog, | |
QTableWidget, QTableWidgetItem, QHeaderView, QHBoxLayout) | |
from PyQt5.QtCore import Qt | |
from PyQt5.QtGui import QClipboard | |
from controller import Controller | |
class SpoofStatusDialog(QDialog): | |
def __init__(self, spoof_data, parent=None): | |
super().__init__(parent) | |
self.spoof_data = spoof_data | |
self.initUI() | |
def initUI(self): | |
self.setWindowTitle('Status Spoofing') | |
self.setGeometry(200, 200, 600, 400) | |
main_layout = QVBoxLayout() | |
# Create table | |
self.table = QTableWidget() | |
self.table.setColumnCount(2) | |
self.table.setHorizontalHeaderLabels(['Parameter', 'Nilai']) | |
# Auto adjust column width | |
header = self.table.horizontalHeader() | |
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) | |
header.setSectionResizeMode(1, QHeaderView.Stretch) | |
self._populate_table() | |
main_layout.addWidget(self.table) | |
# Button layout | |
button_layout = QHBoxLayout() | |
# Copy All button | |
copy_all_btn = QPushButton('Copy Semua', self) | |
copy_all_btn.clicked.connect(self.copy_all_data) | |
button_layout.addWidget(copy_all_btn) | |
# Copy Selected button | |
copy_selected_btn = QPushButton('Copy Terpilih', self) | |
copy_selected_btn.clicked.connect(self.copy_selected_data) | |
button_layout.addWidget(copy_selected_btn) | |
# Export JSON button | |
export_json_btn = QPushButton('Export JSON', self) | |
export_json_btn.clicked.connect(self.export_to_json) | |
button_layout.addWidget(export_json_btn) | |
main_layout.addLayout(button_layout) | |
self.setLayout(main_layout) | |
def _populate_table(self): | |
self.data = [] # Store data for copying | |
# Basic system info | |
if 'HOSTNAME' in self.spoof_data: | |
self.data.append(('Hostname', self.spoof_data['HOSTNAME'])) | |
if 'SPOOFED_UUID' in self.spoof_data: | |
self.data.append(('System UUID', self.spoof_data['SPOOFED_UUID'])) | |
# Hardware profile | |
if 'HARDWARE_PROFILE' in self.spoof_data: | |
hw = json.loads(self.spoof_data['HARDWARE_PROFILE']) | |
self.data.append(('CPU', hw.get('cpu', 'Unknown'))) | |
self.data.append(('GPU', hw.get('gpu', 'Unknown'))) | |
self.data.append(('RAM', f"{hw.get('ram', 0)} MB")) | |
self.data.append(('Screen Resolution', hw.get('screen', 'Unknown'))) | |
self.data.append(('Machine GUID', hw.get('machine_guid', 'Unknown'))) | |
self.data.append(('BIOS UUID', hw.get('bios_uuid', 'Unknown'))) | |
self.data.append(('Disk Serial', hw.get('disk_serial', 'Unknown'))) | |
if 'motherboard' in hw: | |
mb = hw['motherboard'] | |
self.data.append(('Motherboard Manufacturer', mb.get('manufacturer', 'Unknown'))) | |
self.data.append(('Motherboard Model', mb.get('model', 'Unknown'))) | |
self.data.append(('Motherboard Serial', mb.get('serial', 'Unknown'))) | |
# OS Info | |
if 'OS_INFO' in self.spoof_data: | |
os_info = json.loads(self.spoof_data['OS_INFO']) | |
self.data.append(('OS Version', os_info.get('os_version', 'Unknown'))) | |
if 'build_number' in os_info: | |
self.data.append(('Build Number', os_info['build_number'])) | |
if 'kernel' in os_info: | |
self.data.append(('Kernel Version', os_info['kernel'])) | |
# Cursor specific (if running) | |
if 'CURSOR_VERSION' in self.spoof_data: | |
self.data.append(('=== Cursor Information ===', '')) | |
self.data.append(('Version', self.spoof_data['CURSOR_VERSION'])) | |
self.data.append(('VSCode Version', self.spoof_data.get('CURSOR_VSCODE_VERSION', 'Unknown'))) | |
self.data.append(('Electron', self.spoof_data.get('CURSOR_ELECTRON_VERSION', 'Unknown'))) | |
self.data.append(('Chromium', self.spoof_data.get('CURSOR_CHROMIUM_VERSION', 'Unknown'))) | |
self.data.append(('Node.js', self.spoof_data.get('CURSOR_NODEJS_VERSION', 'Unknown'))) | |
self.data.append(('V8', self.spoof_data.get('CURSOR_V8_VERSION', 'Unknown'))) | |
self.data.append(('Commit', self.spoof_data.get('CURSOR_COMMIT', 'Unknown'))) | |
self.data.append(('Build Date', self.spoof_data.get('CURSOR_BUILD_DATE', 'Unknown'))) | |
self.data.append(('OS', self.spoof_data.get('CURSOR_OS', 'Unknown'))) | |
if 'CURSOR_FEATURES' in self.spoof_data: | |
features = json.loads(self.spoof_data['CURSOR_FEATURES']) | |
self.data.append(('Features', ', '.join(features))) | |
if 'CURSOR_SETTINGS' in self.spoof_data: | |
settings = json.loads(self.spoof_data['CURSOR_SETTINGS']) | |
self.data.append(('Font Size', str(settings.get('editor.fontSize', 'Unknown')))) | |
self.data.append(('Font Family', settings.get('editor.fontFamily', 'Unknown'))) | |
self.data.append(('Color Theme', settings.get('workbench.colorTheme', 'Unknown'))) | |
# Set table rows | |
self.table.setRowCount(len(self.data)) | |
# Fill table | |
for i, (param, value) in enumerate(self.data): | |
self.table.setItem(i, 0, QTableWidgetItem(param)) | |
self.table.setItem(i, 1, QTableWidgetItem(str(value))) | |
def copy_all_data(self): | |
"""Copy all data to clipboard""" | |
text = "=== Status Spoofing ===\n\n" | |
for param, value in self.data: | |
text += f"{param}: {value}\n" | |
clipboard = QApplication.clipboard() | |
clipboard.setText(text) | |
QMessageBox.information(self, 'Success', 'Semua data telah disalin ke clipboard!') | |
def copy_selected_data(self): | |
"""Copy only selected rows""" | |
selected_items = self.table.selectedItems() | |
if not selected_items: | |
QMessageBox.warning(self, 'Warning', 'Pilih baris yang ingin disalin terlebih dahulu!') | |
return | |
selected_rows = set() | |
text = "=== Status Spoofing (Selected) ===\n\n" | |
for item in selected_items: | |
row = item.row() | |
if row not in selected_rows: | |
selected_rows.add(row) | |
param, value = self.data[row] | |
text += f"{param}: {value}\n" | |
clipboard = QApplication.clipboard() | |
clipboard.setText(text) | |
QMessageBox.information(self, 'Success', 'Data terpilih telah disalin ke clipboard!') | |
def export_to_json(self): | |
"""Export data to JSON file""" | |
file_name, _ = QFileDialog.getSaveFileName( | |
self, | |
"Simpan JSON", | |
"", | |
"JSON Files (*.json)" | |
) | |
if file_name: | |
if not file_name.endswith('.json'): | |
file_name += '.json' | |
export_data = {param: value for param, value in self.data} | |
try: | |
with open(file_name, 'w') as f: | |
json.dump(export_data, f, indent=4) | |
QMessageBox.information(self, 'Success', f'Data telah disimpan ke {file_name}') | |
except Exception as e: | |
QMessageBox.critical(self, 'Error', f'Gagal menyimpan file: {str(e)}') | |
class MainWindow(QMainWindow): | |
def __init__(self): | |
super().__init__() | |
self.controller = Controller() | |
self.current_spoof_data = None # Untuk menyimpan data spoofing aktif | |
self.initUI() | |
def initUI(self): | |
self.setWindowTitle('Virtual System') | |
self.setGeometry(100, 100, 400, 300) | |
# Widget utama | |
central_widget = QWidget() | |
self.setCentralWidget(central_widget) | |
layout = QVBoxLayout(central_widget) | |
# Label untuk menampilkan path file | |
self.path_label = QLabel('Belum ada file dipilih') | |
self.path_label.setWordWrap(True) | |
layout.addWidget(self.path_label) | |
# Label untuk info kompatibilitas | |
self.compatibility_label = QLabel('') | |
self.compatibility_label.setWordWrap(True) | |
layout.addWidget(self.compatibility_label) | |
# Tombol pilih file | |
select_btn = QPushButton('Pilih Aplikasi', self) | |
select_btn.clicked.connect(self.select_file) | |
layout.addWidget(select_btn) | |
# Tombol cek kompatibilitas | |
check_btn = QPushButton('Cek Kompatibilitas', self) | |
check_btn.clicked.connect(self.check_compatibility) | |
layout.addWidget(check_btn) | |
# Tombol jalankan aplikasi | |
self.run_btn = QPushButton('Jalankan Aplikasi', self) | |
self.run_btn.clicked.connect(self.run_application) | |
self.run_btn.setEnabled(False) # Disabled by default | |
layout.addWidget(self.run_btn) | |
# Tambahkan tombol simulasi | |
simulate_win_btn = QPushButton('Simulasi Check di Windows', self) | |
simulate_win_btn.clicked.connect(lambda: self.check_compatibility(test_os="windows")) | |
layout.addWidget(simulate_win_btn) | |
simulate_linux_btn = QPushButton('Simulasi Check di Linux', self) | |
simulate_linux_btn.clicked.connect(lambda: self.check_compatibility(test_os="linux")) | |
layout.addWidget(simulate_linux_btn) | |
# Tambah tombol status spoof | |
self.spoof_status_btn = QPushButton('Lihat Status Spoof', self) | |
self.spoof_status_btn.clicked.connect(self.show_spoof_status) | |
self.spoof_status_btn.setEnabled(False) # Disabled by default | |
layout.addWidget(self.spoof_status_btn) | |
self.selected_file = None | |
self.compatibility_result = None | |
def select_file(self): | |
file_name, _ = QFileDialog.getOpenFileName( | |
self, | |
"Pilih Aplikasi", | |
"", | |
"All Files (*)" | |
) | |
if file_name: | |
self.selected_file = file_name | |
self.path_label.setText(f'File dipilih: {file_name}') | |
# Auto check compatibility when file is selected | |
self.check_compatibility() | |
def check_compatibility(self, test_os=None): | |
if not self.selected_file: | |
QMessageBox.warning(self, 'Error', 'Pilih file terlebih dahulu!') | |
return | |
try: | |
self.compatibility_result = self.controller.check_application( | |
self.selected_file, | |
test_os=test_os | |
) | |
# Format message | |
message = f""" | |
{'[SIMULASI]' if test_os else '[AKTUAL]'} | |
Status: {'✅ Berhasil' if self.compatibility_result['status'] else '❌ Gagal'} | |
Platform: {self.compatibility_result.get('platform', 'Tidak diketahui')} | |
Tipe File: {self.compatibility_result.get('file_type', 'Tidak diketahui')} | |
OS Saat Ini: {self.compatibility_result.get('current_os', 'Tidak diketahui')} | |
OS Sebenarnya: {self.compatibility_result.get('actual_os', 'Tidak diketahui')} | |
Dapat Dijalankan: {'✅ Ya' if self.compatibility_result.get('can_run', False) else '❌ Tidak'} | |
{self.compatibility_result['message']} | |
""" | |
self.compatibility_label.setText(message) | |
# Enable run button hanya jika bukan simulasi dan kompatibel | |
can_run = (not test_os and self.compatibility_result.get('can_run', False)) | |
self.run_btn.setEnabled(can_run) | |
except Exception as e: | |
QMessageBox.critical(self, 'Error', f'Error saat mengecek kompatibilitas: {str(e)}') | |
def run_application(self): | |
if not self.selected_file: | |
QMessageBox.warning(self, 'Error', 'Pilih file terlebih dahulu!') | |
return | |
if not self.compatibility_result or not self.compatibility_result.get('can_run', False): | |
QMessageBox.warning(self, 'Error', 'Aplikasi tidak kompatibel!') | |
return | |
try: | |
# Jalankan aplikasi dan simpan data spoofing | |
pid, hostname, mac, spoof_data = self.controller.run_application(self.selected_file) | |
self.current_spoof_data = spoof_data | |
# Enable tombol status spoof | |
self.spoof_status_btn.setEnabled(True) | |
except Exception as e: | |
QMessageBox.critical(self, 'Error', f'Error saat menjalankan aplikasi: {str(e)}') | |
def show_spoof_status(self): | |
if not self.current_spoof_data: | |
QMessageBox.warning(self, 'Error', 'Tidak ada data spoofing yang aktif!') | |
return | |
dialog = SpoofStatusDialog(self.current_spoof_data, self) | |
dialog.exec_() | |
def main(): | |
app = QApplication(sys.argv) | |
window = MainWindow() | |
window.show() | |
sys.exit(app.exec_()) | |
if __name__ == '__main__': | |
main() | |
#main.py | |
import sys | |
import os | |
from PyQt5.QtWidgets import QApplication, QMessageBox | |
from gui import MainWindow | |
def check_permissions(): | |
"""Check if we have necessary permissions""" | |
try: | |
# Test write permission to AppData | |
local_appdata = os.getenv('LOCALAPPDATA') | |
roaming_appdata = os.getenv('APPDATA') | |
test_paths = [ | |
os.path.join(local_appdata, 'Programs') if local_appdata else None, | |
os.path.join(roaming_appdata, 'Cursor') if roaming_appdata else None | |
] | |
for path in test_paths: | |
if path and not os.path.exists(path): | |
try: | |
os.makedirs(path, exist_ok=True) | |
# Test write | |
test_file = os.path.join(path, 'test.tmp') | |
with open(test_file, 'w') as f: | |
f.write('test') | |
os.remove(test_file) | |
except Exception as e: | |
return False, str(e) | |
return True, None | |
except Exception as e: | |
return False, str(e) | |
def main(): | |
app = QApplication(sys.argv) | |
# Check permissions first | |
has_permissions, error = check_permissions() | |
if not has_permissions: | |
msg = QMessageBox() | |
msg.setIcon(QMessageBox.Critical) | |
msg.setText("Error: Insufficient Permissions") | |
msg.setInformativeText(f"Please run the application as administrator.\nError: {error}") | |
msg.setWindowTitle("Permission Error") | |
msg.exec_() | |
return | |
try: | |
window = MainWindow() | |
window.show() | |
sys.exit(app.exec_()) | |
except Exception as e: | |
msg = QMessageBox() | |
msg.setIcon(QMessageBox.Critical) | |
msg.setText("Error Starting Application") | |
msg.setInformativeText(str(e)) | |
msg.setWindowTitle("Error") | |
msg.exec_() | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment