Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
CIRCLE3
/
agent
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
e8f8a3e8
authored
Jan 15, 2026
by
Szeberényi Imre
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
update FIX
parent
566e5f53
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
283 additions
and
26 deletions
+283
-26
README.md
+6
-5
agent-wdog-winservice.py
+7
-5
template.state
+9
-0
win_build.bat
+2
-2
windows/_win32context.py
+21
-10
windows/winutils.py
+238
-4
No files found.
README.md
View file @
e8f8a3e8
...
...
@@ -22,20 +22,21 @@ 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
```
Ez utóbbi parancs
-
bemásolja az agent.service-t /etc/systemd/system -be, engedélyezi, de nem indítja el
-
bemásolja a vm_renewal-t a /usr/bin/ -be
*
Célszerűen a cloud usernek van sudo joga jelszó nélkül.
Ha nincs, akkor a /root/.virtualenv/agent és /root/agent katalógusoknak kereshetőknek kell lenniük, és a benne levő
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)
*
Célszerűen a cloud usernek van sudo joga jelszó nélkül.
Ha nincs, akkor a /root/.virtualenv/agent és /root/agent katalógusoknak kereshetőknek kell lenniük, és a benne levő
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
...
...
agent-wdog-winservice.py
View file @
e8f8a3e8
...
...
@@ -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
(
...
...
template.state
0 → 100644
View file @
e8f8a3e8
{
"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
win_build.bat
View file @
e8f8a3e8
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
windows/_win32context.py
View file @
e8f8a3e8
...
...
@@ -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
.
work
ing_
dir
,
Context
.
exe
)
exe_path
=
join
(
Context
.
workdir
,
Context
.
exe
)
return
servicePostUpdate
(
Context
.
service_name
,
exe_path
)
@staticmethod
...
...
@@ -119,15 +121,24 @@ 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
))
logger
.
info
(
'
%
s Updated'
,
old_exe
)
Context
.
exit_code
=
1
reactor
.
callLater
(
0
,
reactor
.
stop
)
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
)
@staticmethod
def
ipaddresses
():
...
...
@@ -148,7 +159,7 @@ class Context(BaseContext):
@staticmethod
def
get_agent_version
():
try
:
with
open
(
join
(
Context
.
work
ing_
dir
,
'version.txt'
))
as
f
:
with
open
(
join
(
Context
.
workdir
,
'version.txt'
))
as
f
:
return
f
.
readline
()
except
IOError
:
return
None
windows/winutils.py
View file @
e8f8a3e8
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 runnin
g 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
:
logg
ing
.
debug
(
"Registry read failed
%
s
\\
%
s:
%
s"
,
logg
er
.
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment