Files
Bridge-and-Join-s/deploy/iso/bj-firstboot-screen
T

249 lines
11 KiB
Python
Executable File

#!/usr/bin/python3
# Экран установки bj при первой загрузке сервера (bj-firstboot) — в том же оформлении, что экран установки
# системы (bj-install-progress): рамка Bridge&Join, шаги установщика с отметками, полоса хода. Запускает
# установщик, весь его вывод пишет в журнал (--log) и по этому выводу рисует экран; код возврата — как у
# установщика. Последний кадр остаётся на экране (сверху) — под ним bj-firstboot выводит учётные записи.
#
# bj-firstboot-screen --log /var/log/bj/firstboot-install.log -- bj-installer install --bundle …
#
# Установщик с BJ_SCREEN=1 печатает в начале «@@plan шаг|шаг|…», дальше — «[i/n] Шаг готово|ошибка»,
# строки-подробности с отступом, в конце «bj установлен» с адресом и кодом или «Установка не завершена.».
# Знаки ✓ ▶ █ ░ ═ — только со шрифтом консоли установщика (BJ_SCREEN_FONT=1), иначе простые символы.
# Проверка без установки: bj-firstboot-screen --demo <файл вывода установщика> [--html кадр.html]
import os
import re
import select
import shutil
import subprocess
import sys
import time
FANCY = os.environ.get("BJ_SCREEN_FONT") == "1"
G = dict(ok="✓", run="▶", wait="·", err="✗", full="█", empty="░", h="═", v="║", tl="╔", tr="╗", bl="╚", br="╝") if FANCY else \
dict(ok="+", run=">", wait=".", err="x", full="#", empty="-", h="=", v="|", tl="+", tr="+", bl="+", br="+")
RESET, BOLD, DIM = "\033[0m", "\033[1m", "\033[2m"
GREEN, CYAN, RED, WHITE, GREY, YELLOW = "\033[32m", "\033[36m", "\033[31m", "\033[97m", "\033[90m", "\033[33m"
STEP_RE = re.compile(r"\[(\d+)/(\d+)\]\s+(.*?)\s*(готово|ошибка)?\s*$")
class State:
def __init__(self):
self.plan, self.status, self.cur = [], {}, 0 # status: номер шага → ok|err
self.cur_started = time.time()
self.started = time.time()
self.caption = ""
self.done = False
self.error = ""
self.urls, self.code = [], ""
self.buf = ""
def feed(self, text):
self.buf += text
while "\n" in self.buf:
line, self.buf = self.buf.split("\n", 1)
self.line(line)
m = STEP_RE.search(self.buf) # строка шага без конца — шаг идёт
if m:
self.start_step(int(m.group(1)), int(m.group(2)), m.group(3))
def start_step(self, i, n, title):
if not self.plan:
self.plan = [""] * n
if i <= len(self.plan) and not self.plan[i - 1]:
self.plan[i - 1] = title
if i != self.cur:
self.cur, self.cur_started, self.caption = i, time.time(), ""
def line(self, line):
s = line.strip()
if s.startswith("@@plan "):
self.plan = s[7:].split("|")
return
m = STEP_RE.search(line)
if m:
i = int(m.group(1))
self.start_step(i, int(m.group(2)), m.group(3))
if m.group(4) == "готово":
self.status[i] = "ok"
elif m.group(4) == "ошибка":
self.status[i] = "err"
return
if s in ("готово", "ошибка") and self.cur: # отметка шага пришла отдельной строкой
self.status[self.cur] = "ok" if s == "готово" else "err"
return
if s.startswith("Установка не завершена."):
self.error = s[len("Установка не завершена."):].strip()
elif s == "bj установлен":
self.done = True
elif "/first-run" in s and s.startswith("http"):
self.urls.append(s)
elif s.startswith("Одноразовый код первичной настройки:"):
self.code = s.split(":", 1)[1].strip()
elif line.startswith(" ") and s and self.cur and not self.done:
self.caption = s # подробность последнего шага
def percent(self):
n = max(1, len(self.plan))
k = sum(1 for v in self.status.values() if v == "ok")
if self.cur and self.cur not in self.status: # текущий шаг — доля по времени, не больше 90 %
k += min(0.9, (time.time() - self.cur_started) / 60.0)
return min(100, int(100 * k / n))
def mmss(sec):
sec = int(max(0, sec))
return "%d:%02d" % (sec // 60, sec % 60)
def frame(st, cols, rows, final=False):
W = 70
inner = W - 2
lines = []
def row(parts=()):
parts, text = list(parts), ""
for i, (t, c) in enumerate(parts):
if len(text) + len(t) > inner:
parts = parts[:i] + [(t[: inner - len(text)], c)]
text += parts[-1][0]
break
text += t
lines.append([(G["v"], GREY)] + parts + [(" " * (inner - len(text)), ""), (G["v"], GREY)])
lines.append([(G["tl"] + G["h"] * inner + G["tr"], GREY)])
row()
row([(" Bridge&Join", BOLD + WHITE)])
row([(" шлюз, объединяющий системы финансовой организации", DIM)])
row()
row([(" Установка bj на сервер", BOLD)])
row()
for i, title in enumerate(st.plan, 1):
title = title or "…"
stv = st.status.get(i)
if stv == "ok":
row([(" " + G["ok"] + " ", GREEN), (title, "")])
elif stv == "err":
row([(" " + G["err"] + " ", RED), (title, BOLD + RED)])
elif i == st.cur and not final:
t = mmss(time.time() - st.cur_started)
row([(" " + G["run"] + " ", CYAN), (title, BOLD + CYAN), (" " * max(1, inner - 9 - len(title) - len(t)), ""), (t, DIM)])
else:
row([(" " + G["wait"] + " ", GREY), (title, GREY)])
row()
if st.error or (final and not st.done):
row([(" " + G["err"] + " Установка не завершена.", BOLD + RED)])
for chunk in wrap(st.error or "подробности — в журнале установки", inner - 6):
row([(" " + chunk, "")])
elif st.done:
row([(" " + G["ok"] + " bj установлен", BOLD + GREEN)])
row()
row([(" Откройте в браузере на своём компьютере:", "")])
for u in st.urls[:3]:
row([(" " + u, BOLD + WHITE)])
if st.code:
row([(" Одноразовый код первичной настройки: ", ""), (st.code, BOLD + YELLOW)])
row()
row([(" Браузер предупредит о сертификате — это сертификат самого", DIM)])
row([(" сервера bj: подтвердите исключение.", DIM)])
else:
pct = st.percent()
bar_w = 48
full = int(bar_w * pct / 100)
row([(" ", ""), (G["full"] * full, CYAN), (G["empty"] * (bar_w - full), GREY), (" %3d %%" % pct, BOLD)])
row([(" " + st.caption, DIM)] if st.caption else [])
row()
row([(" Не выключайте сервер: обычно это занимает несколько минут.", "")])
row()
lines.append([(G["bl"] + G["h"] * inner + G["br"], GREY)])
foot = "прошло " + mmss(time.time() - st.started)
lines.append([(" " * max(0, (W - len(foot)) // 2) + foot, GREY)])
left = max(0, (cols - W) // 2)
top = 1 if final else max(0, (rows - len(lines)) // 2) # итог — сверху: ниже bj-firstboot выведет учётные записи
return [[("", "")]] * top + [[(" " * left, "")] + ln for ln in lines]
def wrap(text, width):
out, cur = [], ""
for w in text.split():
if cur and len(cur) + 1 + len(w) > width:
out.append(cur)
cur = w
else:
cur = (cur + " " + w).strip()
if cur:
out.append(cur)
return out[:4] or [""]
def to_ansi(fr):
return "\033[H\033[2J" + "".join("".join((c + t + RESET) if c else t for t, c in ln) + "\n" for ln in fr)
def to_html(fr):
css = {GREEN: "#4e9a06", CYAN: "#06989a", RED: "#cc0000", GREY: "#888", DIM: "#9a9a9a", WHITE: "#fff", YELLOW: "#c4a000"}
rows = []
for ln in fr:
h = ""
for t, c in ln:
t = t.replace("&", "&amp;").replace("<", "&lt;")
if not c:
h += t
continue
color = next((css[k] for k in (GREEN, CYAN, RED, GREY, WHITE, YELLOW, DIM) if k in c), "#ddd")
h += '<span style="color:%s;font-weight:%s">%s</span>' % (color, "700" if BOLD in c else "400", t)
rows.append(h)
return ('<!doctype html><meta charset="utf-8"><body style="margin:0;background:#000"><pre style="margin:0;padding:8px;'
'font:16px/1 \'DejaVu Sans Mono\',monospace;color:#ddd;background:#000">' + "\n".join(rows) + "</pre>")
def main():
a = sys.argv[1:]
if len(a) >= 2 and a[0] == "--demo":
st = State()
st.feed(open(a[1], encoding="utf-8").read())
st.started -= 95
st.cur_started -= 24
fr = frame(st, 100, 40, final=st.done or bool(st.error))
if len(a) >= 4 and a[2] == "--html":
open(a[3], "w", encoding="utf-8").write(to_html(fr))
else:
sys.stdout.write(to_ansi(fr))
return 0
if len(a) < 4 or a[0] != "--log" or a[2] != "--":
sys.stderr.write("bj-firstboot-screen --log ФАЙЛ -- команда …\n")
return 2
log_path, cmd = a[1], a[3:]
os.makedirs(os.path.dirname(log_path), exist_ok=True)
log = open(log_path, "ab", buffering=0)
log.write(("\n==== %s: %s\n" % (time.strftime("%Y-%m-%d %H:%M:%S"), " ".join(cmd))).encode())
env = dict(os.environ, BJ_SCREEN="1", NO_COLOR="1")
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, env=env, bufsize=0)
st = State()
sys.stdout.write("\033[?25l")
try:
while True:
r, _, _ = select.select([p.stdout], [], [], 1.0)
if r:
chunk = os.read(p.stdout.fileno(), 65536)
if not chunk:
break
log.write(chunk)
st.feed(chunk.decode("utf-8", "replace"))
cols, rows = shutil.get_terminal_size((100, 40))
sys.stdout.write(to_ansi(frame(st, cols, rows)))
sys.stdout.flush()
code = p.wait()
if code != 0 and not st.error:
st.error = "установщик завершился с кодом %d" % code
cols, rows = shutil.get_terminal_size((100, 40))
sys.stdout.write(to_ansi(frame(st, cols, rows, final=True)))
return code
finally:
sys.stdout.write("\033[?25h")
sys.stdout.flush()
if __name__ == "__main__":
sys.exit(main())