228 lines
8.1 KiB
Python
228 lines
8.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
# © 2021-2026 TechnoLibre (http://www.technolibre.ca)
|
||
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||
|
|
|
||
|
|
"""Réveiller l'instance dès qu'elle écoute, puis disparaître.
|
||
|
|
|
||
|
|
Odoo ne charge le registre d'une base qu'à la PREMIÈRE requête qui la
|
||
|
|
concerne. Sur une base migrée, ce premier chargement prend des dizaines de
|
||
|
|
secondes — et c'est la personne qui ouvre la page qui les attend. Lancée
|
||
|
|
en parallèle de `run.sh`, cette sonde prend ce temps à sa place.
|
||
|
|
|
||
|
|
Trois règles, et elles tiennent au fait qu'elle tourne À CÔTÉ du serveur
|
||
|
|
--------------------------------------------------------------------
|
||
|
|
1. Elle se TAIT. Une seule ligne, au démarrage, pour dire quelle adresse
|
||
|
|
elle a choisie — c'est ce qui permet de comprendre après coup un
|
||
|
|
réveil qui n'a pas eu lieu. Ensuite plus rien : ni échec, ni trace.
|
||
|
|
Un serveur qu'on arrête au bout de dix secondes est un cas ordinaire,
|
||
|
|
pas une erreur, et un message d'erreur dans le journal de `run.sh`
|
||
|
|
ferait chercher une panne qui n'existe pas.
|
||
|
|
2. Elle s'arrête à la PREMIÈRE réponse, quelle qu'elle soit. Un 303 vers
|
||
|
|
/web/login, un 404, un 500 : tous prouvent que le registre est chargé,
|
||
|
|
ce qui est l'objet. Seul le refus de connexion ne compte pas.
|
||
|
|
3. Elle meurt au bout de deux minutes. Le réveil est un confort ; une
|
||
|
|
sonde qui survit à son serveur en est un autre, mauvais.
|
||
|
|
|
||
|
|
Où elle trouve le port
|
||
|
|
----------------------
|
||
|
|
D'abord la ligne de commande — `-p 8090` l'emporte sur tout le reste,
|
||
|
|
c'est ce que la personne vient de taper. Puis `config.conf`, que `run.sh`
|
||
|
|
a déjà résolu et qu'elle reçoit tel quel. Le journal d'exécution ne sert
|
||
|
|
qu'en dernier recours : Odoo y écrit « HTTP service (werkzeug) running on
|
||
|
|
127.0.0.1:8069 » une fois qu'il écoute, ce qui arrive parfois APRÈS le
|
||
|
|
premier essai — mais c'est aussi la seule source exacte quand le port
|
||
|
|
demandé était déjà pris.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import re
|
||
|
|
import sys
|
||
|
|
import time
|
||
|
|
|
||
|
|
sys.path.append(
|
||
|
|
os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||
|
|
)
|
||
|
|
|
||
|
|
from script.analyse import lib_analyse # noqa: E402
|
||
|
|
|
||
|
|
DELAI_TOTAL = 120.0
|
||
|
|
DELAI_ENTRE_ESSAIS = 1.0
|
||
|
|
DELAI_REQUETE = 10.0
|
||
|
|
PORT_PAR_DEFAUT = 8069
|
||
|
|
HOTE_PAR_DEFAUT = "127.0.0.1"
|
||
|
|
|
||
|
|
# « HTTP service (werkzeug) running on 127.0.0.1:8069 »
|
||
|
|
MOTIF_JOURNAL = re.compile(
|
||
|
|
r"HTTP service \(werkzeug\) running on ([^:\s]+):(\d+)"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def port_de_la_ligne(argv):
|
||
|
|
"""Le port demandé sur la ligne de commande, ou None.
|
||
|
|
|
||
|
|
`-p 8090`, `--http-port 8090` et `--http-port=8090` : les trois
|
||
|
|
formes existent, et Odoo les accepte toutes.
|
||
|
|
"""
|
||
|
|
for index, argument in enumerate(argv):
|
||
|
|
if argument in ("-p", "--http-port") and index + 1 < len(argv):
|
||
|
|
valeur = argv[index + 1]
|
||
|
|
if valeur.isdigit():
|
||
|
|
return int(valeur)
|
||
|
|
if argument.startswith("--http-port="):
|
||
|
|
valeur = argument.split("=", 1)[1]
|
||
|
|
if valeur.isdigit():
|
||
|
|
return int(valeur)
|
||
|
|
return None
|
||
|
|
|
||
|
|
|
||
|
|
def adresse_du_journal(chemin, taille=8192):
|
||
|
|
"""(hôte, port) lus dans la fin du journal, ou (None, None).
|
||
|
|
|
||
|
|
On ne lit que la queue : un journal de migration pèse des mégaoctets,
|
||
|
|
et la ligne qu'on cherche est la dernière écrite.
|
||
|
|
"""
|
||
|
|
if not chemin or not os.path.isfile(chemin):
|
||
|
|
return None, None
|
||
|
|
try:
|
||
|
|
with open(chemin, "rb") as handle:
|
||
|
|
handle.seek(0, os.SEEK_END)
|
||
|
|
debut = max(0, handle.tell() - taille)
|
||
|
|
handle.seek(debut)
|
||
|
|
queue = handle.read().decode("utf-8", "replace")
|
||
|
|
except OSError:
|
||
|
|
return None, None
|
||
|
|
trouve = MOTIF_JOURNAL.findall(queue)
|
||
|
|
if not trouve:
|
||
|
|
return None, None
|
||
|
|
hote, port = trouve[-1]
|
||
|
|
return hote, int(port)
|
||
|
|
|
||
|
|
|
||
|
|
def adresse(argv=(), config_path=None, journal=None):
|
||
|
|
"""(hôte, port) à réveiller, dans l'ordre de ce qui fait autorité."""
|
||
|
|
config = lib_analyse.read_config(config_path)
|
||
|
|
|
||
|
|
port = port_de_la_ligne(argv)
|
||
|
|
hote = None
|
||
|
|
if port is None:
|
||
|
|
hote, port = adresse_du_journal(journal)
|
||
|
|
if port is None:
|
||
|
|
brut = str(config.get("http_port", "")).strip()
|
||
|
|
port = int(brut) if brut.isdigit() else PORT_PAR_DEFAUT
|
||
|
|
|
||
|
|
if not hote:
|
||
|
|
interface = str(config.get("http_interface", "")).strip()
|
||
|
|
# Une interface vide veut dire « toutes » : on se parle à soi-même.
|
||
|
|
# 0.0.0.0 n'est pas une adresse de destination.
|
||
|
|
if interface.lower() in ("", "false", "none", "0.0.0.0", "::"):
|
||
|
|
hote = HOTE_PAR_DEFAUT
|
||
|
|
else:
|
||
|
|
hote = interface
|
||
|
|
return hote, port
|
||
|
|
|
||
|
|
|
||
|
|
def base_a_reveiller(argv=(), config_path=None):
|
||
|
|
"""La base dont on veut le registre, ou None.
|
||
|
|
|
||
|
|
`-d` d'abord : c'est celle que `run.sh` vient de choisir. Sinon
|
||
|
|
`db_name` de la configuration, qu'Odoo utiliserait de toute façon.
|
||
|
|
"""
|
||
|
|
for index, argument in enumerate(argv):
|
||
|
|
if argument in ("-d", "--database") and index + 1 < len(argv):
|
||
|
|
return argv[index + 1]
|
||
|
|
if argument.startswith("--database="):
|
||
|
|
return argument.split("=", 1)[1]
|
||
|
|
valeur = str(
|
||
|
|
lib_analyse.read_config(config_path).get("db_name", "")
|
||
|
|
).strip()
|
||
|
|
return valeur if valeur.lower() not in ("", "false", "none") else None
|
||
|
|
|
||
|
|
|
||
|
|
def url(hote, port, database=None):
|
||
|
|
"""L'adresse à demander.
|
||
|
|
|
||
|
|
`/web/login` plutôt que `/` : elle ne redirige pas vers un tableau de
|
||
|
|
bord, ne crée pas de session, et suffit à faire charger le registre.
|
||
|
|
Le paramètre `db` dit LAQUELLE quand plusieurs bases répondent.
|
||
|
|
"""
|
||
|
|
base = f"http://{hote}:{port}/web/login"
|
||
|
|
if database:
|
||
|
|
from urllib.parse import quote
|
||
|
|
|
||
|
|
return f"{base}?db={quote(database)}"
|
||
|
|
return base
|
||
|
|
|
||
|
|
|
||
|
|
def sonder(
|
||
|
|
cible,
|
||
|
|
delai_total=DELAI_TOTAL,
|
||
|
|
entre_essais=DELAI_ENTRE_ESSAIS,
|
||
|
|
requete=DELAI_REQUETE,
|
||
|
|
horloge=time.monotonic,
|
||
|
|
dormir=time.sleep,
|
||
|
|
):
|
||
|
|
"""Demander jusqu'à obtenir une réponse. Rendre True si elle est venue.
|
||
|
|
|
||
|
|
N'importe quel code HTTP est une réussite : 303, 404 ou 500 prouvent
|
||
|
|
tous que le serveur a répondu, donc que le registre est chargé. Seul
|
||
|
|
le refus de connexion fait recommencer.
|
||
|
|
"""
|
||
|
|
import urllib.error
|
||
|
|
import urllib.request
|
||
|
|
|
||
|
|
fin = horloge() + delai_total
|
||
|
|
while horloge() < fin:
|
||
|
|
try:
|
||
|
|
with urllib.request.urlopen(cible, timeout=requete):
|
||
|
|
return True
|
||
|
|
except urllib.error.HTTPError:
|
||
|
|
# Le serveur a répondu, et c'est tout ce qu'on demandait.
|
||
|
|
return True
|
||
|
|
except Exception: # noqa: BLE001 - un serveur pas encore prêt
|
||
|
|
pass
|
||
|
|
dormir(entre_essais)
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv=None):
|
||
|
|
"""Toujours rendre 0 : personne ne doit lire ce code de retour.
|
||
|
|
|
||
|
|
La sonde tourne en arrière-plan d'un `run.sh` qui, lui, rapporte
|
||
|
|
l'état d'Odoo. Un code non nul finirait dans un journal et ferait
|
||
|
|
croire à une panne du serveur.
|
||
|
|
"""
|
||
|
|
import argparse
|
||
|
|
|
||
|
|
parser = argparse.ArgumentParser(description="Warm up the HTTP registry.")
|
||
|
|
parser.add_argument("-c", "--config")
|
||
|
|
parser.add_argument("-d", "--database")
|
||
|
|
parser.add_argument("--log")
|
||
|
|
parser.add_argument("--timeout", type=float, default=DELAI_TOTAL)
|
||
|
|
parser.add_argument("--quiet", action="store_true")
|
||
|
|
parser.add_argument("odoo_args", nargs="*")
|
||
|
|
# `parse_known_args` et non `parse_args` : les arguments d'Odoo nous
|
||
|
|
# traversent, et l'un d'eux ressemblera un jour à une option qu'on ne
|
||
|
|
# connaît pas. `parse_args` sortirait alors par SystemExit — ce qui
|
||
|
|
# romprait la seule promesse qui compte ici : ne jamais gêner.
|
||
|
|
args, restes = parser.parse_known_args(argv)
|
||
|
|
args.odoo_args = list(args.odoo_args) + list(restes)
|
||
|
|
|
||
|
|
try:
|
||
|
|
hote, port = adresse(args.odoo_args, args.config, args.log)
|
||
|
|
database = args.database or base_a_reveiller(
|
||
|
|
args.odoo_args, args.config
|
||
|
|
)
|
||
|
|
cible = url(hote, port, database)
|
||
|
|
if not args.quiet:
|
||
|
|
print(f"🔥 warmup: {cible}", flush=True)
|
||
|
|
sonder(cible, delai_total=args.timeout)
|
||
|
|
except Exception: # noqa: BLE001 - le réveil est un confort, pas un dû
|
||
|
|
pass
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|