traducir: lector de OpenRC — el par que cierra el camino de gioser, y casi la mitad NO se puede leer
`openrc → arje` sin escribir ningún traductor de ese par: cuarto plugin, cuarto par. OpenRC es el init de gioser, la máquina que esta mudanza termina borrando. **El hecho que manda acá: un servicio de OpenRC es un PROGRAMA, no una declaración.** Un unit de systemd se lee; un script de OpenRC puede hacer cualquier cosa antes de arrancar nada. Medido sobre el `/etc/init.d` real: **60 de 121 definen su propia `start()`/`stop()`**. En ésos no hay `command=` que valga — leer las variables y emitir tarjeta daría un servicio que arranca OTRA COSA. Regla al revés que en systemd: `start()` propia ⇒ SIN-TRADUCIR y NO se emite tarjeta. Los números del corpus real cierran solos: 121 − 1 (no es openrc-run) − 60 (start propia) − 1 (sin `command=`) = 59, y el lector emitió exactamente 59, con 59 ids únicos. Dos cosas que OpenRC obliga a ir a buscar fuera del script: `/etc/conf.d/<x>`, donde viven los argumentos de verdad (a diferencia del `EnvironmentFile=` de systemd, éste SÍ está en la máquina: se lee y se aplica), y `/etc/runlevels/`, que es lo único que dice si el servicio arranca solo. **Tres defectos que sólo aparecieron corriendo contra las 121 de verdad**, no sobre un ejemplo mío: - `name=` no es un identificador sino un rótulo humano: en gioser vale «Aura Backend», con espacio, y se iba al id de la tarjeta y al path del cgroup. El identificador es el nombre del fichero. - Ids repetidos: `/etc/init.d` guarda copias `*.bak-FECHA` junto a los servicios vivos y son scripts válidos; dos con el mismo nombre dan el MISMO id determinista ⇒ semilla con dos cards homónimas. El escritor lo detecta y no emite la segunda. - La tarjeta decía `"desde": "systemd"` viniendo de OpenRC: el campo que existe para saber de dónde salió algo era justo el que mentía. Estaba cableado. Y el centro deja de filtrar la entrada por extensión: los servicios de OpenRC no tienen ninguna, y filtrar en el centro es que lo que no entra se pierda EN SILENCIO. Filtra el lector, que sabe, y lo anota — así un `.bak` en `/etc/init.d` se REPORTA en vez de desaparecer. Controles: dos corridas dan el fichero byte a byte idéntico; los tres pares previos sin regresión (`nginx → caddy` sigue dando `Valid configuration`, `systemd → arje` sigue emitiendo sus 3 tarjetas). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
+35
-1
@@ -341,7 +341,7 @@ Un unit de systemd no es un sitio web. Meterlo a la fuerza en `Sitio`/`Ruta` ser
|
||||
| familia | modelo | lectores | escritores |
|
||||
|---|---|---|---|
|
||||
| `web` | `Sitio` / `Ruta` / `Accion` | nginx, apache | caddy |
|
||||
| `servicio` | `Servicio` / `Sistema` | systemd | arje |
|
||||
| `servicio` | `Servicio` / `Sistema` | systemd, openrc | arje |
|
||||
|
||||
Un par cruzado se rechaza con un error, no con un intento:
|
||||
|
||||
@@ -368,6 +368,40 @@ Lo que sí tiene destino exacto: `Type=oneshot` → `"supervision": "OneShot"`,
|
||||
semilla del producto. Y el control es que la tarjeta generada tiene **el mismo juego de claves que la
|
||||
tarjeta real de `sshd`**, más `_mudanza` de procedencia.
|
||||
|
||||
#### `openrc → arje`: casi la mitad de los servicios NO son declaraciones
|
||||
|
||||
Un unit de systemd se lee. Un servicio de OpenRC es un **script de shell** que puede hacer cualquier
|
||||
cosa antes de arrancar nada. Medido sobre el `/etc/init.d` real de gioser —la máquina que esta
|
||||
mudanza borra— **60 de 121 definen su propia `start()`/`stop()`**. En ésos no hay `command=` que
|
||||
valga: leer las variables y emitir una tarjeta produciría un servicio que arranca *otra cosa*.
|
||||
|
||||
Por eso la regla va al revés que en systemd: **`start()` propia ⇒ SIN-TRADUCIR y no se emite
|
||||
tarjeta**. Los números del corpus real cierran solos:
|
||||
|
||||
```
|
||||
ficheros en /etc/init.d : 121
|
||||
no son openrc-run : 1
|
||||
con start()/stop() propia : 60 → no se emite tarjeta
|
||||
declarativos sin command= : 1 → no hay qué arrancar
|
||||
⇒ traducibles : 59 ← y el lector emitió exactamente 59
|
||||
```
|
||||
|
||||
Lo otro que OpenRC obliga a ir a buscar es **`/etc/conf.d/<servicio>`**: el servicio está partido en
|
||||
dos ficheros y **los argumentos de verdad viven en el segundo**. A diferencia del `EnvironmentFile=`
|
||||
de systemd, éste sí está en la máquina, así que se lee y se aplica. Y el arranque automático **no lo
|
||||
dice el script** sino el enlace en `/etc/runlevels/`.
|
||||
|
||||
#### Tres defectos que sólo aparecieron corriendo contra las 121 de verdad
|
||||
|
||||
- **`name=` no es un identificador, es un rótulo**: en gioser vale `Aura Backend`, con espacio — que
|
||||
se iba al id de la tarjeta y al path del cgroup. El identificador es el nombre del fichero, que
|
||||
además es lo que usan `rc-update` y `/etc/runlevels`.
|
||||
- **Ids repetidos**: `/etc/init.d` guarda copias `*.bak-FECHA` junto a los servicios vivos, y son
|
||||
scripts de OpenRC válidos. Dos servicios con el mismo nombre dan **el mismo id determinista** ⇒ una
|
||||
semilla con dos cards homónimas. El escritor lo detecta y no emite la segunda.
|
||||
- **Procedencia cableada**: la tarjeta decía `"desde": "systemd"` viniendo de OpenRC. El campo que
|
||||
existe para saber de dónde salió algo era el que mentía.
|
||||
|
||||
#### Lector y escritor no opinan del mismo campo
|
||||
|
||||
`User=` lo **captura** el lector y lo **juzga** el escritor. Cuando los dos anotaban, el acta decía
|
||||
|
||||
@@ -26,7 +26,7 @@ from formatos import escritor # noqa: E402
|
||||
SEM = "takana-mudanza"
|
||||
|
||||
|
||||
def _tarjeta(srv, acta):
|
||||
def _tarjeta(srv, acta, desde):
|
||||
argv_all = shlex.split(srv.cmd) if srv.cmd else []
|
||||
if not argv_all:
|
||||
return None
|
||||
@@ -102,7 +102,7 @@ def _tarjeta(srv, acta):
|
||||
"rlimits": {"mem_bytes": None, "nofile": None, "nproc": None},
|
||||
},
|
||||
"supervision": sup,
|
||||
"_mudanza": {"desde": "systemd", "descripcion": srv.descripcion,
|
||||
"_mudanza": {"desde": desde, "descripcion": srv.descripcion,
|
||||
"tipo_origen": srv.tipo, "cwd_original": srv.cwd,
|
||||
"usuario_original": srv.usuario, "arranque_auto": srv.arranque_auto},
|
||||
}
|
||||
@@ -111,15 +111,25 @@ def _tarjeta(srv, acta):
|
||||
@escritor("arje", familia="servicio")
|
||||
def escribir(sis):
|
||||
acta = sis.acta
|
||||
hijas = []
|
||||
hijas, vistos = [], {}
|
||||
for srv in sis.servicios:
|
||||
# Dos servicios con el mismo nombre ⇒ el mismo id determinista ⇒ una semilla con dos cards
|
||||
# que se llaman igual. Pasa de verdad: `/etc/init.d` de gioser guarda copias `*.bak-FECHA`
|
||||
# junto a los servicios vivos, y son scripts de OpenRC perfectamente válidos.
|
||||
if srv.nombre in vistos:
|
||||
acta.anota(srv.origen, "SIN-TRADUCIR", srv.nombre,
|
||||
f"nombre repetido (ya salió en la línea {vistos[srv.nombre]}): produciría DOS "
|
||||
"tarjetas con el mismo id. No se emite la segunda — decidí cuál vale y borrá "
|
||||
"la otra (suelen ser copias `*.bak` en el mismo directorio)")
|
||||
continue
|
||||
vistos[srv.nombre] = srv.origen
|
||||
if not srv.arranque_auto:
|
||||
# Un unit instalado pero sin `WantedBy=` no lo levanta el arranque. Declararlo igual
|
||||
# cambiaría el sistema: se emite, pero se dice.
|
||||
acta.anota(srv.origen, "decision", f"{srv.nombre} (sin WantedBy=)",
|
||||
"el unit no se levanta al arrancar en el origen; la semilla SÍ lo va a "
|
||||
"levantar. Sacalo si no lo querés al boot")
|
||||
t = _tarjeta(srv, acta)
|
||||
t = _tarjeta(srv, acta, sis.formato_origen or "?")
|
||||
if t is None:
|
||||
acta.anota(srv.origen, "SIN-TRADUCIR", srv.nombre,
|
||||
"sin comando de arranque: NO se emite tarjeta. Una tarjeta que no arranca es "
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
"""Lector de servicios de OpenRC → modelo pivote «servicio» (SDD 29).
|
||||
|
||||
OpenRC es el init de gioser — la máquina que esta mudanza termina BORRANDO — así que este lector es
|
||||
el que cierra el camino: `openrc → arje`, sin escribir ningún traductor de ese par.
|
||||
|
||||
── EL HECHO QUE MANDA ACÁ: UN SERVICIO DE OPENRC ES UN PROGRAMA ────────────────────────────────
|
||||
Un unit de systemd es declarativo: se lee. Un servicio de OpenRC es un **script de shell** que puede
|
||||
hacer cualquier cosa antes de arrancar nada. Medido en `/etc/init.d` de gioser: **59 de 121 definen
|
||||
su propia `start()`**. Casi la mitad. En ésos no hay `command=` que valga — leer las variables y
|
||||
emitir una tarjeta produciría un servicio que arranca OTRA COSA, o nada.
|
||||
|
||||
Por eso la regla es al revés que en systemd: **`start()` propia ⇒ SIN-TRADUCIR, y no se emite
|
||||
tarjeta**. Una tarjeta ausente falla ruidosamente al primer arranque; una tarjeta que arranca lo que
|
||||
no es llega hasta producción diciendo que todo fue bien.
|
||||
|
||||
── LO QUE SÍ HAY QUE IR A BUSCAR: `/etc/conf.d/<servicio>` ─────────────────────────────────────
|
||||
OpenRC parte el servicio en dos ficheros, y **los argumentos de verdad viven en el segundo**. Leer
|
||||
sólo `/etc/init.d/<x>` da un comando que parece completo y le faltan las opciones. A diferencia del
|
||||
`EnvironmentFile=` de systemd, este fichero SÍ está en la máquina: se lee, se aplica y se dice.
|
||||
"""
|
||||
import os, re, shlex, sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from modelo import Servicio, Sistema # noqa: E402
|
||||
from formatos import lector # noqa: E402
|
||||
|
||||
_ASIG = re.compile(r'^([A-Za-z_][A-Za-z0-9_]*)=(.*)$')
|
||||
_FUNC = re.compile(r'^\s*(?:function\s+)?([A-Za-z_][A-Za-z0-9_:-]*)\s*\(\)\s*\{?')
|
||||
|
||||
# Las fases que OpenRC deja redefinir. `start`/`stop` son las que vuelven intraducible el servicio;
|
||||
# las demás son trabajo aparte del arranque, igual que los `ExecStartPre=` de systemd.
|
||||
_FASES_MORTALES = {"start", "stop"}
|
||||
_FASES_EXTRA = {"start_pre", "start_post", "stop_pre", "stop_post", "reload", "restart", "status"}
|
||||
|
||||
|
||||
def _asignaciones(texto):
|
||||
"""Variables de NIVEL SUPERIOR y funciones definidas, en una sola pasada.
|
||||
|
||||
⚠ Hay que saltear el cuerpo de las funciones: un `command=` dentro de una `start()` no es la
|
||||
configuración del servicio, es una línea de un programa. Confundirlos es leer el fichero como si
|
||||
fuera declarativo cuando no lo es."""
|
||||
variables, funciones, cuerpos = {}, {}, {}
|
||||
prof, actual, buf = 0, None, []
|
||||
for n, cruda in enumerate(texto.splitlines(), 1):
|
||||
ln = cruda.strip()
|
||||
if prof == 0:
|
||||
m = _FUNC.match(cruda)
|
||||
if m and not ln.startswith("#"):
|
||||
actual, buf = m.group(1), []
|
||||
funciones[actual] = n
|
||||
prof = cruda.count("{") - cruda.count("}")
|
||||
if prof == 0: # `f() {` con la llave en la línea siguiente
|
||||
prof = 1
|
||||
continue
|
||||
if ln.startswith("#") or not ln:
|
||||
continue
|
||||
m = _ASIG.match(ln)
|
||||
if m:
|
||||
k, v = m.group(1), m.group(2).strip()
|
||||
if v[:1] in ("'", '"') and v[-1:] == v[:1] and len(v) > 1:
|
||||
v = v[1:-1]
|
||||
variables[k] = (v, n)
|
||||
else:
|
||||
buf.append(cruda)
|
||||
prof += cruda.count("{") - cruda.count("}")
|
||||
if prof <= 0:
|
||||
prof, cuerpos[actual], actual = 0, "\n".join(buf), None
|
||||
return variables, funciones, cuerpos
|
||||
|
||||
|
||||
def _expandir(v, variables, acta, linea, texto_orig):
|
||||
"""`${command} ${command_args}` es idioma corriente en estos scripts. Se expande con lo que el
|
||||
propio fichero define; lo que quede sin resolver se DICE, no se deja con el `$` adentro."""
|
||||
def sub(m):
|
||||
k = m.group(1) or m.group(2)
|
||||
if k in variables:
|
||||
return variables[k][0]
|
||||
acta.anota(linea, "SIN-TRADUCIR", texto_orig,
|
||||
f"usa `${{{k}}}`, que el script no define: lo pone el entorno del init y acá no "
|
||||
"se puede saber su valor")
|
||||
return m.group(0)
|
||||
return re.sub(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)", sub, v)
|
||||
|
||||
|
||||
def _depend(cuerpo):
|
||||
"""`depend() { need net; after firewall }` → (necesita, tras)."""
|
||||
nec, tras = [], []
|
||||
for ln in (cuerpo or "").splitlines():
|
||||
for pieza in ln.split(";"):
|
||||
p = pieza.split()
|
||||
if not p:
|
||||
continue
|
||||
if p[0] in ("need", "use"):
|
||||
nec += p[1:]
|
||||
elif p[0] == "after":
|
||||
tras += p[1:]
|
||||
return nec, tras
|
||||
|
||||
|
||||
def _entorno_de_args(val, srv, acta, linea):
|
||||
"""`supervise_daemon_args="--stdout … -e K=V -e K2=V2"`: el entorno del servicio vive ACÁ."""
|
||||
try:
|
||||
piezas = shlex.split(val)
|
||||
except ValueError:
|
||||
return
|
||||
i, otros = 0, []
|
||||
while i < len(piezas):
|
||||
p = piezas[i]
|
||||
if p in ("-e", "--env") and i + 1 < len(piezas) and "=" in piezas[i + 1]:
|
||||
k, _, v = piezas[i + 1].partition("=")
|
||||
srv.entorno[k] = v
|
||||
i += 2
|
||||
continue
|
||||
otros.append(p)
|
||||
i += 1
|
||||
if otros:
|
||||
acta.anota(linea, "no-aplica", " ".join(otros)[:70],
|
||||
"opciones del supervisor de OpenRC (bitácoras, pidfile): el destino las resuelve "
|
||||
"por su cuenta")
|
||||
|
||||
|
||||
@lector("openrc", familia="servicio")
|
||||
def leer(texto, nombre=""):
|
||||
sis = Sistema(formato_origen="openrc")
|
||||
acta = sis.acta
|
||||
base = os.path.basename(nombre or "servicio")
|
||||
srv = Servicio(nombre=base)
|
||||
|
||||
primera = (texto.splitlines() or [""])[0]
|
||||
if "openrc-run" not in primera:
|
||||
# No se saltea en silencio: `/etc/init.d` junta servicios con copias de respaldo y restos, y
|
||||
# decir cuáles se ignoraron es lo único que distingue «no aplica» de «se me pasó».
|
||||
acta.anota(1, "SIN-TRADUCIR", base,
|
||||
"el fichero no arranca con `#!/sbin/openrc-run`: no es un servicio de OpenRC")
|
||||
return sis
|
||||
|
||||
variables, funciones, cuerpos = _asignaciones(texto)
|
||||
|
||||
# ── `/etc/conf.d/<x>` pisa al `/etc/init.d/<x>`, y ahí están los argumentos de verdad ──
|
||||
if nombre:
|
||||
confd = os.path.join(os.path.dirname(os.path.dirname(nombre)), "conf.d", base)
|
||||
if os.path.isfile(confd):
|
||||
ov, _, _ = _asignaciones(open(confd, errors="replace").read())
|
||||
if ov:
|
||||
variables.update(ov)
|
||||
acta.anota(0, "traducido", f"conf.d/{base}",
|
||||
f"se aplicaron {len(ov)} variable(s) de `/etc/conf.d/{base}`: sin esto el "
|
||||
"comando saldría sin sus opciones y parecería completo")
|
||||
else:
|
||||
acta.anota(0, "no-aplica", f"conf.d/{base}", "el servicio no tiene fichero de conf.d")
|
||||
|
||||
# ── la regla dura: `start()`/`stop()` propias ⇒ el servicio es un PROGRAMA ──
|
||||
propias = _FASES_MORTALES & set(funciones)
|
||||
for f in sorted(propias):
|
||||
acta.anota(funciones[f], "SIN-TRADUCIR", f"{f}() {{ … }}",
|
||||
f"el servicio define su propia `{f}()`: es shell arbitrario, no una declaración. "
|
||||
"Leer `command=` acá daría una tarjeta que arranca otra cosa. Hay que portarla a "
|
||||
"mano (59 de 121 servicios de gioser están en este caso)")
|
||||
for f in sorted(_FASES_EXTRA & set(funciones)):
|
||||
acta.anota(funciones[f], "SIN-TRADUCIR", f"{f}() {{ … }}",
|
||||
f"`{f}()` es una fase aparte del arranque; el destino no la tiene y hay que "
|
||||
"portarla a mano")
|
||||
|
||||
def v(k):
|
||||
return variables[k][0] if k in variables else ""
|
||||
|
||||
def ln_de(k):
|
||||
return variables[k][1] if k in variables else 0
|
||||
|
||||
srv.descripcion = v("description")
|
||||
# ⚠ `name=` en OpenRC es un RÓTULO HUMANO, no un identificador: en gioser vale «Aura Backend»,
|
||||
# con espacio y todo. Usarlo como nombre metía el espacio en el id y en el path del cgroup. El
|
||||
# identificador es el NOMBRE DEL FICHERO, que además es lo que dicen `rc-update` y `/etc/runlevels`.
|
||||
if "name" in variables and not srv.descripcion:
|
||||
srv.descripcion = v("name")
|
||||
|
||||
cmd = v("command")
|
||||
if cmd and not propias:
|
||||
args = v("command_args")
|
||||
entero = (cmd + " " + args).strip()
|
||||
entero = _expandir(entero, variables, acta, ln_de("command"), entero)
|
||||
srv.cmd = entero
|
||||
srv.origen = ln_de("command")
|
||||
acta.anota(srv.origen, "traducido", f"command={cmd}" + (f" {args}" if args else ""))
|
||||
elif not propias:
|
||||
acta.anota(0, "SIN-TRADUCIR", base,
|
||||
"el script no define `command=` ni una `start()` propia: no hay de dónde sacar "
|
||||
"qué arrancar")
|
||||
|
||||
if "command_user" in variables:
|
||||
u, _, g = v("command_user").partition(":")
|
||||
srv.usuario, srv.lineas["usuario"] = u, ln_de("command_user")
|
||||
if g:
|
||||
srv.grupo, srv.lineas["grupo"] = g, ln_de("command_user")
|
||||
if "directory" in variables:
|
||||
srv.cwd, srv.lineas["cwd"] = v("directory"), ln_de("directory")
|
||||
acta.anota(srv.lineas["cwd"], "traducido", f"directory={srv.cwd}")
|
||||
|
||||
# ── supervisión ──
|
||||
if v("supervisor") == "supervise-daemon":
|
||||
srv.reinicio, srv.lineas["reinicio"] = "siempre", ln_de("supervisor")
|
||||
acta.anota(ln_de("supervisor"), "traducido", "supervisor=supervise-daemon")
|
||||
elif v("command_background") in ("yes", "true", "YES"):
|
||||
# start-stop-daemon manda el proceso al fondo y OpenRC NO lo vigila: si muere, queda muerto.
|
||||
srv.tipo, srv.reinicio = "forking", "no"
|
||||
acta.anota(ln_de("command_background"), "decision", "command_background=yes",
|
||||
"OpenRC lo manda al fondo y NO lo vigila (si muere, queda muerto). El destino SÍ "
|
||||
"lo va a supervisar: es una mejora, pero es un cambio de comportamiento")
|
||||
else:
|
||||
srv.reinicio = "no"
|
||||
|
||||
if "respawn_delay" in variables:
|
||||
try:
|
||||
srv.reinicio_ms = int(float(v("respawn_delay")) * 1000)
|
||||
acta.anota(ln_de("respawn_delay"), "traducido", f"respawn_delay={v('respawn_delay')}")
|
||||
except ValueError:
|
||||
acta.anota(ln_de("respawn_delay"), "SIN-TRADUCIR", f"respawn_delay={v('respawn_delay')}",
|
||||
"no es un número de segundos")
|
||||
if v("respawn_max") not in ("", "0"):
|
||||
acta.anota(ln_de("respawn_max"), "SIN-TRADUCIR", f"respawn_max={v('respawn_max')}",
|
||||
"OpenRC deja de reintentar tras N veces; arje reintenta SIEMPRE, así que un "
|
||||
"servicio que allá se rendía acá va a reintentar sin fin")
|
||||
|
||||
for k in ("supervise_daemon_args", "start_stop_daemon_args"):
|
||||
if k in variables:
|
||||
_entorno_de_args(v(k), srv, acta, ln_de(k))
|
||||
|
||||
srv.necesita, srv.tras = _depend(cuerpos.get("depend"))
|
||||
if "depend" in funciones:
|
||||
acta.anota(funciones["depend"], "traducido",
|
||||
f"depend() → need {' '.join(srv.necesita) or '—'} / after {' '.join(srv.tras) or '—'}")
|
||||
|
||||
# ── ¿lo levanta el arranque? Lo dice el enlace en `/etc/runlevels/`, no el script ──
|
||||
if nombre:
|
||||
rl = os.path.join(os.path.dirname(os.path.dirname(nombre)), "runlevels")
|
||||
if os.path.isdir(rl):
|
||||
niveles = [d for d in sorted(os.listdir(rl))
|
||||
if os.path.exists(os.path.join(rl, d, base))]
|
||||
srv.arranque_auto = bool(niveles)
|
||||
acta.anota(0, "traducido", f"runlevels: {', '.join(niveles) or 'ninguno'}",
|
||||
"el arranque automático lo dice el enlace en /etc/runlevels, no el script")
|
||||
else:
|
||||
acta.anota(0, "SIN-TRADUCIR", f"runlevels de {base}",
|
||||
"no se pudo mirar `/etc/runlevels`: no se sabe si el servicio arranca solo, y "
|
||||
"suponer que sí lo levantaría en el destino sin que nadie lo haya pedido")
|
||||
|
||||
if not propias:
|
||||
sis.servicios.append(srv)
|
||||
return sis
|
||||
@@ -87,11 +87,13 @@ def _fusionar(a, b):
|
||||
def _entradas(ruta):
|
||||
"""Un fichero, o todos los `*.service`/`*.conf` de un directorio, en orden determinista."""
|
||||
if os.path.isdir(ruta):
|
||||
# TODOS los ficheros regulares no ocultos, no sólo los de cierta extensión: los servicios de
|
||||
# OpenRC no tienen ninguna. Filtrar acá por nombre sería el centro decidiendo qué es de cada
|
||||
# formato — y lo que no entra se pierde EN SILENCIO. Filtra el lector, que sabe, y lo DICE.
|
||||
hs = sorted(f for f in os.listdir(ruta)
|
||||
if f.endswith((".service", ".conf")) and
|
||||
os.path.isfile(os.path.join(ruta, f)))
|
||||
if not f.startswith(".") and os.path.isfile(os.path.join(ruta, f)))
|
||||
if not hs:
|
||||
sys.exit(f"✗ {ruta} no tiene ningún `*.service` ni `*.conf`")
|
||||
sys.exit(f"✗ {ruta} no tiene ningún fichero regular")
|
||||
return [os.path.join(ruta, h) for h in hs]
|
||||
return [ruta]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user