Pacchetto Windows: avvio.py (licenza, config.env, migrazioni, quattro server in thread), vs-camera nel gruppo windows (TieMeasureFlow#76)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,453 @@
|
|||||||
|
"""Entry point del pacchetto Windows di TieMeasureFlow (TieMeasureFlow#76).
|
||||||
|
|
||||||
|
Cosa fa: controlla la licenza (prodotto "tiemeasureflow"); se non e' valida mostra il
|
||||||
|
motivo e l'impronta del computer, scrive l'impronta in
|
||||||
|
%PROGRAMDATA%\\Tielogic\\licenze\\impronta-tiemeasureflow.txt ed esce con 1.
|
||||||
|
Se e' valida: prepara la cartella dati (%PROGRAMDATA%\\Tielogic\\TieMeasureFlow, con
|
||||||
|
config.env dei segreti generati al primo avvio), imposta le variabili d'ambiente del
|
||||||
|
contratto, esegue le migrazioni Alembic (SQLite su file) e avvia in un solo processo
|
||||||
|
quattro server, ciascuno nel suo thread:
|
||||||
|
web Flask (src/frontend/flask_app) servito da waitress, --host (default 127.0.0.1) :8780
|
||||||
|
backend FastAPI src.backend.main:app, 127.0.0.1:8000
|
||||||
|
vision FastAPI src.vision_worker.main:app, 127.0.0.1:8100
|
||||||
|
camera scripts/camera_service.py (camere della stazione), 127.0.0.1:8765
|
||||||
|
Poi apre il browser. Se un'altra copia e' gia' in esecuzione apre solo il browser.
|
||||||
|
|
||||||
|
Come si usa: e' l'exe compilato con Nuitka (build.ps1); da sorgente, dalla radice del repo
|
||||||
|
con l'ambiente 3.13 del pacchetto: `python packaging/windows/avvio.py [--impronta] [--host H]`.
|
||||||
|
Opzioni di servizio: `--senza-finestre` (niente MessageBox, solo stderr: serve alle prove
|
||||||
|
automatiche; NON salta il controllo di licenza) e `--senza-browser`.
|
||||||
|
|
||||||
|
Dipende da: visionsuite.license (vs-license, contratto licenza v1), uvicorn, waitress, alembic.
|
||||||
|
|
||||||
|
⚠️ L'exe e' senza console: stdout/stderr possono essere None, quindi il log va su file
|
||||||
|
(<dati>\\log\\tiemeasureflow.log) e uvicorn gira con log_config=None.
|
||||||
|
⚠️ Le variabili d'ambiente vanno impostate PRIMA di importare backend e frontend (leggono
|
||||||
|
la configurazione all'import): per questo gli import pesanti stanno dentro le funzioni.
|
||||||
|
⚠️ La cartella ProgramData la rende scrivibile agli utenti l'installer (.iss).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ctypes
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import secrets
|
||||||
|
import socket
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import webbrowser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from visionsuite import license as lic # contratto licenza v1: impronta, cerca_licenza, verifica
|
||||||
|
|
||||||
|
PRODOTTO = "tiemeasureflow"
|
||||||
|
TITOLO = "TieMeasureFlow"
|
||||||
|
PORTA_WEB = 8780
|
||||||
|
PORTA_BACKEND = 8000
|
||||||
|
PORTA_VISION = 8100
|
||||||
|
PORTA_CAMERA = 8765
|
||||||
|
log = logging.getLogger("tmf.avvio")
|
||||||
|
|
||||||
|
# Nuitka definisce __compiled__ nel modulo compilato; da sorgente non esiste
|
||||||
|
COMPILATO = "__compiled__" in globals()
|
||||||
|
# Da sorgente (python packaging/windows/avvio.py) la radice e' due livelli sopra questo file
|
||||||
|
RADICE_SORGENTI = Path(__file__).resolve().parents[2]
|
||||||
|
# Segreti generati al primo avvio e riletti dopo (nome -> generatore)
|
||||||
|
SEGRETI = ("SERVER_SECRET_KEY", "CLIENT_SECRET_KEY", "SETUP_PASSWORD", "DB_PASSWORD")
|
||||||
|
|
||||||
|
|
||||||
|
def _stream_sicuri() -> None:
|
||||||
|
# Exe senza console: stdout/stderr sono None e print/logging/fileConfig di alembic
|
||||||
|
# andrebbero in errore; li si sostituisce con il device nullo
|
||||||
|
if sys.stdout is None:
|
||||||
|
sys.stdout = open(os.devnull, "w", encoding="utf-8")
|
||||||
|
if sys.stderr is None:
|
||||||
|
sys.stderr = open(os.devnull, "w", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def cartella_tielogic() -> Path:
|
||||||
|
# %PROGRAMDATA% e' sempre impostata su Windows; il ripiego serve solo a sviluppo/test su altri sistemi
|
||||||
|
base = os.environ.get("PROGRAMDATA") or str(Path.home() / ".tielogic-dati")
|
||||||
|
return Path(base) / "Tielogic"
|
||||||
|
|
||||||
|
|
||||||
|
def cartella_dati() -> Path:
|
||||||
|
return cartella_tielogic() / "TieMeasureFlow"
|
||||||
|
|
||||||
|
|
||||||
|
def cartella_programma() -> Path:
|
||||||
|
# Cartella dell'exe (P); da sorgente, la radice del repo
|
||||||
|
if COMPILATO:
|
||||||
|
return Path(sys.argv[0]).resolve().parent
|
||||||
|
return RADICE_SORGENTI
|
||||||
|
|
||||||
|
|
||||||
|
def percorso_impronta() -> Path:
|
||||||
|
return cartella_tielogic() / "licenze" / f"impronta-{PRODOTTO}.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def messaggio(testo: str, titolo: str = TITOLO, errore: bool = True, senza_finestre: bool = False) -> None:
|
||||||
|
# Sempre su stderr (se esiste); la MessageBox solo su Windows e se non soppressa
|
||||||
|
if sys.stderr is not None:
|
||||||
|
print(testo, file=sys.stderr)
|
||||||
|
if senza_finestre or sys.platform != "win32":
|
||||||
|
return
|
||||||
|
# MB_OK | MB_ICONERROR (0x10) oppure MB_ICONINFORMATION (0x40) | MB_TOPMOST (0x40000)
|
||||||
|
ctypes.windll.user32.MessageBoxW(None, testo, titolo, 0x40000 | (0x10 if errore else 0x40))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- licenza
|
||||||
|
|
||||||
|
def _scrivi_impronta(impronta: str) -> None:
|
||||||
|
try:
|
||||||
|
p = percorso_impronta()
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(impronta + "\n", encoding="utf-8")
|
||||||
|
except OSError as e:
|
||||||
|
log.warning("impronta non scritta: %s", e)
|
||||||
|
|
||||||
|
|
||||||
|
def controlla_licenza(senza_finestre: bool = False) -> bool:
|
||||||
|
"""True se la licenza e' valida; altrimenti mostra il motivo e l'impronta e torna False."""
|
||||||
|
try:
|
||||||
|
percorso = lic.cerca_licenza(PRODOTTO)
|
||||||
|
lic.verifica(percorso, PRODOTTO)
|
||||||
|
return True
|
||||||
|
except lic.LicenzaNonValida as e:
|
||||||
|
motivo = str(e)
|
||||||
|
except Exception as e: # impronta non calcolabile, file illeggibile: mai un'eccezione muta nell'exe
|
||||||
|
motivo = f"Controllo licenza non riuscito: {e}"
|
||||||
|
try:
|
||||||
|
impronta = lic.impronta()
|
||||||
|
except Exception as e:
|
||||||
|
impronta = None
|
||||||
|
motivo += f"\nImpronta macchina non disponibile: {e}"
|
||||||
|
testo = f"{motivo}\n\n"
|
||||||
|
if impronta:
|
||||||
|
_scrivi_impronta(impronta)
|
||||||
|
testo += (f"Impronta di questo computer:\n{impronta}\n\n"
|
||||||
|
f"Comunicala a Tielogic per ottenere la licenza (salvata anche in {percorso_impronta()}). "
|
||||||
|
f"La licenza va copiata in {lic.cerca_licenza(PRODOTTO)}.")
|
||||||
|
messaggio(testo, f"{TITOLO} - licenza", senza_finestre=senza_finestre)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- configurazione
|
||||||
|
|
||||||
|
def _leggi_env(percorso: Path) -> dict[str, str]:
|
||||||
|
valori: dict[str, str] = {}
|
||||||
|
for riga in percorso.read_text(encoding="utf-8").splitlines():
|
||||||
|
riga = riga.strip()
|
||||||
|
if riga and not riga.startswith("#") and "=" in riga:
|
||||||
|
k, v = riga.split("=", 1)
|
||||||
|
valori[k.strip()] = v.strip()
|
||||||
|
return valori
|
||||||
|
|
||||||
|
|
||||||
|
def carica_segreti(dati: Path) -> dict[str, str]:
|
||||||
|
"""Legge <dati>\\config.env; i segreti mancanti si generano e si riscrivono (una volta sola)."""
|
||||||
|
dati.mkdir(parents=True, exist_ok=True)
|
||||||
|
percorso = dati / "config.env"
|
||||||
|
valori = _leggi_env(percorso) if percorso.exists() else {}
|
||||||
|
nuovi = [k for k in SEGRETI if not valori.get(k)]
|
||||||
|
if nuovi:
|
||||||
|
for k in nuovi:
|
||||||
|
valori[k] = secrets.token_urlsafe(32)
|
||||||
|
righe = ["# Generato da TieMeasureFlow al primo avvio. Non condividere questo file.",
|
||||||
|
"# SETUP_PASSWORD serve per la pagina http://127.0.0.1:8000/api/setup (primo amministratore)."]
|
||||||
|
righe += [f"{k}={v}" for k, v in valori.items()]
|
||||||
|
percorso.write_text("\n".join(righe) + "\n", encoding="utf-8")
|
||||||
|
log.info("segreti generati in %s", percorso)
|
||||||
|
return valori
|
||||||
|
|
||||||
|
|
||||||
|
def trova_browser() -> str | None:
|
||||||
|
"""Chrome o Edge per kaleido (grafici dei report PDF, offline)."""
|
||||||
|
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
||||||
|
pf86 = os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")
|
||||||
|
local = os.environ.get("LOCALAPPDATA", "")
|
||||||
|
candidati = [
|
||||||
|
Path(pf) / "Google/Chrome/Application/chrome.exe",
|
||||||
|
Path(pf86) / "Google/Chrome/Application/chrome.exe",
|
||||||
|
Path(local) / "Google/Chrome/Application/chrome.exe" if local else None,
|
||||||
|
Path(pf86) / "Microsoft/Edge/Application/msedge.exe",
|
||||||
|
Path(pf) / "Microsoft/Edge/Application/msedge.exe",
|
||||||
|
]
|
||||||
|
for c in candidati:
|
||||||
|
if c is not None and c.is_file():
|
||||||
|
return str(c)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def imposta_ambiente(dati: Path, host: str, valori: dict[str, str]) -> None:
|
||||||
|
"""Variabili d'ambiente del contratto v1; da chiamare PRIMA di importare backend e frontend."""
|
||||||
|
uploads = dati / "uploads"
|
||||||
|
env = {
|
||||||
|
"APP_ENV": "production",
|
||||||
|
# barre "/" nel percorso: l'URL SQLAlchemy non vuole le barre rovesciate
|
||||||
|
"DB_URL": f"sqlite+aiosqlite:///{(dati / 'tmf.db').as_posix()}",
|
||||||
|
"UPLOAD_DIR": str(uploads),
|
||||||
|
"VISION_WORKER_URL": f"http://127.0.0.1:{PORTA_VISION}",
|
||||||
|
"VISION_RECIPES_ROOT": str(uploads),
|
||||||
|
"API_SERVER_URL": f"http://127.0.0.1:{PORTA_BACKEND}",
|
||||||
|
"SESSION_COOKIE_SECURE": "0", # niente HTTPS in locale: un cookie Secure verrebbe scartato
|
||||||
|
"SERVER_CORS_ORIGINS": f"http://127.0.0.1:{PORTA_WEB},http://localhost:{PORTA_WEB}",
|
||||||
|
**{k: valori[k] for k in SEGRETI},
|
||||||
|
}
|
||||||
|
programma = cartella_programma()
|
||||||
|
versione = programma / "vision-engine-version.txt" # scritto da build.ps1
|
||||||
|
if versione.is_file():
|
||||||
|
env["VISION_ENGINE_VERSION"] = versione.read_text(encoding="utf-8").strip()
|
||||||
|
gtk = programma / "gtk" # DLL di WeasyPrint (piano B)
|
||||||
|
if gtk.is_dir():
|
||||||
|
env["WEASYPRINT_DLL_DIRECTORIES"] = str(gtk)
|
||||||
|
browser = trova_browser()
|
||||||
|
if browser:
|
||||||
|
env["BROWSER_PATH"] = browser
|
||||||
|
os.environ.update(env)
|
||||||
|
for cartella in (uploads, dati / "camera", dati / "log"):
|
||||||
|
cartella.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _prepara_percorsi_sorgenti() -> None:
|
||||||
|
# Da sorgente i moduli del frontend (config, services, blueprints, app) e lo script camera
|
||||||
|
# non sono pacchetti: si aggiungono al path. Compilati, Nuitka li ha gia' incorporati.
|
||||||
|
if COMPILATO:
|
||||||
|
return
|
||||||
|
for p in (RADICE_SORGENTI / "scripts", RADICE_SORGENTI / "src" / "frontend" / "flask_app", RADICE_SORGENTI):
|
||||||
|
if str(p) not in sys.path:
|
||||||
|
sys.path.insert(0, str(p))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- log
|
||||||
|
|
||||||
|
def _prepara_log(dati: Path | None = None) -> None:
|
||||||
|
# Exe senza console: il log va su file; se la cartella non e' scrivibile si prosegue senza.
|
||||||
|
# Idempotente: alembic (fileConfig in env.py) azzera i gestori del root, quindi si richiama dopo.
|
||||||
|
try:
|
||||||
|
d = (dati or cartella_dati()) / "log"
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
percorso = str(d / "tiemeasureflow.log")
|
||||||
|
root = logging.getLogger()
|
||||||
|
for h in root.handlers:
|
||||||
|
if isinstance(h, logging.FileHandler) and h.baseFilename.lower() == os.path.abspath(percorso).lower():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
h = logging.FileHandler(percorso, encoding="utf-8")
|
||||||
|
h.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s"))
|
||||||
|
root.addHandler(h)
|
||||||
|
root.setLevel(logging.INFO)
|
||||||
|
for nome in ("tmf.avvio",):
|
||||||
|
logging.getLogger(nome).disabled = False # fileConfig disattiva i logger esistenti
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- rete
|
||||||
|
|
||||||
|
def _porta_aperta(porta: int) -> bool:
|
||||||
|
try:
|
||||||
|
with socket.create_connection(("127.0.0.1", porta), timeout=0.5):
|
||||||
|
return True
|
||||||
|
except OSError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _e_tmf() -> bool:
|
||||||
|
# Una copia gia' in esecuzione: il backend risponde su /api/health con il nome del servizio
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f"http://127.0.0.1:{PORTA_BACKEND}/api/health", timeout=2) as r:
|
||||||
|
return "TieMeasureFlow" in str(json.load(r).get("service", ""))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _nessun_utente(db: Path) -> bool:
|
||||||
|
"""True se il database esiste ed e' senza utenti (primo avvio: serve la pagina di setup)."""
|
||||||
|
try:
|
||||||
|
con = sqlite3.connect(f"file:{db.as_posix()}?mode=ro", uri=True, timeout=2)
|
||||||
|
try:
|
||||||
|
return con.execute("SELECT COUNT(*) FROM users").fetchone()[0] == 0
|
||||||
|
finally:
|
||||||
|
con.close()
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _apri_browser_quando_pronto(url: str, porte: tuple[int, ...]) -> None:
|
||||||
|
# Aspetta che i server ascoltino (max ~60 s), poi apre il browser predefinito
|
||||||
|
for _ in range(300):
|
||||||
|
if all(_porta_aperta(p) for p in porte):
|
||||||
|
break
|
||||||
|
time.sleep(0.2)
|
||||||
|
webbrowser.open(url)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- migrazioni
|
||||||
|
|
||||||
|
def esegui_migrazioni() -> None:
|
||||||
|
"""alembic upgrade head. Compilato: env.py e versions\\ sono soli .pyc, quindi sourceless."""
|
||||||
|
from alembic import command
|
||||||
|
from alembic.config import Config
|
||||||
|
import src.backend as pacchetto
|
||||||
|
migrazioni = Path(pacchetto.__file__).resolve().parent / "migrations"
|
||||||
|
cfg = Config(str(migrazioni / "alembic.ini"))
|
||||||
|
if COMPILATO:
|
||||||
|
cfg.set_main_option("sourceless", "true")
|
||||||
|
command.upgrade(cfg, "head")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- server
|
||||||
|
|
||||||
|
class ServerThread(threading.Thread):
|
||||||
|
"""Un server (uvicorn o waitress) in un thread; `errore` resta valorizzato se muore."""
|
||||||
|
|
||||||
|
def __init__(self, nome: str, costruisci):
|
||||||
|
super().__init__(name=nome, daemon=True)
|
||||||
|
self._costruisci = costruisci
|
||||||
|
self.server = None
|
||||||
|
self.errore: BaseException | None = None
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
try:
|
||||||
|
self.server = self._costruisci()
|
||||||
|
self.server.run()
|
||||||
|
except BaseException as e: # un server morto deve comparire nel log e fermare l'insieme
|
||||||
|
self.errore = e
|
||||||
|
log.exception("server %s terminato con errore", self.name)
|
||||||
|
|
||||||
|
def ferma(self) -> None:
|
||||||
|
s = self.server
|
||||||
|
try:
|
||||||
|
if hasattr(s, "should_exit"):
|
||||||
|
s.should_exit = True # uvicorn
|
||||||
|
elif s is not None:
|
||||||
|
s.close() # waitress
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _uvicorn(app, porta: int):
|
||||||
|
import uvicorn
|
||||||
|
# log_config=None: niente console; i thread non toccano i signal handler (uvicorn lo gestisce)
|
||||||
|
return uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=porta, log_config=None, workers=1))
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_web(host: str):
|
||||||
|
import waitress
|
||||||
|
from app import create_app # src/frontend/flask_app/app.py
|
||||||
|
return waitress.create_server(create_app(), host=host, port=PORTA_WEB, threads=8)
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_backend():
|
||||||
|
from src.backend.main import app
|
||||||
|
return _uvicorn(app, PORTA_BACKEND)
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_vision():
|
||||||
|
from src.vision_worker.main import app
|
||||||
|
return _uvicorn(app, PORTA_VISION)
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_camera(cartella: Path):
|
||||||
|
from camera_service import create_app # scripts/camera_service.py (non e' un pacchetto)
|
||||||
|
return _uvicorn(create_app(cartella), PORTA_CAMERA)
|
||||||
|
|
||||||
|
|
||||||
|
def avvia_server(host: str, dati: Path) -> list[ServerThread]:
|
||||||
|
thread = [
|
||||||
|
ServerThread("backend", costruisci_backend),
|
||||||
|
ServerThread("vision", costruisci_vision),
|
||||||
|
ServerThread("camera", lambda: costruisci_camera(dati / "camera")),
|
||||||
|
ServerThread("web", lambda: costruisci_web(host)),
|
||||||
|
]
|
||||||
|
for t in thread:
|
||||||
|
t.start()
|
||||||
|
return thread
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
_stream_sicuri()
|
||||||
|
p = argparse.ArgumentParser(prog="TieMeasureFlow", description=TITOLO)
|
||||||
|
p.add_argument("--impronta", action="store_true", help="mostra l'impronta di questo computer")
|
||||||
|
p.add_argument("--host", default="127.0.0.1", help="indirizzo di ascolto dell'interfaccia web (default 127.0.0.1)")
|
||||||
|
p.add_argument("--senza-finestre", action="store_true", help="niente MessageBox (prove automatiche)")
|
||||||
|
p.add_argument("--senza-browser", action="store_true", help="non aprire il browser (prove automatiche)")
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
_prepara_log()
|
||||||
|
|
||||||
|
if args.impronta:
|
||||||
|
try:
|
||||||
|
impronta = lic.impronta()
|
||||||
|
except Exception as e:
|
||||||
|
messaggio(f"Impronta non disponibile: {e}", senza_finestre=args.senza_finestre)
|
||||||
|
return 1
|
||||||
|
print(impronta) if sys.stdout is not None else None
|
||||||
|
messaggio(f"Impronta di questo computer:\n{impronta}", errore=False, senza_finestre=args.senza_finestre)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not controlla_licenza(args.senza_finestre):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if _porta_aperta(PORTA_WEB):
|
||||||
|
if _e_tmf():
|
||||||
|
# TieMeasureFlow e' gia' in esecuzione: basta riaprire la pagina
|
||||||
|
if not args.senza_browser:
|
||||||
|
webbrowser.open(f"http://127.0.0.1:{PORTA_WEB}/")
|
||||||
|
return 0
|
||||||
|
messaggio(f"La porta {PORTA_WEB} e' occupata da un altro programma: chiuderlo e riprovare.",
|
||||||
|
senza_finestre=args.senza_finestre)
|
||||||
|
return 2
|
||||||
|
occupate = [str(x) for x in (PORTA_BACKEND, PORTA_VISION, PORTA_CAMERA) if _porta_aperta(x)]
|
||||||
|
if occupate:
|
||||||
|
messaggio(f"Porte occupate da altri programmi: {', '.join(occupate)}. Chiuderli e riprovare.",
|
||||||
|
senza_finestre=args.senza_finestre)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
dati = cartella_dati()
|
||||||
|
try:
|
||||||
|
valori = carica_segreti(dati)
|
||||||
|
imposta_ambiente(dati, args.host, valori)
|
||||||
|
_prepara_percorsi_sorgenti()
|
||||||
|
primo_avvio = not (dati / "tmf.db").exists()
|
||||||
|
esegui_migrazioni()
|
||||||
|
except Exception as e:
|
||||||
|
_prepara_log(dati)
|
||||||
|
log.exception("avvio non riuscito")
|
||||||
|
messaggio(f"Avvio non riuscito: {e}\nDettagli in {dati / 'log' / 'tiemeasureflow.log'}",
|
||||||
|
senza_finestre=args.senza_finestre)
|
||||||
|
return 3
|
||||||
|
_prepara_log(dati) # alembic ha riconfigurato il logging
|
||||||
|
log.info("migrazioni applicate (primo avvio: %s)", primo_avvio)
|
||||||
|
|
||||||
|
thread = avvia_server(args.host, dati)
|
||||||
|
if not args.senza_browser:
|
||||||
|
# database senza utenti: si apre la pagina di setup del backend (SETUP_PASSWORD in config.env)
|
||||||
|
url = (f"http://127.0.0.1:{PORTA_BACKEND}/api/setup" if _nessun_utente(dati / "tmf.db")
|
||||||
|
else f"http://127.0.0.1:{PORTA_WEB}/")
|
||||||
|
threading.Thread(target=_apri_browser_quando_pronto,
|
||||||
|
args=(url, (PORTA_WEB, PORTA_BACKEND)), daemon=True).start()
|
||||||
|
|
||||||
|
# Si resta vivi finche' tutti i server girano; se uno muore si ferma tutto
|
||||||
|
try:
|
||||||
|
while all(t.is_alive() for t in thread):
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
morti = [t for t in thread if not t.is_alive()]
|
||||||
|
for t in thread:
|
||||||
|
t.ferma()
|
||||||
|
if morti:
|
||||||
|
messaggio("Un componente si e' fermato: " + ", ".join(f"{t.name} ({t.errore})" for t in morti)
|
||||||
|
+ f"\nDettagli in {dati / 'log' / 'tiemeasureflow.log'}", senza_finestre=args.senza_finestre)
|
||||||
|
return 4
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -87,6 +87,7 @@ vision-worker = [
|
|||||||
windows = [
|
windows = [
|
||||||
"aiosqlite>=0.20.0", # database SQLite su file (DB_URL, piano A)
|
"aiosqlite>=0.20.0", # database SQLite su file (DB_URL, piano A)
|
||||||
"waitress>=3.0.0", # server WSGI del frontend Flask (gunicorn non gira su Windows)
|
"waitress>=3.0.0", # server WSGI del frontend Flask (gunicorn non gira su Windows)
|
||||||
|
"vs-camera[genicam]; python_version>='3.13'", # scripts/camera_service.py: camere GigE/GenTL (producer .cti del vendor)
|
||||||
"vs-license; python_version>='3.13'", # licenza Tielogic v1 (impronta macchina, firma Ed25519)
|
"vs-license; python_version>='3.13'", # licenza Tielogic v1 (impronta macchina, firma Ed25519)
|
||||||
"nuitka>=2.5", # compilazione standalone
|
"nuitka>=2.5", # compilazione standalone
|
||||||
"ordered-set>=4.1", # richiesto da Nuitka per la modalita' standalone
|
"ordered-set>=4.1", # richiesto da Nuitka per la modalita' standalone
|
||||||
@@ -117,6 +118,7 @@ vs-measure = { path = "vendor/visionsuite/packages/vs-measure", editable = true
|
|||||||
vs-pm2d = { path = "vendor/visionsuite/packages/vs-pm2d", editable = true }
|
vs-pm2d = { path = "vendor/visionsuite/packages/vs-pm2d", editable = true }
|
||||||
vs-tools = { path = "vendor/visionsuite/packages/vs-tools", editable = true } # #46
|
vs-tools = { path = "vendor/visionsuite/packages/vs-tools", editable = true } # #46
|
||||||
vs-dxf = { path = "vendor/visionsuite/packages/vs-dxf", editable = true } # #46
|
vs-dxf = { path = "vendor/visionsuite/packages/vs-dxf", editable = true } # #46
|
||||||
|
vs-camera = { path = "vendor/visionsuite/packages/vs-camera", editable = true } # #76 camera di stazione
|
||||||
vs-license = { path = "vendor/visionsuite/packages/vs-license", editable = true } # #76 licenza del pacchetto Windows
|
vs-license = { path = "vendor/visionsuite/packages/vs-license", editable = true } # #76 licenza del pacchetto Windows
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
@@ -783,6 +783,33 @@ woff = [
|
|||||||
{ name = "zopfli" },
|
{ name = "zopfli" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "genicam"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/39/97c9a39fde61441731a383bde6a86b079a5a9d1d22c6d9eb16556b77016e/genicam-1.6.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:2a5bfeb3b9796cf6667192c2957f8ea7ec240ff42e76cc6e3e05d1d0230bf68a", size = 2556729, upload-time = "2026-09-01T08:26:57.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/1b/bf993221a155d2fd35c8201be97cd0376e005bf1cc82dec27105a712d3b1/genicam-1.6.0-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:4207d7ccee9deb755ad372c0b473fd2f23e4e42da61e06e4a7873d199b883a0f", size = 2604157, upload-time = "2026-09-01T08:26:58.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/92/68c0033f076ad1defed2e6bf1cec8fc720a24622fb0389013b058e841234/genicam-1.6.0-cp311-cp311-manylinux2014_armv7l.whl", hash = "sha256:a750ffb11194f02740531bc0f62bf7013d39f2eea65f5e283c7811ab757fc987", size = 2190252, upload-time = "2026-09-01T08:27:00.126Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/b3/252e5d64022984de4a78220c456f1f91742dbd7bb02d5b3926ed4c228015/genicam-1.6.0-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:589b1152f22ca766f8b5e1dff9c1b4006c2eb7f5ee33152619ef6ece8a5a3f47", size = 2800290, upload-time = "2026-09-01T08:27:01.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/ec/19092034d0f66646d97b85e1549b6b4e2448f5287bb3a76b5a8cd677ba4a/genicam-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:ef1d25c7e9823689dbfb9e08d8fdb46e0f05982ca9c27d1eeacc1aa8fcd69f6e", size = 1802065, upload-time = "2026-09-01T08:27:02.555Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/84/0dc939a435deef267d82bc6aaa4b7ed9c89510766454640622608d0e7ce5/genicam-1.6.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:d0becabb469d0f51f9d4820d655d2788c620a8ee1c8abcfa4703cac87ba27f53", size = 2557117, upload-time = "2026-09-01T08:27:03.87Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/68/499657bf3e5111fe1c87e3364c2ae6b59fe912d7184fccf591eafd762ceb/genicam-1.6.0-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:3a1a16b89fec69d52142781f280ce2c01a4c1422da1cd08893ed1cd630d11c30", size = 2607960, upload-time = "2026-09-01T08:27:05.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/ac/3b8496c446715008e77dda7439735c428f163aab0a3f643352407a259a52/genicam-1.6.0-cp312-cp312-manylinux2014_armv7l.whl", hash = "sha256:b65beca4b425442922a2f2beefae1e3109ce166cfd726fcd9d2ac59ff1d57e24", size = 2189851, upload-time = "2026-09-01T08:27:06.516Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/9c/49d68f5055e970e4e4d834bc3df686a01f7bd82e070888fc7ad2c86e2eaf/genicam-1.6.0-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:83da90404da189a87273864f2c97c761c1807fc6fc532fabd3bf50c0ae28f8ae", size = 2799503, upload-time = "2026-09-01T08:27:07.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/4a/e643cac4031ad7b1780ab19bcdeb6b0545e829b2ee10ea37ca1185d9417d/genicam-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d2bb3f39443c5bc248fcbde83a3a973370ca43588a7c1ded7d7324d7d9427d3", size = 1802200, upload-time = "2026-09-01T08:27:09.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/f1/b5f641ad64c296462ab2ca314b8a471827142b949606ebe8b69bff7e6a08/genicam-1.6.0-cp313-cp313-macosx_13_0_arm64.whl", hash = "sha256:f5c35275014f4793b34a21ebe8bbcb269a87b5d1bf129daf2818c6160f8758c9", size = 2557108, upload-time = "2026-09-01T08:27:11.211Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/59/86ae5cbc1a89f70c2b3b906e692a46b977e9230664781cd5a304012f6b20/genicam-1.6.0-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:f4d6496660507b32a7f6ef05f5344bcd305ea3e2158a41bf53169663d7a72e70", size = 2608137, upload-time = "2026-09-01T08:27:12.405Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/37/25a84938b5ec34fc6123006a3f4173fead4fde3b460b516749a8ae8c1b02/genicam-1.6.0-cp313-cp313-manylinux2014_armv7l.whl", hash = "sha256:efe646df5e8ca76208c6621e8a5dbea3e9909641801787993ea2d7ec4a704c09", size = 2189839, upload-time = "2026-09-01T08:27:13.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/eb/37363beb1880674ff0976ff7607e2e045933c6388d08d98bc0b512df941c/genicam-1.6.0-cp313-cp313-manylinux2014_x86_64.whl", hash = "sha256:a342980e80735b7b1e4f18f6c33d64eccc3e95887dd72f302409f1296bef3dac", size = 2799503, upload-time = "2026-09-01T08:27:15.253Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/38/895e9effffc858ff06996a9eed28decdc9b5b2176b2cfd82df8c4f65bdec/genicam-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:740156c5fa8d82330d36314112f4b755e8333c16f82792174f1a2a2e150519ae", size = 1802243, upload-time = "2026-09-01T08:27:16.576Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/26/98a864061d25e78b96b7d621814b399a78fb7603ae6a6ac871648587bc49/genicam-1.6.0-cp314-cp314-macosx_13_0_arm64.whl", hash = "sha256:064a87188b6102bac03a8ab1ccff5280e8fa4cf6378448e6286ed6f4cc9f7a64", size = 2557508, upload-time = "2026-09-01T08:27:17.763Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/32/2ef1193d27bb375e3d9822be883e987d75203fcb417bafa96fd77fe8be18/genicam-1.6.0-cp314-cp314-manylinux2014_aarch64.whl", hash = "sha256:3dcf4af1fabb1cee06cac0bed6560f0e49180dab1e078501949eaf07596736c3", size = 2609251, upload-time = "2026-09-01T08:27:19.161Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/06/ea8b6c2f4536cd8f54961040edae1ad9ff8a3dced2081a2377dac600bb73/genicam-1.6.0-cp314-cp314-manylinux2014_armv7l.whl", hash = "sha256:e3f9ff542bbd2fe389e1a94e4a694ec34f94ab378247c5f7e761b78ff5891d24", size = 2189734, upload-time = "2026-09-01T08:27:20.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/ff/a8c90b9a7d99fa6008437491f1198264c22d49572a0013a05bc1c579e034/genicam-1.6.0-cp314-cp314-manylinux2014_x86_64.whl", hash = "sha256:d1e12e276eab6dbe8dec37e060ea5a8cc5758cc8c3502a048782e38a0451517d", size = 2799526, upload-time = "2026-09-01T08:27:21.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/18/c6f0b7f3da480aac0c86d104664e9cf144675986cc4e3f108c909bec0ddf/genicam-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:7630b7844b273080f21f01881c56667cc68521186b75dd15798f5dee1f8f1037", size = 1846842, upload-time = "2026-09-01T08:27:23.18Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.4.0"
|
version = "3.4.0"
|
||||||
@@ -861,6 +888,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "harvesters"
|
||||||
|
version = "1.4.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "genicam" },
|
||||||
|
{ name = "numpy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d3/18/ee45d896fee51dee2e35df9d371e2d213ed4488ddea4dd4bd1df6f92a5fb/harvesters-1.4.3.tar.gz", hash = "sha256:c237dd402735a66ebd570865113a3e80ae19161a21706aed80691627a8cb7da2", size = 104391, upload-time = "2024-05-16T17:09:41.805Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/ab/295e5ec52cc2859ad8fa0d56f953cefa26f3e1eb28cc937162d7df7df872/harvesters-1.4.3-py3-none-any.whl", hash = "sha256:74994cedc941051b9ce9527c5b336e611589a1bc209bc03186c49102a55f1f1f", size = 100253, upload-time = "2024-05-16T17:09:27.933Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "1.0.9"
|
version = "1.0.9"
|
||||||
@@ -2063,6 +2103,7 @@ windows = [
|
|||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
{ name = "nuitka" },
|
{ name = "nuitka" },
|
||||||
{ name = "ordered-set" },
|
{ name = "ordered-set" },
|
||||||
|
{ name = "vs-camera", extra = ["genicam"], marker = "python_full_version >= '3.13'" },
|
||||||
{ name = "vs-license", marker = "python_full_version >= '3.13'" },
|
{ name = "vs-license", marker = "python_full_version >= '3.13'" },
|
||||||
{ name = "waitress" },
|
{ name = "waitress" },
|
||||||
{ name = "zstandard" },
|
{ name = "zstandard" },
|
||||||
@@ -2117,6 +2158,7 @@ windows = [
|
|||||||
{ name = "aiosqlite", specifier = ">=0.20.0" },
|
{ name = "aiosqlite", specifier = ">=0.20.0" },
|
||||||
{ name = "nuitka", specifier = ">=2.5" },
|
{ name = "nuitka", specifier = ">=2.5" },
|
||||||
{ name = "ordered-set", specifier = ">=4.1" },
|
{ name = "ordered-set", specifier = ">=4.1" },
|
||||||
|
{ name = "vs-camera", extras = ["genicam"], marker = "python_full_version >= '3.13'", editable = "vendor/visionsuite/packages/vs-camera" },
|
||||||
{ name = "vs-license", marker = "python_full_version >= '3.13'", editable = "vendor/visionsuite/packages/vs-license" },
|
{ name = "vs-license", marker = "python_full_version >= '3.13'", editable = "vendor/visionsuite/packages/vs-license" },
|
||||||
{ name = "waitress", specifier = ">=3.0.0" },
|
{ name = "waitress", specifier = ">=3.0.0" },
|
||||||
{ name = "zstandard", specifier = ">=0.22" },
|
{ name = "zstandard", specifier = ">=0.22" },
|
||||||
@@ -2265,6 +2307,31 @@ requires-dist = [
|
|||||||
]
|
]
|
||||||
provides-extras = ["dev", "fast", "dxf"]
|
provides-extras = ["dev", "fast", "dxf"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vs-camera"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { editable = "vendor/visionsuite/packages/vs-camera" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "opencv-python" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
genicam = [
|
||||||
|
{ name = "harvesters" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "flask", marker = "extra == 'dev'", specifier = ">=3.0" },
|
||||||
|
{ name = "harvesters", marker = "extra == 'genicam'", specifier = ">=1.4" },
|
||||||
|
{ name = "netifaces", marker = "extra == 'rete'", specifier = ">=0.11" },
|
||||||
|
{ name = "numpy", specifier = ">=1.24" },
|
||||||
|
{ name = "opencv-python", specifier = ">=4.8" },
|
||||||
|
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" },
|
||||||
|
]
|
||||||
|
provides-extras = ["genicam", "rete", "dev"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vs-dxf"
|
name = "vs-dxf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user