File size: 5,911 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
"""openTARS 4.5 Endurance: onde o programa está rodando.

Parte pura (sem rede, sem tela): diz se é WSL, se há interface gráfica, e
traduz caminhos do Windows. Quem usa é o tars.py (modo sem interface, abrir
links pelo Windows, achar o Ollama do Windows a partir do WSL).

Três situações que importam:
  * área de trabalho Linux (X11 ou Wayland): tudo como sempre;
  * WSL com WSLg (o Windows 11 mostra janelas Linux): há DISPLAY, a janela
    abre, mas não existe uma "área de trabalho" Linux pra controlar;
  * sem interface (servidor, SSH, WSL sem WSLg, contêiner): só conversa no
    terminal, comandos, arquivos e a IA; nada de mouse, teclado nem tela.
"""

import os
import re
from pathlib import Path

import tars_plataforma as plataforma


def _ler(caminho):
    try:
        return Path(caminho).read_text(encoding="utf-8", errors="replace")
    except Exception:
        return ""


def versao_wsl(env=None, ler=None):
    """1 ou 2 dentro do WSL; None fora dele."""
    env = os.environ if env is None else env
    ler = ler or _ler
    release = ler("/proc/sys/kernel/osrelease").lower()
    versao = ler("/proc/version").lower()
    dentro = bool(env.get("WSL_DISTRO_NAME") or env.get("WSL_INTEROP")) or "microsoft" in release or "microsoft" in versao
    if not dentro:
        return None
    return 2 if "wsl2" in release or "wsl2" in versao or env.get("WSL_INTEROP") else 1


def e_wsl(env=None, ler=None):
    return versao_wsl(env, ler) is not None


def tem_tela(env=None):
    env = os.environ if env is None else env
    # Windows e macOS sempre têm a área de trabalho do sistema; DISPLAY/WAYLAND_DISPLAY são coisa do Linux
    return plataforma.sistema() != "linux" or bool(env.get("DISPLAY") or env.get("WAYLAND_DISPLAY"))


def forcado_sem_interface(env=None):
    """TARS_SEM_INTERFACE=1: trata a máquina como sem tela mesmo com DISPLAY
    (servidor com X remoto, quem quer só o terminal)."""
    env = os.environ if env is None else env
    return str(env.get("TARS_SEM_INTERFACE", "")).lower() in ("1", "on", "sim", "yes", "true")


def detectar(env=None, ler=None):
    """{'wsl': 1|2|None, 'wslg': bool, 'tela': bool, 'ssh': bool, 'sem_interface': bool,
        'tipo': 'desktop'|'wslg'|'sem_tela', 'rotulo': str (em inglês)}"""
    env = os.environ if env is None else env
    wsl = versao_wsl(env, ler)
    sem = forcado_sem_interface(env) or not tem_tela(env)
    ssh = bool(env.get("SSH_CONNECTION") or env.get("SSH_TTY"))
    wslg = bool(wsl) and not sem and (bool(env.get("WAYLAND_DISPLAY")) or bool(env.get("DISPLAY")))
    tipo = "sem_tela" if sem else ("wslg" if wslg else "desktop")
    if sem:
        rotulo = f"WSL{wsl} (no graphical session)" if wsl else ("SSH (no graphical session)" if ssh else "no graphical session")
    elif wslg:
        rotulo = f"WSL{wsl} with WSLg"
    else:
        rotulo = "desktop"
    return {"wsl": wsl, "wslg": wslg, "tela": not sem, "ssh": ssh, "sem_interface": sem, "tipo": tipo, "rotulo": rotulo}


# ------------------------------------------------------------
# Caminhos do Windows vistos de dentro do WSL
# ------------------------------------------------------------

_UNIDADE = re.compile(r"^([A-Za-z]):[\\/]*(.*)$", re.DOTALL)
_REDE_WSL = re.compile(r"^[\\/]{2}(?:wsl\$|wsl\.localhost)[\\/]+[^\\/]+[\\/]*(.*)$", re.IGNORECASE | re.DOTALL)


def parece_caminho_windows(texto):
    t = (texto or "").strip()
    return bool(_UNIDADE.match(t) or _REDE_WSL.match(t))


def caminho_do_windows(texto):
    """'C:\\Users\\ana\\a b.txt' -> '/mnt/c/Users/ana/a b.txt';
    '\\\\wsl$\\Ubuntu\\home\\ana\\x' -> '/home/ana/x'. Texto que não parece
    caminho do Windows volta como veio."""
    t = (texto or "").strip()
    achado = _REDE_WSL.match(t)
    if achado:
        return "/" + achado.group(1).replace("\\", "/").strip("/")
    achado = _UNIDADE.match(t)
    if achado:
        resto = achado.group(2).replace("\\", "/").strip("/")
        base = f"/mnt/{achado.group(1).lower()}"
        return f"{base}/{resto}" if resto else base
    return t


# ------------------------------------------------------------
# Ollama do Windows, visto de dentro do WSL2
# ------------------------------------------------------------

def gateway_padrao(texto_route):
    """IP do gateway padrão a partir do conteúdo de /proc/net/route."""
    for linha in (texto_route or "").splitlines()[1:]:
        campos = linha.split()
        if len(campos) >= 3 and campos[1] == "00000000":
            try:
                n = int(campos[2], 16)
            except ValueError:
                continue
            if n:
                return ".".join(str((n >> (8 * i)) & 255) for i in range(4))
    return None


def servidor_dns(texto_resolv):
    for linha in (texto_resolv or "").splitlines():
        partes = linha.split()
        if len(partes) >= 2 and partes[0] == "nameserver" and re.fullmatch(r"\d+\.\d+\.\d+\.\d+", partes[1]):
            return partes[1]
    return None


def candidatos_ollama_wsl(porta=11434, ler=None):
    """Endereços onde o Ollama do Windows pode estar, vistos do WSL2 (além
    do 127.0.0.1 de sempre): o IP do Windows aparece como servidor DNS e
    como gateway. Sem repetição, na ordem em que vale tentar."""
    ler = ler or _ler
    achados = []
    for ip in (servidor_dns(ler("/etc/resolv.conf")), gateway_padrao(ler("/proc/net/route"))):
        if ip and ip not in achados and not ip.startswith("127."):
            achados.append(ip)
    return [f"http://{ip}:{porta}" for ip in achados]


def abridor_de_links_do_windows(existe=None):
    """Como abrir um link no navegador do Windows a partir do WSL sem
    interface: ['wslview'] (pacote wslu) ou ['explorer.exe']. None se não houver."""
    import shutil
    existe = existe or shutil.which
    for comando in (["wslview"], ["explorer.exe"]):
        if existe(comando[0]):
            return comando
    return None