pywebexec 1.6.1__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 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, user):
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, user))
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
- with open(basef + ".log", "ab+") as log:
786
- pywebexec = subprocess.Popen([sys.executable] + sys.argv, stdout=log, stderr=log)
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")
@@ -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.30);
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;
@@ -386,7 +390,7 @@ span {
386
390
  font-size: 13px;
387
391
  font-weight: normal;
388
392
  border: 1px solid #aaa;
389
- border-radius: 18px;
390
- min-width: 18px;
393
+ border-radius: 17px;
394
+ min-width: 17px;
391
395
  text-align: center;
392
396
  }
@@ -4,6 +4,7 @@ let paramsInput = document.getElementById('params');
4
4
  let commandListSelect = document.getElementById('commandList');
5
5
  let showCommandListButton = document.getElementById('showCommandListButton');
6
6
  let isHandlingKeydown = false;
7
+ let firstVisibleItem = 0;
7
8
 
8
9
  function unfilterCommands() {
9
10
  const options = commandListSelect.options;
@@ -17,10 +18,14 @@ function filterCommands() {
17
18
  const value = commandInput.value.slice(0, commandInput.selectionStart);
18
19
  const options = commandListSelect.options;
19
20
  let nbVisibleItems = 0;
21
+ firstVisibleItem = -1;
20
22
  for (let i = 0; i < options.length; i++) {
21
23
  if (options[i].text.startsWith(value)) {
22
24
  options[i].style.display = 'block';
23
25
  nbVisibleItems += 1;
26
+ if (firstVisibleItem === -1) {
27
+ firstVisibleItem = i;
28
+ }
24
29
  } else {
25
30
  options[i].style.display = 'none';
26
31
  }
@@ -88,12 +93,12 @@ commandInput.addEventListener('keydown', (event) => {
88
93
  paramsInput.focus();
89
94
  paramsInput.setSelectionRange(0, paramsInput.value.length);
90
95
  } else if (event.key === 'ArrowDown') {
91
- /*setCommandListPosition();*/
92
- unfilterCommands();
93
96
  if (commandListSelect.options.length > 1) {
94
97
  commandListSelect.style.display = 'block';
98
+ commandListSelect.selectedIndex = firstVisibleItem;
95
99
  commandListSelect.focus();
96
- commandListSelect.selectedIndex = 0;
100
+ commandListSelect.options[firstVisibleItem].scrollIntoView();
101
+ commandListSelect.options[firstVisibleItem].focus();
97
102
  }
98
103
  event.preventDefault();
99
104
  }
pywebexec/version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.6.1'
16
- __version_tuple__ = version_tuple = (1, 6, 1)
15
+ __version__ = version = '1.6.3'
16
+ __version_tuple__ = version_tuple = (1, 6, 3)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.6.1
3
+ Version: 1.6.3
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -1,8 +1,8 @@
1
1
  pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
2
- pywebexec/pywebexec.py,sha256=S24FSD4XywesMvM6nqJMdS8E1lF9mklN4nUlnZUp9Zs,28760
3
- pywebexec/version.py,sha256=edFVVa8HpVPfLqL2y6CKtViSqJREfmXxInA-HCy-134,411
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=_v1VCscdxUZjuFRkK4ty012oXTNysyKi2m0SiqoF0Oo,8182
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
@@ -18,7 +18,7 @@ pywebexec/static/images/popup.svg,sha256=0Bl9A_v5cBsMPn6FnOlVWlAQKgd2zqiWQbhjcL9
18
18
  pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65EiuwtvK4A,607
19
19
  pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
20
20
  pywebexec/static/images/success.svg,sha256=PJDcCSTevJh7rkfSFLtc7P0pbeh8PVQBS8DaOLQemmc,489
21
- pywebexec/static/js/commands.js,sha256=8JDb3Q55EJOYf2Q9Uy6qEuqAnn1oGjM0RndgQ4aOjqo,7725
21
+ pywebexec/static/js/commands.js,sha256=h2fkd9qpypLBxvhEEbay23nwuqUwcKJA0vHugcyL8pU,7961
22
22
  pywebexec/static/js/popup.js,sha256=TCzGoPVS_EvwzGc544SvRgLYG7XWXRF8tIQGfu3atrU,8533
23
23
  pywebexec/static/js/script.js,sha256=mGY_-A1rZNjgpTU8FsVH0j6Tj7v4pHpSaREPbTttvEs,17376
24
24
  pywebexec/static/js/xterm/LICENSE,sha256=EU1P4eXTull-_T9I80VuwnJXubB-zLzUl3xpEYj2T1M,1083
@@ -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.1.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
37
- pywebexec-1.6.1.dist-info/METADATA,sha256=UiFTdeosOUAPxCTu68JhlVO5SYAA3qiudlZC3t3rVCI,7801
38
- pywebexec-1.6.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
39
- pywebexec-1.6.1.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
40
- pywebexec-1.6.1.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
41
- pywebexec-1.6.1.dist-info/RECORD,,
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,,