Commit e8f8a3e8 by Szeberényi Imre

update FIX

parent 566e5f53
......@@ -22,9 +22,11 @@ VM kontextualizálását végzi. A felhő menedzserrel egy virtuális soros vona
```
sudo -i
cd /root
chmod oug+x . # fontos a pont!
mkvirtualenv agent
workon agent
git clone https://git.ik.bme.hu/CIRCLE3/agent.git
chmod oug+rx agent
cd agent
python agent.py --install
```
......@@ -36,7 +38,6 @@ VM kontextualizálását végzi. A felhő menedzserrel egy virtuális soros vona
fájloknak olvashatóknak kell lennie a cloud user számára, hogy a vm_reneval működjön.
Ekkor a wm_renewal-bol kivehető a sudo. (Esetleg spec sudo engedéllyel is megoldhato a dolog)
## Windows ##
* Bundled python alkalmazások, melyekből az első kettő szervízként fut, a harmadik a cloud user belépésekor indul
* Fájlok:
......
......@@ -12,10 +12,12 @@ import win32service
import win32serviceutil
from utils import setup_logging
from windows.winutils import getRegistryVal, get_windows_version, servicePostUpdate
from windows.winutils import getRegistryVal, get_windows_version, update_component
workdir = r"C:\circle"
if getattr(sys, "frozen", False):
logger = setup_logging(logfile=r"C:\Circle\watchdog.log")
logger = setup_logging(logfile=workdir + r"\watchdog.log")
else:
logger = setup_logging()
fh = NTEventLogHandler("CIRCLE Watchdog")
......@@ -24,7 +26,7 @@ formatter = logging.Formatter(
fh.setFormatter(formatter)
logger.addHandler(fh)
level = getRegistryVal(
r"SYSTEM\\CurrentControlSet\\Services\\CIRCLE-Agent\\Parameters",
r"SYSTEM\\CurrentControlSet\\Services\\CIRCLE-Watchdog\\Parameters",
"LogLevel",
"INFO"
)
......@@ -55,7 +57,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework):
timo = timo_base
sleep(6*timo) # boot process may have triggered the agent, so we are patient
while not self._stopped:
logger.debug("checking....(timo: %d", timo)
logger.debug("checking....(timo: %d)", timo)
if not check_service(checked_service):
logger.info("Service %s is not running.", checked_service)
try:
......@@ -82,7 +84,7 @@ class AppServerSvc (win32serviceutil.ServiceFramework):
exe = "circle-watchdog.exe"
exe_path = join(working_dir, exe)
logger.debug("hahooo %s %s", self._svc_name_, exe_path)
if servicePostUpdate(self._svc_name_, exe_path):
if update_component(self._svc_name_ + ".state", workdir) == "exit":
# Service updated, Restart needed
logger.debug("update....")
self.ReportServiceStatus(
......
{
"img": "circle-watchdog.exe",
"img_pending": "agent-wdog-winservice.exe",
"service": "circle-watchdog",
"state": "idle",
"last_checked": "2026-01-15T18:08:08.331288+00:00Z",
"status": "idle"
}
\ No newline at end of file
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-wdog-winservice.py
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-winservice.py
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F circle-notify.pyw
\ No newline at end of file
pyinstaller --clean -F --path . --hidden-import pkg_resources --hidden-import infi --hidden-import win32timezone --hidden-import win32traceutil -F agent-notify.pyw
\ No newline at end of file
......@@ -20,7 +20,8 @@ from .network import change_ip_windows
from context import BaseContext
from windows.winutils import (
is_frozen_exe, copy_running_exe,
update_service_binpath, servicePostUpdate
update_service_binpath, servicePostUpdate,
run_with_powershell
)
try:
......@@ -34,12 +35,13 @@ logger = logging.getLogger(__name__)
class Context(BaseContext):
service_name = "CIRCLE-agent"
working_dir = r"C:\circle"
workdir = r"C:\circle"
update_cmd = "update.ps1"
exe = "circle-agent.exe"
@staticmethod
def postUpdate():
exe_path = join(Context.working_dir, Context.exe)
exe_path = join(Context.workdir, Context.exe)
return servicePostUpdate(Context.service_name, exe_path)
@staticmethod
......@@ -119,13 +121,22 @@ class Context(BaseContext):
raise Exception("Checksum missmatch the file is damaged.")
decoded = BytesIO(b64decode(data))
try:
tar = tarfile.TarFile.open("dummy", fileobj=decoded, mode='r|gz')
tar.extractall(Context.working_dir)
tar = tarfile.TarFile.open("dummy", fileobj=decoded, mode='r:gz')
tar.extractall(Context.workdir)
logger.debug("%s file extracted", filename)
except tarfile.ReadError as e:
logger.error(e)
return
logger.info("Transfer completed!")
old_exe = update_service_binpath("CIRCLE-agent", join(Context.working_dir, executable))
if executable.startswith("0000_"):
logger.debug("starting %s in %s", executable, Context.workdir)
start_process_async(executable, workdir=Context.workdir)
else:
old_exe = update_service_binpath("CIRCLE-agent", join(Context.workdir, executable))
logger.info('%s Updated', old_exe)
if os.path.exists(join(Context.workdir, Context.update_cmd)):
logger.debug("starting %s in %s", Context.update_cmd, Context.workdir)
start_process_async(Context.update_cmd, workdir=Context.workdir, delay_seconds=60)
Context.exit_code = 1
reactor.callLater(0, reactor.stop)
......@@ -148,7 +159,7 @@ class Context(BaseContext):
@staticmethod
def get_agent_version():
try:
with open(join(Context.working_dir, 'version.txt')) as f:
with open(join(Context.workdir, 'version.txt')) as f:
return f.readline()
except IOError:
return None
import os
import sys
import logging
import subprocess
import json
import tempfile
from datetime import datetime, timezone
from shutil import copy
from os.path import join, normcase, normpath
......@@ -9,7 +13,7 @@ from winreg import (
HKEY_LOCAL_MACHINE, KEY_ALL_ACCESS, KEY_READ
)
logger = logging.getLogger()
logger = logging.getLogger(__name__)
def is_frozen_exe() -> bool:
return bool(getattr(sys, "frozen", False))
......@@ -33,7 +37,7 @@ def update_service_binpath(service_name: str, exe_path: str) -> str:
def copy_running_exe(dest: str) -> bool:
"""
Startup helper:
- If the runnin executable is not
- If the running executable image is not the dest
then copy it to dest (overwriting old dest if present),
- Otherwise do nothing.
......@@ -74,11 +78,233 @@ def getRegistryVal(reg_path: str, name: str, default=None):
with OpenKeyEx(HKEY_LOCAL_MACHINE, reg_path, 0, KEY_READ) as key:
value, _ = QueryValueEx(key, name)
except Exception as e:
logging.debug("Registry read failed %s\\%s: %s",
logger.debug("Registry read failed %s\\%s: %s",
reg_path, name, e
)
return value
def start_process_async(path, args=None, workdir=None, delay_seconds=0):
"""
Starts a process asynchronously (fire-and-forget) using cmd.exe.
Parameters:
path (str):
Supported types:
- .exe, .bat, .cmd → started directly by cmd.exe
- .ps1 → started via powershell.exe (-File)
args Command-line arguments passed to the target process.
Arguments are appended as-is; quoting is the caller's responsibility.
workdir Working directory for the started process.
delay_seconds
Notes:
- The function does not wait for the process to finish.
- No stdout/stderr is captured; all output is discarded.
"""
if args is None:
args = []
ext = os.path.splitext(path)[1].lower()
if ext == ".ps1":
cmdline = f'powershell -NoProfile -ExecutionPolicy Bypass -File "{path}"'
if args:
cmdline += " " + " ".join(args)
else:
cmdline = f'"{path}"'
if args:
cmdline += " " + " ".join(args)
parts = []
if workdir:
parts.append(f'cd /d "{workdir}"')
parts.append(f'timeout /t {int(delay_seconds)} /nobreak >nul')
parts.append(cmdline)
cmd = " & ".join(parts)
try:
subprocess.Popen(
["cmd.exe", "/c", cmd],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
except Exception:
logger.exception(f"Execution failed for: {path} (workdir={workdir})")
def file_is_newer(file_a, file_b):
"""
Returns True if file_a is newer than file_b.
Raises FileNotFoundError if any file does not exist.
"""
if not os.path.exists(file_a):
raise FileNotFoundError(file_a)
if not os.path.exists(file_b):
raise FileNotFoundError(file_b)
return os.path.getmtime(file_a) > os.path.getmtime(file_b)
def start_delayed_process(command, delay_seconds, workdir=None):
"""
Starts a delayed process asynchronously using cmd.exe (no threads).
"""
if isinstance(command, list):
exe = command[0]
args = " ".join(command[1:])
else:
exe = command
args = ""
cmd = (
f'timeout /t {int(delay_seconds)} /nobreak >nul '
f'& "{exe}" {args}'
)
subprocess.Popen(
["cmd.exe", "/c", cmd],
cwd=workdir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
def load_json(path, default=None):
"""
Loads a JSON file and returns a dict.
If file does not exist, returns default (or empty dict).
"""
if not os.path.exists(path):
return default if default is not None else {}
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def save_json(path, data):
"""
Saves dict to JSON file atomically.
"""
directory = os.path.dirname(path)
if directory:
os.makedirs(directory, exist_ok=True)
fd, temp_path = tempfile.mkstemp(
dir=directory,
prefix=".tmp_",
suffix=".json"
)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(temp_path, path)
finally:
if os.path.exists(temp_path):
try:
os.remove(temp_path)
except OSError:
pass
import os
import sys
import shutil
def update_component(state_file: str, base_dir: str) -> str:
"""
Component self-update state handler.
Uses a per-component JSON state file to coordinate a two-phase update:
1) If a newer pending image is detected, transitions from IDLE → PENDING
and schedules the pending image to start (or updates the service binpath).
2) When running from the pending image, copies it over the standard image,
updates the state to COPIED, and schedules the standard image to restart.
The function is restart-safe and supports both service-managed components
and user-mode processes. The running image is identified by comparing
sys.executable with the configured image names.
"""
state_file = os.path.join(base_dir, state_file)
state = load_json(state_file, default={})
state["last_checked"] = datetime.now(timezone.utc).isoformat() + "Z"
save_json(state_file, state)
img = state.get("img")
img_pending = state.get("img_pending")
status = (state.get("status") or "idle").lower()
service = state.get("service")
if not img or not img_pending:
raise ValueError("status json must contain 'img' and 'img_pending'")
img_path = os.path.join(base_dir, img)
img_pending_path = os.path.join(base_dir, img_pending)
# Determine what THIS running process is (by basename)
self_image = os.path.basename(sys.executable)
is_self_img = self_image.lower() == os.path.basename(img).lower()
is_self_img_pending = self_image.lower() == os.path.basename(img_pending).lower()
logger.debug("executable: %s is_self: %s is_self_pending: %s", self_image, is_self_img, is_self_img_pending)
# Compare mtimes (img_pending newer than img?)
try:
newer = file_is_newer(img_pending_path, img_path)
except FileNotFoundError:
newer = False
logger.debug("One of teh img is not found: %s %s", img_path, img_pending_path)
if newer:
logger.debug("newer: %s", status);
if status == "idle" and is_self_img_pending:
status = "pending" # img_pending started first
if status == "idle":
state["status"] = "pending"
save_json(state_file, state)
if service:
# set pending image as service image
update_service_binpath(service, img_pending_path)
return "exit"
else:
# start pending image after 60 sec, then the caller should exit
logger.debug("Start porcess: %s", imp_pending)
start_delayed_process(img_pending, workdir=base_dir, delay_seconds=60)
return "set_pending_and_started_img_pending"
if status == "pending" and is_self_img_pending:
# we are running as img_pending -> safe to overwrite img (img is not running)
shutil.copy2(img_pending_path, img_path)
logger.debug("Copy: %s ---> %s", img_pending_path, img_path)
state["status"] = "copied"
save_json(state_file, state)
if service:
# set standard image as service image
update_service_binpath(service, img_path)
return "exit"
else:
# start standard image after 60 sec
logger.debug("Start porcess: %s", imp)
start_delayed_process(img, workdir=base_dir, delay_seconds=60)
return "copied_img_pending_to_img_and_started_img"
return "newer_no_action"
else:
if status != "idle":
state["status"] = "idle"
save_json(state_file, state)
return "reset_to_idle"
return "idle_no_change"
def get_windows_version():
if sys.platform != "win32":
return None
......@@ -106,3 +332,10 @@ def get_windows_version():
return f"Windows_{major}_{minor}_{build})"
if __name__ == '__main__':
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s %(name)s %(levelname)s %(message)s"
)
print(update_component("circle-watchdog.state", r"c:\circle"))
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment