Files
tai/tai_pkg/install.py
T

677 lines
30 KiB
Python
Raw Normal View History

"""Install, hook, service, and container operations."""
import getpass
import os
import re
import shutil
import sqlite3
import subprocess
import sys
import importlib.util
from tai_pkg.consts import Consts
from tai_pkg.errors import BackendError
from tai_pkg.sandbox import Sandbox
from tai_pkg.telegram import Telegram
from tai_pkg.telegram import TelegramBot
from tai_pkg.text import Text
__all__ = ["Install"]
class Install:
"""Install."""
@staticmethod
def run_host(argv, timeout=120):
try:
done = subprocess.run(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=timeout)
return done.returncode == 0, (done.stdout or "") + (done.stderr or "")
except (OSError, subprocess.SubprocessError) as exc:
return False, Text.short_error(exc)
@staticmethod
def venv_available():
if importlib.util.find_spec("venv") is None:
return False
if importlib.util.find_spec("ensurepip") is not None:
return True
return shutil.which("pip3") is not None or shutil.which("pip") is not None
@staticmethod
def venv_dir(home):
return os.path.join(home, "venv")
@staticmethod
def venv_python(home):
cand = os.path.join(Install.venv_dir(home), "bin", "python")
if os.path.isfile(cand) and os.access(cand, os.X_OK):
return cand
return None
@staticmethod
def ensure_venv(home):
found = Install.venv_python(home)
if found:
return True, "kept %s" % found
if not Install.venv_available():
return False, "python venv module unavailable"
try:
done = subprocess.run([sys.executable, "-m", "venv", Install.venv_dir(home)], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=300)
except (OSError, subprocess.SubprocessError) as exc:
return False, Text.short_error(exc)
if done.returncode != 0 or not Install.venv_python(home):
return False, ((done.stderr or "").strip()[:200] or "venv creation failed")
return True, "created %s" % Install.venv_python(home)
@staticmethod
def service_exec(home, script, *args):
parts = []
python = Install.venv_python(home)
if python:
parts.append(python)
parts.append(script)
parts.extend(args)
return " ".join(parts)
@staticmethod
def unit_dir(home):
return os.path.join(os.path.expanduser("~"), ".config", "systemd", "user")
@staticmethod
def service_state(name):
if not shutil.which("systemctl"):
return "unknown", "unknown"
active, _out = Install.run_host(["systemctl", "--user", "is-active", name], timeout=15)
enabled, _out = Install.run_host(["systemctl", "--user", "is-enabled", name], timeout=15)
return ("active" if active else "inactive"), ("enabled" if enabled else "disabled")
@staticmethod
def activate_service(name):
if not shutil.which("systemctl"):
return "systemctl missing, unit written only"
Install.run_host(["systemctl", "--user", "daemon-reload"])
active, _enabled = Install.service_state(name)
if active == "active":
ok, out = Install.run_host(["systemctl", "--user", "restart", name])
return "restarted, changes effective immediately" if ok else "restart failed: " + out.strip()[:150]
ok, out = Install.run_host(["systemctl", "--user", "enable", "--now", name])
if ok:
user = os.environ.get("USER") or getpass.getuser()
Install.run_host(["loginctl", "enable-linger", user])
return "enabled and started"
return "enable failed: " + out.strip()[:150]
@staticmethod
def remove_service_unit(home, name):
path = os.path.join(Install.unit_dir(home), name)
if shutil.which("systemctl"):
Install.run_host(["systemctl", "--user", "disable", "--now", name])
Install.run_host(["systemctl", "--user", "daemon-reload"])
if os.path.exists(path):
try:
os.remove(path)
return True, "removed " + path
except OSError as exc:
return False, Text.short_error(exc)
return True, "not present"
@staticmethod
def script_version(path):
try:
with open(path, "r", encoding="utf-8") as handle:
match = Consts.VERSION_RE.search(handle.read())
except OSError:
return None
return ".".join(match.groups()) if match else None
@staticmethod
def shell_rc_path(shell):
"""Where each shell's startup file lives. bash/zsh: a dotfile sourced
for interactive shells only. fish: config.fish, which fish sources on
every startup (interactive or not, login or not) -- hence the
`status is-interactive` guard inside FISH_PLEXER_BLOCK rather than
relying on the file only being read at the right time."""
home = os.path.expanduser("~")
if shell == "fish":
config_home = os.environ.get("XDG_CONFIG_HOME") or os.path.join(home, ".config")
return os.path.join(config_home, "fish", "config.fish")
if shell == "zsh":
return os.path.join(home, ".zshrc")
return os.path.join(home, ".bashrc")
@staticmethod
def shell_present(shell):
return shutil.which(shell) is not None
@staticmethod
def _rc_block_present(path, block):
try:
with open(path, "r", encoding="utf-8") as handle:
return block in handle.read()
except OSError:
return False
@staticmethod
def _rc_block_remove(path, mark_begin, mark_end):
try:
with open(path, "r", encoding="utf-8") as handle:
raw = handle.read()
except OSError:
return False
stripped = re.sub(re.escape(mark_begin) + r".*?" + re.escape(mark_end) + r"\n?", "", raw, flags=re.DOTALL)
if stripped == raw:
return False
backup = path + ".bak-tai"
if raw and not os.path.exists(backup):
with open(backup, "w", encoding="utf-8") as handle:
handle.write(raw)
with open(path, "w", encoding="utf-8") as handle:
handle.write(stripped)
return True
@staticmethod
def _rc_block_upsert(path, mark_begin, mark_end, block):
try:
with open(path, "r", encoding="utf-8") as handle:
raw = handle.read()
except OSError:
raw = ""
if block in raw:
return False
backup = path + ".bak-tai"
if raw and not os.path.exists(backup):
with open(backup, "w", encoding="utf-8") as handle:
handle.write(raw)
stripped = re.sub(re.escape(mark_begin) + r".*?" + re.escape(mark_end) + r"\n?", "", raw, flags=re.DOTALL)
if stripped and not stripped.endswith("\n"):
stripped += "\n"
parent = os.path.dirname(path)
if parent and not os.path.isdir(parent):
os.makedirs(parent, exist_ok=True)
with open(path, "w", encoding="utf-8") as handle:
handle.write(stripped + block)
return True
@staticmethod
def hook_present(path):
return Install._rc_block_present(path, Consts.BASHRC_BLOCK)
@staticmethod
def remove_bashrc_block(path):
return Install._rc_block_remove(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END)
@staticmethod
def zsh_hook_present(path):
return Install._rc_block_present(path, Consts.ZSH_NOTFOUND_BLOCK)
@staticmethod
def remove_zsh_hook_block(path):
return Install._rc_block_remove(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END)
@staticmethod
def upsert_zsh_hook_block(path):
return Install._rc_block_upsert(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END, Consts.ZSH_NOTFOUND_BLOCK)
@staticmethod
def fish_hook_present(path):
return Install._rc_block_present(path, Consts.FISH_NOTFOUND_BLOCK)
@staticmethod
def remove_fish_hook_block(path):
return Install._rc_block_remove(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END)
@staticmethod
def upsert_fish_hook_block(path):
return Install._rc_block_upsert(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END, Consts.FISH_NOTFOUND_BLOCK)
@staticmethod
def plexer_hook_present(path):
return Install._rc_block_present(path, Consts.PLEXER_BASHRC_BLOCK)
@staticmethod
def remove_plexer_bashrc_block(path):
return Install._rc_block_remove(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END)
@staticmethod
def upsert_plexer_bashrc_block(path):
return Install._rc_block_upsert(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END, Consts.PLEXER_BASHRC_BLOCK)
@staticmethod
# zsh reuses PLEXER_BASHRC_BLOCK byte-for-byte -- zsh is POSIX-enough that
# the plain `[`/`case "$-"` bash content works unchanged; only the target
# file differs (~/.zshrc, not ~/.bashrc). No ZSH_PLEXER_BLOCK constant exists
# on purpose -- there is nothing shell-specific to put in one.
def zsh_plexer_hook_present(path):
return Install._rc_block_present(path, Consts.PLEXER_BASHRC_BLOCK)
@staticmethod
def remove_zsh_plexer_block(path):
return Install._rc_block_remove(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END)
@staticmethod
def upsert_zsh_plexer_block(path):
return Install._rc_block_upsert(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END, Consts.PLEXER_BASHRC_BLOCK)
@staticmethod
def fish_plexer_hook_present(path):
return Install._rc_block_present(path, Consts.FISH_PLEXER_BLOCK)
@staticmethod
def remove_fish_plexer_block(path):
return Install._rc_block_remove(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END)
@staticmethod
def upsert_fish_plexer_block(path):
return Install._rc_block_upsert(path, Consts.PLEXER_BASHRC_MARK_BEGIN, Consts.PLEXER_BASHRC_MARK_END, Consts.FISH_PLEXER_BLOCK)
@staticmethod
def box_venv_state(engine):
try:
probe = Sandbox.box_exec(engine, ["test", "-x", Consts.BOX_PYTHON], timeout=30)
except (OSError, subprocess.SubprocessError):
return "unknown"
return "venv ready" if probe.returncode == 0 else "no venv (reinstall container to rebuild)"
@staticmethod
def shell_hook_status_line(label, shell, present_fn):
path = Install.shell_rc_path(shell)
note = "" if Install.shell_present(shell) else " (%s not found)" % shell
return "%s: %s in %s%s" % (label, "installed" if present_fn(path) else "not installed", path, note)
@staticmethod
def status_lines(home, store=None):
lines = []
binary = os.path.join(os.path.expanduser("~"), ".local", "bin", "tai.py")
if os.path.isfile(binary):
consts_path = os.path.join(os.path.dirname(binary), "tai_pkg", "consts.py")
lines.append("binary: installed %s (%s, running %s)" % (binary, Install.script_version(consts_path) or "unknown", Consts.VERSION))
else:
lines.append("binary: not installed")
lines.append("bash-hook: %s in ~/.bashrc" % ("installed" if Install.hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed"))
lines.append(Install.shell_hook_status_line("zsh-hook", "zsh", Install.zsh_hook_present))
lines.append(Install.shell_hook_status_line("fish-hook", "fish", Install.fish_hook_present))
lines.append("plexer-hook: %s in ~/.bashrc" % ("installed" if Install.plexer_hook_present(os.path.join(os.path.expanduser("~"), ".bashrc")) else "not installed"))
lines.append(Install.shell_hook_status_line("zsh-plexer-hook", "zsh", Install.zsh_plexer_hook_present))
lines.append(Install.shell_hook_status_line("fish-plexer-hook", "fish", Install.fish_plexer_hook_present))
python = Install.venv_python(home)
if python:
lines.append("venv: ready %s" % python)
elif Install.venv_available():
lines.append("venv: not created")
else:
lines.append("venv: unavailable (no venv module)")
for name, label in (("tai-scheduler.service", "scheduler-service"), ("tai-telegram.service", "telegram-service")):
unit = os.path.join(Install.unit_dir(home), name)
if not os.path.isfile(unit):
lines.append("%s: not installed" % label)
continue
active, enabled = Install.service_state(name)
lines.append("%s: unit present, %s, %s" % (label, active, enabled))
engine = Sandbox.container_engine()
if not engine:
lines.append("container: no engine (install podman)")
else:
state = Sandbox.box_state(engine)
if state == "running":
lines.append("container: running, %s" % Install.box_venv_state(engine))
else:
lines.append("container: %s" % state)
lines.append(Install.vault_line(home, store))
return lines
@staticmethod
def vault_line(home, store=None):
db_path = os.path.join(home, "memory.db")
if not os.path.isfile(db_path):
return "vault: empty, install ops never touch data"
if store is None:
return "vault: kept %s, install ops never touch data" % db_path
try:
secrets = store.db.execute("SELECT COUNT(*) FROM secrets").fetchone()[0]
schedules = store.db.execute("SELECT COUNT(*) FROM schedules").fetchone()[0]
records = store.db.execute("SELECT COUNT(*) FROM records").fetchone()[0]
except sqlite3.Error:
return "vault: kept %s, install ops never touch data" % db_path
return "vault: kept (%d secrets, %d schedules, %d records), install ops never touch data" % (secrets, schedules, records)
@staticmethod
def op_binary(mode, home):
target = os.path.join(os.path.expanduser("~"), ".local", "bin", "tai.py")
pkg_target = os.path.join(os.path.dirname(target), "tai_pkg")
consts_target = os.path.join(pkg_target, "consts.py")
src = os.path.abspath(Consts.TAI_SCRIPT)
src_pkg = os.path.dirname(os.path.abspath(Consts.TAI_CONSTS))
current = Install.script_version(consts_target) if os.path.isfile(consts_target) else None
if mode == "install" and current is not None:
return "binary: already installed %s (running %s)" % (current, Consts.VERSION)
if mode == "uninstall":
if not os.path.exists(target) and not os.path.exists(pkg_target):
return "binary: not present"
try:
if os.path.exists(target):
os.remove(target)
if os.path.isdir(pkg_target):
shutil.rmtree(pkg_target)
return "binary: removed %s" % target
except OSError as exc:
return "binary: remove failed: %s" % Text.short_error(exc)
try:
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(src, "rb") as handle:
blob = handle.read()
with open(target, "wb") as handle:
handle.write(blob)
os.chmod(target, 0o755)
if os.path.abspath(src_pkg) != os.path.abspath(pkg_target):
if os.path.isdir(pkg_target):
shutil.rmtree(pkg_target)
shutil.copytree(src_pkg, pkg_target, ignore=shutil.ignore_patterns("__pycache__"))
except OSError as exc:
return "binary: copy failed: %s" % Text.short_error(exc)
if current is None:
return "binary: installed %s" % Consts.VERSION
return "binary: refreshed %s -> %s" % (current, Consts.VERSION)
@staticmethod
def op_hook(mode, home):
path = os.path.join(os.path.expanduser("~"), ".bashrc")
if mode == "uninstall":
return "bash-hook: %s" % ("removed" if Install.remove_bashrc_block(path) else "not present")
if mode == "reinstall":
Install.remove_bashrc_block(path)
try:
changed = Install.upsert_bashrc_block(path)
except OSError as exc:
return "bash-hook: write failed: %s" % Text.short_error(exc)
if changed:
return "bash-hook: installed, run: source ~/.bashrc"
return "bash-hook: already present"
@staticmethod
def op_plexer_hook(mode, home):
path = os.path.join(os.path.expanduser("~"), ".bashrc")
if mode == "uninstall":
return "plexer-hook: %s" % ("removed" if Install.remove_plexer_bashrc_block(path) else "not present")
if mode == "reinstall":
Install.remove_plexer_bashrc_block(path)
try:
changed = Install.upsert_plexer_bashrc_block(path)
except OSError as exc:
return "plexer-hook: write failed: %s" % Text.short_error(exc)
if changed:
return "plexer-hook: installed, run: source ~/.bashrc (new interactive shells auto-wrap; existing ones are unaffected until restarted)"
return "plexer-hook: already present"
@staticmethod
def op_shell_hook(mode, shell, label, upsert_fn, remove_fn, source_hint):
"""Shared body for the zsh/fish hook variants -- mirrors op_hook/op_plexer_hook
exactly, plus one addition: install/upgrade skip loudly (never write a dangling
hook into a config file for a shell that isn't even on this machine) when the
shell binary isn't found. uninstall/reinstall/status still act on the file
regardless, so a hook installed before a shell was later removed still cleans up."""
path = Install.shell_rc_path(shell)
if mode in ("install", "upgrade") and not Install.shell_present(shell):
return "%s: %s not found, skipped" % (label, shell)
if mode == "uninstall":
return "%s: %s" % (label, "removed" if remove_fn(path) else "not present")
if mode == "reinstall":
remove_fn(path)
try:
changed = upsert_fn(path)
except OSError as exc:
return "%s: write failed: %s" % (label, Text.short_error(exc))
if changed:
return "%s: installed, run: %s" % (label, source_hint)
return "%s: already present" % label
@staticmethod
def op_zsh_hook(mode, home):
return Install.op_shell_hook(mode, "zsh", "zsh-hook", Install.upsert_zsh_hook_block, Install.remove_zsh_hook_block, "source ~/.zshrc")
@staticmethod
def op_fish_hook(mode, home):
return Install.op_shell_hook(mode, "fish", "fish-hook", Install.upsert_fish_hook_block, Install.remove_fish_hook_block, "open a new fish shell")
@staticmethod
def op_zsh_plexer_hook(mode, home):
return Install.op_shell_hook(
mode, "zsh", "zsh-plexer-hook", Install.upsert_zsh_plexer_block, Install.remove_zsh_plexer_block,
"source ~/.zshrc (new interactive shells auto-wrap; existing ones are unaffected until restarted)",
)
@staticmethod
def op_fish_plexer_hook(mode, home):
return Install.op_shell_hook(
mode, "fish", "fish-plexer-hook", Install.upsert_fish_plexer_block, Install.remove_fish_plexer_block,
"open a new fish shell (new interactive shells auto-wrap; existing ones are unaffected until restarted)",
)
@staticmethod
def op_venv(mode, home):
if mode == "uninstall":
folder = Install.venv_dir(home)
if not os.path.isdir(folder):
return "venv: not present"
try:
shutil.rmtree(folder)
return "venv: removed %s" % folder
except OSError as exc:
return "venv: remove failed: %s" % Text.short_error(exc)
if mode == "reinstall" and os.path.isdir(Install.venv_dir(home)):
try:
shutil.rmtree(Install.venv_dir(home))
except OSError as exc:
return "venv: rebuild failed: %s" % Text.short_error(exc)
ok, detail = Install.ensure_venv(home)
return "venv: %s" % detail
@staticmethod
def op_service(mode, home, name, label, body):
unit = os.path.join(Install.unit_dir(home), name)
if mode == "uninstall":
_done, detail = Install.remove_service_unit(home, name)
return "%s: %s" % (label, detail)
existed = os.path.isfile(unit)
if mode == "install" and existed:
active, enabled = Install.service_state(name)
return "%s: already installed (%s, %s)" % (label, active, enabled)
if mode == "reinstall" and shutil.which("systemctl"):
Install.run_host(["systemctl", "--user", "disable", "--now", name])
try:
os.makedirs(os.path.dirname(unit), exist_ok=True)
with open(unit, "w", encoding="utf-8") as handle:
handle.write(body)
except OSError as exc:
return "%s: unit write failed: %s" % (label, Text.short_error(exc))
return "%s: unit %s, %s" % (label, "rewritten" if existed else "written", Install.activate_service(name))
@staticmethod
def op_scheduler_service(mode, home):
body = Consts.SCHEDULER_UNIT % Install.service_exec(home, os.path.abspath(Consts.TAI_SCRIPT))
return Install.op_service(mode, home, "tai-scheduler.service", "scheduler-service", body)
@staticmethod
def op_telegram_service(mode, home):
if mode == "uninstall":
_done, detail = Install.remove_service_unit(home, "tai-telegram.service")
return "telegram-service: %s" % detail
unit = os.path.join(Install.unit_dir(home), "tai-telegram.service")
if mode == "install" and os.path.isfile(unit):
active, enabled = Install.service_state("tai-telegram.service")
return "telegram-service: already installed (%s, %s)" % (active, enabled)
token = os.environ.get("TELEGRAM_BOT_TOKEN") or TelegramBot.load_telegram_token(home)
if not token:
return "telegram-service: needs TELEGRAM_BOT_TOKEN in the environment"
try:
me = Telegram(token).call("getMe", {})
except (OSError, ValueError) as exc:
return "telegram-service: token rejected: %s" % Text.short_error(exc)
env_path = TelegramBot.telegram_env_path(home)
try:
with open(env_path, "w", encoding="utf-8") as handle:
handle.write("TELEGRAM_BOT_TOKEN=%s\n" % token)
os.chmod(env_path, 0o600)
except OSError as exc:
return "telegram-service: env write failed: %s" % Text.short_error(exc)
body = Consts.TELEGRAM_UNIT % (Install.service_exec(home, os.path.abspath(Consts.TAI_SCRIPT)), env_path)
if mode == "reinstall" and shutil.which("systemctl"):
Install.run_host(["systemctl", "--user", "disable", "--now", "tai-telegram.service"])
try:
os.makedirs(os.path.dirname(unit), exist_ok=True)
with open(unit, "w", encoding="utf-8") as handle:
handle.write(body)
except OSError as exc:
return "telegram-service: unit write failed: %s" % Text.short_error(exc)
who = (me.get("result") or {}).get("username", "?")
return "telegram-service: @%s, %s" % (who, Install.activate_service("tai-telegram.service"))
@staticmethod
def op_container(mode, _home):
engine = Sandbox.container_engine()
if not engine:
return "container: no engine (install podman)"
if mode == "uninstall":
Install.run_host([engine, "rm", "-f", Consts.BOX_NAME])
ok, _out = Install.run_host([engine, "rmi", "-f", Consts.BOX_IMAGE])
return "container: removed%s" % (", image purged" if ok else "")
if mode == "reinstall":
Install.run_host([engine, "rm", "-f", Consts.BOX_NAME])
Install.run_host([engine, "rmi", "-f", Consts.BOX_IMAGE])
try:
engine = Sandbox.ensure_box()
except BackendError as exc:
return "container: failed: %s" % exc
state = Sandbox.box_state(engine)
detail = ", %s" % Install.box_venv_state(engine) if state == "running" else ""
if mode == "reinstall":
return "container: rebuilt and %s%s" % (state, detail)
if mode == "install" and state == "running":
return "container: already running%s" % detail
return "container: %s%s" % (state, detail)
@staticmethod
def install_report(action, targets, home, store=None):
if action == "status":
return "\n".join(Install.status_lines(home, store))
lines = []
for target in targets:
if target == "binary":
lines.append(Install.op_binary(action, home))
elif target == "bash-hook":
lines.append(Install.op_hook(action, home))
elif target == "zsh-hook":
lines.append(Install.op_zsh_hook(action, home))
elif target == "fish-hook":
lines.append(Install.op_fish_hook(action, home))
elif target == "plexer-hook":
lines.append(Install.op_plexer_hook(action, home))
elif target == "zsh-plexer-hook":
lines.append(Install.op_zsh_plexer_hook(action, home))
elif target == "fish-plexer-hook":
lines.append(Install.op_fish_plexer_hook(action, home))
elif target == "venv":
lines.append(Install.op_venv(action, home))
elif target == "scheduler-service":
lines.append(Install.op_scheduler_service(action, home))
elif target == "telegram-service":
lines.append(Install.op_telegram_service(action, home))
elif target == "container":
lines.append(Install.op_container(action, home))
lines.append(Install.vault_line(home, store))
return "\n".join(lines)
@staticmethod
def install_telegram():
token = os.environ.get("TELEGRAM_BOT_TOKEN") or ""
if token:
print("using TELEGRAM_BOT_TOKEN from environment")
else:
try:
token = getpass.getpass("Telegram bot token: ").strip()
except (EOFError, KeyboardInterrupt):
print("\naborted")
return 1
if not token:
print("empty token, aborted")
return 1
try:
me = Telegram(token).call("getMe", {})
except (OSError, ValueError) as exc:
print("token rejected: %s" % Text.short_error(exc))
return 1
print("token ok for @%s" % (me.get("result") or {}).get("username", "?"))
home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai")
os.makedirs(home, exist_ok=True)
env_path = TelegramBot.telegram_env_path(home)
with open(env_path, "w", encoding="utf-8") as handle:
handle.write("TELEGRAM_BOT_TOKEN=%s\n" % token)
try:
os.chmod(env_path, 0o600)
except OSError:
pass
print("wrote %s" % env_path)
try:
Sandbox.ensure_box()
except BackendError as exc:
print("container failed: %s" % exc)
return 1
unit_folder = os.path.join(os.path.expanduser("~"), ".config", "systemd", "user")
os.makedirs(unit_folder, exist_ok=True)
unit_path = os.path.join(unit_folder, "tai-telegram.service")
with open(unit_path, "w", encoding="utf-8") as handle:
handle.write(Consts.TELEGRAM_UNIT % (Install.service_exec(home, os.path.abspath(Consts.TAI_SCRIPT)), env_path))
print("wrote %s" % unit_path)
print("service: %s" % Install.activate_service("tai-telegram.service"))
return 0
@staticmethod
def uninstall_telegram():
unit_path = os.path.join(os.path.expanduser("~"), ".config", "systemd", "user", "tai-telegram.service")
if shutil.which("systemctl"):
Install.run_host(["systemctl", "--user", "disable", "--now", "tai-telegram.service"])
if os.path.exists(unit_path):
try:
os.remove(unit_path)
print("removed %s" % unit_path)
except OSError as exc:
print("unit remove failed: %s" % Text.short_error(exc))
else:
print("no service unit found")
if shutil.which("systemctl"):
Install.run_host(["systemctl", "--user", "daemon-reload"])
engine = Sandbox.container_engine()
if not engine:
print("no container engine found")
return 0
ok, _out = Install.run_host([engine, "rm", "-f", Consts.BOX_NAME])
print("container remove: %s" % ("ok" if ok else "not present"))
ok, _out = Install.run_host([engine, "rmi", "-f", Consts.BOX_IMAGE])
print("image purge: %s" % ("ok" if ok else "not present"))
return 0
@staticmethod
def upsert_bashrc_block(path):
return Install._rc_block_upsert(path, Consts.BASHRC_MARK_BEGIN, Consts.BASHRC_MARK_END, Consts.BASHRC_BLOCK)
@staticmethod
def install_self():
home = os.environ.get("TAI_HOME") or os.path.join(os.path.expanduser("~"), ".tai")
print(Install.op_binary("upgrade", home))
print(Install.op_hook("install", home))
# bash-hook always goes in, unconditionally, for back-compat -- it's harmless clutter
# in ~/.bashrc for a zsh/fish user who never touches bash. $SHELL additionally gets its
# own matching hook so the one-shot CLI bootstrap (--install) actually works out of the
# box on a non-bash login shell, not just when the user later runs /install zsh-hook.
login_shell = os.path.basename(os.environ.get("SHELL") or "")
rc = "~/.bashrc"
if login_shell == "zsh":
print(Install.op_zsh_hook("install", home))
rc = "~/.zshrc"
elif login_shell == "fish":
print(Install.op_fish_hook("install", home))
rc = "~/.config/fish/config.fish"
print(Install.op_venv("install", home))
print("restart your shell or run: source %s" % rc)
return 0