Compare commits
46 Commits
oggit-chan
...
main
Author | SHA1 | Date |
---|---|---|
|
c0483c93c1 | |
|
83a25947bd | |
|
d180917d3b | |
|
8b8204d10f | |
|
25b2cb6cd8 | |
|
fc8b072860 | |
|
860fb61677 | |
|
33f65a45b7 | |
|
75d222c425 | |
|
e783d7c1fa | |
|
ee183f6ad3 | |
|
054071a5ab | |
|
95360a244e | |
|
cbfcc22d8a | |
|
9fe1b5d1d5 | |
|
565299c7c0 | |
|
c53be3f3ec | |
|
198353b214 | |
|
4a8fc2b469 | |
|
61bd7a90dd | |
|
1a00b715e0 | |
|
82abfbdd10 | |
|
99e2d3b7bc | |
|
fb5cc452cf | |
|
f833a31a82 | |
|
de74923fd8 | |
|
771ed4b378 | |
|
663338920e | |
|
e0da448e90 | |
|
41a9f7ce21 | |
|
950b163602 | |
|
72b832d654 | |
|
65c07fea1e | |
|
9cf7ec5a56 | |
|
60bab01556 | |
|
b4c86bb175 | |
|
2f5ac81235 | |
|
b1a67b1191 | |
|
7be441ca99 | |
|
bb856e5b63 | |
|
22f7ce0bb9 | |
|
250de7a070 | |
|
2feff97a91 | |
|
10349a35f1 | |
|
a84bed3439 | |
|
05d2375e21 |
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -6,6 +6,90 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [7.3.2] - 2025-08-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix syntax
|
||||
|
||||
## [7.3.1] - 2025-08-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- On user logout, write the user to the log
|
||||
- On EjecutarScript with client=true, return a job ID
|
||||
|
||||
## [7.3.0] - 2025-07-31
|
||||
|
||||
### Fixed
|
||||
|
||||
- Wait for zombies
|
||||
|
||||
### Changed
|
||||
|
||||
- Change "jobid" for "job_id" for consistency
|
||||
|
||||
## [7.2.2] - 2025-07-30
|
||||
|
||||
### Added
|
||||
|
||||
- Add missing file stop-agent.ps1
|
||||
|
||||
## [7.2.1] - 2025-07-29
|
||||
|
||||
### Changed
|
||||
|
||||
- Run process in a new POSIX session and group, send termination signals to the whole process group
|
||||
|
||||
## [7.2.0] - 2025-07-28
|
||||
|
||||
### Added
|
||||
|
||||
- Log whether we are in ogLive or not
|
||||
|
||||
## [7.1.0] - 2025-07-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Don't pass the "tag" parameter to CrearImagenGit
|
||||
|
||||
## [7.0.0] - 2025-07-18
|
||||
|
||||
### Changed
|
||||
|
||||
- Run the new extension-less scripts from the cloning engine
|
||||
|
||||
## [6.3.0] - 2025-07-18
|
||||
|
||||
### Added
|
||||
|
||||
- Add new parameter for Configurar
|
||||
|
||||
## [6.2.0] - 2025-07-18
|
||||
|
||||
### Added
|
||||
|
||||
- Add endpoint for GetGitData
|
||||
|
||||
## [6.1.1] - 2025-06-26
|
||||
|
||||
### Changed
|
||||
|
||||
- Write output of launch_browser into a file
|
||||
|
||||
## [6.1.0] - 2025-06-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added ModificarImagenGit
|
||||
|
||||
## [6.0.0] - 2025-06-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Changed the names of some endpoints for consistency between oglive and OS
|
||||
- Changed label in the windows installer
|
||||
|
||||
## [5.9.0] - 2025-06-16
|
||||
|
||||
### Added
|
||||
|
|
|
@ -1,3 +1,90 @@
|
|||
ogagent (7.3.2-1) stable; urgency=medium
|
||||
|
||||
* Fix syntax
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Mon, 04 Aug 2025 14:36:47 +0200
|
||||
|
||||
ogagent (7.3.1-1) stable; urgency=medium
|
||||
|
||||
* On user logout, write the user to the log
|
||||
* On EjecutarScript with client=true, return a job ID
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Mon, 04 Aug 2025 14:22:52 +0200
|
||||
|
||||
ogagent (7.3.0-1) stable; urgency=medium
|
||||
|
||||
* Wait for zombies
|
||||
* Change "jobid" for "job_id" for consistency
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Thu, 31 Jul 2025 10:35:16 +0200
|
||||
|
||||
ogagent (7.2.2-1) stable; urgency=medium
|
||||
|
||||
* Add missing file stop-agent.ps1
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Wed, 30 Jul 2025 10:20:21 +0200
|
||||
|
||||
ogagent (7.2.1-1) stable; urgency=medium
|
||||
|
||||
* Run process in a new POSIX session and group, send termination signals to the whole process group
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Tue, 29 Jul 2025 12:52:08 +0200
|
||||
|
||||
ogagent (7.2.0-1) stable; urgency=medium
|
||||
|
||||
* Log whether we are in ogLive or not
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Mon, 28 Jul 2025 13:55:28 +0200
|
||||
|
||||
ogagent (7.1.0-1) stable; urgency=medium
|
||||
|
||||
* Don't pass the "tag" parameter to CrearImagenGit
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Thu, 24 Jul 2025 15:31:59 +0200
|
||||
|
||||
ogagent (7.0.0-1) stable; urgency=medium
|
||||
|
||||
* Run the new extension-less scripts from the cloning engine
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Fri, 18 Jul 2025 14:27:04 +0200
|
||||
|
||||
ogagent (6.3.0-1) stable; urgency=medium
|
||||
|
||||
* Add new parameter for Configurar
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Fri, 18 Jul 2025 14:20:44 +0200
|
||||
|
||||
ogagent (6.2.0-1) stable; urgency=medium
|
||||
|
||||
* Add endpoint for GetGitData
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Fri, 18 Jul 2025 14:10:06 +0200
|
||||
|
||||
ogagent (6.1.1-1) stable; urgency=medium
|
||||
|
||||
* Write output of launch_browser into a file
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Thu, 26 Jun 2025 12:45:19 +0200
|
||||
|
||||
ogagent (6.1.0-1) stable; urgency=medium
|
||||
|
||||
* Add ModificarImagenGit
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Wed, 25 Jun 2025 10:26:42 +0200
|
||||
|
||||
ogagent (6.0.0-1) stable; urgency=medium
|
||||
|
||||
* Unify API methods for poweroff, reboot and run script
|
||||
* Change label in the windows installer
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Fri, 20 Jun 2025 10:03:15 +0200
|
||||
|
||||
ogagent (5.9.0-1) stable; urgency=medium
|
||||
|
||||
* Add changes for oggit
|
||||
|
||||
-- OpenGnsys developers <info@opengnsys.es> Mon, 16 Jun 2025 11:12:55 +0200
|
||||
|
||||
ogagent (5.8.1-1) stable; urgency=medium
|
||||
|
||||
* Improve fail condition when no network is detected
|
||||
|
|
|
@ -1 +1 @@
|
|||
5.8.1
|
||||
7.3.2
|
||||
|
|
|
@ -7,7 +7,7 @@ port=8000
|
|||
#path=test_modules/server,more_modules/server
|
||||
|
||||
# Remote OpenGnsys Service
|
||||
remote=https://192.168.2.1/opengnsys/rest
|
||||
remote=https://192.168.2.1:8443/opengnsys/rest
|
||||
# Alternate OpenGnsys Service (comment out to enable this option)
|
||||
#altremote=https://10.0.2.2/opengnsys/rest
|
||||
|
||||
|
|
|
@ -187,7 +187,6 @@ class REST(object):
|
|||
if self.verify_tls:
|
||||
r = requests.get(url, cert=(self.crt_file, self.key_file), verify=self.ca_file, timeout=TIMEOUT)
|
||||
else:
|
||||
logger.warning ('using insecure TLS for GET')
|
||||
r = requests.get(url, verify=False, timeout=TIMEOUT)
|
||||
else:
|
||||
r = requests.get(url, timeout=TIMEOUT)
|
||||
|
@ -200,7 +199,6 @@ class REST(object):
|
|||
if self.verify_tls:
|
||||
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, cert=(self.crt_file, self.key_file), verify=self.ca_file, timeout=TIMEOUT)
|
||||
else:
|
||||
logger.warning ('using insecure TLS for POST')
|
||||
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=False, timeout=TIMEOUT)
|
||||
else:
|
||||
r = requests.post(url, data=data, headers={'content-type': 'application/json'}, timeout=TIMEOUT)
|
||||
|
@ -209,7 +207,7 @@ class REST(object):
|
|||
|
||||
r.raise_for_status()
|
||||
ct = r.headers['Content-Type']
|
||||
if 'application/json' != ct:
|
||||
if len(ct) < 16 or 'application/json' != ct[0:16]:
|
||||
raise Exception (f'response content-type is not "application/json" but "{ct}"')
|
||||
r = json.loads(r.content) # Using instead of r.json() to make compatible with old requests lib versions
|
||||
except requests.exceptions.RequestException as e:
|
||||
|
|
|
@ -110,6 +110,11 @@ class ClientProcessor(threading.Thread):
|
|||
logger.debug('Got Client message {}={}'.format(msg, REV_DICT.get(msg)))
|
||||
if self.parent.clientMessageProcessor is not None:
|
||||
self.parent.clientMessageProcessor(msg, data)
|
||||
if msg == REQ_LOGIN:
|
||||
if b',' in data:
|
||||
self.user = data.split (b',')[0]
|
||||
else:
|
||||
self.user = data
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
|
@ -165,6 +170,9 @@ class ClientProcessor(threading.Thread):
|
|||
logger.debug('Got invalid message from request: {}, state: {}'.format(buf, state))
|
||||
except socket.error as e:
|
||||
# If no data is present, no problem at all, pass to check messages
|
||||
if '[WinError 10054]' in str(e):
|
||||
## windows: client disconnected
|
||||
self.running = False
|
||||
pass
|
||||
except Exception as e:
|
||||
tb = traceback.format_exc()
|
||||
|
@ -194,8 +202,14 @@ class ClientProcessor(threading.Thread):
|
|||
logger.error('Invalid message in queue: {}'.format(e))
|
||||
|
||||
logger.debug('Client processor stopped')
|
||||
if os.path.exists ('/windows/temp'): open ('/windows/temp/ogagentuser_died', 'w').close()
|
||||
else: open ( '/tmp/ogagentuser_died', 'w').close()
|
||||
if os.path.exists ('/windows/temp'):
|
||||
fd = open ('/windows/temp/ogagentuser_died', 'wb')
|
||||
fd.write (self.user)
|
||||
fd.close()
|
||||
else:
|
||||
fd = open ('/tmp/ogagentuser_died', 'wb')
|
||||
fd.write (self.user)
|
||||
fd.close()
|
||||
try:
|
||||
self.clientSocket.close()
|
||||
except Exception:
|
||||
|
|
|
@ -23,32 +23,32 @@ class JobMgr():
|
|||
logger.debug ('args "{}"'.format (args))
|
||||
now = datetime.now (tz=timezone.utc)
|
||||
ts = now.strftime ('%Y-%m-%d %H:%M:%S.%f%z') ## '%s' doesn't work on windows
|
||||
jobid = hashlib.sha256 (now.isoformat().encode('UTF-8') + script.encode ('UTF-8')).hexdigest()[0:12]
|
||||
job_id = hashlib.sha256 (now.isoformat().encode('UTF-8') + script.encode ('UTF-8')).hexdigest()[0:12]
|
||||
p = subprocess.Popen (args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
self.jobs[jobid] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '' }
|
||||
self.jobs[jobid]['t1'] = threading.Thread (target=job_readstdout, args=(self.jobs[jobid],))
|
||||
self.jobs[jobid]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[jobid],))
|
||||
self.jobs[jobid]['t1'].start()
|
||||
self.jobs[jobid]['t2'].start()
|
||||
self.jobs[job_id] = { 'p': p, 'pid': p.pid, 'starttime': ts, 'script': script, 'client': is_client, 'status': 'running', 'stdout': '', 'stderr': '' }
|
||||
self.jobs[job_id]['t1'] = threading.Thread (target=job_readstdout, args=(self.jobs[job_id],))
|
||||
self.jobs[job_id]['t2'] = threading.Thread (target=job_readstderr, args=(self.jobs[job_id],))
|
||||
self.jobs[job_id]['t1'].start()
|
||||
self.jobs[job_id]['t2'].start()
|
||||
logger.debug ('jobs "{}"'.format (self.jobs))
|
||||
return jobid
|
||||
return job_id
|
||||
|
||||
def prepare_jobs(self):
|
||||
## can't return self.jobs because the Popen object at self.jobs[id]['p'] is not serializable. So, need to create a new dict to return
|
||||
st = []
|
||||
for jobid in self.jobs:
|
||||
j = self.jobs[jobid]
|
||||
for job_id in self.jobs:
|
||||
j = self.jobs[job_id]
|
||||
entry = dict ((k, j[k]) for k in ['pid', 'starttime', 'script', 'client', 'status', 'stdout', 'stderr'])
|
||||
entry['jobid'] = jobid
|
||||
entry['job_id'] = job_id
|
||||
if j['p'].poll() is not None: ## process finished
|
||||
entry['rc'] = j['p'].returncode
|
||||
entry['status'] = 'finished'
|
||||
st.append (entry)
|
||||
return st
|
||||
|
||||
def terminate_job(self, jobid):
|
||||
if jobid not in self.jobs: return {}
|
||||
p = self.jobs[jobid]['p']
|
||||
def terminate_job(self, job_id):
|
||||
if job_id not in self.jobs: return {}
|
||||
p = self.jobs[job_id]['p']
|
||||
p.terminate()
|
||||
time.sleep (1)
|
||||
if p.poll() is not None:
|
||||
|
|
|
@ -39,6 +39,7 @@ from opengnsys.log import logger
|
|||
|
||||
from opengnsys.linux.daemon import Daemon
|
||||
|
||||
import os
|
||||
import sys
|
||||
import signal
|
||||
import json
|
||||
|
@ -71,6 +72,15 @@ class OGAgentSvc(Daemon, CommonService):
|
|||
# example
|
||||
try:
|
||||
while self.isAlive:
|
||||
client_died=False
|
||||
if os.path.exists ('/tmp/ogagentuser_died'):
|
||||
with open ('/tmp/ogagentuser_died', 'rb') as fd:
|
||||
u = fd.read()
|
||||
os.unlink ('/tmp/ogagentuser_died')
|
||||
client_died=True
|
||||
if client_died:
|
||||
self.notifyLogout (u)
|
||||
|
||||
# In milliseconds, will break
|
||||
self.doWait(1000)
|
||||
except (KeyboardInterrupt, SystemExit) as e:
|
||||
|
|
|
@ -42,6 +42,8 @@ OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6))
|
|||
|
||||
class LocalLogger(object):
|
||||
def __init__(self):
|
||||
self.extra = { 'in_oglive': None }
|
||||
|
||||
# tempdir is different for "user application" and "service"
|
||||
# service wil get c:\windows\temp, while user will get c:\users\XXX\temp
|
||||
# Try to open logger at /var/log path
|
||||
|
@ -51,13 +53,13 @@ class LocalLogger(object):
|
|||
for logDir in ('/var/log', os.path.expanduser('~'), tempfile.gettempdir()):
|
||||
try:
|
||||
fname1 = os.path.join (logDir, 'opengnsys.log')
|
||||
fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s')
|
||||
fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s in_oglive=%(in_oglive)s (%(threadName)s) (%(funcName)s) %(message)s')
|
||||
fh1 = logging.FileHandler (filename=fname1, mode='a')
|
||||
fh1.setFormatter (fmt1)
|
||||
fh1.setLevel (logging.DEBUG)
|
||||
|
||||
fname2 = os.path.join (logDir, 'opengnsys.json.log')
|
||||
fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='')
|
||||
fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "in_oglive": "in_oglive", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='')
|
||||
fh2 = logging.FileHandler (filename=fname2, mode='a')
|
||||
fh2.setFormatter (fmt2)
|
||||
fh2.setLevel (logging.DEBUG)
|
||||
|
@ -77,11 +79,14 @@ class LocalLogger(object):
|
|||
self.logger = None
|
||||
|
||||
def log(self, level, message):
|
||||
if self.extra['in_oglive'] is None:
|
||||
self.extra['in_oglive'] = os.path.exists ('/scripts/functions')
|
||||
|
||||
# Debug messages are logged to a file
|
||||
# our loglevels are 10000 (other), 20000 (debug), ....
|
||||
# logging levels are 10 (debug), 20 (info)
|
||||
# OTHER = logging.NOTSET
|
||||
self.logger.log(int(level / 1000) - 10, message, stacklevel=4)
|
||||
self.logger.log(int(level / 1000) - 10, message, stacklevel=4, extra=self.extra)
|
||||
|
||||
def isWindows(self):
|
||||
return False
|
||||
|
|
|
@ -50,13 +50,13 @@ class OpenGnSysWorker(ClientWorker):
|
|||
def process_script(self, json_params):
|
||||
script = json_params['code']
|
||||
logger.debug('Processing message: script({})'.format(script))
|
||||
self.jobmgr.launch_job (script, True)
|
||||
#self.sendServerMessage('script', {'op', 'launched'})
|
||||
job_id = self.jobmgr.launch_job (script, True)
|
||||
self.sendServerMessage('script_launched', {'op': 'launched', 'job_id': job_id})
|
||||
|
||||
def process_terminatescript(self, json_params):
|
||||
jobid = json_params['jobid']
|
||||
logger.debug('Processing terminatescript request, jobid "{}"'.format (jobid))
|
||||
self.jobmgr.terminate_job (jobid)
|
||||
job_id = json_params['job_id']
|
||||
logger.debug('Processing terminatescript request, job_id "{}"'.format (job_id))
|
||||
self.jobmgr.terminate_job (job_id)
|
||||
|
||||
def process_preparescripts(self, json_params):
|
||||
logger.debug('Processing preparescripts request')
|
||||
|
|
|
@ -112,7 +112,7 @@ class OpenGnSysWorker(ServerWorker):
|
|||
"iph": self.interface.ip,
|
||||
"timestamp": int (time.time()),
|
||||
}
|
||||
logger.debug (f'about to send ping ({body})')
|
||||
#logger.debug (f'about to send ping ({body})')
|
||||
self.REST.sendMessage ('clients/status/webhook', body)
|
||||
|
||||
def onActivation(self):
|
||||
|
@ -308,7 +308,7 @@ class OpenGnSysWorker(ServerWorker):
|
|||
|
||||
@execution_level('halt')
|
||||
@check_secret
|
||||
def process_reboot(self, path, get_params, post_params, server):
|
||||
def process_Reiniciar(self, path, get_params, post_params, server):
|
||||
"""
|
||||
Launches a system reboot operation
|
||||
:param path:
|
||||
|
@ -327,7 +327,7 @@ class OpenGnSysWorker(ServerWorker):
|
|||
|
||||
@execution_level('halt')
|
||||
@check_secret
|
||||
def process_poweroff(self, path, get_params, post_params, server):
|
||||
def process_Apagar(self, path, get_params, post_params, server):
|
||||
"""
|
||||
Launches a system power off operation
|
||||
:param path:
|
||||
|
@ -347,7 +347,7 @@ class OpenGnSysWorker(ServerWorker):
|
|||
|
||||
@execution_level('full')
|
||||
@check_secret
|
||||
def process_script(self, path, get_params, post_params, server):
|
||||
def process_EjecutarScript(self, path, get_params, post_params, server):
|
||||
"""
|
||||
Processes an script execution (script should be encoded in base64)
|
||||
:param path:
|
||||
|
@ -358,28 +358,57 @@ class OpenGnSysWorker(ServerWorker):
|
|||
"""
|
||||
logger.debug('Processing script request')
|
||||
# Decoding script
|
||||
script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8'))
|
||||
param_script = post_params.get('script')
|
||||
if not param_script:
|
||||
return {'op': 'error', 'err': 'Required parameter "script" is missing or empty'}
|
||||
try:
|
||||
b64decoded = base64.b64decode (param_script)
|
||||
except Exception as e:
|
||||
return {'op': 'error', 'err': f'Failed to decode base64: {e}'}
|
||||
script = urllib.parse.unquote (b64decoded.decode ('utf-8'))
|
||||
logger.debug('received script "{}"'.format(script))
|
||||
|
||||
if post_params.get('client', 'false') == 'false':
|
||||
jobid = self.jobmgr.launch_job (script, False)
|
||||
return {'op': 'launched', 'jobid': jobid}
|
||||
job_id = self.jobmgr.launch_job (script, False)
|
||||
return {'op': 'launched', 'job_id': job_id}
|
||||
|
||||
else: ## post_params.get('client') is not 'false'
|
||||
## send script as-is
|
||||
self.sendClientMessage('script', {'code': script})
|
||||
#return {'op': 'launched', 'jobid': jobid} ## TODO obtain jobid generated at the client (can it be done?)
|
||||
return {'op': 'launched'}
|
||||
|
||||
## wait for job_id generated at the client
|
||||
job_id = None
|
||||
iters = 0
|
||||
while True:
|
||||
time.sleep (0.2)
|
||||
if os.path.exists ('/tmp/EjecutarScript-jobid'):
|
||||
with open ('/tmp/EjecutarScript-jobid', 'r') as fd:
|
||||
job_id = fd.read()
|
||||
break
|
||||
iters += 1
|
||||
if iters >= 10: break
|
||||
|
||||
try: os.unlink ('/tmp/EjecutarScript-jobid')
|
||||
except: pass
|
||||
|
||||
if job_id is None: return {'op': 'launched'}
|
||||
else: return {'op': 'launched', 'job_id': job_id}
|
||||
|
||||
def process_client_script_launched(self, data):
|
||||
fd = open ('/tmp/EjecutarScript-jobid', 'w')
|
||||
fd.write (data['job_id'])
|
||||
fd.close()
|
||||
return True
|
||||
|
||||
@execution_level('full')
|
||||
@check_secret
|
||||
def process_terminatescript(self, path, get_params, post_params, server):
|
||||
jobid = post_params.get('jobid', None)
|
||||
logger.debug('Processing terminate_script request, jobid "{}"'.format (jobid))
|
||||
if jobid is None:
|
||||
job_id = post_params.get('job_id', None)
|
||||
logger.debug('Processing terminate_script request, job_id "{}"'.format (job_id))
|
||||
if job_id is None:
|
||||
return {}
|
||||
self.sendClientMessage('terminatescript', {'jobid': jobid})
|
||||
self.jobmgr.terminate_job (jobid)
|
||||
self.sendClientMessage('terminatescript', {'job_id': job_id})
|
||||
self.jobmgr.terminate_job (job_id)
|
||||
return {}
|
||||
|
||||
@execution_level('full')
|
||||
|
|
|
@ -361,20 +361,17 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
return self.respuestaEjecucionComando (cmd, herror, ids)
|
||||
|
||||
def do_CrearImagenGit (self, post_params):
|
||||
for k in ['dsk', 'par', 'cpt', 'idi', 'nci', 'ipr', 'nfn', 'ids']:
|
||||
for k in ['dsk', 'par', 'nci', 'ipr', 'nfn', 'ids']:
|
||||
if k not in post_params:
|
||||
logger.error (f'required parameter ({k}) not in POST params')
|
||||
return {}
|
||||
|
||||
dsk = post_params['dsk'] ## Disco
|
||||
par = post_params['par'] ## Número de partición
|
||||
cpt = post_params['cpt'] ## Código de la partición
|
||||
idi = post_params['idi'] ## Identificador de la imagen
|
||||
nci = post_params['nci'] ## Nombre canónico de la imagen
|
||||
ipr = post_params['ipr'] ## Ip del repositorio
|
||||
nfn = post_params['nfn']
|
||||
ids = post_params['ids']
|
||||
tag = post_params['tag'] ## Tag a crear en git una vez hecho el commit
|
||||
|
||||
self.muestraMensaje (7)
|
||||
|
||||
|
@ -388,7 +385,7 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
self.muestraMensaje (2)
|
||||
inv_sft = res['contents']
|
||||
try:
|
||||
self.interfaceAdmin (nfn, [dsk, par, nci, ipr, tag])
|
||||
self.interfaceAdmin (nfn, [dsk, par, nci, ipr])
|
||||
self.muestraMensaje (9)
|
||||
herror = 0
|
||||
except:
|
||||
|
@ -404,10 +401,58 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
|
||||
cmd = {
|
||||
'nfn': 'RESPUESTA_CrearImagenGit',
|
||||
'idi': idi, ## Identificador de la imagen
|
||||
'dsk': dsk, ## Número de disco
|
||||
'par': par, ## Número de partición de donde se creó
|
||||
'cpt': cpt, ## Tipo o código de partición
|
||||
'ipr': ipr, ## Ip del repositorio donde se alojó
|
||||
'inv_sft': inv_sft
|
||||
}
|
||||
return self.respuestaEjecucionComando (cmd, herror, ids)
|
||||
|
||||
|
||||
def do_ModificarImagenGit (self, post_params):
|
||||
for k in ['dsk', 'par', 'nci', 'ipr', 'nfn', 'ids', 'msg']:
|
||||
if k not in post_params:
|
||||
logger.error (f'required parameter ({k}) not in POST params')
|
||||
return {}
|
||||
|
||||
dsk = post_params['dsk'] ## Disco
|
||||
par = post_params['par'] ## Número de partición
|
||||
nci = post_params['nci'] ## Nombre canónico de la imagen
|
||||
ipr = post_params['ipr'] ## Ip del repositorio
|
||||
nfn = post_params['nfn']
|
||||
ids = post_params['ids']
|
||||
msg = post_params['msg'] ## Mensaje de commit
|
||||
|
||||
self.muestraMensaje (7)
|
||||
|
||||
try:
|
||||
res = self.InventariandoSoftware (dsk, par, 'InventarioSoftware') ## Crea inventario Software previamente
|
||||
except:
|
||||
logger.warning ('Error al ejecutar el comando')
|
||||
return {}
|
||||
|
||||
if res['contents']:
|
||||
self.muestraMensaje (2)
|
||||
inv_sft = res['contents']
|
||||
try:
|
||||
self.interfaceAdmin (nfn, [dsk, par, nci, msg])
|
||||
self.muestraMensaje (9)
|
||||
herror = 0
|
||||
except:
|
||||
logger.warning ('Error al ejecutar el comando')
|
||||
self.muestraMensaje (10)
|
||||
herror = 1
|
||||
else:
|
||||
logger.warning ('Error al ejecutar el comando')
|
||||
herror = 1
|
||||
inv_sft = ''
|
||||
|
||||
self.muestraMenu()
|
||||
|
||||
cmd = {
|
||||
'nfn': 'RESPUESTA_ModificarImagenGit',
|
||||
'dsk': dsk, ## Número de disco
|
||||
'par': par, ## Número de partición de donde se creó
|
||||
'ipr': ipr, ## Ip del repositorio donde se alojó
|
||||
'inv_sft': inv_sft
|
||||
}
|
||||
|
@ -461,17 +506,15 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
return self.respuestaEjecucionComando (cmd, herror, ids)
|
||||
|
||||
def do_RestaurarImagenGit (self, post_params):
|
||||
for k in ['dsk', 'par', 'idi', 'ipr', 'nci', 'ifs', 'ptc', 'nfn', 'ids', 'ref']:
|
||||
for k in ['dsk', 'par', 'ipr', 'nci', 'ptc', 'nfn', 'ids', 'ref']:
|
||||
if k not in post_params:
|
||||
logger.error (f'required parameter ({k}) not in POST params')
|
||||
return {}
|
||||
|
||||
dsk = post_params['dsk']
|
||||
par = post_params['par']
|
||||
idi = post_params['idi']
|
||||
ipr = post_params['ipr']
|
||||
nci = post_params['nci']
|
||||
ifs = post_params['ifs']
|
||||
ptc = post_params['ptc'] ## Protocolo de clonación: Unicast, Multicast, Torrent
|
||||
nfn = post_params['nfn']
|
||||
ids = post_params['ids']
|
||||
|
@ -499,10 +542,8 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
|
||||
cmd = {
|
||||
'nfn': 'RESPUESTA_RestaurarImagenGit',
|
||||
'idi': idi, ## Identificador de la imagen
|
||||
'dsk': dsk, ## Número de disco
|
||||
'par': par, ## Número de partición
|
||||
'ifs': ifs, ## Identificador del perfil software
|
||||
'cfg': self.cfg2obj(cfg), ## Configuración de discos
|
||||
}
|
||||
return self.respuestaEjecucionComando (cmd, herror, ids)
|
||||
|
@ -518,8 +559,9 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
dsk = post_params['dsk']
|
||||
cfg = post_params['cfg']
|
||||
ids = post_params['ids']
|
||||
check_sizes = str ('check-sizes' in post_params and 'true' == post_params['check-sizes']).lower()
|
||||
|
||||
self.muestraMensaje (4)
|
||||
if 'true' != check_sizes: self.muestraMensaje (4)
|
||||
|
||||
params = []
|
||||
disk_info = cfg.pop (0)
|
||||
|
@ -540,23 +582,24 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
cfg_str = f'{disk_info_str}!{part_info_str}%'
|
||||
|
||||
try:
|
||||
self.interfaceAdmin (nfn, ['ignored', cfg_str])
|
||||
self.muestraMensaje (14)
|
||||
self.interfaceAdmin (nfn, ['ignored', cfg_str, check_sizes])
|
||||
if 'true' != check_sizes: self.muestraMensaje (14)
|
||||
herror = 0
|
||||
except:
|
||||
logger.warning ('Error al ejecutar el comando')
|
||||
self.muestraMensaje (13)
|
||||
if 'true' != check_sizes: self.muestraMensaje (13)
|
||||
herror = 1
|
||||
|
||||
cfg = self.LeeConfiguracion()
|
||||
if not cfg:
|
||||
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
|
||||
return {}
|
||||
|
||||
cmd = {
|
||||
'nfn': 'RESPUESTA_Configurar',
|
||||
'cfg': self.cfg2obj (cfg),
|
||||
}
|
||||
if 'true' != check_sizes:
|
||||
cfg = self.LeeConfiguracion()
|
||||
if not cfg:
|
||||
logger.warning ('No se ha podido recuperar la configuración de las particiones del disco')
|
||||
return {}
|
||||
cmd['cfg'] = self.cfg2obj (cfg)
|
||||
|
||||
self.muestraMenu()
|
||||
return self.respuestaEjecucionComando (cmd, herror, ids)
|
||||
|
||||
|
@ -923,6 +966,11 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
logger.debug ('type(post_params) "{}"'.format (type (post_params)))
|
||||
return self._long_running_job ('CrearImagenGit', self.do_CrearImagenGit, args=(post_params,))
|
||||
|
||||
def process_ModificarImagenGit (self, path, get_params, post_params, server):
|
||||
logger.debug ('in process_ModificarImagenGit, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
|
||||
logger.debug ('type(post_params) "{}"'.format (type (post_params)))
|
||||
return self._long_running_job ('ModificarImagenGit', self.do_ModificarImagenGit, args=(post_params,))
|
||||
|
||||
def process_RestaurarImagenGit (self, path, get_params, post_params, server):
|
||||
logger.debug ('in process_RestaurarImagenGit, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
|
||||
logger.debug ('type(post_params) "{}"'.format (type (post_params)))
|
||||
|
@ -961,6 +1009,11 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
@check_secret
|
||||
def process_Configurar (self, path, get_params, post_params, server):
|
||||
logger.debug ('in process_Configurar, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
|
||||
|
||||
check_sizes = str ('check-sizes' in post_params and 'true' == post_params['check-sizes']).lower()
|
||||
if 'true' == check_sizes:
|
||||
return self.do_Configurar (post_params)
|
||||
|
||||
return self._long_running_job ('Configurar', self.do_Configurar, args=(post_params,))
|
||||
|
||||
@execution_level('full')
|
||||
|
@ -985,3 +1038,38 @@ class ogAdmClientWorker (ogLiveWorker):
|
|||
r.update ({ 'nfn':'RESPUESTA_KillJob', 'job':jid })
|
||||
logger.debug (f'r aft ({r})')
|
||||
return r
|
||||
|
||||
@execution_level('full')
|
||||
@check_secret
|
||||
def process_GetGitData (self, path, get_params, post_params, server):
|
||||
logger.debug ('in process_GetGitData, path "{}" get_params "{}" post_params "{}" server "{}"'.format (path, get_params, post_params, server))
|
||||
|
||||
for k in ['nfn', 'dsk', 'par', 'nfn']:
|
||||
if k not in post_params:
|
||||
logger.error (f'required parameter ({k}) not in POST params')
|
||||
return {}
|
||||
|
||||
tmp_gitdata = f'/tmp/gitdata-{self.IPlocal}'
|
||||
nfn = post_params['nfn']
|
||||
dsk = post_params['dsk']
|
||||
par = post_params['par']
|
||||
|
||||
try:
|
||||
self.interfaceAdmin (nfn, [dsk, par, tmp_gitdata])
|
||||
herror = 0
|
||||
except:
|
||||
herror = 1
|
||||
|
||||
if not os.path.exists (tmp_gitdata):
|
||||
return self.respuestaEjecucionComando ({'nfn':'RESPUESTA_GetGitData'}, 1)
|
||||
|
||||
with open (tmp_gitdata, 'r') as fd:
|
||||
gitdata = fd.read().strip()
|
||||
|
||||
branch, repo = gitdata.split (':')
|
||||
cmd = {
|
||||
'nfn': 'RESPUESTA_GetGitData',
|
||||
'branch': branch,
|
||||
'repo': repo,
|
||||
}
|
||||
return self.respuestaEjecucionComando (cmd, herror)
|
||||
|
|
|
@ -198,16 +198,6 @@ class CommonService(object):
|
|||
Invoked to wait a bit
|
||||
CAN be OVERRIDDEN
|
||||
"""
|
||||
client_died=False
|
||||
if os.path.exists ('/windows/temp/ogagentuser_died'):
|
||||
os.unlink ('/windows/temp/ogagentuser_died')
|
||||
client_died=True
|
||||
elif os.path.exists ('/tmp/ogagentuser_died'):
|
||||
os.unlink ('/tmp/ogagentuser_died')
|
||||
client_died=True
|
||||
if client_died:
|
||||
self.notifyLogout (b'')
|
||||
|
||||
time.sleep(float(miliseconds) / 1000)
|
||||
|
||||
def notifyStop(self):
|
||||
|
|
|
@ -106,6 +106,15 @@ class OGAgentSvc(win32serviceutil.ServiceFramework, CommonService):
|
|||
# *********************
|
||||
try:
|
||||
while self.isAlive:
|
||||
client_died=False
|
||||
if os.path.exists ('/windows/temp/ogagentuser_died'):
|
||||
with open ('/windows/temp/ogagentuser_died', 'rb') as fd:
|
||||
u = fd.read()
|
||||
os.unlink ('/windows/temp/ogagentuser_died')
|
||||
client_died=True
|
||||
if client_died:
|
||||
self.notifyLogout (u)
|
||||
|
||||
# Pumps & processes any waiting messages
|
||||
pythoncom.PumpWaitingMessages()
|
||||
win32event.WaitForSingleObject(self.hWaitStop, 1000)
|
||||
|
|
|
@ -44,17 +44,19 @@ OTHER, DEBUG, INFO, WARN, ERROR, FATAL = (10000 * (x + 1) for x in range(6))
|
|||
|
||||
class LocalLogger(object):
|
||||
def __init__(self):
|
||||
self.extra = { 'in_oglive': False }
|
||||
|
||||
# tempdir is different for "user application" and "service"
|
||||
# service wil get c:\windows\temp, while user will get c:\users\XXX\appdata\local\temp
|
||||
|
||||
fname1 = os.path.join (tempfile.gettempdir(), 'opengnsys.log')
|
||||
fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s (%(threadName)s) (%(funcName)s) %(message)s')
|
||||
fmt1 = logging.Formatter (fmt='%(levelname)s %(asctime)s in_oglive=%(in_oglive)s (%(threadName)s) (%(funcName)s) %(message)s')
|
||||
fh1 = logging.FileHandler (filename=fname1, mode='a')
|
||||
fh1.setFormatter (fmt1)
|
||||
fh1.setLevel (logging.DEBUG)
|
||||
|
||||
fname2 = os.path.join (tempfile.gettempdir(), 'opengnsys.json.log')
|
||||
fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='')
|
||||
fmt2 = JsonFormatter ({"timestamp": "asctime", "severity": "levelname", "in_oglive": "in_oglive", "threadName": "threadName", "function": "funcName", "message": "message"}, time_format='%Y-%m-%d %H:%M:%S', msec_format='')
|
||||
fh2 = logging.FileHandler (filename=fname2, mode='a')
|
||||
fh2.setFormatter (fmt2)
|
||||
fh2.setLevel (logging.DEBUG)
|
||||
|
@ -71,7 +73,7 @@ class LocalLogger(object):
|
|||
# our loglevels are 10000 (other), 20000 (debug), ....
|
||||
# logging levels are 10 (debug), 20 (info)
|
||||
# OTHER = logging.NOTSET
|
||||
self.logger.log(int(level / 1000 - 10), message, stacklevel=4)
|
||||
self.logger.log(int(level / 1000 - 10), message, stacklevel=4, extra=self.extra)
|
||||
|
||||
if level < INFO or self.serviceLogger is False: # Only information and above will be on event log
|
||||
return
|
||||
|
|
|
@ -35,6 +35,7 @@ import re
|
|||
import time
|
||||
try: import dbus ## don't fail on windows (the worker will later refuse to load anyway)
|
||||
except: pass
|
||||
import select
|
||||
import random
|
||||
import subprocess
|
||||
import threading
|
||||
|
@ -158,10 +159,10 @@ class ogLiveWorker(ServerWorker):
|
|||
if 'thread' not in self.thread_list[job_id]: return { 'res': 2, 'der': 'Job is not running' }
|
||||
t = self.thread_list[job_id]['thread']
|
||||
pid = self.thread_list[job_id]['child_pid']
|
||||
logger.debug (f'pid ({pid})')
|
||||
logger.debug (f'pid/pgid/sid ({pid})')
|
||||
try_times = 8
|
||||
sig = signal.SIGTERM
|
||||
msg = f'could not kill pid ({pid}) after ({try_times}) tries'
|
||||
msg = f'could not killpg pid ({pid}) after ({try_times}) tries'
|
||||
success = 2 ## mimic cmd['res'] in respuestaEjecucionComando(): "1" means success, "2" means failed
|
||||
while True:
|
||||
t.join (0.05)
|
||||
|
@ -175,10 +176,10 @@ class ogLiveWorker(ServerWorker):
|
|||
## this is fine in the first iteration of the loop, before we send any signals. In the rest of iterations, after some signals were sent, msg should be 'job terminated' instead.
|
||||
if pid:
|
||||
if os.path.exists (f'/proc/{pid}'):
|
||||
logger.debug (f'sending signal ({sig}) to pid ({pid})')
|
||||
logger.debug (f'sending signal ({sig}) to process group ({pid})')
|
||||
## if the process finishes just here, nothing happens: the signal is sent to the void
|
||||
os.kill (pid, sig)
|
||||
#subprocess.run (['kill', '--signal', str(sig), str(pid)])
|
||||
os.killpg (pid, sig)
|
||||
#subprocess.run (['kill', '--signal', str(sig), f'-{pid}']) ## negative PID is used for sending signals to the process group
|
||||
else:
|
||||
msg = f'pid ({pid}) is gone, nothing to kill'
|
||||
success = 1
|
||||
|
@ -259,12 +260,12 @@ class ogLiveWorker(ServerWorker):
|
|||
self.REST.sendMessage ('clients/status/webhook', body)
|
||||
|
||||
def interfaceAdmin (self, method, parametros=[]):
|
||||
if method in ['Apagar', 'CambiarAcceso', 'Configurar', 'CrearImagen', 'CrearImagenGit', 'EjecutarScript', 'getConfiguration', 'getIpAddress', 'IniciarSesion', 'InventarioHardware', 'InventarioSoftware', 'Reiniciar', 'RestaurarImagen', 'RestaurarImagenGit']:
|
||||
if method not in ['ConsolaRemota', 'procesaCache']:
|
||||
## python
|
||||
logger.debug (f'({method}) is a python method')
|
||||
exe = '{}/{}.py'.format (self.pathinterface, method)
|
||||
exe = '{}/{}'.format (self.pathinterface, method)
|
||||
proc = [exe]+parametros
|
||||
else: ## ConsolaRemota procesaCache
|
||||
else:
|
||||
## bash
|
||||
logger.debug (f'({method}) is a bash method')
|
||||
exe = '{}/{}'.format (self.pathinterface, method)
|
||||
|
@ -283,24 +284,35 @@ class ogLiveWorker(ServerWorker):
|
|||
proc = ['bash', '-c', '{} {}'.format (devel_bash_prefix, exe)]
|
||||
|
||||
logger.debug ('subprocess.run ("{}")'.format (' '.join (proc)))
|
||||
p = subprocess.Popen (proc, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
p = subprocess.Popen (proc, stdout=subprocess.PIPE, stderr=subprocess.PIPE, start_new_session=True)
|
||||
if self.pid_q:
|
||||
self.pid_q.put (p.pid)
|
||||
self.pid_q.put (p.pid) ## p.pid is also a session ID and a process group ID--we'll use it later to send signals to the whole group
|
||||
else:
|
||||
## esto sucede por ejemplo cuando arranca el agente, que estamos en interfaceAdmin() en el mismo hilo, sin _long_running_job ni hilo separado
|
||||
#logger.debug ('no queue--not writing any PID to it')
|
||||
pass
|
||||
|
||||
sout = serr = ''
|
||||
while p.poll() is None:
|
||||
for l in iter (p.stdout.readline, b''):
|
||||
finished = False
|
||||
while True:
|
||||
try:
|
||||
p.wait (0.05)
|
||||
finished = True
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
ready_to_read, _, _ = select.select ([p.stdout, p.stderr], [], [], 0.2)
|
||||
if p.stdout in ready_to_read:
|
||||
l = p.stdout.readline()
|
||||
partial = l.decode ('utf-8', 'ignore')
|
||||
if self.stdout_q: self.stdout_q.put (partial)
|
||||
sout += partial
|
||||
for l in iter (p.stderr.readline, b''):
|
||||
if p.stderr in ready_to_read:
|
||||
l = p.stderr.readline()
|
||||
partial = l.decode ('utf-8', 'ignore')
|
||||
serr += partial
|
||||
time.sleep (1)
|
||||
|
||||
if finished: break
|
||||
sout = sout.strip()
|
||||
serr = serr.strip()
|
||||
|
||||
|
@ -385,8 +397,10 @@ class ogLiveWorker(ServerWorker):
|
|||
b.call_blocking (dest, path, interface, method, 's', [url])
|
||||
except Exception as e:
|
||||
if 'ServiceUnknown' in str(e):
|
||||
## browser not running
|
||||
subprocess.Popen (['/usr/bin/launch_browser', url])
|
||||
logger.warning ('browser is not running, launching a new one')
|
||||
browser_log_fd = open ('/var/log/launch_browser.log', 'a')
|
||||
subprocess.Popen (['/usr/bin/launch_browser', url], stdout=browser_log_fd, stderr=subprocess.STDOUT)
|
||||
browser_log_fd.close()
|
||||
else:
|
||||
logger.error (f'Error al cambiar URL: ({e})')
|
||||
return False
|
||||
|
@ -423,7 +437,7 @@ class ogLiveWorker(ServerWorker):
|
|||
k, v = item.split ('=', maxsplit=1)
|
||||
elem[k] = v
|
||||
|
||||
obj.append (elem)
|
||||
if elem: obj.append (elem)
|
||||
|
||||
return obj
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
get-process -name ogagentuser|stop-process
|
|
@ -217,7 +217,7 @@ FunctionEnd
|
|||
Function GetParameters
|
||||
${GetOptions} $CMDLINE "/server" $SERVERIP_VALUE
|
||||
${If} $SERVERIP_VALUE == ""
|
||||
StrCpy $SERVERIP_VALUE "192.168.2.10"
|
||||
StrCpy $SERVERIP_VALUE "192.168.2.1:8443"
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
|
@ -226,10 +226,12 @@ LangString PARAMS_TITLE ${LANG_ENGLISH} "Setup parameters"
|
|||
LangString PARAMS_TITLE ${LANG_SPANISH} "Parametros de configuracion"
|
||||
LangString PARAMS_TITLE ${LANG_FRENCH} "Parametres de configuration"
|
||||
LangString PARAMS_TITLE ${LANG_GERMAN} "Setup-Parameter"
|
||||
LangString SERVER_LABEL ${LANG_ENGLISH} "OpenGnsys Server IP Address"
|
||||
LangString SERVER_LABEL ${LANG_SPANISH} "Direccion IP del Servidor OpenGnsys"
|
||||
LangString SERVER_LABEL ${LANG_FRENCH} "Adresse IP du Serveur OpenGnsys"
|
||||
LangString SERVER_LABEL ${LANG_GERMAN} "OpenGnsys-Server-IP-Adresse"
|
||||
|
||||
LangString SERVER_LABEL ${LANG_ENGLISH} "OpenGnsys Server IP Address and port (eg. 192.168.98.99:8443)"
|
||||
LangString SERVER_LABEL ${LANG_SPANISH} "Direccion IP y puerto del Servidor OpenGnsys (p. ej. 192.168.98.99:8443)"
|
||||
LangString SERVER_LABEL ${LANG_FRENCH} "Adresse IP et port du Serveur OpenGnsys (ex. 192.168.98.99:8443)"
|
||||
LangString SERVER_LABEL ${LANG_GERMAN} "OpenGnsys Server IP-Adresse und Port (z. B. 192.168.98.99:8443)"
|
||||
|
||||
LangString ^UninstallLink ${LANG_ENGLISH} "Uninstall $(^Name)"
|
||||
LangString ^UninstallLink ${LANG_SPANISH} "Desinstalar $(^Name)"
|
||||
LangString ^UninstallLink ${LANG_FRENCH} "D<>sinstaller $(^Name)"
|
||||
|
|
Loading…
Reference in New Issue