pywebexec 1.2.0__py3-none-any.whl → 1.4.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pywebexec/pywebexec.py +228 -96
- pywebexec/static/css/Consolas NF.ttf +0 -0
- pywebexec/static/css/style.css +121 -8
- pywebexec/static/css/xterm.css +209 -0
- pywebexec/static/images/down-arrow.svg +1 -0
- pywebexec/static/images/favicon.svg +8 -1
- pywebexec/static/images/popup.svg +1 -0
- pywebexec/static/js/commands.js +223 -0
- pywebexec/static/js/popup.js +141 -0
- pywebexec/static/js/script.js +130 -52
- pywebexec/static/js/xterm/LICENSE +21 -0
- pywebexec/static/js/xterm/ansi_up.min.js +7 -0
- pywebexec/static/js/xterm/xterm-addon-fit.js +1 -0
- pywebexec/static/js/xterm/xterm.js +1 -0
- pywebexec/templates/index.html +19 -6
- pywebexec/templates/popup.html +24 -0
- pywebexec/version.py +2 -2
- {pywebexec-1.2.0.dist-info → pywebexec-1.4.12.dist-info}/METADATA +41 -13
- pywebexec-1.4.12.dist-info/RECORD +31 -0
- pywebexec-1.2.0.dist-info/RECORD +0 -20
- {pywebexec-1.2.0.dist-info → pywebexec-1.4.12.dist-info}/LICENSE +0 -0
- {pywebexec-1.2.0.dist-info → pywebexec-1.4.12.dist-info}/WHEEL +0 -0
- {pywebexec-1.2.0.dist-info → pywebexec-1.4.12.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.2.0.dist-info → pywebexec-1.4.12.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import sys
|
2
2
|
from flask import Flask, request, jsonify, render_template, session, redirect, url_for
|
3
3
|
from flask_httpauth import HTTPBasicAuth
|
4
|
-
import subprocess
|
5
4
|
import threading
|
6
5
|
import os
|
7
6
|
import json
|
@@ -10,12 +9,23 @@ import argparse
|
|
10
9
|
import random
|
11
10
|
import string
|
12
11
|
from datetime import datetime, timezone, timedelta
|
12
|
+
import time
|
13
13
|
import shlex
|
14
14
|
from gunicorn.app.base import Application
|
15
15
|
import ipaddress
|
16
|
-
from socket import gethostname, gethostbyname_ex
|
16
|
+
from socket import gethostname, gethostbyname_ex, gethostbyaddr, inet_aton, inet_ntoa
|
17
17
|
import ssl
|
18
18
|
import re
|
19
|
+
import pwd
|
20
|
+
from secrets import token_urlsafe
|
21
|
+
import pexpect
|
22
|
+
import signal
|
23
|
+
import fcntl
|
24
|
+
import termios
|
25
|
+
import struct
|
26
|
+
import subprocess
|
27
|
+
|
28
|
+
|
19
29
|
if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
|
20
30
|
from ldap3 import Server, Connection, ALL, SIMPLE, SUBTREE, Tls
|
21
31
|
|
@@ -32,6 +42,7 @@ app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
|
|
32
42
|
app.config['LDAP_BIND_PASSWORD'] = os.environ.get('PYWEBEXEC_LDAP_BIND_PASSWORD')
|
33
43
|
|
34
44
|
# Directory to store the command status and output
|
45
|
+
CWD = os.getcwd()
|
35
46
|
COMMAND_STATUS_DIR = '.web_status'
|
36
47
|
CONFDIR = os.path.expanduser("~/")
|
37
48
|
if os.path.isdir(f"{CONFDIR}/.config"):
|
@@ -48,11 +59,38 @@ def generate_random_password(length=12):
|
|
48
59
|
|
49
60
|
|
50
61
|
def resolve_hostname(host):
|
51
|
-
"""try get fqdn from DNS"""
|
62
|
+
"""try get fqdn from DNS/hosts"""
|
52
63
|
try:
|
53
|
-
|
64
|
+
hostinfo = gethostbyname_ex(host)
|
65
|
+
return (hostinfo[0].rstrip('.'), hostinfo[2][0])
|
54
66
|
except OSError:
|
55
|
-
return host
|
67
|
+
return (host, host)
|
68
|
+
|
69
|
+
|
70
|
+
def resolve_ip(ip):
|
71
|
+
"""try resolve hostname by reverse dns query on ip addr"""
|
72
|
+
ip = inet_ntoa(inet_aton(ip))
|
73
|
+
try:
|
74
|
+
ipinfo = gethostbyaddr(ip)
|
75
|
+
return (ipinfo[0].rstrip('.'), ipinfo[2][0])
|
76
|
+
except OSError:
|
77
|
+
return (ip, ip)
|
78
|
+
|
79
|
+
|
80
|
+
def is_ip(host):
|
81
|
+
"""determine if host is valid ip"""
|
82
|
+
try:
|
83
|
+
inet_aton(host)
|
84
|
+
return True
|
85
|
+
except OSError:
|
86
|
+
return False
|
87
|
+
|
88
|
+
|
89
|
+
def resolve(host_or_ip):
|
90
|
+
"""resolve hostname from ip / hostname"""
|
91
|
+
if is_ip(host_or_ip):
|
92
|
+
return resolve_ip(host_or_ip)
|
93
|
+
return resolve_hostname(host_or_ip)
|
56
94
|
|
57
95
|
|
58
96
|
def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
@@ -118,7 +156,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
|
|
118
156
|
|
119
157
|
|
120
158
|
|
121
|
-
class
|
159
|
+
class PyWebExec(Application):
|
122
160
|
|
123
161
|
def __init__(self, app, options=None):
|
124
162
|
self.options = options or {}
|
@@ -137,22 +175,11 @@ class StandaloneApplication(Application):
|
|
137
175
|
return self.application
|
138
176
|
|
139
177
|
|
178
|
+
ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[[0-9]{1,2};[0-9]{1,2}[m|K]|\x1B\[[0-9;]*[mGKHF]|[\x00-\x1F\x7F])')
|
179
|
+
|
140
180
|
def strip_ansi_control_chars(text):
|
141
181
|
"""Remove ANSI and control characters from the text."""
|
142
|
-
|
143
|
-
# ansi_escape = re.compile(r'''
|
144
|
-
# (?:\x1B[@-_]| # ANSI ESCape sequences
|
145
|
-
# \x1B\[.*?[ -/]*[@-~]| # ANSI CSI sequences
|
146
|
-
# \x1B\].*?\x07| # ANSI OSC sequences
|
147
|
-
# \x1B=P| # ANSI DCS sequences
|
148
|
-
# \x1B\\| # ANSI ST sequences
|
149
|
-
# \x1B\^| # ANSI PM sequences
|
150
|
-
# \x1B_.*?\x1B\\| # ANSI APC sequences
|
151
|
-
# [\x00-\x1F\x7F]) # Control characters
|
152
|
-
# ''', re.VERBOSE)
|
153
|
-
# ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|[(]B)|>')
|
154
|
-
ansi_escape = re.compile(br'(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B([(]B|>))')
|
155
|
-
return ansi_escape.sub(b'', text)
|
182
|
+
return ANSI_ESCAPE.sub(b'', text)
|
156
183
|
|
157
184
|
|
158
185
|
def decode_line(line: bytes) -> str:
|
@@ -191,31 +218,35 @@ def get_last_non_empty_line_of_file(file_path):
|
|
191
218
|
return last_line(f)
|
192
219
|
|
193
220
|
|
194
|
-
def start_gunicorn(
|
195
|
-
|
221
|
+
def start_gunicorn(daemonized=False, baselog=None):
|
222
|
+
pidfile = f"{baselog}.pid"
|
223
|
+
if daemonized:
|
224
|
+
if daemon_d('status', pidfilepath=baselog, silent=True):
|
225
|
+
print(f"Error: pywebexec already running on {args.listen}:{args.port}", file=sys.stderr)
|
226
|
+
sys.exit(1)
|
227
|
+
|
228
|
+
if sys.stdout.isatty():
|
229
|
+
errorlog = "-"
|
230
|
+
accesslog = None #"-"
|
231
|
+
else:
|
196
232
|
errorlog = f"{baselog}.log"
|
197
233
|
accesslog = None # f"{baselog}.access.log"
|
198
|
-
|
199
|
-
else:
|
200
|
-
errorlog = "-"
|
201
|
-
accesslog = "-"
|
202
|
-
pidfile = None
|
234
|
+
|
203
235
|
options = {
|
204
236
|
'bind': '%s:%s' % (args.listen, args.port),
|
205
237
|
'workers': 4,
|
206
238
|
'timeout': 600,
|
207
239
|
'certfile': args.cert,
|
208
240
|
'keyfile': args.key,
|
209
|
-
'daemon':
|
241
|
+
'daemon': daemonized,
|
210
242
|
'errorlog': errorlog,
|
211
243
|
'accesslog': accesslog,
|
212
244
|
'pidfile': pidfile,
|
213
245
|
}
|
214
|
-
|
246
|
+
PyWebExec(app, options=options).run()
|
215
247
|
|
216
|
-
def daemon_d(action, pidfilepath, hostname=None, args=None):
|
248
|
+
def daemon_d(action, pidfilepath, silent=False, hostname=None, args=None):
|
217
249
|
"""start/stop daemon"""
|
218
|
-
import signal
|
219
250
|
import daemon, daemon.pidfile
|
220
251
|
|
221
252
|
pidfile = daemon.pidfile.TimeoutPIDLockFile(pidfilepath+".pid", acquire_timeout=30)
|
@@ -233,10 +264,14 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
|
|
233
264
|
if status:
|
234
265
|
print(f"pywebexec running pid {pidfile.read_pid()}")
|
235
266
|
return True
|
236
|
-
|
267
|
+
if not silent:
|
268
|
+
print("pywebexec not running")
|
237
269
|
return False
|
238
270
|
elif action == "start":
|
239
|
-
|
271
|
+
status = pidfile.is_locked()
|
272
|
+
if status:
|
273
|
+
print(f"pywebexc already running pid {pidfile.read_pid()}", file=sys.stderr)
|
274
|
+
sys.exit(1)
|
240
275
|
log = open(pidfilepath + ".log", "ab+")
|
241
276
|
daemon_context = daemon.DaemonContext(
|
242
277
|
stderr=log,
|
@@ -250,8 +285,22 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
|
|
250
285
|
except Exception as e:
|
251
286
|
print(e)
|
252
287
|
|
288
|
+
def start_term():
|
289
|
+
os.environ["PYWEBEXEC"] = " (shared)"
|
290
|
+
os.chdir(CWD)
|
291
|
+
command_id = str(uuid.uuid4())
|
292
|
+
start_time = datetime.now().isoformat()
|
293
|
+
user = pwd.getpwuid(os.getuid())[0]
|
294
|
+
update_command_status(command_id, 'running', command="term", params=[user,os.ttyname(sys.stdout.fileno())], start_time=start_time, user=user)
|
295
|
+
output_file_path = get_output_file_path(command_id)
|
296
|
+
res = script(output_file_path)
|
297
|
+
end_time = datetime.now().isoformat()
|
298
|
+
update_command_status(command_id, status="success", end_time=end_time, exit_code=res)
|
299
|
+
return res
|
300
|
+
|
253
301
|
def parseargs():
|
254
|
-
global app, args
|
302
|
+
global app, args, COMMAND_STATUS_DIR
|
303
|
+
|
255
304
|
parser = argparse.ArgumentParser(description='Run the command execution server.')
|
256
305
|
parser.add_argument('-u', '--user', help='Username for basic auth')
|
257
306
|
parser.add_argument('-P', '--password', help='Password for basic auth')
|
@@ -268,13 +317,15 @@ def parseargs():
|
|
268
317
|
"-t",
|
269
318
|
"--title",
|
270
319
|
type=str,
|
271
|
-
default="
|
320
|
+
default="PyWebExec",
|
272
321
|
help="Web html title",
|
273
322
|
)
|
274
323
|
parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
|
275
324
|
parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
|
276
325
|
parser.add_argument("-g", "--gencert", action="store_true", help="https server self signed cert")
|
277
|
-
parser.add_argument("
|
326
|
+
parser.add_argument("-T", "--tokenurl", action="store_true", help="generate safe url to access")
|
327
|
+
parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status/shareterm/term",
|
328
|
+
choices=["start","stop","restart","status","shareterm", "term"])
|
278
329
|
|
279
330
|
args = parser.parse_args()
|
280
331
|
if os.path.isdir(args.dir):
|
@@ -288,8 +339,21 @@ def parseargs():
|
|
288
339
|
sys.exit(1)
|
289
340
|
if not os.path.exists(COMMAND_STATUS_DIR):
|
290
341
|
os.makedirs(COMMAND_STATUS_DIR)
|
342
|
+
if not os.path.exists(CONFDIR):
|
343
|
+
os.mkdir(CONFDIR, mode=0o700)
|
344
|
+
if args.action == "term":
|
345
|
+
COMMAND_STATUS_DIR = f"{os.getcwd()}/{COMMAND_STATUS_DIR}"
|
346
|
+
sys.exit(start_term())
|
347
|
+
(hostname, ip) = resolve(gethostname()) if args.listen == '0.0.0.0' else resolve(args.listen)
|
348
|
+
url_params = ""
|
349
|
+
|
350
|
+
if args.tokenurl:
|
351
|
+
token = os.environ.get("PYWEBEXEC_TOKEN", token_urlsafe())
|
352
|
+
os.environ["PYWEBEXEC_TOKEN"] = token
|
353
|
+
app.config["TOKEN_URL"] = token
|
354
|
+
url_params = f"?token={token}"
|
355
|
+
|
291
356
|
if args.gencert:
|
292
|
-
hostname = resolve_hostname(gethostname())
|
293
357
|
args.cert = args.cert or f"{CONFDIR}/pywebexec.crt"
|
294
358
|
args.key = args.key or f"{CONFDIR}/pywebexec.key"
|
295
359
|
if not os.path.exists(args.cert):
|
@@ -310,9 +374,13 @@ def parseargs():
|
|
310
374
|
app.config['USER'] = None
|
311
375
|
app.config['PASSWORD'] = None
|
312
376
|
|
313
|
-
|
377
|
+
if args.action != 'stop':
|
378
|
+
print("Starting server:")
|
379
|
+
protocol = 'https' if args.cert else 'http'
|
380
|
+
print(f"{protocol}://{hostname}:{args.port}{url_params}")
|
381
|
+
print(f"{protocol}://{ip}:{args.port}{url_params}")
|
314
382
|
|
315
|
-
|
383
|
+
return args
|
316
384
|
|
317
385
|
def get_status_file_path(command_id):
|
318
386
|
return os.path.join(COMMAND_STATUS_DIR, f'{command_id}.json')
|
@@ -320,7 +388,7 @@ def get_status_file_path(command_id):
|
|
320
388
|
def get_output_file_path(command_id):
|
321
389
|
return os.path.join(COMMAND_STATUS_DIR, f'{command_id}_output.txt')
|
322
390
|
|
323
|
-
def update_command_status(command_id, status, command=None, params=None, start_time=None, end_time=None, exit_code=None, pid=None):
|
391
|
+
def update_command_status(command_id, status, command=None, params=None, start_time=None, end_time=None, exit_code=None, pid=None, user=None):
|
324
392
|
status_file_path = get_status_file_path(command_id)
|
325
393
|
status_data = read_command_status(command_id) or {}
|
326
394
|
status_data['status'] = status
|
@@ -336,10 +404,11 @@ def update_command_status(command_id, status, command=None, params=None, start_t
|
|
336
404
|
status_data['exit_code'] = exit_code
|
337
405
|
if pid is not None:
|
338
406
|
status_data['pid'] = pid
|
407
|
+
if user is not None:
|
408
|
+
status_data['user'] = user
|
339
409
|
if status != 'running':
|
340
410
|
output_file_path = get_output_file_path(command_id)
|
341
411
|
if os.path.exists(output_file_path):
|
342
|
-
print(output_file_path)
|
343
412
|
status_data['last_output_line'] = get_last_non_empty_line_of_file(output_file_path)
|
344
413
|
with open(status_file_path, 'w') as f:
|
345
414
|
json.dump(status_data, f)
|
@@ -370,40 +439,93 @@ def read_command_status(command_id):
|
|
370
439
|
|
371
440
|
return status_data
|
372
441
|
|
373
|
-
|
374
|
-
|
442
|
+
def sigwinch_passthrough(sig, data):
|
443
|
+
s = struct.pack("HHHH", 0, 0, 0, 0)
|
444
|
+
a = struct.unpack('hhhh', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, s))
|
445
|
+
global p
|
446
|
+
p.setwinsize(a[0], a[1])
|
447
|
+
|
448
|
+
|
449
|
+
def script(output_file):
|
450
|
+
global p
|
451
|
+
shell = os.environ.get('SHELL', 'sh')
|
452
|
+
with open(output_file, 'wb') as fd:
|
453
|
+
p = pexpect.spawn(shell, echo=True)
|
454
|
+
p.logfile_read = fd
|
455
|
+
# Set the window size
|
456
|
+
sigwinch_passthrough(None, None)
|
457
|
+
signal.signal(signal.SIGWINCH, sigwinch_passthrough)
|
458
|
+
p.interact()
|
459
|
+
|
375
460
|
|
376
|
-
def run_command(command, params, command_id):
|
461
|
+
def run_command(command, params, command_id, user):
|
377
462
|
start_time = datetime.now().isoformat()
|
378
|
-
update_command_status(command_id, 'running', command=command, params=params, start_time=start_time)
|
463
|
+
update_command_status(command_id, 'running', command=command, params=params, start_time=start_time, user=user)
|
464
|
+
output_file_path = get_output_file_path(command_id)
|
379
465
|
try:
|
380
|
-
output_file_path
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
466
|
+
with open(output_file_path, 'wb') as fd:
|
467
|
+
p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None)
|
468
|
+
update_command_status(command_id, 'running', pid=p.pid, user=user)
|
469
|
+
p.setwinsize(24, 125)
|
470
|
+
p.logfile = fd
|
471
|
+
p.expect(pexpect.EOF)
|
472
|
+
fd.flush()
|
473
|
+
status = p.wait()
|
474
|
+
end_time = datetime.now().isoformat()
|
475
|
+
# Update the status based on the result
|
476
|
+
if status is None:
|
477
|
+
exit_code = -15
|
478
|
+
update_command_status(command_id, 'aborted', end_time=end_time, exit_code=exit_code, user=user)
|
479
|
+
else:
|
480
|
+
exit_code = status
|
481
|
+
if exit_code == 0:
|
482
|
+
update_command_status(command_id, 'success', end_time=end_time, exit_code=exit_code, user=user)
|
483
|
+
else:
|
484
|
+
update_command_status(command_id, 'failed', end_time=end_time, exit_code=exit_code, user=user)
|
397
485
|
except Exception as e:
|
398
486
|
end_time = datetime.now().isoformat()
|
399
|
-
update_command_status(command_id, 'failed', end_time=end_time, exit_code=1)
|
487
|
+
update_command_status(command_id, 'failed', end_time=end_time, exit_code=1, user=user)
|
400
488
|
with open(get_output_file_path(command_id), 'a') as output_file:
|
401
489
|
output_file.write(str(e))
|
402
490
|
|
491
|
+
@app.route('/stop_command/<command_id>', methods=['POST'])
|
492
|
+
def stop_command(command_id):
|
493
|
+
status = read_command_status(command_id)
|
494
|
+
if not status or 'pid' not in status:
|
495
|
+
return jsonify({'error': 'Invalid command_id or command not running'}), 400
|
496
|
+
|
497
|
+
pid = status['pid']
|
498
|
+
end_time = datetime.now().isoformat()
|
499
|
+
try:
|
500
|
+
#update_command_status(command_id, 'aborted', end_time=end_time, exit_code=-15)
|
501
|
+
os.killpg(os.getpgid(pid), 15) # Send SIGTERM to the process group
|
502
|
+
return jsonify({'message': 'Command aborted'})
|
503
|
+
except Exception as e:
|
504
|
+
status_data = read_command_status(command_id) or {}
|
505
|
+
status_data['status'] = 'failed'
|
506
|
+
status_data['end_time'] = end_time
|
507
|
+
status_data['exit_code'] = 1
|
508
|
+
with open(get_status_file_path(command_id), 'w') as f:
|
509
|
+
json.dump(status_data, f)
|
510
|
+
with open(get_output_file_path(command_id), 'a') as output_file:
|
511
|
+
output_file.write(str(e))
|
512
|
+
return jsonify({'error': 'Failed to terminate command'}), 500
|
513
|
+
|
514
|
+
parseargs()
|
515
|
+
|
516
|
+
|
403
517
|
@app.before_request
|
404
518
|
def check_authentication():
|
519
|
+
# Check for token in URL if TOKEN_URL is set
|
520
|
+
token = app.config.get('TOKEN_URL')
|
521
|
+
if token and request.endpoint not in ['login', 'static']:
|
522
|
+
if request.args.get('token') == token:
|
523
|
+
return
|
524
|
+
return jsonify({'error': 'Forbidden'}), 403
|
525
|
+
|
405
526
|
if not app.config['USER'] and not app.config['LDAP_SERVER']:
|
406
527
|
return
|
528
|
+
|
407
529
|
if 'username' not in session and request.endpoint not in ['login', 'static']:
|
408
530
|
return auth.login_required(lambda: None)()
|
409
531
|
|
@@ -478,38 +600,18 @@ def run_command_endpoint():
|
|
478
600
|
# Generate a unique command_id
|
479
601
|
command_id = str(uuid.uuid4())
|
480
602
|
|
603
|
+
# Get the user from the session
|
604
|
+
user = session.get('username', '-')
|
605
|
+
|
481
606
|
# Set the initial status to running and save command details
|
482
|
-
update_command_status(command_id, 'running', command, params)
|
607
|
+
update_command_status(command_id, 'running', command, params, user=user)
|
483
608
|
|
484
609
|
# Run the command in a separate thread
|
485
|
-
thread = threading.Thread(target=run_command, args=(command_path, params, command_id))
|
610
|
+
thread = threading.Thread(target=run_command, args=(command_path, params, command_id, user))
|
486
611
|
thread.start()
|
487
612
|
|
488
613
|
return jsonify({'message': 'Command is running', 'command_id': command_id})
|
489
614
|
|
490
|
-
@app.route('/stop_command/<command_id>', methods=['POST'])
|
491
|
-
def stop_command(command_id):
|
492
|
-
status = read_command_status(command_id)
|
493
|
-
if not status or 'pid' not in status:
|
494
|
-
return jsonify({'error': 'Invalid command_id or command not running'}), 400
|
495
|
-
|
496
|
-
pid = status['pid']
|
497
|
-
end_time = datetime.now().isoformat()
|
498
|
-
try:
|
499
|
-
os.kill(pid, 15) # Send SIGTERM
|
500
|
-
#update_command_status(command_id, 'aborted', end_time=end_time, exit_code=-15)
|
501
|
-
return jsonify({'message': 'Command aborted'})
|
502
|
-
except Exception as e:
|
503
|
-
status_data = read_command_status(command_id) or {}
|
504
|
-
status_data['status'] = 'failed'
|
505
|
-
status_data['end_time'] = end_time
|
506
|
-
status_data['exit_code'] = 1
|
507
|
-
with open(get_status_file_path(command_id), 'w') as f:
|
508
|
-
json.dump(status_data, f)
|
509
|
-
with open(get_output_file_path(command_id), 'a') as output_file:
|
510
|
-
output_file.write(str(e))
|
511
|
-
return jsonify({'error': 'Failed to terminate command'}), 500
|
512
|
-
|
513
615
|
@app.route('/command_status/<command_id>', methods=['GET'])
|
514
616
|
def get_command_status(command_id):
|
515
617
|
status = read_command_status(command_id)
|
@@ -537,7 +639,7 @@ def list_commands():
|
|
537
639
|
status = read_command_status(command_id)
|
538
640
|
if status:
|
539
641
|
try:
|
540
|
-
params = shlex.join(status
|
642
|
+
params = shlex.join(status.get('params', []))
|
541
643
|
except AttributeError:
|
542
644
|
params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
|
543
645
|
command = status.get('command', '-') + ' ' + params
|
@@ -561,30 +663,60 @@ def list_commands():
|
|
561
663
|
|
562
664
|
@app.route('/command_output/<command_id>', methods=['GET'])
|
563
665
|
def get_command_output(command_id):
|
666
|
+
offset = int(request.args.get('offset', 0))
|
667
|
+
maxsize = int(request.args.get('maxsize', 10485760))
|
564
668
|
output_file_path = get_output_file_path(command_id)
|
565
669
|
if os.path.exists(output_file_path):
|
566
|
-
with open(output_file_path, '
|
567
|
-
|
670
|
+
with open(output_file_path, 'rb') as output_file:
|
671
|
+
output_file.seek(offset)
|
672
|
+
output = output_file.read().decode('utf-8', errors='replace')
|
673
|
+
new_offset = output_file.tell()
|
568
674
|
status_data = read_command_status(command_id) or {}
|
675
|
+
token = app.config.get("TOKEN_URL")
|
676
|
+
token_param = f"&token={token}" if token else ""
|
677
|
+
response = {
|
678
|
+
'output': output[-maxsize:],
|
679
|
+
'status': status_data.get("status"),
|
680
|
+
'links': {
|
681
|
+
'next': f'{request.url_root}command_output/{command_id}?offset={new_offset}&maxsize={maxsize}{token_param}'
|
682
|
+
}
|
683
|
+
}
|
569
684
|
if request.headers.get('Accept') == 'text/plain':
|
570
685
|
return f"{output}\nstatus: {status_data.get('status')}", 200, {'Content-Type': 'text/plain'}
|
571
|
-
return jsonify(
|
686
|
+
return jsonify(response)
|
572
687
|
return jsonify({'error': 'Invalid command_id'}), 404
|
573
688
|
|
574
689
|
@app.route('/executables', methods=['GET'])
|
575
690
|
def list_executables():
|
576
691
|
executables = [f for f in os.listdir('.') if os.path.isfile(f) and os.access(f, os.X_OK)]
|
692
|
+
executables.sort() # Sort the list of executables alphabetically
|
577
693
|
return jsonify(executables)
|
578
694
|
|
695
|
+
@app.route('/popup/<command_id>')
|
696
|
+
def popup(command_id):
|
697
|
+
return render_template('popup.html', command_id=command_id)
|
698
|
+
|
579
699
|
def main():
|
700
|
+
global COMMAND_STATUS_DIR
|
580
701
|
basef = f"{CONFDIR}/pywebexec_{args.listen}:{args.port}"
|
581
|
-
if
|
582
|
-
os.
|
702
|
+
if args.action == "shareterm":
|
703
|
+
COMMAND_STATUS_DIR = f"{os.getcwd()}/{COMMAND_STATUS_DIR}"
|
704
|
+
with open(basef + ".log", "ab+") as log:
|
705
|
+
pywebexec = subprocess.Popen([sys.executable] + sys.argv[:-1], stdout=log, stderr=log)
|
706
|
+
start_term()
|
707
|
+
time.sleep(1)
|
708
|
+
pywebexec.terminate()
|
709
|
+
sys.exit()
|
710
|
+
|
711
|
+
if args.action == "restart":
|
712
|
+
daemon_d('stop', pidfilepath=basef)
|
713
|
+
args.action = "start"
|
583
714
|
if args.action == "start":
|
584
|
-
return start_gunicorn(
|
715
|
+
return start_gunicorn(daemonized=True, baselog=basef)
|
585
716
|
if args.action:
|
586
717
|
return daemon_d(args.action, pidfilepath=basef)
|
587
|
-
return start_gunicorn()
|
718
|
+
return start_gunicorn(baselog=basef)
|
719
|
+
|
588
720
|
|
589
721
|
if __name__ == '__main__':
|
590
722
|
main()
|
Binary file
|