pywebexec 1.6.2__py3-none-any.whl → 1.6.3__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 +114 -42
- pywebexec/static/css/style.css +5 -1
- pywebexec/version.py +2 -2
- {pywebexec-1.6.2.dist-info → pywebexec-1.6.3.dist-info}/METADATA +1 -1
- {pywebexec-1.6.2.dist-info → pywebexec-1.6.3.dist-info}/RECORD +9 -9
- {pywebexec-1.6.2.dist-info → pywebexec-1.6.3.dist-info}/LICENSE +0 -0
- {pywebexec-1.6.2.dist-info → pywebexec-1.6.3.dist-info}/WHEEL +0 -0
- {pywebexec-1.6.2.dist-info → pywebexec-1.6.3.dist-info}/entry_points.txt +0 -0
- {pywebexec-1.6.2.dist-info → pywebexec-1.6.3.dist-info}/top_level.txt +0 -0
pywebexec/pywebexec.py
CHANGED
@@ -24,6 +24,7 @@ import fcntl
|
|
24
24
|
import termios
|
25
25
|
import struct
|
26
26
|
import subprocess
|
27
|
+
import logging
|
27
28
|
|
28
29
|
|
29
30
|
if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
|
@@ -41,8 +42,14 @@ app.config['LDAP_BASE_DN'] = os.environ.get('PYWEBEXEC_LDAP_BASE_DN')
|
|
41
42
|
app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
|
42
43
|
app.config['LDAP_BIND_PASSWORD'] = os.environ.get('PYWEBEXEC_LDAP_BIND_PASSWORD')
|
43
44
|
|
45
|
+
# Get the Gunicorn error logger
|
46
|
+
gunicorn_logger = logging.getLogger('gunicorn.error')
|
47
|
+
app.logger.handlers = gunicorn_logger.handlers
|
48
|
+
app.logger.setLevel(logging.INFO) # Set the logging level to INFO
|
49
|
+
|
44
50
|
# Directory to store the command status and output
|
45
51
|
CWD = os.getcwd()
|
52
|
+
PYWEBEXEC = os.path.abspath(__file__)
|
46
53
|
COMMAND_STATUS_DIR = '.web_status'
|
47
54
|
CONFDIR = os.path.expanduser("~/").rstrip('/')
|
48
55
|
if os.path.isdir(f"{CONFDIR}/.config"):
|
@@ -219,19 +226,19 @@ def get_last_non_empty_line_of_file(file_path):
|
|
219
226
|
|
220
227
|
|
221
228
|
def start_gunicorn(daemonized=False, baselog=None):
|
229
|
+
check_processes()
|
222
230
|
pidfile = f"{baselog}.pid"
|
223
231
|
if daemonized:
|
224
232
|
if daemon_d('status', pidfilepath=baselog, silent=True):
|
225
233
|
print(f"Error: pywebexec already running on {args.listen}:{args.port}", file=sys.stderr)
|
226
234
|
return 1
|
227
235
|
|
228
|
-
if sys.stdout.isatty():
|
236
|
+
if daemonized or not sys.stdout.isatty():
|
237
|
+
errorlog = f"{baselog}.log"
|
238
|
+
accesslog = None #f"{baselog}.access.log"
|
239
|
+
else:
|
229
240
|
errorlog = "-"
|
230
241
|
accesslog = None #"-"
|
231
|
-
else:
|
232
|
-
errorlog = f"{baselog}.log"
|
233
|
-
accesslog = None # f"{baselog}.access.log"
|
234
|
-
|
235
242
|
options = {
|
236
243
|
'bind': '%s:%s' % (args.listen, args.port),
|
237
244
|
'workers': 4,
|
@@ -323,11 +330,11 @@ def print_urls(command_id=None):
|
|
323
330
|
if token:
|
324
331
|
url_params = f"?token={token}"
|
325
332
|
if command_id:
|
326
|
-
print(f"{protocol}://{hostname}:{args.port}/dopopup/{command_id}{url_params}")
|
327
|
-
print(f"{protocol}://{ip}:{args.port}/dopopup/{command_id}{url_params}")
|
333
|
+
print(f"{protocol}://{hostname}:{args.port}/dopopup/{command_id}{url_params}", flush=True)
|
334
|
+
print(f"{protocol}://{ip}:{args.port}/dopopup/{command_id}{url_params}", flush=True)
|
328
335
|
else:
|
329
|
-
print(f"{protocol}://{hostname}:{args.port}{url_params}")
|
330
|
-
print(f"{protocol}://{ip}:{args.port}{url_params}")
|
336
|
+
print(f"{protocol}://{hostname}:{args.port}{url_params}", flush=True)
|
337
|
+
print(f"{protocol}://{ip}:{args.port}{url_params}", flush=True)
|
331
338
|
|
332
339
|
|
333
340
|
def is_port_in_use(address, port):
|
@@ -473,13 +480,16 @@ def script(output_file):
|
|
473
480
|
with open(output_file, 'wb') as fd:
|
474
481
|
p = pexpect.spawn(shell, echo=True)
|
475
482
|
p.logfile_read = fd
|
483
|
+
update_command_status(term_command_id, {"pid": p.pid})
|
476
484
|
# Set the window size
|
477
485
|
sigwinch_passthrough(None, None)
|
478
486
|
signal.signal(signal.SIGWINCH, sigwinch_passthrough)
|
479
487
|
p.interact()
|
480
488
|
|
481
489
|
|
482
|
-
def run_command(command, params, command_id
|
490
|
+
def run_command(fromip, user, command, params, command_id):
|
491
|
+
# app.logger.info(f'{fromip} run_command {command_id} {user}: {command} {params}')
|
492
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}')
|
483
493
|
start_time = datetime.now().isoformat()
|
484
494
|
update_command_status(command_id, {
|
485
495
|
'status': 'running',
|
@@ -512,6 +522,7 @@ def run_command(command, params, command_id, user):
|
|
512
522
|
'exit_code': exit_code,
|
513
523
|
'user': user
|
514
524
|
})
|
525
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: command aborted')
|
515
526
|
else:
|
516
527
|
exit_code = status
|
517
528
|
if exit_code == 0:
|
@@ -521,6 +532,7 @@ def run_command(command, params, command_id, user):
|
|
521
532
|
'exit_code': exit_code,
|
522
533
|
'user': user
|
523
534
|
})
|
535
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
|
524
536
|
else:
|
525
537
|
update_command_status(command_id, {
|
526
538
|
'status': 'failed',
|
@@ -528,6 +540,8 @@ def run_command(command, params, command_id, user):
|
|
528
540
|
'exit_code': exit_code,
|
529
541
|
'user': user
|
530
542
|
})
|
543
|
+
log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: exit code {exit_code}')
|
544
|
+
|
531
545
|
except Exception as e:
|
532
546
|
end_time = datetime.now().isoformat()
|
533
547
|
update_command_status(command_id, {
|
@@ -538,9 +552,78 @@ def run_command(command, params, command_id, user):
|
|
538
552
|
})
|
539
553
|
with open(get_output_file_path(command_id), 'a') as output_file:
|
540
554
|
output_file.write(str(e))
|
555
|
+
app.logger.error(fromip, user, f'Error running command {command_id}: {e}')
|
556
|
+
|
557
|
+
|
558
|
+
def command_str(command, params):
|
559
|
+
try:
|
560
|
+
params = shlex.join(params)
|
561
|
+
except AttributeError:
|
562
|
+
params = " ".join([shlex.quote(p) if " " in p else p for p in params])
|
563
|
+
return f"{command} {params}"
|
564
|
+
|
565
|
+
|
566
|
+
def read_commands():
|
567
|
+
commands = []
|
568
|
+
for filename in os.listdir(COMMAND_STATUS_DIR):
|
569
|
+
if filename.endswith('.json'):
|
570
|
+
command_id = filename[:-5]
|
571
|
+
status = read_command_status(command_id)
|
572
|
+
if status:
|
573
|
+
command = command_str(status.get('command', '-'), status.get('params', []))
|
574
|
+
last_line = status.get('last_output_line')
|
575
|
+
if last_line is None:
|
576
|
+
output_file_path = get_output_file_path(command_id)
|
577
|
+
if os.path.exists(output_file_path):
|
578
|
+
last_line = get_last_non_empty_line_of_file(output_file_path)
|
579
|
+
commands.append({
|
580
|
+
'command_id': command_id,
|
581
|
+
'status': status['status'],
|
582
|
+
'start_time': status.get('start_time', 'N/A'),
|
583
|
+
'end_time': status.get('end_time', 'N/A'),
|
584
|
+
'command': command,
|
585
|
+
'exit_code': status.get('exit_code', 'N/A'),
|
586
|
+
'last_output_line': last_line,
|
587
|
+
})
|
588
|
+
return commands
|
589
|
+
|
590
|
+
def is_process_alive(pid):
|
591
|
+
try:
|
592
|
+
os.kill(pid, 0)
|
593
|
+
except ProcessLookupError:
|
594
|
+
return False
|
595
|
+
except PermissionError:
|
596
|
+
return True
|
597
|
+
return True
|
598
|
+
|
599
|
+
def check_processes():
|
600
|
+
for filename in os.listdir(COMMAND_STATUS_DIR):
|
601
|
+
if filename.endswith('.json'):
|
602
|
+
command_id = filename[:-5]
|
603
|
+
status = read_command_status(command_id)
|
604
|
+
if status.get('status') == 'running' and 'pid' in status:
|
605
|
+
if not is_process_alive(status['pid']):
|
606
|
+
end_time = datetime.now().isoformat()
|
607
|
+
update_command_status(command_id, {
|
608
|
+
'status': 'aborted',
|
609
|
+
'end_time': end_time,
|
610
|
+
'exit_code': -1,
|
611
|
+
})
|
612
|
+
|
613
|
+
parseargs()
|
614
|
+
|
615
|
+
def log_info(fromip, user, message):
|
616
|
+
app.logger.info(f"{user} {fromip}: {message}")
|
617
|
+
|
618
|
+
def log_error(fromip, user, message):
|
619
|
+
app.logger.error(f"{user} {fromip}: {message}")
|
620
|
+
|
621
|
+
def log_request(message):
|
622
|
+
log_info(request.remote_addr, session.get('username', '-'), message)
|
541
623
|
|
542
624
|
@app.route('/stop_command/<command_id>', methods=['POST'])
|
543
625
|
def stop_command(command_id):
|
626
|
+
log_request(f"stop_command {command_id}")
|
544
627
|
status = read_command_status(command_id)
|
545
628
|
if not status or 'pid' not in status:
|
546
629
|
return jsonify({'error': 'Invalid command_id or command not running'}), 400
|
@@ -559,8 +642,6 @@ def stop_command(command_id):
|
|
559
642
|
})
|
560
643
|
return jsonify({'error': 'Failed to terminate command'}), 500
|
561
644
|
|
562
|
-
parseargs()
|
563
|
-
|
564
645
|
|
565
646
|
@app.before_request
|
566
647
|
def check_authentication():
|
@@ -653,11 +734,12 @@ def run_command_endpoint():
|
|
653
734
|
'status': 'running',
|
654
735
|
'command': command,
|
655
736
|
'params': params,
|
656
|
-
'user': user
|
737
|
+
'user': user,
|
738
|
+
'from': request.remote_addr,
|
657
739
|
})
|
658
740
|
|
659
741
|
# Run the command in a separate thread
|
660
|
-
thread = threading.Thread(target=run_command, args=(command_path, params, command_id
|
742
|
+
thread = threading.Thread(target=run_command, args=(request.remote_addr, user, command_path, params, command_id))
|
661
743
|
thread.start()
|
662
744
|
|
663
745
|
return jsonify({'message': 'Command is running', 'command_id': command_id})
|
@@ -682,32 +764,8 @@ def index():
|
|
682
764
|
|
683
765
|
@app.route('/commands', methods=['GET'])
|
684
766
|
def list_commands():
|
685
|
-
commands = []
|
686
|
-
for filename in os.listdir(COMMAND_STATUS_DIR):
|
687
|
-
if filename.endswith('.json'):
|
688
|
-
command_id = filename[:-5]
|
689
|
-
status = read_command_status(command_id)
|
690
|
-
if status:
|
691
|
-
try:
|
692
|
-
params = shlex.join(status.get('params', []))
|
693
|
-
except AttributeError:
|
694
|
-
params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
|
695
|
-
command = status.get('command', '-') + ' ' + params
|
696
|
-
last_line = status.get('last_output_line')
|
697
|
-
if last_line is None:
|
698
|
-
output_file_path = get_output_file_path(command_id)
|
699
|
-
if os.path.exists(output_file_path):
|
700
|
-
last_line = get_last_non_empty_line_of_file(output_file_path)
|
701
|
-
commands.append({
|
702
|
-
'command_id': command_id,
|
703
|
-
'status': status['status'],
|
704
|
-
'start_time': status.get('start_time', 'N/A'),
|
705
|
-
'end_time': status.get('end_time', 'N/A'),
|
706
|
-
'command': command,
|
707
|
-
'exit_code': status.get('exit_code', 'N/A'),
|
708
|
-
'last_output_line': last_line,
|
709
|
-
})
|
710
767
|
# Sort commands by start_time in descending order
|
768
|
+
commands = read_commands()
|
711
769
|
commands.sort(key=lambda x: x['start_time'], reverse=True)
|
712
770
|
return jsonify(commands)
|
713
771
|
|
@@ -737,6 +795,18 @@ def get_command_output(command_id):
|
|
737
795
|
return jsonify(response)
|
738
796
|
return jsonify({'error': 'Invalid command_id'}), 404
|
739
797
|
|
798
|
+
@app.route('/command_output_raw/<command_id>', methods=['GET'])
|
799
|
+
def get_command_output_raw(command_id):
|
800
|
+
offset = int(request.args.get('offset', 0))
|
801
|
+
maxsize = int(request.args.get('maxsize', 10485760))
|
802
|
+
output_file_path = get_output_file_path(command_id)
|
803
|
+
if os.path.exists(output_file_path):
|
804
|
+
with open(output_file_path, 'rb') as output_file:
|
805
|
+
output_file.seek(offset)
|
806
|
+
output = output_file.read(maxsize)
|
807
|
+
return output, 200, {'Content-Type': 'text/plain'}
|
808
|
+
return jsonify({'error': 'Invalid command_id'}), 404
|
809
|
+
|
740
810
|
@app.route('/executables', methods=['GET'])
|
741
811
|
def list_executables():
|
742
812
|
executables = [f for f in os.listdir('.') if os.path.isfile(f) and os.access(f, os.X_OK)]
|
@@ -774,16 +844,18 @@ def main():
|
|
774
844
|
args.action = "start"
|
775
845
|
port_used = is_port_in_use(args.listen, args.port)
|
776
846
|
if args.action != "stop":
|
777
|
-
print("Starting server:")
|
847
|
+
print("Starting server:", flush=True)
|
778
848
|
print_urls()
|
779
849
|
if args.action != "stop" and port_used:
|
780
850
|
print(f"Error: port {args.port} already in use", file=sys.stderr)
|
781
851
|
return 1
|
782
852
|
if args.action == "shareterm":
|
783
853
|
COMMAND_STATUS_DIR = f"{os.getcwd()}/{COMMAND_STATUS_DIR}"
|
854
|
+
check_processes()
|
784
855
|
sys.argv.remove("shareterm")
|
785
|
-
|
786
|
-
|
856
|
+
sys.argv[0] = PYWEBEXEC
|
857
|
+
with open(basef + ".log", "a") as log:
|
858
|
+
pywebexec = subprocess.Popen([sys.executable] + sys.argv, stdout=log, stderr=log, bufsize=1)
|
787
859
|
print_urls(term_command_id)
|
788
860
|
res = start_term()
|
789
861
|
print("Stopping server")
|
pywebexec/static/css/style.css
CHANGED
@@ -14,7 +14,7 @@ body {
|
|
14
14
|
position: relative;
|
15
15
|
border-radius: 10px;
|
16
16
|
border: 1px solid #aaa;
|
17
|
-
box-shadow: 0 0 10px rgba(0, 0, 0, 0.
|
17
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.40);
|
18
18
|
}
|
19
19
|
table {
|
20
20
|
width: 100%;
|
@@ -54,6 +54,7 @@ select { /* Safari bug */
|
|
54
54
|
border: 1px solid #ccc;
|
55
55
|
border-radius: 10px;
|
56
56
|
overflow-y: hidden;
|
57
|
+
box-shadow: 0 0 10px rgba(0, 0, 0, 0.60);
|
57
58
|
}
|
58
59
|
.copy-icon {
|
59
60
|
cursor: pointer;
|
@@ -90,6 +91,9 @@ form {
|
|
90
91
|
align-items: center;
|
91
92
|
gap: 10px;
|
92
93
|
}
|
94
|
+
#commandStatus {
|
95
|
+
vertical-align: bottom;
|
96
|
+
}
|
93
97
|
.status-icon {
|
94
98
|
display: inline-block;
|
95
99
|
width: 16px;
|
pywebexec/version.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
|
2
|
-
pywebexec/pywebexec.py,sha256=
|
3
|
-
pywebexec/version.py,sha256=
|
2
|
+
pywebexec/pywebexec.py,sha256=6roMSuUnfNx5fj9-gg0E1WEUEDbX82XYNIHzxsjoreE,31701
|
3
|
+
pywebexec/version.py,sha256=mQ_8947spH9F9E4bJgRMJ3LZK_sGORi1ak9UVDzTrr8,411
|
4
4
|
pywebexec/static/css/Consolas NF.ttf,sha256=DJEOzF0eqZ-kxu3Gs_VE8X0NJqiobBzmxWDGpdgGRxI,1313900
|
5
|
-
pywebexec/static/css/style.css,sha256=
|
5
|
+
pywebexec/static/css/style.css,sha256=oSMdSvQnjGa75p4666qkiekfzvD_cdAVxpCCjvqTApg,8275
|
6
6
|
pywebexec/static/css/xterm.css,sha256=uo5phWaUiJgcz0DAzv46uoByLLbJLeetYosL1xf68rY,5559
|
7
7
|
pywebexec/static/images/aborted.svg,sha256=_mP43hU5QdRLFZIknBgjx-dIXrHgQG23-QV27ApXK2A,381
|
8
8
|
pywebexec/static/images/copy.svg,sha256=d9OwtGh5GzzZHzYcDrLfNxZYLth1Q64x7bRyYxu4Px0,622
|
@@ -33,9 +33,9 @@ pywebexec/static/js/xterm/xterm.js.map,sha256=Y7O2Pb-fIS7Z8AC1D5s04_aiW_Jf1f4mCf
|
|
33
33
|
pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
34
34
|
pywebexec/templates/index.html,sha256=3XAXlpluzmTxC75KLzHDI4aJvQyQrOShB_weJ27PE2U,2815
|
35
35
|
pywebexec/templates/popup.html,sha256=6FfzMjxuAC_xwyMGPf3p5tm6iPVY0QCsqY80eotD0z8,1362
|
36
|
-
pywebexec-1.6.
|
37
|
-
pywebexec-1.6.
|
38
|
-
pywebexec-1.6.
|
39
|
-
pywebexec-1.6.
|
40
|
-
pywebexec-1.6.
|
41
|
-
pywebexec-1.6.
|
36
|
+
pywebexec-1.6.3.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
|
37
|
+
pywebexec-1.6.3.dist-info/METADATA,sha256=u1r62zAugZzXBTjXCCcE6dryvFdpbRBwkioDtFVDYbw,7801
|
38
|
+
pywebexec-1.6.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
39
|
+
pywebexec-1.6.3.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
|
40
|
+
pywebexec-1.6.3.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
|
41
|
+
pywebexec-1.6.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|