Ctrl K
ComunidadGuías…

setup_windows.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Tlamatini Author Banner — Angela López Mendoza · @angelahack1
"""Small, explicitly scoped Windows setup primitives; no application imports."""
import base64
import ctypes
from ctypes import wintypes
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys


def file_lock_description(path):
    """Ask Restart Manager who holds this file; never close another program."""
    if sys.platform != "win32":
        return ""
    class UniqueProcess(ctypes.Structure):
        _fields_ = [("pid", wintypes.DWORD), ("started", wintypes.FILETIME)]
    class ProcessInfo(ctypes.Structure):
        _fields_ = [("process", UniqueProcess), ("name", wintypes.WCHAR * 256),
                    ("service", wintypes.WCHAR * 64), ("kind", wintypes.DWORD),
                    ("status", wintypes.ULONG), ("session", wintypes.DWORD),
                    ("restartable", wintypes.BOOL)]
    session = wintypes.DWORD()
    manager = ctypes.WinDLL("Rstrtmgr")
    if manager.RmStartSession(ctypes.byref(session), 0, ctypes.create_unicode_buffer(33)):
        return ""
    try:
        paths = (wintypes.LPCWSTR * 1)(os.fspath(path))
        if manager.RmRegisterResources(session, 1, paths, 0, None, 0, None):
            return ""
        needed, count, reason = wintypes.UINT(), wintypes.UINT(), wintypes.DWORD()
        for _ in range(3):
            rows = (ProcessInfo * count.value)() if count.value else None
            result = manager.RmGetList(session, ctypes.byref(needed), ctypes.byref(count), rows, ctypes.byref(reason))
            if result == 0:
                return ", ".join(f"{row.name or 'Process'} (PID {row.process.pid})" for row in (rows or ())[:count.value])
            if result != 234 or needed.value > 1024:
                break
            count.value = needed.value
        return ""
    except OSError:
        return ""
    finally:
        manager.RmEndSession(session)


class WindowsSnapshot:
    """Restore product keys and selected shared values, never UserChoice."""
    TREES = (
        r"Software\Classes\Tlamatini.FlowFile",
        r"Software\Classes\Tlamatini.PromptFlowFile",
        r"Software\Classes\SystemAgent.FlowFile",
        r"Software\Tlamatini\FlowCapabilities",
        r"Software\Tlamatini\FileTypes",
        r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Tlamatini",
    )
    VALUES = [(r"Software\RegisteredApplications", "TlamatiniFlows")] + [
        (r"Software\XAIHT\Tlamatini", name) for name in (
            "InstallLocation", "AgentsRoot", "SourceAgentsRoot", "AgentManifestPath", "Version", "AgentCatalogVersion")
    ] + [(rf"Software\Classes\{ext}", "") for ext in (".flw", ".fpmt")] + [
        (prefix+rf"\{ext}\OpenWithProgids", prog)
        for prefix in (r"Software\Classes", r"Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts")
        for ext, prog in ((".flw", "Tlamatini.FlowFile"), (".flw", "SystemAgent.FlowFile"), (".fpmt", "Tlamatini.PromptFlowFile"))
    ]

    def __init__(self, folder):
        self.folder = Path(folder)

    @staticmethod
    def desktop_shortcut():
        # CSIDL_DESKTOPDIRECTORY respects redirected / OneDrive desktops.
        buffer = ctypes.create_unicode_buffer(32768)
        result = ctypes.windll.shell32.SHGetFolderPathW(None, 0x10, None, 0, buffer)
        if result != 0 or not buffer.value:
            raise RuntimeError("Windows could not resolve the Desktop folder")
        return Path(buffer.value) / "Tlamatini.lnk"

    @staticmethod
    def _encode(value, kind):
        return {"type": kind, "binary": isinstance(value, bytes),
                "value": base64.b64encode(value).decode("ascii") if isinstance(value, bytes) else value}

    @staticmethod
    def _read_value(path, name):
        import winreg
        try:
            with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
                return WindowsSnapshot._encode(*winreg.QueryValueEx(key, name))
        except FileNotFoundError:
            return None

    @staticmethod
    def _tree(path):
        import winreg
        result = {}
        try:
            with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
                subkeys, values, _ = winreg.QueryInfoKey(key)
                result[""] = {name: WindowsSnapshot._encode(value, kind)
                              for name, value, kind in (winreg.EnumValue(key, i) for i in range(values))}
                children = [winreg.EnumKey(key, i) for i in range(subkeys)]
            for child in children:
                for suffix, values in WindowsSnapshot._tree(path+"\\"+child).items():
                    result[child+("\\"+suffix if suffix else "")] = values
        except FileNotFoundError:
            pass
        return result

    def capture(self):
        from setup_recovery import atomic_json
        shortcut = self.desktop_shortcut()
        data = {"trees": {path: self._tree(path) for path in self.TREES},
                "values": [[path, name, self._read_value(path, name)] for path, name in self.VALUES],
                "shortcut": shortcut.is_file()}
        if data["shortcut"]:
            shutil.copy2(shortcut, self.folder / "desktop.lnk")
        atomic_json(self.folder / "windows.json", data)

    @staticmethod
    def _set(path, name, record):
        import winreg
        if record is None:
            try:
                with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path, 0, winreg.KEY_SET_VALUE) as key:
                    winreg.DeleteValue(key, name)
            except FileNotFoundError:
                pass
            WindowsSnapshot._remove_empty(path)
        else:
            value = base64.b64decode(record["value"]) if record["binary"] else record["value"]
            with winreg.CreateKey(winreg.HKEY_CURRENT_USER, path) as key:
                winreg.SetValueEx(key, name, 0, record["type"], value)

    @staticmethod
    def _remove_empty(path):
        import winreg
        try:
            with winreg.OpenKey(winreg.HKEY_CURRENT_USER, path) as key:
                children, values, _ = winreg.QueryInfoKey(key)
            if not children and not values:
                winreg.DeleteKey(winreg.HKEY_CURRENT_USER, path)
        except FileNotFoundError:
            pass

    def restore(self):
        data = json.loads((self.folder / "windows.json").read_text(encoding="utf-8"))
        if set(data["trees"]) != set(self.TREES):
            raise RuntimeError("Recovery registry scope differs from the product allowlist")
        if [(p, n) for p, n, _ in data["values"]] != self.VALUES:
            raise RuntimeError("Recovery registry values differ from the product allowlist")
        for path, before in data["trees"].items():
            current = self._tree(path)
            for suffix in set(before) | set(current):
                if any(part in (".", "..", "") for part in suffix.split("\\")) and suffix:
                    raise RuntimeError("Unsafe registry recovery suffix")
                full = path + ("\\"+suffix if suffix else "")
                wanted = before.get(suffix, {})
                for name in set(wanted) | set(current.get(suffix, {})):
                    self._set(full, name, wanted.get(name))
            for suffix in sorted(set(current)-set(before), key=len, reverse=True):
                self._remove_empty(path+("\\"+suffix if suffix else ""))
        for path, name, record in data["values"]:
            self._set(path, name, record)
        shortcut = self.desktop_shortcut()
        if data["shortcut"]:
            shutil.copy2(self.folder / "desktop.lnk", shortcut)
        elif shortcut.is_file():
            shortcut.unlink()
        ctypes.windll.shell32.SHChangeNotify(0x08000000, 0, None, None)


class HelperJob:
    """Kill only this helper's process tree when its bounded lifetime ends."""
    def __init__(self):
        class IO(ctypes.Structure):
            _fields_ = [(name, ctypes.c_ulonglong) for name in (
                "ReadOperationCount", "WriteOperationCount", "OtherOperationCount",
                "ReadTransferCount", "WriteTransferCount", "OtherTransferCount")]
        class Basic(ctypes.Structure):
            _fields_ = [("PerProcessUserTimeLimit", ctypes.c_longlong), ("PerJobUserTimeLimit", ctypes.c_longlong),
                        ("LimitFlags", wintypes.DWORD), ("MinimumWorkingSetSize", ctypes.c_size_t),
                        ("MaximumWorkingSetSize", ctypes.c_size_t), ("ActiveProcessLimit", wintypes.DWORD),
                        ("Affinity", ctypes.c_size_t), ("PriorityClass", wintypes.DWORD), ("SchedulingClass", wintypes.DWORD)]
        class Extended(ctypes.Structure):
            _fields_ = [("BasicLimitInformation", Basic), ("IoInfo", IO),
                        ("ProcessMemoryLimit", ctypes.c_size_t), ("JobMemoryLimit", ctypes.c_size_t),
                        ("PeakProcessMemoryUsed", ctypes.c_size_t), ("PeakJobMemoryUsed", ctypes.c_size_t)]
        self.kernel = ctypes.WinDLL("kernel32", use_last_error=True)
        self.kernel.CreateJobObjectW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p]
        self.kernel.CreateJobObjectW.restype = ctypes.c_void_p
        self.kernel.SetInformationJobObject.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p, wintypes.DWORD]
        self.kernel.AssignProcessToJobObject.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
        self.kernel.CloseHandle.argtypes = [ctypes.c_void_p]
        self.handle = self.kernel.CreateJobObjectW(None, None)
        if not self.handle:
            raise ctypes.WinError(ctypes.get_last_error())
        limits = Extended()
        limits.BasicLimitInformation.LimitFlags = 0x2000  # KILL_ON_JOB_CLOSE, no breakaway
        if not self.kernel.SetInformationJobObject(self.handle, 9, ctypes.byref(limits), ctypes.sizeof(limits)):
            self.close()
            raise ctypes.WinError(ctypes.get_last_error())

    def assign(self, process):
        if not self.kernel.AssignProcessToJobObject(self.handle, int(process._handle)):
            raise ctypes.WinError(ctypes.get_last_error())

    def close(self):
        if self.handle:
            self.kernel.CloseHandle(self.handle)
            self.handle = None


def run_powershell(script, *, env=None, timeout=120):
    """No interactive prompts, bounded child-process job, no product console flash.

    The visible product wizard owns helper status. Only frozen windowed clients
    suppress an extra helper console; source development calls inherit their
    verified visible console. Neither branch targets unrelated processes.
    """
    script = Path(script).resolve(strict=True)
    executable = Path(os.environ["SystemRoot"]) / "System32/WindowsPowerShell/v1.0/powershell.exe"
    job = HelperJob()
    process = None
    try:
        # The helper cannot start its script (or spawn descendants) before it
        # belongs to the job. Quoting is PowerShell literal-string quoting.
        quoted = str(script).replace("'", "''")
        gate = ("if ([Console]::ReadLine() -ne 'TLAMATINI_SETUP_READY') { exit 125 }; "
                "$ErrorActionPreference = 'Stop'; try { & '" + quoted + "'; "
                "if (-not $?) { exit 1 }; exit $LASTEXITCODE } catch { "
                "[Console]::Error.WriteLine($_.Exception.Message); exit 1 }")
        encoded = base64.b64encode(gate.encode("utf-16le")).decode("ascii")
        process = subprocess.Popen(
            [str(executable), "-NoLogo", "-NoProfile", "-NonInteractive",
             "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded],
            cwd=script.parent, env=env, stdin=subprocess.PIPE,
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, errors="replace",
            close_fds=True,
            creationflags=(subprocess.CREATE_NO_WINDOW
                           if getattr(sys, "frozen", False) and not ctypes.windll.kernel32.GetConsoleWindow()
                           else 0),
        )
        try:
            job.assign(process)
        except Exception:
            process.kill()
            process.wait(timeout=5)
            raise RuntimeError("Windows could not contain the setup helper. No helper was allowed to continue.")
        try:
            output, error = process.communicate(input="TLAMATINI_SETUP_READY\n", timeout=timeout)
        except subprocess.TimeoutExpired as exc:
            job.close()  # Close inherited writers too, before draining their pipes.
            try:
                process.communicate(timeout=5)
            except subprocess.TimeoutExpired:
                process.kill()
                process.wait(timeout=5)
            raise RuntimeError(f"{script.name} exceeded {timeout} seconds. Its helper processes were stopped; retry after checking Windows permissions.") from exc
        if process.returncode:
            raise RuntimeError(f"{script.name} failed (exit {process.returncode}):\n{(error.strip() or output.strip())[-4000:]}")
        if output.strip():
            print(output.strip(), flush=True)
        return output
    finally:
        job.close()
        if process is not None:
            for stream in (process.stdin, process.stdout, process.stderr):
                if stream is not None:
                    stream.close()