From 775a2f435d5217f98791bf21e836c79163c39258 Mon Sep 17 00:00:00 2001 From: Leonmmcoset Date: Mon, 13 Apr 2026 18:32:22 +0800 Subject: [PATCH] =?UTF-8?q?Wine=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wine/README.md | 15 +- wine/cleonos_wine.py | 1020 +---------------- wine/cleonos_wine_lib/__init__.py | 5 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 323 bytes .../__pycache__/cli.cpython-313.pyc | Bin 0 -> 2956 bytes .../__pycache__/constants.cpython-313.pyc | Bin 0 -> 2327 bytes .../__pycache__/input_pump.cpython-313.pyc | Bin 0 -> 5150 bytes .../__pycache__/platform.cpython-313.pyc | Bin 0 -> 1157 bytes .../__pycache__/runner.cpython-313.pyc | Bin 0 -> 38744 bytes .../__pycache__/state.cpython-313.pyc | Bin 0 -> 5301 bytes wine/cleonos_wine_lib/cli.py | 54 + wine/cleonos_wine_lib/constants.py | 66 ++ wine/cleonos_wine_lib/input_pump.py | 99 ++ wine/cleonos_wine_lib/platform.py | 41 + wine/cleonos_wine_lib/runner.py | 785 +++++++++++++ wine/cleonos_wine_lib/state.py | 82 ++ 16 files changed, 1145 insertions(+), 1022 deletions(-) create mode 100644 wine/cleonos_wine_lib/__init__.py create mode 100644 wine/cleonos_wine_lib/__pycache__/__init__.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/__pycache__/input_pump.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/__pycache__/platform.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/__pycache__/runner.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/__pycache__/state.cpython-313.pyc create mode 100644 wine/cleonos_wine_lib/cli.py create mode 100644 wine/cleonos_wine_lib/constants.py create mode 100644 wine/cleonos_wine_lib/input_pump.py create mode 100644 wine/cleonos_wine_lib/platform.py create mode 100644 wine/cleonos_wine_lib/runner.py create mode 100644 wine/cleonos_wine_lib/state.py diff --git a/wine/README.md b/wine/README.md index e418d79..3928318 100644 --- a/wine/README.md +++ b/wine/README.md @@ -4,9 +4,15 @@ CLeonOS-Wine 现在改为自研运行器:基于 Python + Unicorn,直接运 不再依赖 Qiling。 -## 文件 +## 文件结构 -- `wine/cleonos_wine.py`:主运行器(ELF 装载 + `int 0x80` syscall 桥接) +- `wine/cleonos_wine.py`:兼容入口脚本 +- `wine/cleonos_wine_lib/cli.py`:命令行参数与启动流程 +- `wine/cleonos_wine_lib/runner.py`:ELF 装载、执行、syscall 分发 +- `wine/cleonos_wine_lib/state.py`:内核态统计与共享状态 +- `wine/cleonos_wine_lib/input_pump.py`:主机键盘输入线程 +- `wine/cleonos_wine_lib/constants.py`:常量与 syscall ID +- `wine/cleonos_wine_lib/platform.py`:Unicorn 导入与平台适配 - `wine/requirements.txt`:Python 依赖(Unicorn) ## 安装 @@ -31,13 +37,14 @@ python wine/cleonos_wine.py build/x86_64/ramdisk_root/shell/shell.elf --rootfs b ## 支持 - ELF64 (x86_64) PT_LOAD 段装载 -- CLeonOS `int 0x80` syscall 0..26 +- CLeonOS `int 0x80` syscall 0..39 - TTY 输出与键盘输入队列 - rootfs 文件/目录访问(`FS_*`) +- `/temp` 写入限制(`FS_MKDIR/WRITE/APPEND/REMOVE`) - `EXEC_PATH` 递归执行 ELF(带深度限制) ## 参数 - `--no-kbd`:关闭输入线程 - `--max-exec-depth N`:设置 exec 嵌套深度上限 -- `--verbose`:打印更多日志 +- `--verbose`:打印更多日志 \ No newline at end of file diff --git a/wine/cleonos_wine.py b/wine/cleonos_wine.py index 4a0e3f0..8cfad7c 100644 --- a/wine/cleonos_wine.py +++ b/wine/cleonos_wine.py @@ -1,1023 +1,7 @@ #!/usr/bin/env python3 -""" -CLeonOS-Wine (native Unicorn backend) -A lightweight user-mode runner for CLeonOS x86_64 ELF applications. -This version does NOT depend on qiling. -""" - -from __future__ import annotations - -import argparse -import collections -import os -import struct -import sys -import threading -import time -from dataclasses import dataclass, field -from pathlib import Path -from typing import Deque, List, Optional, Tuple - -try: - from unicorn import Uc, UcError - from unicorn import UC_ARCH_X86, UC_MODE_64 - from unicorn import UC_HOOK_CODE, UC_HOOK_INTR - from unicorn import UC_PROT_ALL, UC_PROT_EXEC, UC_PROT_READ, UC_PROT_WRITE - from unicorn.x86_const import ( - UC_X86_REG_RAX, - UC_X86_REG_RBX, - UC_X86_REG_RCX, - UC_X86_REG_RDX, - UC_X86_REG_RBP, - UC_X86_REG_RSP, - ) -except Exception as exc: - print("[WINE][ERROR] unicorn import failed. Install dependencies first:", file=sys.stderr) - print(" pip install -r wine/requirements.txt", file=sys.stderr) - raise SystemExit(1) from exc - - -U64_MASK = (1 << 64) - 1 -PAGE_SIZE = 0x1000 -MAX_CSTR = 4096 -MAX_IO_READ = 1 << 20 -DEFAULT_MAX_EXEC_DEPTH = 6 -FS_NAME_MAX = 96 - -# CLeonOS syscall IDs from cleonos/c/include/cleonos_syscall.h -SYS_LOG_WRITE = 0 -SYS_TIMER_TICKS = 1 -SYS_TASK_COUNT = 2 -SYS_CUR_TASK = 3 -SYS_SERVICE_COUNT = 4 -SYS_SERVICE_READY_COUNT = 5 -SYS_CONTEXT_SWITCHES = 6 -SYS_KELF_COUNT = 7 -SYS_KELF_RUNS = 8 -SYS_FS_NODE_COUNT = 9 -SYS_FS_CHILD_COUNT = 10 -SYS_FS_GET_CHILD_NAME = 11 -SYS_FS_READ = 12 -SYS_EXEC_PATH = 13 -SYS_EXEC_REQUESTS = 14 -SYS_EXEC_SUCCESS = 15 -SYS_USER_SHELL_READY = 16 -SYS_USER_EXEC_REQUESTED = 17 -SYS_USER_LAUNCH_TRIES = 18 -SYS_USER_LAUNCH_OK = 19 -SYS_USER_LAUNCH_FAIL = 20 -SYS_TTY_COUNT = 21 -SYS_TTY_ACTIVE = 22 -SYS_TTY_SWITCH = 23 -SYS_TTY_WRITE = 24 -SYS_TTY_WRITE_CHAR = 25 -SYS_KBD_GET_CHAR = 26 -SYS_FS_STAT_TYPE = 27 -SYS_FS_STAT_SIZE = 28 -SYS_FS_MKDIR = 29 -SYS_FS_WRITE = 30 -SYS_FS_APPEND = 31 -SYS_FS_REMOVE = 32 -SYS_LOG_JOURNAL_COUNT = 33 -SYS_LOG_JOURNAL_READ = 34 -SYS_KBD_BUFFERED = 35 -SYS_KBD_PUSHED = 36 -SYS_KBD_POPPED = 37 -SYS_KBD_DROPPED = 38 -SYS_KBD_HOTKEY_SWITCHES = 39 - - -def u64(value: int) -> int: - return value & U64_MASK - - -def u64_neg1() -> int: - return U64_MASK - - -def page_floor(addr: int) -> int: - return addr & ~(PAGE_SIZE - 1) - - -def page_ceil(addr: int) -> int: - return (addr + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1) - - -@dataclass -class ELFSegment: - vaddr: int - memsz: int - flags: int - data: bytes - - -@dataclass -class ELFImage: - entry: int - segments: List[ELFSegment] - - -@dataclass -class SharedKernelState: - start_ns: int = field(default_factory=time.monotonic_ns) - task_count: int = 5 - current_task: int = 0 - service_count: int = 7 - service_ready: int = 7 - context_switches: int = 0 - kelf_count: int = 2 - kelf_runs: int = 0 - exec_requests: int = 0 - exec_success: int = 0 - user_shell_ready: int = 1 - user_exec_requested: int = 0 - user_launch_tries: int = 0 - user_launch_ok: int = 0 - user_launch_fail: int = 0 - tty_count: int = 4 - tty_active: int = 0 - kbd_queue: Deque[int] = field(default_factory=collections.deque) - kbd_lock: threading.Lock = field(default_factory=threading.Lock) - kbd_queue_cap: int = 256 - kbd_drop_count: int = 0 - kbd_push_count: int = 0 - kbd_pop_count: int = 0 - kbd_hotkey_switches: int = 0 - log_journal_cap: int = 256 - log_journal: Deque[str] = field(default_factory=lambda: collections.deque(maxlen=256)) - fs_write_max: int = 65536 - - def timer_ticks(self) -> int: - return (time.monotonic_ns() - self.start_ns) // 1_000_000 - - def push_key(self, key: int) -> None: - with self.kbd_lock: - if len(self.kbd_queue) >= self.kbd_queue_cap: - self.kbd_queue.popleft() - self.kbd_drop_count = u64(self.kbd_drop_count + 1) - self.kbd_queue.append(key & 0xFF) - self.kbd_push_count = u64(self.kbd_push_count + 1) - - def pop_key(self) -> Optional[int]: - with self.kbd_lock: - if not self.kbd_queue: - return None - self.kbd_pop_count = u64(self.kbd_pop_count + 1) - return self.kbd_queue.popleft() - - def buffered_count(self) -> int: - with self.kbd_lock: - return len(self.kbd_queue) - - def log_journal_push(self, text: str) -> None: - if text is None: - return - - normalized = text.replace("\r", "") - lines = normalized.split("\n") - - for line in lines: - if len(line) > 255: - line = line[:255] - self.log_journal.append(line) - - def log_journal_count(self) -> int: - return len(self.log_journal) - - def log_journal_read(self, index_from_oldest: int) -> Optional[str]: - if index_from_oldest < 0 or index_from_oldest >= len(self.log_journal): - return None - return list(self.log_journal)[index_from_oldest] - - -class InputPump: - def __init__(self, state: SharedKernelState) -> None: - self.state = state - self._stop = threading.Event() - self._thread: Optional[threading.Thread] = None - self._posix_term_state = None - - def start(self) -> None: - if self._thread is not None: - return - if not sys.stdin or not hasattr(sys.stdin, "isatty") or not sys.stdin.isatty(): - return - self._thread = threading.Thread(target=self._run, name="cleonos-wine-input", daemon=True) - self._thread.start() - - def stop(self) -> None: - self._stop.set() - if self._thread is not None: - self._thread.join(timeout=0.2) - self._thread = None - self._restore_posix_tty() - - def _run(self) -> None: - if os.name == "nt": - self._run_windows() - else: - self._run_posix() - - def _run_windows(self) -> None: - import msvcrt # pylint: disable=import-error - - while not self._stop.is_set(): - if not msvcrt.kbhit(): - time.sleep(0.005) - continue - - ch = msvcrt.getwch() - if ch in ("\x00", "\xe0"): - _ = msvcrt.getwch() - continue - - norm = self._normalize_char(ch) - if norm is None: - continue - self.state.push_key(ord(norm)) - - def _run_posix(self) -> None: - import select - import termios - import tty - - fd = sys.stdin.fileno() - self._posix_term_state = termios.tcgetattr(fd) - tty.setcbreak(fd) - - try: - while not self._stop.is_set(): - readable, _, _ = select.select([sys.stdin], [], [], 0.05) - if not readable: - continue - ch = sys.stdin.read(1) - norm = self._normalize_char(ch) - if norm is None: - continue - self.state.push_key(ord(norm)) - finally: - self._restore_posix_tty() - - def _restore_posix_tty(self) -> None: - if self._posix_term_state is None: - return - try: - import termios - - fd = sys.stdin.fileno() - termios.tcsetattr(fd, termios.TCSADRAIN, self._posix_term_state) - except Exception: - pass - finally: - self._posix_term_state = None - - @staticmethod - def _normalize_char(ch: str) -> Optional[str]: - if not ch: - return None - if ch == "\r": - return "\n" - return ch - - -class CLeonOSWineNative: - def __init__( - self, - elf_path: Path, - rootfs: Path, - guest_path_hint: str, - *, - state: Optional[SharedKernelState] = None, - depth: int = 0, - max_exec_depth: int = DEFAULT_MAX_EXEC_DEPTH, - no_kbd: bool = False, - verbose: bool = False, - top_level: bool = True, - ) -> None: - self.elf_path = elf_path - self.rootfs = rootfs - self.guest_path_hint = guest_path_hint - self.state = state if state is not None else SharedKernelState() - self.depth = depth - self.max_exec_depth = max_exec_depth - self.no_kbd = no_kbd - self.verbose = verbose - self.top_level = top_level - - self.image = self._parse_elf(self.elf_path) - self.exit_code: Optional[int] = None - self._input_pump: Optional[InputPump] = None - - self._stack_base = 0x00007FFF00000000 - self._stack_size = 0x0000000000020000 - self._ret_sentinel = 0x00007FFF10000000 - self._mapped_ranges: List[Tuple[int, int]] = [] - - def run(self) -> Optional[int]: - uc = Uc(UC_ARCH_X86, UC_MODE_64) - self._install_hooks(uc) - self._load_segments(uc) - self._prepare_stack_and_return(uc) - - if self.top_level and not self.no_kbd: - self._input_pump = InputPump(self.state) - self._input_pump.start() - - try: - uc.emu_start(self.image.entry, 0) - except KeyboardInterrupt: - if self.top_level: - print("\n[WINE] interrupted by user", file=sys.stderr) - return None - except UcError as exc: - if self.verbose or self.top_level: - print(f"[WINE][ERROR] runtime crashed: {exc}", file=sys.stderr) - return None - finally: - if self.top_level and self._input_pump is not None: - self._input_pump.stop() - - if self.exit_code is None: - self.exit_code = self._reg_read(uc, UC_X86_REG_RAX) - - return u64(self.exit_code) - - def _install_hooks(self, uc: Uc) -> None: - uc.hook_add(UC_HOOK_INTR, self._hook_intr) - uc.hook_add(UC_HOOK_CODE, self._hook_code, begin=self._ret_sentinel, end=self._ret_sentinel) - - def _hook_code(self, uc: Uc, address: int, size: int, _user_data) -> None: - _ = size - if address == self._ret_sentinel: - self.exit_code = self._reg_read(uc, UC_X86_REG_RAX) - uc.emu_stop() - - def _hook_intr(self, uc: Uc, intno: int, _user_data) -> None: - if intno != 0x80: - raise UcError(1) - - syscall_id = self._reg_read(uc, UC_X86_REG_RAX) - arg0 = self._reg_read(uc, UC_X86_REG_RBX) - arg1 = self._reg_read(uc, UC_X86_REG_RCX) - arg2 = self._reg_read(uc, UC_X86_REG_RDX) - - self.state.context_switches = u64(self.state.context_switches + 1) - ret = self._dispatch_syscall(uc, syscall_id, arg0, arg1, arg2) - self._reg_write(uc, UC_X86_REG_RAX, u64(ret)) - - def _dispatch_syscall(self, uc: Uc, sid: int, arg0: int, arg1: int, arg2: int) -> int: - if sid == SYS_LOG_WRITE: - data = self._read_guest_bytes(uc, arg0, arg1) - text = data.decode("utf-8", errors="replace") - self._host_write(text) - self.state.log_journal_push(text) - return len(data) - if sid == SYS_TIMER_TICKS: - return self.state.timer_ticks() - if sid == SYS_TASK_COUNT: - return self.state.task_count - if sid == SYS_CUR_TASK: - return self.state.current_task - if sid == SYS_SERVICE_COUNT: - return self.state.service_count - if sid == SYS_SERVICE_READY_COUNT: - return self.state.service_ready - if sid == SYS_CONTEXT_SWITCHES: - return self.state.context_switches - if sid == SYS_KELF_COUNT: - return self.state.kelf_count - if sid == SYS_KELF_RUNS: - return self.state.kelf_runs - if sid == SYS_FS_NODE_COUNT: - return self._fs_node_count() - if sid == SYS_FS_CHILD_COUNT: - return self._fs_child_count(uc, arg0) - if sid == SYS_FS_GET_CHILD_NAME: - return self._fs_get_child_name(uc, arg0, arg1, arg2) - if sid == SYS_FS_READ: - return self._fs_read(uc, arg0, arg1, arg2) - if sid == SYS_EXEC_PATH: - return self._exec_path(uc, arg0) - if sid == SYS_EXEC_REQUESTS: - return self.state.exec_requests - if sid == SYS_EXEC_SUCCESS: - return self.state.exec_success - if sid == SYS_USER_SHELL_READY: - return self.state.user_shell_ready - if sid == SYS_USER_EXEC_REQUESTED: - return self.state.user_exec_requested - if sid == SYS_USER_LAUNCH_TRIES: - return self.state.user_launch_tries - if sid == SYS_USER_LAUNCH_OK: - return self.state.user_launch_ok - if sid == SYS_USER_LAUNCH_FAIL: - return self.state.user_launch_fail - if sid == SYS_TTY_COUNT: - return self.state.tty_count - if sid == SYS_TTY_ACTIVE: - return self.state.tty_active - if sid == SYS_TTY_SWITCH: - if arg0 >= self.state.tty_count: - return u64_neg1() - self.state.tty_active = int(arg0) - return self.state.tty_active - if sid == SYS_TTY_WRITE: - data = self._read_guest_bytes(uc, arg0, arg1) - self._host_write(data.decode("utf-8", errors="replace")) - return len(data) - if sid == SYS_TTY_WRITE_CHAR: - ch = chr(arg0 & 0xFF) - if ch in ("\b", "\x7f"): - self._host_write("\b \b") - else: - self._host_write(ch) - return 1 - if sid == SYS_KBD_GET_CHAR: - key = self.state.pop_key() - return u64_neg1() if key is None else key - if sid == SYS_FS_STAT_TYPE: - return self._fs_stat_type(uc, arg0) - if sid == SYS_FS_STAT_SIZE: - return self._fs_stat_size(uc, arg0) - if sid == SYS_FS_MKDIR: - return self._fs_mkdir(uc, arg0) - if sid == SYS_FS_WRITE: - return self._fs_write(uc, arg0, arg1, arg2) - if sid == SYS_FS_APPEND: - return self._fs_append(uc, arg0, arg1, arg2) - if sid == SYS_FS_REMOVE: - return self._fs_remove(uc, arg0) - if sid == SYS_LOG_JOURNAL_COUNT: - return self.state.log_journal_count() - if sid == SYS_LOG_JOURNAL_READ: - return self._log_journal_read(uc, arg0, arg1, arg2) - if sid == SYS_KBD_BUFFERED: - return self.state.buffered_count() - if sid == SYS_KBD_PUSHED: - return self.state.kbd_push_count - if sid == SYS_KBD_POPPED: - return self.state.kbd_pop_count - if sid == SYS_KBD_DROPPED: - return self.state.kbd_drop_count - if sid == SYS_KBD_HOTKEY_SWITCHES: - return self.state.kbd_hotkey_switches - - return u64_neg1() - - def _host_write(self, text: str) -> None: - if not text: - return - sys.stdout.write(text) - sys.stdout.flush() - - def _load_segments(self, uc: Uc) -> None: - for seg in self.image.segments: - start = page_floor(seg.vaddr) - end = page_ceil(seg.vaddr + seg.memsz) - self._map_region(uc, start, end - start, UC_PROT_ALL) - - for seg in self.image.segments: - if seg.data: - self._mem_write(uc, seg.vaddr, seg.data) - - # Try to tighten protections after data is in place. - for seg in self.image.segments: - start = page_floor(seg.vaddr) - end = page_ceil(seg.vaddr + seg.memsz) - size = end - start - perms = 0 - if seg.flags & 0x4: - perms |= UC_PROT_READ - if seg.flags & 0x2: - perms |= UC_PROT_WRITE - if seg.flags & 0x1: - perms |= UC_PROT_EXEC - if perms == 0: - perms = UC_PROT_READ - try: - uc.mem_protect(start, size, perms) - except Exception: - pass - - def _prepare_stack_and_return(self, uc: Uc) -> None: - self._map_region(uc, self._stack_base, self._stack_size, UC_PROT_READ | UC_PROT_WRITE) - self._map_region(uc, self._ret_sentinel, PAGE_SIZE, UC_PROT_READ | UC_PROT_EXEC) - self._mem_write(uc, self._ret_sentinel, b"\x90") - - rsp = self._stack_base + self._stack_size - 8 - self._mem_write(uc, rsp, struct.pack(" None: - if size <= 0: - return - start = page_floor(addr) - end = page_ceil(addr + size) - - if self._is_range_mapped(start, end): - return - - uc.mem_map(start, end - start, perms) - self._mapped_ranges.append((start, end)) - - def _is_range_mapped(self, start: int, end: int) -> bool: - for ms, me in self._mapped_ranges: - if start >= ms and end <= me: - return True - return False - - @staticmethod - def _reg_read(uc: Uc, reg: int) -> int: - return int(uc.reg_read(reg)) - - @staticmethod - def _reg_write(uc: Uc, reg: int, value: int) -> None: - uc.reg_write(reg, u64(value)) - - @staticmethod - def _mem_write(uc: Uc, addr: int, data: bytes) -> None: - if addr == 0 or not data: - return - uc.mem_write(addr, data) - - def _read_guest_cstring(self, uc: Uc, addr: int, max_len: int = MAX_CSTR) -> str: - if addr == 0: - return "" - - out = bytearray() - for i in range(max_len): - try: - ch = uc.mem_read(addr + i, 1) - except UcError: - break - if not ch or ch[0] == 0: - break - out.append(ch[0]) - return out.decode("utf-8", errors="replace") - - def _read_guest_bytes(self, uc: Uc, addr: int, size: int) -> bytes: - if addr == 0 or size == 0: - return b"" - safe_size = int(min(size, MAX_IO_READ)) - try: - return bytes(uc.mem_read(addr, safe_size)) - except UcError: - return b"" - - def _write_guest_bytes(self, uc: Uc, addr: int, data: bytes) -> bool: - if addr == 0: - return False - try: - uc.mem_write(addr, data) - return True - except UcError: - return False - - @staticmethod - def _parse_elf(path: Path) -> ELFImage: - data = path.read_bytes() - if len(data) < 64: - raise RuntimeError(f"ELF too small: {path}") - if data[0:4] != b"\x7fELF": - raise RuntimeError(f"invalid ELF magic: {path}") - if data[4] != 2 or data[5] != 1: - raise RuntimeError(f"unsupported ELF class/endianness: {path}") - - entry = struct.unpack_from(" len(data): - break - - p_type, p_flags, p_offset, p_vaddr, _p_paddr, p_filesz, p_memsz, _p_align = struct.unpack_from( - " 0: - if fo >= len(data): - seg_data = b"" - else: - seg_data = data[fo : min(len(data), fo + fs)] - else: - seg_data = b"" - - segments.append(ELFSegment(vaddr=int(p_vaddr), memsz=int(p_memsz), flags=int(p_flags), data=seg_data)) - - if not segments: - raise RuntimeError(f"ELF has no PT_LOAD segments: {path}") - - return ELFImage(entry=int(entry), segments=segments) - - def _fs_node_count(self) -> int: - count = 1 - for _root, dirs, files in os.walk(self.rootfs): - dirs[:] = [d for d in dirs if not d.startswith(".")] - files = [f for f in files if not f.startswith(".")] - count += len(dirs) + len(files) - return count - - def _fs_child_count(self, uc: Uc, dir_ptr: int) -> int: - path = self._read_guest_cstring(uc, dir_ptr) - host_dir = self._guest_to_host(path, must_exist=True) - if host_dir is None or not host_dir.is_dir(): - return u64_neg1() - return len(self._list_children(host_dir)) - - def _fs_get_child_name(self, uc: Uc, dir_ptr: int, index: int, out_ptr: int) -> int: - if out_ptr == 0: - return 0 - path = self._read_guest_cstring(uc, dir_ptr) - host_dir = self._guest_to_host(path, must_exist=True) - if host_dir is None or not host_dir.is_dir(): - return 0 - - children = self._list_children(host_dir) - if index >= len(children): - return 0 - - name = children[int(index)] - encoded = name.encode("utf-8", errors="replace") - if len(encoded) >= FS_NAME_MAX: - encoded = encoded[: FS_NAME_MAX - 1] - - return 1 if self._write_guest_bytes(uc, out_ptr, encoded + b"\x00") else 0 - - def _fs_read(self, uc: Uc, path_ptr: int, out_ptr: int, buf_size: int) -> int: - if out_ptr == 0 or buf_size == 0: - return 0 - - path = self._read_guest_cstring(uc, path_ptr) - host_path = self._guest_to_host(path, must_exist=True) - if host_path is None or not host_path.is_file(): - return 0 - - read_size = int(min(buf_size, MAX_IO_READ)) - try: - data = host_path.read_bytes()[:read_size] - except Exception: - return 0 - - if not data: - return 0 - return len(data) if self._write_guest_bytes(uc, out_ptr, data) else 0 - - def _fs_stat_type(self, uc: Uc, path_ptr: int) -> int: - path = self._read_guest_cstring(uc, path_ptr) - host_path = self._guest_to_host(path, must_exist=True) - if host_path is None: - return u64_neg1() - if host_path.is_dir(): - return 2 - if host_path.is_file(): - return 1 - return u64_neg1() - - def _fs_stat_size(self, uc: Uc, path_ptr: int) -> int: - path = self._read_guest_cstring(uc, path_ptr) - host_path = self._guest_to_host(path, must_exist=True) - if host_path is None: - return u64_neg1() - if host_path.is_dir(): - return 0 - if host_path.is_file(): - try: - return host_path.stat().st_size - except Exception: - return u64_neg1() - return u64_neg1() - - @staticmethod - def _guest_path_is_under_temp(path: str) -> bool: - return path == "/temp" or path.startswith("/temp/") - - def _fs_mkdir(self, uc: Uc, path_ptr: int) -> int: - path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr)) - if not self._guest_path_is_under_temp(path): - return 0 - - host_path = self._guest_to_host(path, must_exist=False) - if host_path is None: - return 0 - - if host_path.exists() and host_path.is_file(): - return 0 - - try: - host_path.mkdir(parents=True, exist_ok=True) - return 1 - except Exception: - return 0 - - def _fs_write_common(self, uc: Uc, path_ptr: int, data_ptr: int, size: int, append_mode: bool) -> int: - path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr)) - - if not self._guest_path_is_under_temp(path) or path == "/temp": - return 0 - - if size < 0 or size > self.state.fs_write_max: - return 0 - - host_path = self._guest_to_host(path, must_exist=False) - if host_path is None: - return 0 - - if host_path.exists() and host_path.is_dir(): - return 0 - - data = b"" - if size > 0: - if data_ptr == 0: - return 0 - data = self._read_guest_bytes(uc, data_ptr, size) - if len(data) != int(size): - return 0 - - try: - host_path.parent.mkdir(parents=True, exist_ok=True) - mode = "ab" if append_mode else "wb" - with host_path.open(mode) as fh: - if data: - fh.write(data) - return 1 - except Exception: - return 0 - - def _fs_write(self, uc: Uc, path_ptr: int, data_ptr: int, size: int) -> int: - return self._fs_write_common(uc, path_ptr, data_ptr, size, append_mode=False) - - def _fs_append(self, uc: Uc, path_ptr: int, data_ptr: int, size: int) -> int: - return self._fs_write_common(uc, path_ptr, data_ptr, size, append_mode=True) - - def _fs_remove(self, uc: Uc, path_ptr: int) -> int: - path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr)) - - if not self._guest_path_is_under_temp(path) or path == "/temp": - return 0 - - host_path = self._guest_to_host(path, must_exist=True) - if host_path is None: - return 0 - - try: - if host_path.is_dir(): - if any(host_path.iterdir()): - return 0 - host_path.rmdir() - else: - host_path.unlink() - return 1 - except Exception: - return 0 - - def _log_journal_read(self, uc: Uc, index_from_oldest: int, out_ptr: int, out_size: int) -> int: - if out_ptr == 0 or out_size == 0: - return 0 - - line = self.state.log_journal_read(int(index_from_oldest)) - if line is None: - return 0 - - encoded = line.encode("utf-8", errors="replace") - max_payload = int(out_size) - 1 - if max_payload < 0: - return 0 - - if len(encoded) > max_payload: - encoded = encoded[:max_payload] - - return 1 if self._write_guest_bytes(uc, out_ptr, encoded + b"\x00") else 0 - - def _exec_path(self, uc: Uc, path_ptr: int) -> int: - path = self._read_guest_cstring(uc, path_ptr) - guest_path = self._normalize_guest_path(path) - host_path = self._guest_to_host(guest_path, must_exist=True) - - self.state.exec_requests = u64(self.state.exec_requests + 1) - self.state.user_exec_requested = 1 - self.state.user_launch_tries = u64(self.state.user_launch_tries + 1) - - if host_path is None or not host_path.is_file(): - self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) - return u64_neg1() - - if self.depth >= self.max_exec_depth: - print(f"[WINE][WARN] exec depth exceeded: {guest_path}", file=sys.stderr) - self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) - return u64_neg1() - - child = CLeonOSWineNative( - elf_path=host_path, - rootfs=self.rootfs, - guest_path_hint=guest_path, - state=self.state, - depth=self.depth + 1, - max_exec_depth=self.max_exec_depth, - no_kbd=True, - verbose=self.verbose, - top_level=False, - ) - child_ret = child.run() - if child_ret is None: - self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) - return u64_neg1() - - self.state.exec_success = u64(self.state.exec_success + 1) - self.state.user_launch_ok = u64(self.state.user_launch_ok + 1) - if guest_path.lower().startswith("/system/"): - self.state.kelf_runs = u64(self.state.kelf_runs + 1) - return 0 - - def _guest_to_host(self, guest_path: str, *, must_exist: bool) -> Optional[Path]: - norm = self._normalize_guest_path(guest_path) - if norm == "/": - return self.rootfs if (not must_exist or self.rootfs.exists()) else None - - current = self.rootfs - for part in [p for p in norm.split("/") if p]: - candidate = current / part - if candidate.exists(): - current = candidate - continue - - if current.exists() and current.is_dir(): - match = self._find_case_insensitive(current, part) - if match is not None: - current = match - continue - - current = candidate - - if must_exist and not current.exists(): - return None - return current - - @staticmethod - def _find_case_insensitive(parent: Path, name: str) -> Optional[Path]: - target = name.lower() - try: - for entry in parent.iterdir(): - if entry.name.lower() == target: - return entry - except Exception: - return None - return None - - @staticmethod - def _normalize_guest_path(path: str) -> str: - p = (path or "").replace("\\", "/").strip() - if not p: - return "/" - if not p.startswith("/"): - p = "/" + p - - parts = [] - for token in p.split("/"): - if token in ("", "."): - continue - if token == "..": - if parts: - parts.pop() - continue - parts.append(token) - - return "/" + "/".join(parts) - - @staticmethod - def _list_children(dir_path: Path) -> List[str]: - try: - names = [entry.name for entry in dir_path.iterdir() if not entry.name.startswith(".")] - except Exception: - return [] - names.sort(key=lambda x: x.lower()) - return names - - -def resolve_rootfs(path_arg: Optional[str]) -> Path: - if path_arg: - root = Path(path_arg).expanduser().resolve() - if not root.exists() or not root.is_dir(): - raise FileNotFoundError(f"rootfs not found: {root}") - return root - - candidates = [ - Path("build/x86_64/ramdisk_root"), - Path("ramdisk"), - ] - for candidate in candidates: - if candidate.exists() and candidate.is_dir(): - return candidate.resolve() - - raise FileNotFoundError("rootfs not found; pass --rootfs") - - -def _guest_to_host_for_resolve(rootfs: Path, guest_path: str) -> Optional[Path]: - norm = CLeonOSWineNative._normalize_guest_path(guest_path) - if norm == "/": - return rootfs - - current = rootfs - for part in [p for p in norm.split("/") if p]: - candidate = current / part - if candidate.exists(): - current = candidate - continue - - if current.exists() and current.is_dir(): - match = None - for entry in current.iterdir(): - if entry.name.lower() == part.lower(): - match = entry - break - if match is not None: - current = match - continue - - current = candidate - - return current if current.exists() else None - - -def resolve_elf_target(elf_arg: str, rootfs: Path) -> Tuple[Path, str]: - host_candidate = Path(elf_arg).expanduser() - if host_candidate.exists(): - host_path = host_candidate.resolve() - try: - rel = host_path.relative_to(rootfs) - guest_path = "/" + rel.as_posix() - except ValueError: - guest_path = "/" + host_path.name - return host_path, guest_path - - guest_path = CLeonOSWineNative._normalize_guest_path(elf_arg) - host_path = _guest_to_host_for_resolve(rootfs, guest_path) - if host_path is None: - raise FileNotFoundError(f"ELF not found as host path or guest path: {elf_arg}") - return host_path.resolve(), guest_path - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="CLeonOS-Wine: run CLeonOS ELF with Unicorn.") - parser.add_argument("elf", help="Target ELF path. Supports /guest/path or host file path.") - parser.add_argument("--rootfs", help="Rootfs directory (default: build/x86_64/ramdisk_root).") - parser.add_argument("--no-kbd", action="store_true", help="Disable host keyboard input pump.") - parser.add_argument("--max-exec-depth", type=int, default=DEFAULT_MAX_EXEC_DEPTH, help="Nested exec depth guard.") - parser.add_argument("--verbose", action="store_true", help="Enable verbose runner output.") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - - try: - rootfs = resolve_rootfs(args.rootfs) - elf_path, guest_path = resolve_elf_target(args.elf, rootfs) - except Exception as exc: - print(f"[WINE][ERROR] {exc}", file=sys.stderr) - return 2 - - if args.verbose: - print(f"[WINE] backend=unicorn", file=sys.stderr) - print(f"[WINE] rootfs={rootfs}", file=sys.stderr) - print(f"[WINE] elf={elf_path}", file=sys.stderr) - print(f"[WINE] guest={guest_path}", file=sys.stderr) - - state = SharedKernelState() - runner = CLeonOSWineNative( - elf_path=elf_path, - rootfs=rootfs, - guest_path_hint=guest_path, - state=state, - max_exec_depth=max(1, args.max_exec_depth), - no_kbd=args.no_kbd, - verbose=args.verbose, - top_level=True, - ) - ret = runner.run() - if ret is None: - return 1 - - if args.verbose: - print(f"\n[WINE] exit=0x{ret:016X}", file=sys.stderr) - return int(ret & 0xFF) +from cleonos_wine_lib.cli import main if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file diff --git a/wine/cleonos_wine_lib/__init__.py b/wine/cleonos_wine_lib/__init__.py new file mode 100644 index 0000000..9e23296 --- /dev/null +++ b/wine/cleonos_wine_lib/__init__.py @@ -0,0 +1,5 @@ +from .cli import main +from .runner import CLeonOSWineNative +from .state import SharedKernelState + +__all__ = ["main", "CLeonOSWineNative", "SharedKernelState"] \ No newline at end of file diff --git a/wine/cleonos_wine_lib/__pycache__/__init__.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b7e53133dc89c0a897ff520defabbdecaff5262 GIT binary patch literal 323 zcmXv}Jx{|h5IyG;6=gt(y+awY02>mjSjv>30`kBT$qK0jMy^v`QV=6Qf|b2Lgg;@C z1tvD6`~l7pz2V(^&-d=+K1oJEwfgu}@6^AY*p~NC`V%5=z=5+IGKOsIxR-mGkN&{@ zJjg-}$8ZchxM3LGhiUY~D3&rCEK6mm@69e{ZLavOGICzF>Pb42Ka?mJxkMv1Z_8Gu zw3LDvI0y;mX>9#UtHHJru8om6Jln8wiDAmdLX=tyv4NgKU{5d3ifgPNWz{yttf({{ zubbkTu5X2KLTGhY2%(H>g*aNh*q!OJUaYk|$AtE{Pc-`kzdmEEgM$tZJD7Zprmr#X GlK%&uLsTCC literal 0 HcmV?d00001 diff --git a/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/cli.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f83dbe1fac7f28ce57b0d101f64b4c0ac6414f81 GIT binary patch literal 2956 zcmbVO-*4N-9Y2Z`MTrt+C2^d(iOnS17P*xYCyg5$af-l}(b$y+jL}7 zJ(8}i^uc{Qy~)a zR+g{|t8i1?WSE3O#wy_{elkKLlL8SYqa^C__^H^WNJOBcN~9Mpb0n_B6v3ksi=mA8 zoVv>}uBaJ?X=}D_8rJl4#%CTL&re(~T%A>aefheYzn&jc$Me@_-_EdZYV0aDjdzrH zbpua>)hc!m5Nw&1RjlI5f@*7I5!{ech!YSSB{a+!sDNg|`z1b28<=dVu4 zAL;gzTrl*KNsJs#WEoo}qE~4qT^4dtyD;k|!_%&6_EJt(oNCo1wk4llbg*Tgrd-)1 z@{(!U@`7H$vZn{9aZ9*TtqIwzpTOGCjF(8ctP@ zbyljmOvvT668#pEfT+~Eve^}FEsNK1DO<)>d#QG08omIRWy+F0mb~b|DLJT8&R(x# zH`2bLe?l~yUBzVHv~cZM-tfG)_cXKyCbH?+;6In)T>%1D)rf^%=`vY#Rp4&ewIjYCDM(-4>l9(#zd2gJ>z;dxt@Bsk^Z1T zNwvd^H8PsVo8QO#@?zuC54nva1798d=F)7V=y>|89ewf3tBtpUlf~3C@#v;_bX)9e zaDBUQC-gr(Ot0$o@ZX?kp@-?$dn2f81NEUIX!rwqmka35dcJLCFhz<9-T=+L9t(H~ z6`1pFY;a($o&>D}9ce(FY7?4{P?46BHx#ZY1@uf;Ky_9?7eJ4F`r!g2p>v_1XvG&o zKf@|gU|-s*JGk>+jKjsAfL90=m;ze}7hefDiU3jS4R)P1h$KjlcDMoc|07E=a#%3< z0zcCqXm-}U`$P(n66AD|dj7HefZu)b77Gz2o_yJe9}oP3RU!-Cf+|X*S7tMUE6`__!nDi# zW^U5c@ObJHAd(xl==WLe@H($_4w4+6Wje` zo5Ims!cGhc@lQYg^y8M0Xh^?&s62Y_!FyZ61=!w|>Z|pohV-k4rANyTmbZjq$`tEw z*GGP+nW!(<=Nb~A>XY8by<5V?zr>Gi$NM+(twdil^-43@-8^!lDIIL}^wtOK`nx|DK9U{Kmx2Ata_gAGkem=k<-TW+e8>&1SOe z&k-@WXqSBd;QiVAv4-^8S0|qgJRaB*Mq8qE%ep&zyLRWT2G{$~@83l6?p+ju7DDV9?EjsZXO8ybW d{))1HLRU7?m2Xk{dp^SS|Krec=2h>M{{qfAllK4s literal 0 HcmV?d00001 diff --git a/wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/constants.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..162caed5328dceafd8d3a27ca3510ee67b4a2543 GIT binary patch literal 2327 zcmaKsOK%fb6vyu*PH-}QKjOp*kT~ICAc25EK%av>A;!=8j3H?wP19hIk+JEq)2gd_ z*=;3|x@@Ix*!4s712{WtM5Rhyd509bZqK>bF3zT7%}@6{?z!jw@Aw{jy|aRzAAWyR ze>f$?Uu=w?2{q7;-_qcf00CM~JkU^E7BGPmFo~1&y1<1~Fon}Fji*4M8&2U2xN#O{ zP={IcfR0{@`@n;K@L~Xb7z96tAb?>AVgy1Mg)q)R1Y;1zd6>fmh+!P&aS;}93F3Gf z7V!)$;aND1%Wwu);4CI!8CPKi*C2uCU=`298eV{NcoEKH5-#8+xQLe_iC5qfuES;A zfGcEr= z=X>?@i@FF10&y@+fx(vyjltRtCK)aUPaB+KxEV7H7Ji^JJPa?x$M7?#uowgxAx4-H zVMG~oj2L5{vA~Ek78y&7(~L8Wvy5fN3M0W-WvnsIG0rnC5DvKe(ZiddJ!r?g@GDUj zUyLGRo$}ViYpwWNbKKQiDZ5}mj>MY!X1{S*Yjc%l=XU1qUKv}@Y6qS6UV87KQEN3@ z?Y(d7&DzMY*{~b+ul5c|-p+or)82UToillOD}~po$W40NEKOJR^}x&9NBUyVwRlVp zaljZoyr68A6?`?;=$gQFa^VKywZI!Z-Xu6~TEcGDo^IkTGVjpa<^$@AH+r~Rl8Gn8 zpz3M`R`;YBpsmwkCP$)76p_CMQf8)X+&68=er}TC+JF2Ql`^V>*%$jyYxa{ytA+Rx zP2ji9QuAK@FS9>uKNfnS#F3upxf1`=&^4NJvLW8FYU=%oooo_sQws8~@dLtmTQjn) zgIc|TpHRr)Fn>qXl_&7>&XH&7r>~DZ%fHrtTlwXAZ!LACr+TiG?5~)le-@=Wll5lD znZ{b@5S#cZd6E;zh1A%EACAUj-jp+I+fNQ@O3k*B<;JUwy9CEwFm~)TSQSU-QMP1P zOe5pWWy~$3nz!il+p*0ZGi_(gvbF8#TVNNBg2{fz3%g+FOZV+Zm8@ktem1Ra!K@I| zIdFW^LEo>PE>(+`<6%2pr7-!NAX{Ko9%j?#Fd;VX;y!kVfry-0Dq7}_Wy7PamEJao z^>b!^YnUbbk#?n81hoM7Tgr?U2yq>9rMI*B%y3q8=(}%PLqBiO(T8p+hpa4RC>x}L zY+afa^Ruc67B~S;moBKL(~9Y zY$eUeL;8VbShlrWHdW5Z!+%J1%H%2JGTF+1mRd#XNHL>aHj5e61DO@GP@)#41pGZc zC{-&(BR_ul<8FB*QgCkWUUh5BtdN3o$LuLrN%3*JM9XE=_sQ{0MLFbIuw%Pq<;;;X zCddm?-d^(hkpEWX3v>LI`|x?|@nNI(F}5hcw`4m`d^@RW+8-kEr#|0x_dTKR4Ba?1 z@N1{Cu?8g^-BG!-1_e4a+0zpbu zv1qI(Lj8Fv6qSYg0WK4n<9T749q7jvcwT&oYp6UmNBL!XFiZ~Z>o)RKfu jlTZ~^EtUJeSSUi9kqf_@*0iN}ZcU3YR{tj4l-m6ZX+>JR literal 0 HcmV?d00001 diff --git a/wine/cleonos_wine_lib/__pycache__/input_pump.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/input_pump.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9735ec20bce7f5736cee53c4fb2e3921f3f233bb GIT binary patch literal 5150 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 literal 0 HcmV?d00001 diff --git a/wine/cleonos_wine_lib/__pycache__/state.cpython-313.pyc b/wine/cleonos_wine_lib/__pycache__/state.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b533d6597ac90c653b914dc5e60586430743225e GIT binary patch literal 5301 zcmbVQ%WoUU8K31YDZV69rru9Vq-ax?oQLIy97(Zc1xTe>DYOxg#8S32xs+(Fmdflh zv2;jvtAZdk4b-rY&Z#j_py{bTw2t#hv+fxW^~`vw7qC-y&H85i)E_2`t~Sz7 zBs-~xBNssmqh#g$TK9-DR+V-x~iOA^Ov5% z*&}xYXAfVYm-g}%!uEP$j{0oW$5FqH`Z2XaEuJQ;vZRp?(JBld#q) z&K0LI&Naxq!=`J6n&Yp3M%@-VN(J62CkV&c*+X19%BDKMS$*gXwH%-O( zFk>cHRAZOV<6Tv?v^~PZ>olhutX{pTCKZYfVweW=b7HXP1PYz`I1>pQ{nUw%a}8TI(b0$a3C9*zO?vrn!N$N z#^RO5q^4%|tg-lEMpr99L7-@v>x*Dfrg~<5gSp4F#M<>#VuE!XvC1=*KJ)=22xN5X+!ytW=vIqQzS92Rtg@U3+4@| z{J3B$(S3o;+fleO5q^i^{*i@Ym=i?jn9E$A3V;ZiWM=3fI@m{~oO1TUfiV1FPm z$pCXVa6@5w7AW%}FICJ;a@AnnN5xUojjA+yfoze-!KPBM@9SV+F&Md@`t90hYlYz1 zhn_-Ua>qT%1(dASvkM0CLvO;$cL*E?8V(l+36pEUWtHO(e9jgn^x~X{%B3#}Nn9W} z|1rXv_Z(seSP?I*?5p&`=;JOkVHo+V>uc5ZpRpPW{7_i{D<6g#Dzh9~6^-ps0p&tV zg@1u)7rzhyEm_L8)K-h~tF{ufBxLkHl) zL~t@1g&IYRXLaE9nwmB#N{ixr%#~PQSM?N)1Bg557`BNJ3Jg?n*tR1#j~>u^oKIEy z@G!;}kS$WKZ@jhi$z(zDR!|CyI2fdEC-wJHr#F~1!IL^>{0O4 z<5>LjXlZb~FgX4vp)fdEj7{aGN5M$hAG|g9@m$H@SMc}Uk9;nDHu%Ut{J4Jr=Nc`< zMjuWVV&ldBiS6k}{@y1(($INp>DNpDF;K&wPPI%8iC+zQr!Lh!<)c+|-3PAcnJcKi z*35w$OH~QrojqJlPb=!0oh5QZt(uWF7U>bC*E+>9>pjn~4a_&6GL+~~F%*YoC~+sZ z5z9QdL0}A^Q%GI`QX5Fna1_*k`_Uj`ccHf#b3m%1}U0*ij0G3&HUHreZL*?fkx}eS4b6$xk-!o&DsO;4Js1@5%Yi zyaeH|(%<6~GwKT$$wVE}~v z1CbrJ*v8sJ@)Vs#o-YZ`gb9!l0s`p--U6jiq!5Y}1JNCK^k@oEwNc9V4_B#h=>#FE zcCZ}br=Er<2ypUcQe%t9#_-Dd7X;Kg&#w5p1N^rt2s8`BtlWr=89k-mRMIrNrew7g zJ{!=-j(cGR}`rw+|yV? z|Drs!pyKE_lGl*Djs%UKzK!G>l3xH}&5D9g6UfsOCcF@@=S%H}a99)Lw4y*t(iO zTa-F#YF*tsQd1s=bk_`NZrS?Two#N?Y8VfusTtB6-jQ1Gg^E(R+}*PyHRoYaPr0LW zYc7AKD0P-=SP$l`Zta_{zAsLfUcOR*|E_6RaZocV>6h_`D^t`(@o9`$}@0Q!TEqQeBOM-Js*mt>{ zukHt(&Y}GVhx6pV+v^Gyeo6dp9fXz}2rIwN0z#5t-^HqOe zQw@r-ikmKTnH%fyB&T?vQT`)^2Y!YCZR@6CW!PM;PpcZ4L-*@EHcclNHq~iw{armA%UK51T9|+Pt5gPu*8~8E)2f9|0&;S4c literal 0 HcmV?d00001 diff --git a/wine/cleonos_wine_lib/cli.py b/wine/cleonos_wine_lib/cli.py new file mode 100644 index 0000000..4f50527 --- /dev/null +++ b/wine/cleonos_wine_lib/cli.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import argparse +import sys + +from .constants import DEFAULT_MAX_EXEC_DEPTH +from .runner import CLeonOSWineNative, resolve_elf_target, resolve_rootfs +from .state import SharedKernelState + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="CLeonOS-Wine: run CLeonOS ELF with Unicorn.") + parser.add_argument("elf", help="Target ELF path. Supports /guest/path or host file path.") + parser.add_argument("--rootfs", help="Rootfs directory (default: build/x86_64/ramdisk_root).") + parser.add_argument("--no-kbd", action="store_true", help="Disable host keyboard input pump.") + parser.add_argument("--max-exec-depth", type=int, default=DEFAULT_MAX_EXEC_DEPTH, help="Nested exec depth guard.") + parser.add_argument("--verbose", action="store_true", help="Enable verbose runner output.") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + + try: + rootfs = resolve_rootfs(args.rootfs) + elf_path, guest_path = resolve_elf_target(args.elf, rootfs) + except Exception as exc: + print(f"[WINE][ERROR] {exc}", file=sys.stderr) + return 2 + + if args.verbose: + print("[WINE] backend=unicorn", file=sys.stderr) + print(f"[WINE] rootfs={rootfs}", file=sys.stderr) + print(f"[WINE] elf={elf_path}", file=sys.stderr) + print(f"[WINE] guest={guest_path}", file=sys.stderr) + + state = SharedKernelState() + runner = CLeonOSWineNative( + elf_path=elf_path, + rootfs=rootfs, + guest_path_hint=guest_path, + state=state, + max_exec_depth=max(1, args.max_exec_depth), + no_kbd=args.no_kbd, + verbose=args.verbose, + top_level=True, + ) + ret = runner.run() + if ret is None: + return 1 + + if args.verbose: + print(f"\n[WINE] exit=0x{ret:016X}", file=sys.stderr) + return int(ret & 0xFF) \ No newline at end of file diff --git a/wine/cleonos_wine_lib/constants.py b/wine/cleonos_wine_lib/constants.py new file mode 100644 index 0000000..f07240d --- /dev/null +++ b/wine/cleonos_wine_lib/constants.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +U64_MASK = (1 << 64) - 1 +PAGE_SIZE = 0x1000 +MAX_CSTR = 4096 +MAX_IO_READ = 1 << 20 +DEFAULT_MAX_EXEC_DEPTH = 6 +FS_NAME_MAX = 96 + +# CLeonOS syscall IDs from cleonos/c/include/cleonos_syscall.h +SYS_LOG_WRITE = 0 +SYS_TIMER_TICKS = 1 +SYS_TASK_COUNT = 2 +SYS_CUR_TASK = 3 +SYS_SERVICE_COUNT = 4 +SYS_SERVICE_READY_COUNT = 5 +SYS_CONTEXT_SWITCHES = 6 +SYS_KELF_COUNT = 7 +SYS_KELF_RUNS = 8 +SYS_FS_NODE_COUNT = 9 +SYS_FS_CHILD_COUNT = 10 +SYS_FS_GET_CHILD_NAME = 11 +SYS_FS_READ = 12 +SYS_EXEC_PATH = 13 +SYS_EXEC_REQUESTS = 14 +SYS_EXEC_SUCCESS = 15 +SYS_USER_SHELL_READY = 16 +SYS_USER_EXEC_REQUESTED = 17 +SYS_USER_LAUNCH_TRIES = 18 +SYS_USER_LAUNCH_OK = 19 +SYS_USER_LAUNCH_FAIL = 20 +SYS_TTY_COUNT = 21 +SYS_TTY_ACTIVE = 22 +SYS_TTY_SWITCH = 23 +SYS_TTY_WRITE = 24 +SYS_TTY_WRITE_CHAR = 25 +SYS_KBD_GET_CHAR = 26 +SYS_FS_STAT_TYPE = 27 +SYS_FS_STAT_SIZE = 28 +SYS_FS_MKDIR = 29 +SYS_FS_WRITE = 30 +SYS_FS_APPEND = 31 +SYS_FS_REMOVE = 32 +SYS_LOG_JOURNAL_COUNT = 33 +SYS_LOG_JOURNAL_READ = 34 +SYS_KBD_BUFFERED = 35 +SYS_KBD_PUSHED = 36 +SYS_KBD_POPPED = 37 +SYS_KBD_DROPPED = 38 +SYS_KBD_HOTKEY_SWITCHES = 39 + + +def u64(value: int) -> int: + return value & U64_MASK + + +def u64_neg1() -> int: + return U64_MASK + + +def page_floor(addr: int) -> int: + return addr & ~(PAGE_SIZE - 1) + + +def page_ceil(addr: int) -> int: + return (addr + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1) \ No newline at end of file diff --git a/wine/cleonos_wine_lib/input_pump.py b/wine/cleonos_wine_lib/input_pump.py new file mode 100644 index 0000000..0581a18 --- /dev/null +++ b/wine/cleonos_wine_lib/input_pump.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import os +import sys +import threading +import time +from typing import Optional + +from .state import SharedKernelState + + +class InputPump: + def __init__(self, state: SharedKernelState) -> None: + self.state = state + self._stop = threading.Event() + self._thread: Optional[threading.Thread] = None + self._posix_term_state = None + + def start(self) -> None: + if self._thread is not None: + return + if not sys.stdin or not hasattr(sys.stdin, "isatty") or not sys.stdin.isatty(): + return + self._thread = threading.Thread(target=self._run, name="cleonos-wine-input", daemon=True) + self._thread.start() + + def stop(self) -> None: + self._stop.set() + if self._thread is not None: + self._thread.join(timeout=0.2) + self._thread = None + self._restore_posix_tty() + + def _run(self) -> None: + if os.name == "nt": + self._run_windows() + else: + self._run_posix() + + def _run_windows(self) -> None: + import msvcrt # pylint: disable=import-error + + while not self._stop.is_set(): + if not msvcrt.kbhit(): + time.sleep(0.005) + continue + + ch = msvcrt.getwch() + if ch in ("\x00", "\xe0"): + _ = msvcrt.getwch() + continue + + norm = self._normalize_char(ch) + if norm is None: + continue + self.state.push_key(ord(norm)) + + def _run_posix(self) -> None: + import select + import termios + import tty + + fd = sys.stdin.fileno() + self._posix_term_state = termios.tcgetattr(fd) + tty.setcbreak(fd) + + try: + while not self._stop.is_set(): + readable, _, _ = select.select([sys.stdin], [], [], 0.05) + if not readable: + continue + ch = sys.stdin.read(1) + norm = self._normalize_char(ch) + if norm is None: + continue + self.state.push_key(ord(norm)) + finally: + self._restore_posix_tty() + + def _restore_posix_tty(self) -> None: + if self._posix_term_state is None: + return + try: + import termios + + fd = sys.stdin.fileno() + termios.tcsetattr(fd, termios.TCSADRAIN, self._posix_term_state) + except Exception: + pass + finally: + self._posix_term_state = None + + @staticmethod + def _normalize_char(ch: str) -> Optional[str]: + if not ch: + return None + if ch == "\r": + return "\n" + return ch \ No newline at end of file diff --git a/wine/cleonos_wine_lib/platform.py b/wine/cleonos_wine_lib/platform.py new file mode 100644 index 0000000..b93a427 --- /dev/null +++ b/wine/cleonos_wine_lib/platform.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import sys + +try: + from unicorn import Uc, UcError + from unicorn import UC_ARCH_X86, UC_MODE_64 + from unicorn import UC_HOOK_CODE, UC_HOOK_INTR + from unicorn import UC_PROT_ALL, UC_PROT_EXEC, UC_PROT_READ, UC_PROT_WRITE + from unicorn.x86_const import ( + UC_X86_REG_RAX, + UC_X86_REG_RBX, + UC_X86_REG_RCX, + UC_X86_REG_RDX, + UC_X86_REG_RBP, + UC_X86_REG_RSP, + ) +except Exception as exc: + print("[WINE][ERROR] unicorn import failed. Install dependencies first:", file=sys.stderr) + print(" pip install -r wine/requirements.txt", file=sys.stderr) + raise SystemExit(1) from exc + + +__all__ = [ + "Uc", + "UcError", + "UC_ARCH_X86", + "UC_MODE_64", + "UC_HOOK_CODE", + "UC_HOOK_INTR", + "UC_PROT_ALL", + "UC_PROT_EXEC", + "UC_PROT_READ", + "UC_PROT_WRITE", + "UC_X86_REG_RAX", + "UC_X86_REG_RBX", + "UC_X86_REG_RCX", + "UC_X86_REG_RDX", + "UC_X86_REG_RBP", + "UC_X86_REG_RSP", +] \ No newline at end of file diff --git a/wine/cleonos_wine_lib/runner.py b/wine/cleonos_wine_lib/runner.py new file mode 100644 index 0000000..72d8f06 --- /dev/null +++ b/wine/cleonos_wine_lib/runner.py @@ -0,0 +1,785 @@ +from __future__ import annotations + +import os +import struct +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import List, Optional, Tuple + +from .constants import ( + DEFAULT_MAX_EXEC_DEPTH, + FS_NAME_MAX, + MAX_CSTR, + MAX_IO_READ, + PAGE_SIZE, + SYS_CONTEXT_SWITCHES, + SYS_CUR_TASK, + SYS_EXEC_PATH, + SYS_EXEC_REQUESTS, + SYS_EXEC_SUCCESS, + SYS_FS_APPEND, + SYS_FS_CHILD_COUNT, + SYS_FS_GET_CHILD_NAME, + SYS_FS_MKDIR, + SYS_FS_NODE_COUNT, + SYS_FS_READ, + SYS_FS_REMOVE, + SYS_FS_STAT_SIZE, + SYS_FS_STAT_TYPE, + SYS_FS_WRITE, + SYS_KBD_BUFFERED, + SYS_KBD_DROPPED, + SYS_KBD_GET_CHAR, + SYS_KBD_HOTKEY_SWITCHES, + SYS_KBD_POPPED, + SYS_KBD_PUSHED, + SYS_KELF_COUNT, + SYS_KELF_RUNS, + SYS_LOG_JOURNAL_COUNT, + SYS_LOG_JOURNAL_READ, + SYS_LOG_WRITE, + SYS_SERVICE_COUNT, + SYS_SERVICE_READY_COUNT, + SYS_TASK_COUNT, + SYS_TIMER_TICKS, + SYS_TTY_ACTIVE, + SYS_TTY_COUNT, + SYS_TTY_SWITCH, + SYS_TTY_WRITE, + SYS_TTY_WRITE_CHAR, + SYS_USER_EXEC_REQUESTED, + SYS_USER_LAUNCH_FAIL, + SYS_USER_LAUNCH_OK, + SYS_USER_LAUNCH_TRIES, + SYS_USER_SHELL_READY, + page_ceil, + page_floor, + u64, + u64_neg1, +) +from .input_pump import InputPump +from .platform import ( + Uc, + UcError, + UC_ARCH_X86, + UC_HOOK_CODE, + UC_HOOK_INTR, + UC_MODE_64, + UC_PROT_ALL, + UC_PROT_EXEC, + UC_PROT_READ, + UC_PROT_WRITE, + UC_X86_REG_RAX, + UC_X86_REG_RBP, + UC_X86_REG_RBX, + UC_X86_REG_RCX, + UC_X86_REG_RDX, + UC_X86_REG_RSP, +) +from .state import SharedKernelState + + +@dataclass +class ELFSegment: + vaddr: int + memsz: int + flags: int + data: bytes + + +@dataclass +class ELFImage: + entry: int + segments: List[ELFSegment] + + +class CLeonOSWineNative: + def __init__( + self, + elf_path: Path, + rootfs: Path, + guest_path_hint: str, + *, + state: Optional[SharedKernelState] = None, + depth: int = 0, + max_exec_depth: int = DEFAULT_MAX_EXEC_DEPTH, + no_kbd: bool = False, + verbose: bool = False, + top_level: bool = True, + ) -> None: + self.elf_path = elf_path + self.rootfs = rootfs + self.guest_path_hint = guest_path_hint + self.state = state if state is not None else SharedKernelState() + self.depth = depth + self.max_exec_depth = max_exec_depth + self.no_kbd = no_kbd + self.verbose = verbose + self.top_level = top_level + + self.image = self._parse_elf(self.elf_path) + self.exit_code: Optional[int] = None + self._input_pump: Optional[InputPump] = None + + self._stack_base = 0x00007FFF00000000 + self._stack_size = 0x0000000000020000 + self._ret_sentinel = 0x00007FFF10000000 + self._mapped_ranges: List[Tuple[int, int]] = [] + + def run(self) -> Optional[int]: + uc = Uc(UC_ARCH_X86, UC_MODE_64) + self._install_hooks(uc) + self._load_segments(uc) + self._prepare_stack_and_return(uc) + + if self.top_level and not self.no_kbd: + self._input_pump = InputPump(self.state) + self._input_pump.start() + + try: + uc.emu_start(self.image.entry, 0) + except KeyboardInterrupt: + if self.top_level: + print("\n[WINE] interrupted by user", file=sys.stderr) + return None + except UcError as exc: + if self.verbose or self.top_level: + print(f"[WINE][ERROR] runtime crashed: {exc}", file=sys.stderr) + return None + finally: + if self.top_level and self._input_pump is not None: + self._input_pump.stop() + + if self.exit_code is None: + self.exit_code = self._reg_read(uc, UC_X86_REG_RAX) + + return u64(self.exit_code) + + def _install_hooks(self, uc: Uc) -> None: + uc.hook_add(UC_HOOK_INTR, self._hook_intr) + uc.hook_add(UC_HOOK_CODE, self._hook_code, begin=self._ret_sentinel, end=self._ret_sentinel) + + def _hook_code(self, uc: Uc, address: int, size: int, _user_data) -> None: + _ = size + if address == self._ret_sentinel: + self.exit_code = self._reg_read(uc, UC_X86_REG_RAX) + uc.emu_stop() + + def _hook_intr(self, uc: Uc, intno: int, _user_data) -> None: + if intno != 0x80: + raise UcError(1) + + syscall_id = self._reg_read(uc, UC_X86_REG_RAX) + arg0 = self._reg_read(uc, UC_X86_REG_RBX) + arg1 = self._reg_read(uc, UC_X86_REG_RCX) + arg2 = self._reg_read(uc, UC_X86_REG_RDX) + + self.state.context_switches = u64(self.state.context_switches + 1) + ret = self._dispatch_syscall(uc, syscall_id, arg0, arg1, arg2) + self._reg_write(uc, UC_X86_REG_RAX, u64(ret)) + + def _dispatch_syscall(self, uc: Uc, sid: int, arg0: int, arg1: int, arg2: int) -> int: + if sid == SYS_LOG_WRITE: + data = self._read_guest_bytes(uc, arg0, arg1) + text = data.decode("utf-8", errors="replace") + self._host_write(text) + self.state.log_journal_push(text) + return len(data) + if sid == SYS_TIMER_TICKS: + return self.state.timer_ticks() + if sid == SYS_TASK_COUNT: + return self.state.task_count + if sid == SYS_CUR_TASK: + return self.state.current_task + if sid == SYS_SERVICE_COUNT: + return self.state.service_count + if sid == SYS_SERVICE_READY_COUNT: + return self.state.service_ready + if sid == SYS_CONTEXT_SWITCHES: + return self.state.context_switches + if sid == SYS_KELF_COUNT: + return self.state.kelf_count + if sid == SYS_KELF_RUNS: + return self.state.kelf_runs + if sid == SYS_FS_NODE_COUNT: + return self._fs_node_count() + if sid == SYS_FS_CHILD_COUNT: + return self._fs_child_count(uc, arg0) + if sid == SYS_FS_GET_CHILD_NAME: + return self._fs_get_child_name(uc, arg0, arg1, arg2) + if sid == SYS_FS_READ: + return self._fs_read(uc, arg0, arg1, arg2) + if sid == SYS_EXEC_PATH: + return self._exec_path(uc, arg0) + if sid == SYS_EXEC_REQUESTS: + return self.state.exec_requests + if sid == SYS_EXEC_SUCCESS: + return self.state.exec_success + if sid == SYS_USER_SHELL_READY: + return self.state.user_shell_ready + if sid == SYS_USER_EXEC_REQUESTED: + return self.state.user_exec_requested + if sid == SYS_USER_LAUNCH_TRIES: + return self.state.user_launch_tries + if sid == SYS_USER_LAUNCH_OK: + return self.state.user_launch_ok + if sid == SYS_USER_LAUNCH_FAIL: + return self.state.user_launch_fail + if sid == SYS_TTY_COUNT: + return self.state.tty_count + if sid == SYS_TTY_ACTIVE: + return self.state.tty_active + if sid == SYS_TTY_SWITCH: + if arg0 >= self.state.tty_count: + return u64_neg1() + self.state.tty_active = int(arg0) + return self.state.tty_active + if sid == SYS_TTY_WRITE: + data = self._read_guest_bytes(uc, arg0, arg1) + self._host_write(data.decode("utf-8", errors="replace")) + return len(data) + if sid == SYS_TTY_WRITE_CHAR: + ch = chr(arg0 & 0xFF) + if ch in ("\b", "\x7f"): + self._host_write("\b \b") + else: + self._host_write(ch) + return 1 + if sid == SYS_KBD_GET_CHAR: + key = self.state.pop_key() + return u64_neg1() if key is None else key + if sid == SYS_FS_STAT_TYPE: + return self._fs_stat_type(uc, arg0) + if sid == SYS_FS_STAT_SIZE: + return self._fs_stat_size(uc, arg0) + if sid == SYS_FS_MKDIR: + return self._fs_mkdir(uc, arg0) + if sid == SYS_FS_WRITE: + return self._fs_write(uc, arg0, arg1, arg2) + if sid == SYS_FS_APPEND: + return self._fs_append(uc, arg0, arg1, arg2) + if sid == SYS_FS_REMOVE: + return self._fs_remove(uc, arg0) + if sid == SYS_LOG_JOURNAL_COUNT: + return self.state.log_journal_count() + if sid == SYS_LOG_JOURNAL_READ: + return self._log_journal_read(uc, arg0, arg1, arg2) + if sid == SYS_KBD_BUFFERED: + return self.state.buffered_count() + if sid == SYS_KBD_PUSHED: + return self.state.kbd_push_count + if sid == SYS_KBD_POPPED: + return self.state.kbd_pop_count + if sid == SYS_KBD_DROPPED: + return self.state.kbd_drop_count + if sid == SYS_KBD_HOTKEY_SWITCHES: + return self.state.kbd_hotkey_switches + + return u64_neg1() + + def _host_write(self, text: str) -> None: + if not text: + return + sys.stdout.write(text) + sys.stdout.flush() + + def _load_segments(self, uc: Uc) -> None: + for seg in self.image.segments: + start = page_floor(seg.vaddr) + end = page_ceil(seg.vaddr + seg.memsz) + self._map_region(uc, start, end - start, UC_PROT_ALL) + + for seg in self.image.segments: + if seg.data: + self._mem_write(uc, seg.vaddr, seg.data) + + for seg in self.image.segments: + start = page_floor(seg.vaddr) + end = page_ceil(seg.vaddr + seg.memsz) + size = end - start + perms = 0 + if seg.flags & 0x4: + perms |= UC_PROT_READ + if seg.flags & 0x2: + perms |= UC_PROT_WRITE + if seg.flags & 0x1: + perms |= UC_PROT_EXEC + if perms == 0: + perms = UC_PROT_READ + try: + uc.mem_protect(start, size, perms) + except Exception: + pass + + def _prepare_stack_and_return(self, uc: Uc) -> None: + self._map_region(uc, self._stack_base, self._stack_size, UC_PROT_READ | UC_PROT_WRITE) + self._map_region(uc, self._ret_sentinel, PAGE_SIZE, UC_PROT_READ | UC_PROT_EXEC) + self._mem_write(uc, self._ret_sentinel, b"\x90") + + rsp = self._stack_base + self._stack_size - 8 + self._mem_write(uc, rsp, struct.pack(" None: + if size <= 0: + return + start = page_floor(addr) + end = page_ceil(addr + size) + + if self._is_range_mapped(start, end): + return + + uc.mem_map(start, end - start, perms) + self._mapped_ranges.append((start, end)) + + def _is_range_mapped(self, start: int, end: int) -> bool: + for ms, me in self._mapped_ranges: + if start >= ms and end <= me: + return True + return False + + @staticmethod + def _reg_read(uc: Uc, reg: int) -> int: + return int(uc.reg_read(reg)) + + @staticmethod + def _reg_write(uc: Uc, reg: int, value: int) -> None: + uc.reg_write(reg, u64(value)) + + @staticmethod + def _mem_write(uc: Uc, addr: int, data: bytes) -> None: + if addr == 0 or not data: + return + uc.mem_write(addr, data) + + def _read_guest_cstring(self, uc: Uc, addr: int, max_len: int = MAX_CSTR) -> str: + if addr == 0: + return "" + + out = bytearray() + for i in range(max_len): + try: + ch = uc.mem_read(addr + i, 1) + except UcError: + break + if not ch or ch[0] == 0: + break + out.append(ch[0]) + return out.decode("utf-8", errors="replace") + + def _read_guest_bytes(self, uc: Uc, addr: int, size: int) -> bytes: + if addr == 0 or size == 0: + return b"" + safe_size = int(min(size, MAX_IO_READ)) + try: + return bytes(uc.mem_read(addr, safe_size)) + except UcError: + return b"" + + def _write_guest_bytes(self, uc: Uc, addr: int, data: bytes) -> bool: + if addr == 0: + return False + try: + uc.mem_write(addr, data) + return True + except UcError: + return False + + @staticmethod + def _parse_elf(path: Path) -> ELFImage: + data = path.read_bytes() + if len(data) < 64: + raise RuntimeError(f"ELF too small: {path}") + if data[0:4] != b"\x7fELF": + raise RuntimeError(f"invalid ELF magic: {path}") + if data[4] != 2 or data[5] != 1: + raise RuntimeError(f"unsupported ELF class/endianness: {path}") + + entry = struct.unpack_from(" len(data): + break + + p_type, p_flags, p_offset, p_vaddr, _p_paddr, p_filesz, p_memsz, _p_align = struct.unpack_from( + " 0: + if fo >= len(data): + seg_data = b"" + else: + seg_data = data[fo : min(len(data), fo + fs)] + else: + seg_data = b"" + + segments.append(ELFSegment(vaddr=int(p_vaddr), memsz=int(p_memsz), flags=int(p_flags), data=seg_data)) + + if not segments: + raise RuntimeError(f"ELF has no PT_LOAD segments: {path}") + + return ELFImage(entry=int(entry), segments=segments) + + def _fs_node_count(self) -> int: + count = 1 + for _root, dirs, files in os.walk(self.rootfs): + dirs[:] = [d for d in dirs if not d.startswith(".")] + files = [f for f in files if not f.startswith(".")] + count += len(dirs) + len(files) + return count + + def _fs_child_count(self, uc: Uc, dir_ptr: int) -> int: + path = self._read_guest_cstring(uc, dir_ptr) + host_dir = self._guest_to_host(path, must_exist=True) + if host_dir is None or not host_dir.is_dir(): + return u64_neg1() + return len(self._list_children(host_dir)) + + def _fs_get_child_name(self, uc: Uc, dir_ptr: int, index: int, out_ptr: int) -> int: + if out_ptr == 0: + return 0 + path = self._read_guest_cstring(uc, dir_ptr) + host_dir = self._guest_to_host(path, must_exist=True) + if host_dir is None or not host_dir.is_dir(): + return 0 + + children = self._list_children(host_dir) + if index >= len(children): + return 0 + + name = children[int(index)] + encoded = name.encode("utf-8", errors="replace") + if len(encoded) >= FS_NAME_MAX: + encoded = encoded[: FS_NAME_MAX - 1] + + return 1 if self._write_guest_bytes(uc, out_ptr, encoded + b"\x00") else 0 + + def _fs_read(self, uc: Uc, path_ptr: int, out_ptr: int, buf_size: int) -> int: + if out_ptr == 0 or buf_size == 0: + return 0 + + path = self._read_guest_cstring(uc, path_ptr) + host_path = self._guest_to_host(path, must_exist=True) + if host_path is None or not host_path.is_file(): + return 0 + + read_size = int(min(buf_size, MAX_IO_READ)) + try: + data = host_path.read_bytes()[:read_size] + except Exception: + return 0 + + if not data: + return 0 + return len(data) if self._write_guest_bytes(uc, out_ptr, data) else 0 + + def _fs_stat_type(self, uc: Uc, path_ptr: int) -> int: + path = self._read_guest_cstring(uc, path_ptr) + host_path = self._guest_to_host(path, must_exist=True) + if host_path is None: + return u64_neg1() + if host_path.is_dir(): + return 2 + if host_path.is_file(): + return 1 + return u64_neg1() + + def _fs_stat_size(self, uc: Uc, path_ptr: int) -> int: + path = self._read_guest_cstring(uc, path_ptr) + host_path = self._guest_to_host(path, must_exist=True) + if host_path is None: + return u64_neg1() + if host_path.is_dir(): + return 0 + if host_path.is_file(): + try: + return host_path.stat().st_size + except Exception: + return u64_neg1() + return u64_neg1() + + @staticmethod + def _guest_path_is_under_temp(path: str) -> bool: + return path == "/temp" or path.startswith("/temp/") + + def _fs_mkdir(self, uc: Uc, path_ptr: int) -> int: + path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr)) + if not self._guest_path_is_under_temp(path): + return 0 + + host_path = self._guest_to_host(path, must_exist=False) + if host_path is None: + return 0 + + if host_path.exists() and host_path.is_file(): + return 0 + + try: + host_path.mkdir(parents=True, exist_ok=True) + return 1 + except Exception: + return 0 + + def _fs_write_common(self, uc: Uc, path_ptr: int, data_ptr: int, size: int, append_mode: bool) -> int: + path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr)) + + if not self._guest_path_is_under_temp(path) or path == "/temp": + return 0 + + if size < 0 or size > self.state.fs_write_max: + return 0 + + host_path = self._guest_to_host(path, must_exist=False) + if host_path is None: + return 0 + + if host_path.exists() and host_path.is_dir(): + return 0 + + data = b"" + if size > 0: + if data_ptr == 0: + return 0 + data = self._read_guest_bytes(uc, data_ptr, size) + if len(data) != int(size): + return 0 + + try: + host_path.parent.mkdir(parents=True, exist_ok=True) + mode = "ab" if append_mode else "wb" + with host_path.open(mode) as fh: + if data: + fh.write(data) + return 1 + except Exception: + return 0 + + def _fs_write(self, uc: Uc, path_ptr: int, data_ptr: int, size: int) -> int: + return self._fs_write_common(uc, path_ptr, data_ptr, size, append_mode=False) + + def _fs_append(self, uc: Uc, path_ptr: int, data_ptr: int, size: int) -> int: + return self._fs_write_common(uc, path_ptr, data_ptr, size, append_mode=True) + + def _fs_remove(self, uc: Uc, path_ptr: int) -> int: + path = self._normalize_guest_path(self._read_guest_cstring(uc, path_ptr)) + + if not self._guest_path_is_under_temp(path) or path == "/temp": + return 0 + + host_path = self._guest_to_host(path, must_exist=True) + if host_path is None: + return 0 + + try: + if host_path.is_dir(): + if any(host_path.iterdir()): + return 0 + host_path.rmdir() + else: + host_path.unlink() + return 1 + except Exception: + return 0 + + def _log_journal_read(self, uc: Uc, index_from_oldest: int, out_ptr: int, out_size: int) -> int: + if out_ptr == 0 or out_size == 0: + return 0 + + line = self.state.log_journal_read(int(index_from_oldest)) + if line is None: + return 0 + + encoded = line.encode("utf-8", errors="replace") + max_payload = int(out_size) - 1 + if max_payload < 0: + return 0 + + if len(encoded) > max_payload: + encoded = encoded[:max_payload] + + return 1 if self._write_guest_bytes(uc, out_ptr, encoded + b"\x00") else 0 + + def _exec_path(self, uc: Uc, path_ptr: int) -> int: + path = self._read_guest_cstring(uc, path_ptr) + guest_path = self._normalize_guest_path(path) + host_path = self._guest_to_host(guest_path, must_exist=True) + + self.state.exec_requests = u64(self.state.exec_requests + 1) + self.state.user_exec_requested = 1 + self.state.user_launch_tries = u64(self.state.user_launch_tries + 1) + + if host_path is None or not host_path.is_file(): + self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) + return u64_neg1() + + if self.depth >= self.max_exec_depth: + print(f"[WINE][WARN] exec depth exceeded: {guest_path}", file=sys.stderr) + self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) + return u64_neg1() + + child = CLeonOSWineNative( + elf_path=host_path, + rootfs=self.rootfs, + guest_path_hint=guest_path, + state=self.state, + depth=self.depth + 1, + max_exec_depth=self.max_exec_depth, + no_kbd=True, + verbose=self.verbose, + top_level=False, + ) + child_ret = child.run() + if child_ret is None: + self.state.user_launch_fail = u64(self.state.user_launch_fail + 1) + return u64_neg1() + + self.state.exec_success = u64(self.state.exec_success + 1) + self.state.user_launch_ok = u64(self.state.user_launch_ok + 1) + if guest_path.lower().startswith("/system/"): + self.state.kelf_runs = u64(self.state.kelf_runs + 1) + return 0 + + def _guest_to_host(self, guest_path: str, *, must_exist: bool) -> Optional[Path]: + norm = self._normalize_guest_path(guest_path) + if norm == "/": + return self.rootfs if (not must_exist or self.rootfs.exists()) else None + + current = self.rootfs + for part in [p for p in norm.split("/") if p]: + candidate = current / part + if candidate.exists(): + current = candidate + continue + + if current.exists() and current.is_dir(): + match = self._find_case_insensitive(current, part) + if match is not None: + current = match + continue + + current = candidate + + if must_exist and not current.exists(): + return None + return current + + @staticmethod + def _find_case_insensitive(parent: Path, name: str) -> Optional[Path]: + target = name.lower() + try: + for entry in parent.iterdir(): + if entry.name.lower() == target: + return entry + except Exception: + return None + return None + + @staticmethod + def _normalize_guest_path(path: str) -> str: + p = (path or "").replace("\\", "/").strip() + if not p: + return "/" + if not p.startswith("/"): + p = "/" + p + + parts = [] + for token in p.split("/"): + if token in ("", "."): + continue + if token == "..": + if parts: + parts.pop() + continue + parts.append(token) + + return "/" + "/".join(parts) + + @staticmethod + def _list_children(dir_path: Path) -> List[str]: + try: + names = [entry.name for entry in dir_path.iterdir() if not entry.name.startswith(".")] + except Exception: + return [] + names.sort(key=lambda x: x.lower()) + return names + + +def resolve_rootfs(path_arg: Optional[str]) -> Path: + if path_arg: + root = Path(path_arg).expanduser().resolve() + if not root.exists() or not root.is_dir(): + raise FileNotFoundError(f"rootfs not found: {root}") + return root + + candidates = [ + Path("build/x86_64/ramdisk_root"), + Path("ramdisk"), + ] + for candidate in candidates: + if candidate.exists() and candidate.is_dir(): + return candidate.resolve() + + raise FileNotFoundError("rootfs not found; pass --rootfs") + + +def _guest_to_host_for_resolve(rootfs: Path, guest_path: str) -> Optional[Path]: + norm = CLeonOSWineNative._normalize_guest_path(guest_path) + if norm == "/": + return rootfs + + current = rootfs + for part in [p for p in norm.split("/") if p]: + candidate = current / part + if candidate.exists(): + current = candidate + continue + + if current.exists() and current.is_dir(): + match = None + for entry in current.iterdir(): + if entry.name.lower() == part.lower(): + match = entry + break + if match is not None: + current = match + continue + + current = candidate + + return current if current.exists() else None + + +def resolve_elf_target(elf_arg: str, rootfs: Path) -> Tuple[Path, str]: + host_candidate = Path(elf_arg).expanduser() + if host_candidate.exists(): + host_path = host_candidate.resolve() + try: + rel = host_path.relative_to(rootfs) + guest_path = "/" + rel.as_posix() + except ValueError: + guest_path = "/" + host_path.name + return host_path, guest_path + + guest_path = CLeonOSWineNative._normalize_guest_path(elf_arg) + host_path = _guest_to_host_for_resolve(rootfs, guest_path) + if host_path is None: + raise FileNotFoundError(f"ELF not found as host path or guest path: {elf_arg}") + return host_path.resolve(), guest_path \ No newline at end of file diff --git a/wine/cleonos_wine_lib/state.py b/wine/cleonos_wine_lib/state.py new file mode 100644 index 0000000..c3bcbee --- /dev/null +++ b/wine/cleonos_wine_lib/state.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import collections +import threading +import time +from dataclasses import dataclass, field +from typing import Deque, Optional + +from .constants import u64 + + +@dataclass +class SharedKernelState: + start_ns: int = field(default_factory=time.monotonic_ns) + task_count: int = 5 + current_task: int = 0 + service_count: int = 7 + service_ready: int = 7 + context_switches: int = 0 + kelf_count: int = 2 + kelf_runs: int = 0 + exec_requests: int = 0 + exec_success: int = 0 + user_shell_ready: int = 1 + user_exec_requested: int = 0 + user_launch_tries: int = 0 + user_launch_ok: int = 0 + user_launch_fail: int = 0 + tty_count: int = 4 + tty_active: int = 0 + kbd_queue: Deque[int] = field(default_factory=collections.deque) + kbd_lock: threading.Lock = field(default_factory=threading.Lock) + kbd_queue_cap: int = 256 + kbd_drop_count: int = 0 + kbd_push_count: int = 0 + kbd_pop_count: int = 0 + kbd_hotkey_switches: int = 0 + log_journal_cap: int = 256 + log_journal: Deque[str] = field(default_factory=lambda: collections.deque(maxlen=256)) + fs_write_max: int = 65536 + + def timer_ticks(self) -> int: + return (time.monotonic_ns() - self.start_ns) // 1_000_000 + + def push_key(self, key: int) -> None: + with self.kbd_lock: + if len(self.kbd_queue) >= self.kbd_queue_cap: + self.kbd_queue.popleft() + self.kbd_drop_count = u64(self.kbd_drop_count + 1) + self.kbd_queue.append(key & 0xFF) + self.kbd_push_count = u64(self.kbd_push_count + 1) + + def pop_key(self) -> Optional[int]: + with self.kbd_lock: + if not self.kbd_queue: + return None + self.kbd_pop_count = u64(self.kbd_pop_count + 1) + return self.kbd_queue.popleft() + + def buffered_count(self) -> int: + with self.kbd_lock: + return len(self.kbd_queue) + + def log_journal_push(self, text: str) -> None: + if text is None: + return + + normalized = text.replace("\r", "") + lines = normalized.split("\n") + + for line in lines: + if len(line) > 255: + line = line[:255] + self.log_journal.append(line) + + def log_journal_count(self) -> int: + return len(self.log_journal) + + def log_journal_read(self, index_from_oldest: int) -> Optional[str]: + if index_from_oldest < 0 or index_from_oldest >= len(self.log_journal): + return None + return list(self.log_journal)[index_from_oldest] \ No newline at end of file