mirror of https://git.48k.eu/ogclient
Compare commits
35 Commits
Author | SHA1 | Date |
---|---|---|
|
cf9577a40e | |
|
6503d0ffe7 | |
|
0ca16bc46c | |
|
bd190f8d44 | |
|
e0ba9cc98c | |
|
ccdcb7bfc7 | |
|
72406a7d89 | |
|
0f519ecfeb | |
|
c260534534 | |
|
f67f3c598a | |
|
574822907d | |
|
30e0e1dca3 | |
|
59d642f6b5 | |
|
24568356bc | |
|
40e4545bb7 | |
|
d29b601f17 | |
|
476d82e6a9 | |
|
e91f6c5e2c | |
|
4465c6a25a | |
|
203f3e5533 | |
|
bf15491435 | |
|
855768e144 | |
|
aa570e66e6 | |
|
ffaf2aac05 | |
|
62b52ff364 | |
|
e4be5c34eb | |
|
a36c4daa23 | |
|
a1bd0c36f3 | |
|
c8674a4e93 | |
|
179d17cae8 | |
|
bc7fe848ac | |
|
525958ae85 | |
|
d7658f03ab | |
|
90a9ba9543 | |
|
f5f8771b6f |
|
@ -0,0 +1,8 @@
|
|||
:: Create version_info.txt
|
||||
python utils\create_version_file.py
|
||||
|
||||
:: Build the service binary with clean
|
||||
pyinstaller --onefile --noconsole --version-file=ogclient-version-info.txt --clean ogclient
|
||||
|
||||
:: Build the systray binary with clean
|
||||
pyinstaller --onefile --noconsole --version-file=systray-version-info.txt --clean systray\ogclient-systray
|
20
ogclient
20
ogclient
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -24,27 +24,9 @@ from src.ogClient import *
|
|||
from src.log import *
|
||||
|
||||
|
||||
def send_event_dgram(msg, ip='127.0.0.1', port=55885):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.sendto(bytes(msg, "utf-8"), (ip, port))
|
||||
|
||||
|
||||
def create_parser():
|
||||
events = ['login', 'logout']
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.set_defaults(func=None)
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
parser_event = subparsers.add_parser('event')
|
||||
|
||||
subparsers_event = parser_event.add_subparsers()
|
||||
parser_event_login = subparsers_event.add_parser('login')
|
||||
parser_event_login.set_defaults(func=lambda x: send_event_dgram(f'session start {x.user}'))
|
||||
parser_event_login.add_argument('user', type=str)
|
||||
parser_event_logout = subparsers_event.add_parser('logout')
|
||||
parser_event_logout.set_defaults(func=lambda x: send_event_dgram(f'session stop {x.user}'))
|
||||
parser_event_logout.add_argument('user', type=str)
|
||||
|
||||
parser.add_argument('-c', '--config', default="",
|
||||
help='ogClient JSON config file path')
|
||||
parser.add_argument('--debug', default=False,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -7,14 +7,18 @@
|
|||
# (at your option) any later version.
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import psutil
|
||||
import subprocess
|
||||
from subprocess import CalledProcessError
|
||||
from src.log import OgError
|
||||
|
||||
from src.ogRest import ThreadState
|
||||
|
||||
class OgLinuxOperations:
|
||||
|
||||
def __init__(self):
|
||||
self.session = False
|
||||
|
||||
def _restartBrowser(self, url):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
|
@ -26,25 +30,30 @@ class OgLinuxOperations:
|
|||
|
||||
def shellrun(self, request, ogRest):
|
||||
cmd = request.getrun()
|
||||
is_inline = request.get_inline()
|
||||
|
||||
if not is_inline:
|
||||
raise OgError("Only inline mode is supported on Linux")
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd,
|
||||
shell=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True)
|
||||
except CalledProcessError as error:
|
||||
if error.stderr:
|
||||
return error.stderr
|
||||
if error.stdout:
|
||||
return error.stdout
|
||||
return "{Non zero exit code and empty output}"
|
||||
return result.stdout
|
||||
ogRest.proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
shell=True)
|
||||
(output, error) = ogRest.proc.communicate()
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise OgError(f'Error when running "shell run" subprocess: {e}') from e
|
||||
|
||||
output = output.decode('utf-8-sig', errors='replace')
|
||||
return (ogRest.proc.returncode, cmd, output)
|
||||
|
||||
def session(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def hardware(self, path, ogRest):
|
||||
def software(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def hardware(self, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def setup(self, request, ogRest):
|
||||
|
@ -53,7 +62,7 @@ class OgLinuxOperations:
|
|||
def image_restore(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def image_create(self, path, request, ogRest):
|
||||
def image_create(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def cache_delete(self, request, ogRest):
|
||||
|
@ -63,7 +72,21 @@ class OgLinuxOperations:
|
|||
raise OgError('Function not implemented')
|
||||
|
||||
def refresh(self, ogRest):
|
||||
return {"status": "LINUX"}
|
||||
if self.session:
|
||||
session_value = 'LINUXS'
|
||||
else:
|
||||
session_value = 'LINUX'
|
||||
return {"status": session_value}
|
||||
|
||||
def probe(self, ogRest):
|
||||
return {'status': 'LINUX' if ogRest.state != ThreadState.BUSY else 'BSY'}
|
||||
def check_interactive_session_change(self):
|
||||
old_status = self.session
|
||||
has_logged_user = False
|
||||
for user in psutil.users():
|
||||
if user.terminal:
|
||||
has_logged_user = True
|
||||
break
|
||||
self.session = has_logged_user
|
||||
|
||||
if self.session != old_status:
|
||||
return self.session
|
||||
return None
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -27,7 +27,7 @@ from src.utils.postinstall import configure_os
|
|||
from src.utils.net import *
|
||||
from src.utils.menu import generate_menu
|
||||
from src.utils.fs import *
|
||||
from src.utils.probe import os_probe, get_cache_dev_path
|
||||
from src.utils.probe import *
|
||||
from src.utils.disk import *
|
||||
from src.utils.cache import *
|
||||
from src.utils.tiptorrent import *
|
||||
|
@ -38,8 +38,6 @@ from src.utils.hw_inventory import get_hardware_inventory, legacy_list_hardware_
|
|||
from src.log import OgError
|
||||
|
||||
|
||||
OG_SHELL = '/bin/bash'
|
||||
|
||||
class OgLiveOperations:
|
||||
def __init__(self, config):
|
||||
self._url = config['opengnsys']['url']
|
||||
|
@ -84,8 +82,10 @@ class OgLiveOperations:
|
|||
code = int(pa.parttype, base=16)
|
||||
|
||||
if mount_mkdir(pa.padev, target):
|
||||
probe_result = os_probe(target)
|
||||
part_setup['os'] = probe_result
|
||||
part_setup['os'] = ''
|
||||
if part_setup['disk'] == '1':
|
||||
probe_result = os_probe(target)
|
||||
part_setup['os'] = probe_result
|
||||
|
||||
total, used, free = shutil.disk_usage(target)
|
||||
part_setup['used_size'] = used
|
||||
|
@ -381,32 +381,46 @@ class OgLiveOperations:
|
|||
|
||||
def shellrun(self, request, ogRest):
|
||||
cmd = request.getrun()
|
||||
cmds = cmd.split(";|\n\r")
|
||||
is_inline = request.get_inline()
|
||||
|
||||
if not cmd:
|
||||
raise OgError("No command provided for shell run")
|
||||
|
||||
self._restartBrowser(self._url_log)
|
||||
|
||||
shell_path = '/opt/opengnsys/shell/'
|
||||
if is_inline:
|
||||
cmds = cmd
|
||||
else:
|
||||
cmds = shlex.split(cmd)
|
||||
shell_path = '/opt/opengnsys/shell/'
|
||||
|
||||
restricted_mode = False
|
||||
if not cmds:
|
||||
raise OgError("Parsed shell command list is empty")
|
||||
|
||||
for file_name in os.listdir(shell_path):
|
||||
file_path = os.path.join(shell_path, file_name)
|
||||
try:
|
||||
shell_path_files = os.listdir(shell_path)
|
||||
except OSError as e:
|
||||
raise OgError(f'Error accessing {shell_path}: {e}') from e
|
||||
|
||||
if cmds[0] == file_name:
|
||||
cmds[0] = file_path
|
||||
restricted_mode = True
|
||||
break
|
||||
for file_name in shell_path_files:
|
||||
file_path = os.path.join(shell_path, file_name)
|
||||
|
||||
try:
|
||||
if restricted_mode:
|
||||
ogRest.proc = subprocess.Popen(cmds, stdout=subprocess.PIPE)
|
||||
if cmds[0] == file_name:
|
||||
cmds[0] = file_path
|
||||
cmd = " ".join(cmds)
|
||||
break
|
||||
else:
|
||||
ogRest.proc = subprocess.Popen(cmds,
|
||||
stdout=subprocess.PIPE,
|
||||
shell=True,
|
||||
executable=OG_SHELL)
|
||||
raise OgError(f'Script {cmds[0]} not found in {shell_path}')
|
||||
|
||||
if not os.path.isfile(cmds[0]) or not os.access(cmds[0], os.X_OK):
|
||||
raise OgError(f"Command '{cmds[0]}' is not a valid executable")
|
||||
try:
|
||||
ogRest.proc = subprocess.Popen(
|
||||
cmds,
|
||||
stdout=subprocess.PIPE,
|
||||
shell=is_inline)
|
||||
(output, error) = ogRest.proc.communicate()
|
||||
except OSError as e:
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise OgError(f'Error when running "shell run" subprocess: {e}') from e
|
||||
|
||||
if ogRest.proc.returncode != 0:
|
||||
|
@ -416,7 +430,8 @@ class OgLiveOperations:
|
|||
|
||||
self.refresh(ogRest)
|
||||
|
||||
return (ogRest.proc.returncode, " ".join(cmds), output.decode('utf-8'))
|
||||
output = output.decode('utf-8-sig', errors='replace')
|
||||
return (ogRest.proc.returncode, cmd, output)
|
||||
|
||||
def session(self, request, ogRest):
|
||||
disk = request.getDisk()
|
||||
|
@ -513,7 +528,12 @@ class OgLiveOperations:
|
|||
raise OgError(f'Invalid disk number {disk}, {len(get_disks())} disks available.')
|
||||
|
||||
diskname = get_disks()[disk-1]
|
||||
self._partition(diskname, table_type, partlist)
|
||||
try:
|
||||
self._partition(diskname, table_type, partlist)
|
||||
except Exception as e:
|
||||
ret = subprocess.run(['wipefs', '-af', f'/dev/{diskname}'])
|
||||
logging.warning(f'wipefs on /dev/{diskname} after failure for consistency, reports {ret.returncode}')
|
||||
raise
|
||||
|
||||
ret = subprocess.run(['partprobe', f'/dev/{diskname}'])
|
||||
logging.info(f'first partprobe /dev/{diskname} reports {ret.returncode}')
|
||||
|
@ -581,19 +601,14 @@ class OgLiveOperations:
|
|||
|
||||
extend_filesystem(disk, partition)
|
||||
|
||||
configure_os(disk, partition)
|
||||
if disk == 1:
|
||||
configure_os(disk, partition)
|
||||
|
||||
self.refresh(ogRest)
|
||||
result = self.refresh(ogRest)
|
||||
|
||||
logging.info('Image restore command OK')
|
||||
|
||||
json_dict = {
|
||||
'disk': request.getDisk(),
|
||||
'partition': request.getPartition(),
|
||||
'image_id': request.getId(),
|
||||
'cache': self._get_cache_contents(),
|
||||
}
|
||||
return json_dict
|
||||
return result
|
||||
|
||||
def image_create(self, request, ogRest):
|
||||
disk = int(request.getDisk())
|
||||
|
@ -642,6 +657,7 @@ class OgLiveOperations:
|
|||
raise OgError(f'Cannot mount {ro_mountpoint} as readonly')
|
||||
|
||||
try:
|
||||
is_windows = get_os_family(ro_mountpoint) == OSFamily.WINDOWS
|
||||
if is_hibernation_enabled(ro_mountpoint):
|
||||
raise OgError(f'Target system in {padev} has hibernation enabled')
|
||||
finally:
|
||||
|
@ -653,10 +669,12 @@ class OgLiveOperations:
|
|||
if os.access(f'/opt/opengnsys/images', os.R_OK | os.W_OK) == False:
|
||||
raise OgError('Cannot access /opt/opengnsys/images in read and write mode, check permissions')
|
||||
|
||||
if os.access(f'{image_path}', os.R_OK) == True:
|
||||
if os.access(image_path, os.R_OK) == True:
|
||||
logging.info(f'image file {image_path} already exists, updating.')
|
||||
|
||||
copy_windows_efi_bootloader(disk, partition)
|
||||
if is_windows and is_uefi_supported():
|
||||
copy_windows_efi_bootloader(disk, partition)
|
||||
|
||||
if ogReduceFs(disk, partition) == -1:
|
||||
raise OgError(f'Failed to shrink {fstype} filesystem in {padev}')
|
||||
|
||||
|
@ -728,7 +746,6 @@ class OgLiveOperations:
|
|||
|
||||
def cache_delete(self, request, ogRest):
|
||||
images = request.getImages()
|
||||
deleted_images = []
|
||||
|
||||
logging.info(f'Request to remove files from cache')
|
||||
|
||||
|
@ -814,7 +831,6 @@ class OgLiveOperations:
|
|||
partitions = get_partition_data(device=f'/dev/{disk}')
|
||||
disk_data = get_disk_data(device=f'/dev/{disk}')
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
continue
|
||||
|
||||
self._refresh_payload_disk(disk_data, part_setup, num_disk)
|
||||
|
@ -838,3 +854,6 @@ class OgLiveOperations:
|
|||
|
||||
logging.info('Sending response to refresh request')
|
||||
return json_body
|
||||
|
||||
def check_interactive_session_change(self):
|
||||
return False
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -98,7 +98,7 @@ def _default_logging_win():
|
|||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'level': 'INFO',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'formatter.console',
|
||||
'stream': 'ext://sys.stdout',
|
||||
|
@ -107,7 +107,7 @@ def _default_logging_win():
|
|||
'loggers': {
|
||||
'': {
|
||||
'handlers': ['console'],
|
||||
'level': 'DEBUG',
|
||||
'level': 'INFO',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
import threading
|
||||
import errno
|
||||
import select
|
||||
import socket
|
||||
|
@ -26,6 +27,8 @@ class State(Enum):
|
|||
|
||||
class ogClient:
|
||||
OG_PATH = '/opt/opengnsys/'
|
||||
SESSION_POLL_INTERVAL = 5
|
||||
EVENT_SOCKET_PORT = 55885
|
||||
|
||||
def __init__(self, config):
|
||||
self.CONFIG = config
|
||||
|
@ -36,7 +39,7 @@ class ogClient:
|
|||
if self.mode in {'linux', 'windows'}:
|
||||
self.event_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.event_sock.setblocking(0)
|
||||
self.event_sock.bind(('127.0.0.1', 55885))
|
||||
self.event_sock.bind(('127.0.0.1', ogClient.EVENT_SOCKET_PORT))
|
||||
else:
|
||||
self.event_sock = None
|
||||
|
||||
|
@ -49,6 +52,26 @@ class ogClient:
|
|||
self.ogrest = ogRest(self.CONFIG)
|
||||
self.seq = None
|
||||
|
||||
if self.event_sock:
|
||||
self.session_check_thread = threading.Thread(target=self._session_check_loop, daemon=True)
|
||||
self.session_check_thread.start()
|
||||
|
||||
def _session_check_loop(self):
|
||||
while True:
|
||||
session_status = self.ogrest.check_interactive_session_change()
|
||||
if session_status is True:
|
||||
message = "session start user"
|
||||
elif session_status is False:
|
||||
message = "session stop user"
|
||||
else:
|
||||
message = None
|
||||
|
||||
if message:
|
||||
self.event_sock.sendto(message.encode('utf-8'),
|
||||
('127.0.0.1', ogClient.EVENT_SOCKET_PORT))
|
||||
|
||||
time.sleep(ogClient.SESSION_POLL_INTERVAL)
|
||||
|
||||
def get_socket(self):
|
||||
return self.sock
|
||||
|
||||
|
@ -58,7 +81,7 @@ class ogClient:
|
|||
def get_state(self):
|
||||
return self.state
|
||||
|
||||
def send_event_hint(self, message):
|
||||
def handle_session_event(self, message):
|
||||
try:
|
||||
event, action, user = message.split(" ")
|
||||
logging.debug('Sending event: %s, %s, %s', event, action, user)
|
||||
|
@ -188,6 +211,6 @@ class ogClient:
|
|||
self.connect2()
|
||||
elif state == State.RECEIVING and event_sock in readable:
|
||||
message = event_sock.recv(4096).decode('utf-8').rstrip()
|
||||
self.send_event_hint(message)
|
||||
self.handle_session_event(message)
|
||||
else:
|
||||
raise OgError(f'Invalid ogClient run state: {str(state)}.')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -97,16 +97,12 @@ class ogThread():
|
|||
ogRest.send_internal_server_error(client, exc=e)
|
||||
return
|
||||
|
||||
if request.getEcho():
|
||||
json_body = jsonBody()
|
||||
json_body.add_element('cmd', cmd)
|
||||
json_body.add_element('out', shellout)
|
||||
json_body.add_element('retcode', retcode)
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
else:
|
||||
response = restResponse(ogResponses.OK, seq=client.seq)
|
||||
client.send(response.get())
|
||||
json_body = jsonBody()
|
||||
json_body.add_element('cmd', cmd)
|
||||
json_body.add_element('out', shellout)
|
||||
json_body.add_element('retcode', retcode)
|
||||
response = restResponse(ogResponses.OK, json_body, seq=client.seq)
|
||||
client.send(response.get())
|
||||
|
||||
ogRest.state = ThreadState.IDLE
|
||||
|
||||
|
@ -347,7 +343,7 @@ class ogRest():
|
|||
self.process_imagerestore(client, request)
|
||||
elif ("stop" in URI):
|
||||
self.process_stop(client)
|
||||
elif ("image/create" in URI):
|
||||
elif ("image/create" in URI or "image/update" in URI):
|
||||
self.process_imagecreate(client, request)
|
||||
elif ("cache/delete" in URI):
|
||||
self.process_cache_delete(client, request)
|
||||
|
@ -443,3 +439,6 @@ class ogRest():
|
|||
|
||||
def process_refresh(self, client):
|
||||
threading.Thread(target=ogThread.refresh, args=(client, self,)).start()
|
||||
|
||||
def check_interactive_session_change(self):
|
||||
return self.operations.check_interactive_session_change()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -33,7 +33,7 @@ class restRequest:
|
|||
self.type = None
|
||||
self.profile = None
|
||||
self.id = None
|
||||
self.echo = None
|
||||
self.inline = None
|
||||
self.code = None
|
||||
self.seq = None
|
||||
self.backup = None
|
||||
|
@ -72,7 +72,7 @@ class restRequest:
|
|||
if "run" in json_param:
|
||||
self.run = json_param["run"]
|
||||
try:
|
||||
self.echo = json_param["echo"]
|
||||
self.inline = json_param["inline"]
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -160,8 +160,8 @@ class restRequest:
|
|||
def getId(self):
|
||||
return self.id
|
||||
|
||||
def getEcho(self):
|
||||
return self.echo
|
||||
def get_inline(self):
|
||||
return self.inline
|
||||
|
||||
def getCode(self):
|
||||
return self.code
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -11,6 +11,7 @@ import logging
|
|||
import shlex
|
||||
import subprocess
|
||||
import json
|
||||
from uuid import UUID
|
||||
from src.log import OgError
|
||||
|
||||
import fdisk
|
||||
|
@ -190,3 +191,38 @@ def get_partition_start_offset(disk, partition):
|
|||
raise OgError(f'Error while trying to parse sfdisk: {e}') from e
|
||||
|
||||
return start_offset
|
||||
|
||||
|
||||
def uuid_to_bytes(uuid):
|
||||
uuid = uuid.replace('-', '')
|
||||
|
||||
group0 = f'{uuid[6:8]}{uuid[4:6]}{uuid[2:4]}{uuid[0:2]}'
|
||||
group1 = f'{uuid[10:12]}{uuid[8:10]}'
|
||||
group2 = f'{uuid[14:16]}{uuid[12:14]}'
|
||||
group3 = uuid[16:20]
|
||||
group4 = uuid[20:32]
|
||||
res = f'{group0}-{group1}-{group2}-{group3}-{group4}'
|
||||
return UUID(res).bytes
|
||||
|
||||
|
||||
def get_disk_id_bytes(disk):
|
||||
from src.utils.uefi import is_uefi_supported
|
||||
disk_id = get_disk_id(disk)
|
||||
|
||||
if is_uefi_supported():
|
||||
return uuid_to_bytes(disk_id)
|
||||
|
||||
return bytes.fromhex(disk_id)[::-1]
|
||||
|
||||
|
||||
def get_part_id_bytes(disk, partition):
|
||||
from src.utils.uefi import is_uefi_supported
|
||||
if is_uefi_supported():
|
||||
part_id = get_partition_id(disk, partition)
|
||||
return uuid_to_bytes(part_id)
|
||||
|
||||
partition_start_offset = get_partition_start_offset(disk, partition)
|
||||
sector_size = get_sector_size(disk)
|
||||
byte_offset = partition_start_offset * sector_size
|
||||
byte_offset = "{0:016x}".format(byte_offset)
|
||||
return bytes.fromhex(byte_offset)[::-1]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -153,13 +153,19 @@ def mkfs(fs, disk, partition, label=None):
|
|||
if fs not in fsdict:
|
||||
raise OgError(f'mkfs failed, unsupported target filesystem {fs}')
|
||||
|
||||
try:
|
||||
partdev = get_partition_device(disk, partition)
|
||||
except ValueError as e:
|
||||
raise OgError(f'mkfs aborted: {e}') from e
|
||||
partdev = get_partition_device(disk, partition)
|
||||
|
||||
return fsdict[fs](partdev, label)
|
||||
ret = subprocess.run(['wipefs', '-af', f'{partdev}'])
|
||||
if ret.returncode != 0:
|
||||
logging.warning(f'wipefs on {partdev}, fails with {ret.returncode}')
|
||||
|
||||
err = fsdict[fs](partdev, label)
|
||||
if err != 0:
|
||||
ret = subprocess.run(['wipefs', '-af', f'{partdev}'])
|
||||
if ret.returncode != 0:
|
||||
logging.warning(f'wipefs on {partdev} for consistency, fails with {ret.returncode}')
|
||||
|
||||
return err
|
||||
|
||||
def mkfs_ext4(partdev, label=None):
|
||||
err = -1
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2024 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2024 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -266,7 +266,10 @@ def _get_os_entries(esp_mountpoint):
|
|||
available_disks = get_disks()
|
||||
for disk_num, diskname in enumerate(available_disks, start=1):
|
||||
disk_device = f'/dev/{diskname}'
|
||||
partitions_data = get_partition_data(device=disk_device)
|
||||
try:
|
||||
partitions_data = get_partition_data(device=disk_device)
|
||||
except OgError as e:
|
||||
continue
|
||||
|
||||
for p in partitions_data:
|
||||
part_num = p.partno + 1
|
||||
|
@ -276,7 +279,7 @@ def _get_os_entries(esp_mountpoint):
|
|||
continue
|
||||
|
||||
if not mount_mkdir(p.padev, mountpoint):
|
||||
raise OgError(f'Unable to mount {p.padev} into {mountpoint}')
|
||||
continue
|
||||
|
||||
try:
|
||||
os_family = get_os_family(mountpoint)
|
||||
|
@ -331,6 +334,7 @@ def get_disk_part_type(disk_num):
|
|||
def _update_nvram(esp_disk, esp_part_number):
|
||||
loader_path = '/EFI/grub/Boot/shimx64.efi'
|
||||
bootlabel = 'grub'
|
||||
egibootmgr_reorder_disabled_entries()
|
||||
efibootmgr_delete_bootentry(bootlabel)
|
||||
efibootmgr_create_bootentry(esp_disk, esp_part_number, loader_path, bootlabel, add_to_bootorder=False)
|
||||
efibootmgr_set_entry_order(bootlabel, 1)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -293,10 +293,10 @@ def legacy_list_hardware_inventory(inventory):
|
|||
|
||||
def get_hardware_inventory():
|
||||
proc = subprocess.run(['lshw', '-json'], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
|
||||
j = json.loads(proc.stdout)
|
||||
root = json.loads(proc.stdout)
|
||||
|
||||
if type(j) is list:
|
||||
root = j[0]
|
||||
if type(root) is list:
|
||||
root = root[0]
|
||||
if type(root) is not dict:
|
||||
raise OgError('Invalid lshw json output')
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -7,12 +7,15 @@
|
|||
# (at your option) any later version.
|
||||
|
||||
import array
|
||||
import fcntl
|
||||
import sys
|
||||
import socket
|
||||
import struct
|
||||
import psutil
|
||||
import logging
|
||||
|
||||
if sys.platform != "win32":
|
||||
import fcntl
|
||||
|
||||
def is_ethernet(interface):
|
||||
SIOCGIFHWADDR = 0x8927
|
||||
ARPHRD_ETHER = 1
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -12,6 +12,7 @@ import platform
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from src.utils.winreg import *
|
||||
from enum import Enum
|
||||
from subprocess import PIPE
|
||||
|
||||
|
@ -45,35 +46,23 @@ def getlinuxversion(osrelease):
|
|||
|
||||
def getwindowsversion(winreghives):
|
||||
"""
|
||||
Tries to obtain windows version information by
|
||||
querying the SOFTWARE registry hive. Registry
|
||||
hives path is a required parameter.
|
||||
|
||||
Runs hivexget(1) to fetch ProductName and
|
||||
ReleaseId. If something fails (hivexget is
|
||||
not installed, or registry is not found) it
|
||||
returns a generic "Microsoft Windows" string.
|
||||
Try to obtain windows version information by querying the SOFTWARE registry
|
||||
hive to fetch ProductName and ReleaseId.
|
||||
Return a generic "Microsoft Windows" string if something fails.
|
||||
"""
|
||||
|
||||
# XXX: 3.6 friendly
|
||||
try:
|
||||
proc_prodname = subprocess.run(['hivexget',
|
||||
f'{winreghives}/SOFTWARE',
|
||||
'microsoft\windows nt\currentversion',
|
||||
'ProductName'], stdout=PIPE)
|
||||
proc_releaseid = subprocess.run(['hivexget',
|
||||
f'{winreghives}/SOFTWARE',
|
||||
'microsoft\windows nt\currentversion',
|
||||
'ReleaseId'], stdout=PIPE)
|
||||
hivepath = f'{winreghives}/SOFTWARE'
|
||||
hive = hive_handler_open(hivepath, write = False)
|
||||
root_node = hive.root()
|
||||
version_node = get_node_child_from_path(hive, root_node, 'Microsoft/Windows NT/CurrentVersion')
|
||||
|
||||
prodname = proc_prodname.stdout.decode().replace('\n', '')
|
||||
releaseid = proc_releaseid.stdout.decode().replace('\n', '')
|
||||
bits = ' 64 bits' if windows_is64bit(winreghives) else ''
|
||||
prodname = get_value_from_node(hive, version_node, 'ProductName')
|
||||
releaseid = get_value_from_node(hive, version_node, 'ReleaseId')
|
||||
|
||||
if proc_prodname.returncode == 0 and proc_releaseid.returncode == 0:
|
||||
return f'{prodname} {releaseid}{bits}'
|
||||
except FileNotFoundError: # hivexget command not found
|
||||
pass
|
||||
return f'{prodname} {releaseid}'
|
||||
except (RuntimeError, OgError) as e:
|
||||
logging.error(f'Hivex was not able to operate over {hivepath}. Reported: {e}')
|
||||
return 'Microsoft Windows'
|
||||
|
||||
|
||||
|
@ -81,28 +70,6 @@ def interpreter_is64bit():
|
|||
return sys.maxsize > 2**32
|
||||
|
||||
|
||||
def windows_is64bit(winreghives):
|
||||
"""
|
||||
Check for 64 bit Windows by means of retrieving the value of
|
||||
ProgramW6432Dir. This key is set if Windows is running 64 bit.
|
||||
|
||||
If set returns True.
|
||||
If not set or hivexget exits with non-zero, returns False.
|
||||
"""
|
||||
try:
|
||||
proc_hivexget = subprocess.run(['hivexget',
|
||||
f'{winreghives}/SOFTWARE',
|
||||
'Microsoft\Windows\CurrentVersion',
|
||||
'ProgramW6432Dir'], stdout=PIPE)
|
||||
stdout = proc_hivexget.stdout.decode().replace('\n', '')
|
||||
|
||||
if proc_hivexget.returncode == 0 and stdout:
|
||||
return True
|
||||
except FileNotFoundError: # hivexget command not found
|
||||
pass
|
||||
return False
|
||||
|
||||
|
||||
def linux_is64bit(mountpoint):
|
||||
"""
|
||||
If /sbin/init is detected, check if compiled for 64-bit machine.
|
||||
|
@ -174,7 +141,7 @@ def os_probe(mountpoint):
|
|||
|
||||
Returns a string depending on the OS it detects.
|
||||
"""
|
||||
winreghives = f'{mountpoint}/Windows/System32/config'
|
||||
winreghives = f'{mountpoint}{WINDOWS_HIVES_PATH}'
|
||||
osrelease = f'{mountpoint}/etc/os-release'
|
||||
|
||||
if os.path.exists(osrelease):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -16,33 +16,39 @@ from collections import namedtuple
|
|||
import hivex
|
||||
|
||||
from src.utils.probe import os_probe
|
||||
from src.utils.winreg import *
|
||||
|
||||
|
||||
Package = namedtuple('Package', ['name', 'version'])
|
||||
Package.__str__ = lambda pkg: f'{pkg.name} {pkg.version}'
|
||||
|
||||
WINDOWS_HIVES_PATH = '/Windows/System32/config'
|
||||
WINDOWS_HIVES_SOFTWARE = f'{WINDOWS_HIVES_PATH}/SOFTWARE'
|
||||
DPKG_STATUS_PATH = '/var/lib/dpkg/status'
|
||||
OSRELEASE_PATH = '/etc/os-release'
|
||||
|
||||
|
||||
def _fill_package_set(h, key, pkg_set):
|
||||
def _fill_package_set(hive, key, pkg_set):
|
||||
"""
|
||||
Fill the package set looking for entries at the current registry
|
||||
node childs.
|
||||
|
||||
Any valid node child must have "DisplayVersion" or "DisplayName" keys.
|
||||
"""
|
||||
childs = h.node_children(key)
|
||||
valid_childs = [h.node_get_child(key, h.node_name(child))
|
||||
for child in childs
|
||||
for value in h.node_values(child) if h.value_key(value) == 'DisplayVersion']
|
||||
childs = hive.node_children(key)
|
||||
valid_childs = []
|
||||
for child in childs:
|
||||
child_name = hive.node_name(child)
|
||||
values = hive.node_values(child)
|
||||
|
||||
for value in values:
|
||||
if hive.value_key(value) == 'DisplayVersion':
|
||||
valid_child = hive.node_get_child(key, child_name)
|
||||
valid_childs.append(valid_child)
|
||||
|
||||
for ch in valid_childs:
|
||||
try:
|
||||
name = h.value_string(h.node_get_value(ch, 'DisplayName'))
|
||||
value = h.node_get_value(ch, 'DisplayVersion')
|
||||
version = h.value_string(value)
|
||||
name = hive.value_string(hive.node_get_value(ch, 'DisplayName'))
|
||||
value = hive.node_get_value(ch, 'DisplayVersion')
|
||||
version = hive.value_string(value)
|
||||
pkg = Package(name, version)
|
||||
pkg_set.add(pkg)
|
||||
except RuntimeError:
|
||||
|
@ -50,22 +56,19 @@ def _fill_package_set(h, key, pkg_set):
|
|||
pass
|
||||
|
||||
|
||||
def _fill_package_set_1(h, pkg_set):
|
||||
def _fill_package_set_1(hive, pkg_set):
|
||||
"""
|
||||
Looks for entries in registry path
|
||||
/Microsoft/Windows/CurrentVersion/Uninstall
|
||||
|
||||
Fills the given set with Package instances for each program found.
|
||||
"""
|
||||
key = h.root()
|
||||
key = h.node_get_child(key, 'Microsoft')
|
||||
key = h.node_get_child(key, 'Windows')
|
||||
key = h.node_get_child(key, 'CurrentVersion')
|
||||
key = h.node_get_child(key, 'Uninstall')
|
||||
_fill_package_set(h, key, pkg_set)
|
||||
root_node = hive.root()
|
||||
key = get_node_child_from_path(hive, root_node, 'Microsoft/Windows/CurrentVersion/Uninstall')
|
||||
_fill_package_set(hive, key, pkg_set)
|
||||
|
||||
|
||||
def _fill_package_set_2(h, pkg_set):
|
||||
def _fill_package_set_32_bit_compat(hive, pkg_set):
|
||||
"""
|
||||
Looks for entries in registry path
|
||||
/Wow6432Node/Microsoft/Windows/CurrentVersion/Uninstall
|
||||
|
@ -73,23 +76,22 @@ def _fill_package_set_2(h, pkg_set):
|
|||
|
||||
Fills the given set with Package instances for each program found.
|
||||
"""
|
||||
key = h.root()
|
||||
key = h.node_get_child(key, 'Wow6432Node')
|
||||
key = h.node_get_child(key, 'Microsoft')
|
||||
key = h.node_get_child(key, 'Windows')
|
||||
key = h.node_get_child(key, 'CurrentVersion')
|
||||
key = h.node_get_child(key, 'Uninstall')
|
||||
_fill_package_set(h, key, pkg_set)
|
||||
root_node = hive.root()
|
||||
key = get_node_child_from_path(hive, root_node, 'Wow6432Node/Windows/CurrentVersion/Uninstall')
|
||||
_fill_package_set(hive, key, pkg_set)
|
||||
|
||||
|
||||
def _get_package_set_windows(hivepath):
|
||||
packages = set()
|
||||
try:
|
||||
h = hivex.Hivex(hivepath)
|
||||
h = hive_handler_open(hivepath, write = False)
|
||||
_fill_package_set_1(h, packages)
|
||||
_fill_package_set_2(h, packages)
|
||||
except RuntimeError as e:
|
||||
except (RuntimeError, OgError) as e:
|
||||
logging.error(f'Hivex was not able to operate over {hivepath}. Reported: {e}')
|
||||
try:
|
||||
_fill_package_set_32_bit_compat(h, packages)
|
||||
except (RuntimeError, OgError) as e:
|
||||
pass
|
||||
return packages
|
||||
|
||||
|
||||
|
@ -119,7 +121,7 @@ def _get_package_set_dpkg(dpkg_status_path):
|
|||
|
||||
def get_package_set(mountpoint):
|
||||
dpkg_status_path = f'{mountpoint}{DPKG_STATUS_PATH}'
|
||||
softwarehive = f'{mountpoint}{WINDOWS_HIVES_SOFTWARE}'
|
||||
softwarehive = f'{mountpoint}{WINDOWS_HIVE_SOFTWARE}'
|
||||
if os.path.exists(softwarehive):
|
||||
pkgset = _get_package_set_windows(softwarehive)
|
||||
elif os.path.exists(dpkg_status_path):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -14,7 +14,6 @@ import subprocess
|
|||
import shutil
|
||||
from src.utils.disk import *
|
||||
from src.utils.fs import *
|
||||
from src.utils.probe import *
|
||||
from src.log import OgError
|
||||
|
||||
import fdisk
|
||||
|
@ -26,7 +25,7 @@ def _find_bootentry(entries, label):
|
|||
if entry['description'] == label:
|
||||
return entry
|
||||
else:
|
||||
raise OgError('Boot entry {label} not found')
|
||||
raise OgError(f'Boot entry {label} not found')
|
||||
|
||||
|
||||
def _strip_boot_prefix(entry):
|
||||
|
@ -96,8 +95,13 @@ def efibootmgr_bootnext(description):
|
|||
entry = _find_bootentry(boot_entries, description)
|
||||
num = _strip_boot_prefix(entry) # efibootmgr output uses BootXXXX for each entry, remove the "Boot" prefix.
|
||||
bootnext_cmd = bootnext_cmd.format(bootnum=num, efibootmgr=EFIBOOTMGR_BIN)
|
||||
subprocess.run(shlex.split(bootnext_cmd), check=True,
|
||||
stdout=subprocess.DEVNULL)
|
||||
try:
|
||||
subprocess.run(shlex.split(bootnext_cmd), check=True,
|
||||
stdout=subprocess.DEVNULL)
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning("Failed to update BootNext. UEFI firmware might be buggy")
|
||||
except OSError as e:
|
||||
raise OgError(f"Unexpected error updating BootNext: {e}") from e
|
||||
|
||||
|
||||
def efibootmgr_delete_bootentry(label):
|
||||
|
@ -108,7 +112,12 @@ def efibootmgr_delete_bootentry(label):
|
|||
if entry['description'] == label:
|
||||
num = entry['name'][4:] # Remove "Boot" prefix to extract num
|
||||
efibootmgr_cmd = efibootmgr_cmd.format(bootnum=num, efibootmgr=EFIBOOTMGR_BIN)
|
||||
subprocess.run(shlex.split(efibootmgr_cmd), check=True)
|
||||
try:
|
||||
subprocess.run(shlex.split(efibootmgr_cmd), check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning(f"Failed to delete boot entry {label}. UEFI firmware might be buggy")
|
||||
except OSError as e:
|
||||
raise OgError(f"Unexpected error deleting boot entry {label}: {e}") from e
|
||||
break
|
||||
else:
|
||||
logging.info(f'Cannot delete boot entry {label} because it was not found.')
|
||||
|
@ -122,8 +131,10 @@ def efibootmgr_create_bootentry(disk, part, loader, label, add_to_bootorder=True
|
|||
logging.info(f'{EFIBOOTMGR_BIN} command creating boot entry: {efibootmgr_cmd}')
|
||||
try:
|
||||
proc = subprocess.run(shlex.split(efibootmgr_cmd), check=True, text=True)
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning(f"Failed to add boot entry {label} to NVRAM. UEFI firmware might be buggy")
|
||||
except OSError as e:
|
||||
raise OgError(f'Unexpected error adding boot entry to nvram. UEFI firmware might be buggy') from e
|
||||
raise OgError(f"Unexpected error adding boot entry {label}: {e}") from e
|
||||
|
||||
def efibootmgr_set_entry_order(label, position):
|
||||
logging.info(f'Setting {label} entry to position {position} of boot order')
|
||||
|
@ -141,8 +152,29 @@ def efibootmgr_set_entry_order(label, position):
|
|||
|
||||
try:
|
||||
proc = subprocess.run([EFIBOOTMGR_BIN, "-o", ",".join(boot_order)], check=True, text=True)
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning("Failed to set boot order to NVRAM. UEFI firmware might be buggy")
|
||||
except OSError as e:
|
||||
raise OgError(f'Unexpected error setting boot order to NVRAM. UEFI firmware might be buggy') from e
|
||||
raise OgError(f"Unexpected error updating boot order: {e}") from e
|
||||
|
||||
def egibootmgr_reorder_disabled_entries():
|
||||
logging.info(f'Setting disabled entries at the end of boot order')
|
||||
boot_info = run_efibootmgr_json(validate=False)
|
||||
boot_entries = boot_info.get('vars', [])
|
||||
boot_order = boot_info.get('BootOrder', [])
|
||||
|
||||
for entry in boot_entries:
|
||||
entry_number = _strip_boot_prefix(entry)
|
||||
if not entry['active'] and entry_number in boot_order:
|
||||
boot_order.remove(entry_number)
|
||||
boot_order.append(entry_number)
|
||||
|
||||
try:
|
||||
proc = subprocess.run([EFIBOOTMGR_BIN, "-o", ",".join(boot_order)], check=True, text=True)
|
||||
except subprocess.CalledProcessError:
|
||||
logging.warning("Failed to update boot order. UEFI firmware might be buggy")
|
||||
except OSError as e:
|
||||
raise OgError(f"Unexpected error updating boot order: {e}") from e
|
||||
|
||||
def _find_efi_loader(loader_paths):
|
||||
for efi_app in loader_paths:
|
||||
|
@ -170,13 +202,7 @@ def copy_windows_efi_bootloader(disk, partition):
|
|||
device = get_partition_device(disk, partition)
|
||||
mountpoint = device.replace('dev', 'mnt')
|
||||
if not mount_mkdir(device, mountpoint):
|
||||
raise OgError(f'Cannot probe OS family. Unable to mount {device} into {mountpoint}')
|
||||
|
||||
os_family = get_os_family(mountpoint)
|
||||
is_uefi = is_uefi_supported()
|
||||
|
||||
if not is_uefi or os_family != OSFamily.WINDOWS:
|
||||
return
|
||||
raise OgError(f'Unable to mount {device} into {mountpoint}')
|
||||
|
||||
bootlabel = f'Part-{disk:02d}-{partition:02d}'
|
||||
esp, esp_disk, esp_part_number = get_efi_partition(disk, enforce_gpt=True)
|
||||
|
@ -209,7 +235,7 @@ def restore_windows_efi_bootloader(disk, partition):
|
|||
device = get_partition_device(disk, partition)
|
||||
mountpoint = device.replace('dev', 'mnt')
|
||||
if not mount_mkdir(device, mountpoint):
|
||||
raise OgError(f'Cannot probe OS family. Unable to mount {device} into {mountpoint}')
|
||||
raise OgError(f'Unable to mount {device} into {mountpoint}')
|
||||
|
||||
bootlabel = f'Part-{disk:02d}-{partition:02d}'
|
||||
esp, esp_disk, esp_part_number = get_efi_partition(disk, enforce_gpt=True)
|
||||
|
@ -233,4 +259,4 @@ def restore_windows_efi_bootloader(disk, partition):
|
|||
raise OgError(f'Failed to copy {loader_dir} into {destination_dir}: {e}') from e
|
||||
finally:
|
||||
umount(mountpoint)
|
||||
umount(esp_mountpoint)
|
||||
umount(esp_mountpoint)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2022 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -10,9 +10,7 @@ import libhivexmod
|
|||
import hivex
|
||||
from enum import Enum
|
||||
from src.log import OgError
|
||||
from uuid import UUID
|
||||
from src.utils.disk import *
|
||||
from src.utils.uefi import is_uefi_supported
|
||||
|
||||
|
||||
WINDOWS_HIVES_PATH = '/Windows/System32/config'
|
||||
|
@ -102,36 +100,3 @@ def get_node_child_from_path(hive, node, path):
|
|||
child_node = get_node_child(hive, child_node, node_name)
|
||||
|
||||
return child_node
|
||||
|
||||
|
||||
def uuid_to_bytes(uuid):
|
||||
uuid = uuid.replace('-', '')
|
||||
|
||||
group0 = f'{uuid[6:8]}{uuid[4:6]}{uuid[2:4]}{uuid[0:2]}'
|
||||
group1 = f'{uuid[10:12]}{uuid[8:10]}'
|
||||
group2 = f'{uuid[14:16]}{uuid[12:14]}'
|
||||
group3 = uuid[16:20]
|
||||
group4 = uuid[20:32]
|
||||
res = f'{group0}-{group1}-{group2}-{group3}-{group4}'
|
||||
return UUID(res).bytes
|
||||
|
||||
|
||||
def get_disk_id_bytes(disk):
|
||||
disk_id = get_disk_id(disk)
|
||||
|
||||
if is_uefi_supported():
|
||||
return uuid_to_bytes(disk_id)
|
||||
|
||||
return bytes.fromhex(disk_id)[::-1]
|
||||
|
||||
|
||||
def get_part_id_bytes(disk, partition):
|
||||
if is_uefi_supported():
|
||||
part_id = get_partition_id(disk, partition)
|
||||
return uuid_to_bytes(part_id)
|
||||
|
||||
partition_start_offset = get_partition_start_offset(disk, partition)
|
||||
sector_size = get_sector_size(disk)
|
||||
byte_offset = partition_start_offset * sector_size
|
||||
byte_offset = "{0:016x}".format(byte_offset)
|
||||
return bytes.fromhex(byte_offset)[::-1]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -253,6 +253,9 @@ class OgVirtualOperations:
|
|||
qemu = OgVM(part_path)
|
||||
qemu.run_vm()
|
||||
|
||||
def software(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def partitions_cfg_to_json(self, data):
|
||||
for part in data['partition_setup']:
|
||||
part.pop('virt-drive')
|
||||
|
@ -469,6 +472,9 @@ class OgVirtualOperations:
|
|||
|
||||
return json_dict
|
||||
|
||||
def image_create(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def cache_delete(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
|
@ -623,3 +629,6 @@ class OgVirtualOperations:
|
|||
for k, v in device_names.items():
|
||||
f.write(f'{k}={v}\n')
|
||||
f.truncate()
|
||||
|
||||
def check_interactive_session_change(self):
|
||||
return None
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2023 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
@ -7,99 +7,56 @@
|
|||
# (at your option) any later version.
|
||||
|
||||
import os
|
||||
import ctypes
|
||||
import psutil
|
||||
import subprocess
|
||||
from subprocess import CalledProcessError
|
||||
import multiprocessing as mp
|
||||
from multiprocessing import Process, freeze_support
|
||||
from src.log import OgError
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from pystray import Icon, Menu, MenuItem
|
||||
|
||||
from src.ogRest import ThreadState
|
||||
|
||||
|
||||
def _create_default_image():
|
||||
"""
|
||||
Creates a default image for the tray icon. Use in case
|
||||
no favicon.ico is found.
|
||||
"""
|
||||
width = height = 250
|
||||
color1 = (255, 255, 255)
|
||||
color2 = (255, 0, 255)
|
||||
|
||||
image = Image.new('RGB', (width, height), color1)
|
||||
dc = ImageDraw.Draw(image)
|
||||
dc.rectangle(
|
||||
(width // 2, 0, width, height // 2),
|
||||
fill=color2)
|
||||
dc.rectangle(
|
||||
(0, height // 2, width // 2, height),
|
||||
fill=color2)
|
||||
|
||||
return image
|
||||
|
||||
|
||||
def create_image():
|
||||
try:
|
||||
image = Image.open(r'./favicon.ico')
|
||||
image = Image.composite(image, Image.new('RGB', image.size, 'white'), image)
|
||||
except:
|
||||
image = _create_default_image()
|
||||
return image
|
||||
|
||||
|
||||
def create_systray():
|
||||
menu = Menu(MenuItem('Powered by Soleta Networks!',
|
||||
lambda icon, item: 1))
|
||||
icon = Icon('ogClient', create_image(), menu=menu)
|
||||
assert icon.icon
|
||||
icon.run()
|
||||
|
||||
|
||||
systray_p = Process(target=create_systray)
|
||||
|
||||
|
||||
class OgWindowsOperations:
|
||||
|
||||
def __init__(self):
|
||||
freeze_support()
|
||||
mp.set_start_method('spawn')
|
||||
systray_p.start()
|
||||
self.session = False
|
||||
|
||||
def _restartBrowser(self, url):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def poweroff(self):
|
||||
systray_p.terminate()
|
||||
os.system('shutdown -s -t 0')
|
||||
|
||||
def reboot(self):
|
||||
systray_p.terminate()
|
||||
os.system('shutdown -r -t 0')
|
||||
|
||||
def shellrun(self, request, ogRest):
|
||||
cmd = request.getrun()
|
||||
is_inline = request.get_inline()
|
||||
|
||||
if not is_inline:
|
||||
raise OgError("Only inline mode is supported on Windows")
|
||||
|
||||
env = os.environ.copy()
|
||||
env['OutputEncoding'] = 'utf8'
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd,
|
||||
shell=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True)
|
||||
except CalledProcessError as error:
|
||||
if error.stderr:
|
||||
return error.stderr
|
||||
if error.stdout:
|
||||
return error.stdout
|
||||
return "{Non zero exit code and empty output}"
|
||||
return (result.returncode, cmd, result.stdout)
|
||||
ogRest.proc = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
shell=True,
|
||||
env=env,
|
||||
)
|
||||
output, error = ogRest.proc.communicate()
|
||||
except (OSError, subprocess.SubprocessError) as e:
|
||||
raise OgError(f'Error when running "shell run" subprocess: {e}') from e
|
||||
|
||||
output = output.decode('utf-8-sig', errors='replace')
|
||||
return (ogRest.proc.returncode, cmd, output)
|
||||
|
||||
def session(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def hardware(self, path, ogRest):
|
||||
def software(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def hardware(self, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def setup(self, request, ogRest):
|
||||
|
@ -108,7 +65,7 @@ class OgWindowsOperations:
|
|||
def image_restore(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def image_create(self, path, request, ogRest):
|
||||
def image_create(self, request, ogRest):
|
||||
raise OgError('Function not implemented')
|
||||
|
||||
def cache_delete(self, request, ogRest):
|
||||
|
@ -118,4 +75,20 @@ class OgWindowsOperations:
|
|||
raise OgError('Function not implemented')
|
||||
|
||||
def refresh(self, ogRest):
|
||||
return {"status": "WIN"}
|
||||
if self.session:
|
||||
session_value = 'WINS'
|
||||
else:
|
||||
session_value = 'WIN'
|
||||
return {"status": session_value}
|
||||
|
||||
def check_interactive_session_change(self):
|
||||
old_status = self.session
|
||||
has_logged_user = False
|
||||
for user in psutil.users():
|
||||
has_logged_user = True
|
||||
break
|
||||
self.session = has_logged_user
|
||||
|
||||
if self.session != old_status:
|
||||
return self.session
|
||||
return None
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
#
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
import time
|
||||
import multiprocessing as mp
|
||||
import psutil
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
from pystray import Icon, Menu, MenuItem
|
||||
|
||||
SERVICE_POLL_INTERVAL = 5
|
||||
|
||||
|
||||
def _create_default_image():
|
||||
"""
|
||||
Creates a default image for the tray icon. Use in case
|
||||
no favicon.ico is found. The image will be a blue circle.
|
||||
"""
|
||||
width = height = 250
|
||||
circle_color = (45, 158, 251)
|
||||
|
||||
# Create a new image with a transparent background
|
||||
image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
|
||||
dc = ImageDraw.Draw(image)
|
||||
|
||||
# Draw circle
|
||||
circle_radius = min(width, height) // 2 - 10
|
||||
circle_center = (width // 2, height // 2)
|
||||
dc.ellipse(
|
||||
(circle_center[0] - circle_radius, circle_center[1] - circle_radius,
|
||||
circle_center[0] + circle_radius, circle_center[1] + circle_radius),
|
||||
fill=circle_color
|
||||
)
|
||||
return image
|
||||
|
||||
|
||||
def create_icon_image():
|
||||
try:
|
||||
image = Image.open(r'./favicon.ico')
|
||||
image = Image.composite(image, Image.new('RGB', image.size, 'white'), image)
|
||||
except FileNotFoundError:
|
||||
image = _create_default_image()
|
||||
return image
|
||||
|
||||
|
||||
def create_systray():
|
||||
menu = Menu(MenuItem('ogclient service running',
|
||||
lambda icon, item: None))
|
||||
icon = Icon('ogClient', create_icon_image(), menu=menu)
|
||||
icon.run()
|
||||
|
||||
|
||||
def is_ogclient_service_active():
|
||||
service_name = 'ogclient'
|
||||
try:
|
||||
service = psutil.win_service_get(service_name)
|
||||
service = service.as_dict()
|
||||
return service.get('status') == 'running'
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _session_check_loop():
|
||||
systray_process = None
|
||||
try:
|
||||
while True:
|
||||
session_status = is_ogclient_service_active()
|
||||
is_systray_active = systray_process and systray_process.is_alive()
|
||||
|
||||
if session_status and not is_systray_active:
|
||||
systray_process = mp.Process(target=create_systray, daemon=True)
|
||||
systray_process.start()
|
||||
elif not session_status and is_systray_active:
|
||||
systray_process.terminate()
|
||||
systray_process.join()
|
||||
systray_process = None
|
||||
|
||||
time.sleep(SERVICE_POLL_INTERVAL)
|
||||
except KeyboardInterrupt:
|
||||
if systray_process and systray_process.is_alive():
|
||||
systray_process.terminate()
|
||||
systray_process.join()
|
||||
|
||||
|
||||
def main():
|
||||
mp.freeze_support()
|
||||
mp.set_start_method('spawn')
|
||||
_session_check_loop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#
|
||||
# Copyright (C) 2020-2021 Soleta Networks <info@soleta.eu>
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
#
|
||||
# Copyright (C) 2020-2024 Soleta Networks <info@soleta.eu>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the
|
||||
# Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
version_template = """
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=({major}, {minor}, {patch}, 0),
|
||||
prodvers=({major}, {minor}, {patch}, 0),
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
'040904B0',
|
||||
[
|
||||
StringStruct('CompanyName', 'Soleta Networks'),
|
||||
StringStruct('FileDescription', '{appname} - OpenGnsys Client Application'),
|
||||
StringStruct('FileVersion', '{version}'),
|
||||
StringStruct('InternalName', '{appname}'),
|
||||
StringStruct('LegalCopyright', 'Copyright © {year} Soleta Networks'),
|
||||
StringStruct('OriginalFilename', '{appname}.exe'),
|
||||
StringStruct('ProductName', '{appname}'),
|
||||
StringStruct('ProductVersion', '{version}')
|
||||
]
|
||||
)
|
||||
]
|
||||
),
|
||||
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||
]
|
||||
)
|
||||
"""
|
||||
|
||||
def get_git_version():
|
||||
try:
|
||||
version = subprocess.check_output(
|
||||
["git", "describe", "--tags", "--always"],
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True
|
||||
).strip()
|
||||
return version
|
||||
except subprocess.CalledProcessError:
|
||||
return "0.0.0"
|
||||
|
||||
def version_to_tuple(version):
|
||||
parts = version.lstrip("v").split("-")[0].split(".")
|
||||
version_tuple = []
|
||||
|
||||
for part in parts:
|
||||
if part.isdigit():
|
||||
version_tuple.append(int(part))
|
||||
else:
|
||||
version_tuple.append(0)
|
||||
return tuple(version_tuple)
|
||||
|
||||
if __name__ == "__main__":
|
||||
version = get_git_version()
|
||||
major, minor, patch = version_to_tuple(version)
|
||||
current_year = datetime.now().year
|
||||
version_file = version_template.format(major=major, minor=minor,
|
||||
patch=patch, version=version,
|
||||
year=current_year, appname='ogclient')
|
||||
with open('ogclient-version-info.txt', 'w', encoding='utf-8') as f:
|
||||
f.write(version_file)
|
||||
version_file = version_template.format(major=major, minor=minor,
|
||||
patch=patch, version=version,
|
||||
year=current_year, appname='ogclient-systray')
|
||||
with open('systray-version-info.txt', 'w', encoding='utf-8') as f:
|
||||
f.write(version_file)
|
Loading…
Reference in New Issue