pywebexec 2.2.8__py3-none-any.whl → 2.3.0__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
@@ -12,17 +12,23 @@ import string
12
12
  from datetime import datetime, timezone, timedelta
13
13
  import time
14
14
  import shlex
15
- from gunicorn.app.base import Application
15
+ import platform
16
+ import pexpect
17
+
18
+ if platform.system() != 'Windows':
19
+ from gunicorn.app.base import Application
20
+ import pwd
21
+ import fcntl
22
+ import termios
23
+ else:
24
+ from waitress import serve
25
+ import winpty
16
26
  import ipaddress
17
27
  from socket import socket, AF_INET, SOCK_STREAM
18
28
  import ssl
19
29
  import re
20
- import pwd
21
30
  from secrets import token_urlsafe
22
- import pexpect
23
31
  import signal
24
- import fcntl
25
- import termios
26
32
  import struct
27
33
  import subprocess
28
34
  import logging
@@ -138,24 +144,24 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
138
144
 
139
145
  return cert_pem, key_pem
140
146
 
147
+ if platform.system() != 'Windows':
148
+ class PyWebExec(Application):
141
149
 
142
- class PyWebExec(Application):
143
-
144
- def __init__(self, app, options=None):
145
- self.options = options or {}
146
- self.application = app
147
- super().__init__()
150
+ def __init__(self, app, options=None):
151
+ self.options = options or {}
152
+ self.application = app
153
+ super().__init__()
148
154
 
149
- def load_config(self):
150
- config = {
151
- key: value for key, value in self.options.items()
152
- if key in self.cfg.settings and value is not None
153
- }
154
- for key, value in config.items():
155
- self.cfg.set(key.lower(), value)
155
+ def load_config(self):
156
+ config = {
157
+ key: value for key, value in self.options.items()
158
+ if key in self.cfg.settings and value is not None
159
+ }
160
+ for key, value in config.items():
161
+ self.cfg.set(key.lower(), value)
156
162
 
157
- def load(self):
158
- return self.application
163
+ def load(self):
164
+ return self.application
159
165
  #38;2;66;59;165m
160
166
  ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF]|[\x00-\x1F\x7F])')
161
167
  ANSI_ESCAPE = re.compile(br'(?:\x1B[@-Z\\-_]|\x1B([(]B|>)|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~]|\x1B\[([0-9]{1,2};){0,4}[0-9]{1,3}[m|K]|\x1B\[[0-9;]*[mGKHF])')
@@ -455,7 +461,7 @@ def update_command_status(command_id, updates):
455
461
  del status['last_read']
456
462
  with open(status_file_path, 'w') as f:
457
463
  json.dump(status, f)
458
- os.sync()
464
+ os.fsync(f)
459
465
  status_cache[command_id] = status
460
466
 
461
467
 
@@ -507,7 +513,6 @@ def script(output_file):
507
513
 
508
514
 
509
515
  def run_command(fromip, user, command, params, command_id, rows, cols):
510
- # app.logger.info(f'{fromip} run_command {command_id} {user}: {command} {params}')
511
516
  log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}')
512
517
  start_time = datetime.now(timezone.utc).isoformat()
513
518
  if user:
@@ -524,41 +529,68 @@ def run_command(fromip, user, command, params, command_id, rows, cols):
524
529
  })
525
530
  output_file_path = get_output_file_path(command_id)
526
531
  try:
527
- with open(output_file_path, 'wb') as fd:
528
- p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None, dimensions=(rows, cols))
532
+ if platform.system() == 'Windows':
533
+ # On Windows, use winpty
534
+ cmdline = f"{sys.executable} -u {command} " + " ".join(shlex.quote(p) for p in params)
535
+ with open(output_file_path, 'wb', buffering=0) as fd:
536
+ p = winpty.PTY(cols, rows)
537
+ p.spawn(cmdline)
538
+ pid = p.pid
539
+ update_command_status(command_id, {
540
+ 'pid': pid,
541
+ })
542
+ while True:
543
+ try:
544
+ if not p.isalive():
545
+ time.sleep(1)
546
+ data = p.read(10485760, blocking=False)
547
+ fd.write(data.encode())
548
+ if not p.isalive():
549
+ break
550
+ time.sleep(0.1)
551
+ except (EOFError, winpty.WinptyError):
552
+ break
553
+ status = p.get_exitstatus()
554
+ del p
555
+ print("end", status)
556
+ else:
557
+ # On Unix, use pexpect
558
+ with open(output_file_path, 'wb') as fd:
559
+ p = pexpect.spawn(command, params, ignore_sighup=True, timeout=None, dimensions=(rows, cols))
560
+ update_command_status(command_id, {
561
+ 'pid': p.pid,
562
+ })
563
+ p.logfile = fd
564
+ p.expect(pexpect.EOF)
565
+ fd.flush()
566
+ status = p.wait()
567
+
568
+ end_time = datetime.now(timezone.utc).isoformat()
569
+ # Update the status based on the result
570
+ if status is None:
571
+ exit_code = -15
529
572
  update_command_status(command_id, {
530
- 'pid': p.pid,
573
+ 'status': 'aborted',
574
+ 'end_time': end_time,
575
+ 'exit_code': exit_code,
531
576
  })
532
- p.logfile = fd
533
- p.expect(pexpect.EOF)
534
- fd.flush()
535
- status = p.wait()
536
- end_time = datetime.now(timezone.utc).isoformat()
537
- # Update the status based on the result
538
- if status is None:
539
- exit_code = -15
577
+ log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: command aborted')
578
+ else:
579
+ exit_code = status
580
+ if exit_code == 0:
540
581
  update_command_status(command_id, {
541
- 'status': 'aborted',
582
+ 'status': 'success',
542
583
  'end_time': end_time,
543
584
  'exit_code': exit_code,
544
585
  })
545
- log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: command aborted')
586
+ log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
546
587
  else:
547
- exit_code = status
548
- if exit_code == 0:
549
- update_command_status(command_id, {
550
- 'status': 'success',
551
- 'end_time': end_time,
552
- 'exit_code': exit_code,
553
- })
554
- log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: completed successfully')
555
- else:
556
- update_command_status(command_id, {
557
- 'status': 'failed',
558
- 'end_time': end_time,
559
- 'exit_code': exit_code,
560
- })
561
- log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: exit code {exit_code}')
588
+ update_command_status(command_id, {
589
+ 'status': 'failed',
590
+ 'end_time': end_time,
591
+ 'exit_code': exit_code,
592
+ })
593
+ log_info(fromip, user, f'run_command {command_id}: {command_str(command, params)}: exit code {exit_code}')
562
594
 
563
595
  except Exception as e:
564
596
  end_time = datetime.now(timezone.utc).isoformat()
@@ -661,7 +693,7 @@ app.config['TITLE'] = f"{args.title} API"
661
693
 
662
694
 
663
695
  def get_executable(cmd):
664
- if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
696
+ if os.path.isfile(cmd) and os.access(cmd, os.X_OK) and Path(cmd).suffix not in [".help", ".yaml", ".env", ".swp"]:
665
697
  help_file = f"{cmd}.help"
666
698
  help_text = ""
667
699
  if os.path.exists(help_file) and os.path.isfile(help_file):
@@ -688,14 +720,17 @@ def get_executables():
688
720
  def stop_command(command_id):
689
721
  log_request(f"stop_command {command_id}")
690
722
  status = read_command_status(command_id)
723
+ user = session.get('username', '-')
691
724
  if not status or 'pid' not in status:
692
725
  return jsonify({'error': 'Invalid command_id or command not running'}), 400
693
726
 
694
727
  pid = status['pid']
695
728
  end_time = datetime.now(timezone.utc).isoformat()
696
729
  try:
697
- os.killpg(os.getpgid(pid), 15) # Send SIGTERM to the process group
698
- return jsonify({'message': 'Command aborted'})
730
+ try:
731
+ os.killpg(os.getpgid(pid), signal.SIGTERM) # Send SIGTERM to the process group
732
+ except:
733
+ os.kill(pid, signal.SIGINT) # Send SIGTERM to the process
699
734
  except Exception as e:
700
735
  update_command_status(command_id, {
701
736
  'status': 'aborted',
@@ -703,6 +738,10 @@ def stop_command(command_id):
703
738
  'exit_code': -15,
704
739
  })
705
740
  return jsonify({'error': 'Failed to terminate command'}), 500
741
+ output_file = get_output_file_path(command_id)
742
+ with open(output_file, 'a') as f:
743
+ f.write(f"\n\nCommand aborted by user {user} at {end_time}\n")
744
+ return jsonify({'message': 'Command aborted'})
706
745
 
707
746
 
708
747
  @app.before_request
@@ -846,6 +885,8 @@ def run_dynamic_command(cmd):
846
885
  if isinstance(data_params, dict):
847
886
  params = ""
848
887
  for param in schema_params.keys():
888
+ if not data_params.get(param, None) and schema_params[param].get("type", None) == "object":
889
+ data_params[param] = '{}'
849
890
  if not param in data_params:
850
891
  continue
851
892
  value = data_params[param]
@@ -1160,6 +1201,15 @@ def main():
1160
1201
  pywebexec.terminate()
1161
1202
  sys.exit(res)
1162
1203
 
1204
+ if platform.system() == 'Windows':
1205
+ # Use waitress on Windows
1206
+ ssl_context = None
1207
+ if args.cert:
1208
+ ssl_context = (args.cert, args.key)
1209
+ serve(app, host=args.listen, port=args.port, url_scheme='https' if args.cert else 'http', threads=8)
1210
+ return 0
1211
+
1212
+ # Use gunicorn on Unix-like systems
1163
1213
  if args.action == "start":
1164
1214
  return start_gunicorn(daemonized=True, baselog=basef)
1165
1215
  if args.action:
@@ -260,12 +260,13 @@ paramsInput.addEventListener('focus', () => {
260
260
  const currentCmd = commandInput.value;
261
261
  paramsInput.name = currentCmd;
262
262
  if (gExecutables[currentCmd] && gExecutables[currentCmd].schema && gExecutables[currentCmd].schema.properties && paramsContainer.style.display == 'none') {
263
+ const schema = gExecutables[currentCmd].schema;
263
264
  jsForm = createSchemaForm($('#schemaForm'), gExecutables[currentCmd].schema, async function (errors, values) {
264
265
  const commandName = commandInput.value;
265
266
  fitAddon.fit();
266
267
  terminal.clear();
267
268
  payload = { params: values, rows: terminal.rows, cols: terminal.cols }
268
- if ('parallel' in values) {
269
+ if (schema.schema_options && schema.schema_options.batch_param && 'parallel' in values) {
269
270
  payload['parallel'] = values['parallel'];
270
271
  payload['delay'] = values['delay'];
271
272
  delete payload['params']['parallel'];
@@ -301,7 +302,6 @@ paramsInput.addEventListener('focus', () => {
301
302
  input1.focus();
302
303
  }
303
304
  schemaFormPW.addEventListener('submit', (event) => {
304
- console.log('Form submitted');
305
305
  paramsContainer.style.display = 'none';
306
306
  event.preventDefault();
307
307
  });
@@ -251,7 +251,7 @@ function createSchemaForm($form, schema, onSubmit, schemaName) {
251
251
  err.classList.add('alert');
252
252
  err.style.display = 'none';
253
253
  schemaForm.appendChild(err);
254
- validateSchemaForm(schemaForm, formDesc, schema, value, schemaName);
254
+ validateSchemaForm(schemaForm, formDesc, schema, jsform.root.getFormValues(), schemaName);
255
255
  schemaForm.querySelectorAll('textarea').forEach(txt => {
256
256
  txt.style.height = "0";
257
257
  setTimeout(() => adjustTxtHeight(txt), 1);
pywebexec/version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '2.2.8'
21
- __version_tuple__ = version_tuple = (2, 2, 8)
20
+ __version__ = version = '2.3.0'
21
+ __version_tuple__ = version_tuple = (2, 3, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pywebexec
3
- Version: 2.2.8
3
+ Version: 2.3.0
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -52,16 +52,18 @@ Classifier: Topic :: System :: Systems Administration
52
52
  Requires-Python: >=3.6
53
53
  Description-Content-Type: text/markdown
54
54
  License-File: LICENSE
55
- Requires-Dist: python-daemon>=2.3.2
55
+ Requires-Dist: python-daemon>=2.3.2; platform_system != "Windows"
56
56
  Requires-Dist: cryptography>=40.0.2
57
57
  Requires-Dist: Flask>=2.0.3
58
58
  Requires-Dist: Flask-HTTPAuth>=4.8.0
59
59
  Requires-Dist: pexpect>=4.9.0
60
- Requires-Dist: gunicorn>=21.2.0
60
+ Requires-Dist: gunicorn>=21.2.0; platform_system != "Windows"
61
61
  Requires-Dist: ldap3>=2.9.1
62
62
  Requires-Dist: pyte>=0.8.1
63
63
  Requires-Dist: PyYAML>=6.0.1
64
64
  Requires-Dist: run-para>=1.0.2
65
+ Requires-Dist: waitress>=3.0.2; platform_system == "Windows"
66
+ Requires-Dist: pywinpty>=2.0.15; platform_system == "Windows"
65
67
  Dynamic: license-file
66
68
 
67
69
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
@@ -1,8 +1,8 @@
1
1
  pywebexec/__init__.py,sha256=197fHJy0UDBwTTpGCGortZRr-w2kTaD7MxqdbVmTEi0,61
2
2
  pywebexec/host_ip.py,sha256=Ud_HTflWVQ8789aoQ2RZdT1wGI-ccvrwSWGz_c7T3TI,1241
3
- pywebexec/pywebexec.py,sha256=R-jp9BiUMJZW7m9N9kQC6dpIueUgFVDo4n9_GFb1rOs,45734
3
+ pywebexec/pywebexec.py,sha256=q2q4NVgWeMmAegM-iK8J9B175snHyHkW45WYPlHYJpQ,47727
4
4
  pywebexec/swagger.yaml,sha256=I_oLpp7Hqel8SDEEykvpmCT-Gv3ytGlziq9bvQOrtZY,7598
5
- pywebexec/version.py,sha256=IzlbUXKZ3t6SO6-P3X0oVBlXDGkJ-oDKEhIOPY9IVKQ,511
5
+ pywebexec/version.py,sha256=U--yqU7RFo8hQQm8oopUGYLkafj4phNIVfkf5HFEal8,511
6
6
  pywebexec/static/css/form.css,sha256=XC_0ES5yMHYz0S2OHR0RAboQN7fBUmg5ZIq8Qm5rHP0,5806
7
7
  pywebexec/static/css/markdown.css,sha256=br4-iK9wigTs54N2KHtjgZ4KLH0THVSvJo-XZAdMHiE,1970
8
8
  pywebexec/static/css/style.css,sha256=R1VOPNV2ztROKy9Fgf3tvUrtuKagY027tFJ8C866yWU,9991
@@ -33,9 +33,9 @@ pywebexec/static/images/resume.svg,sha256=99LP1Ya2JXakRCO9kW8JMuT_4a_CannF65Eiuw
33
33
  pywebexec/static/images/running.svg,sha256=fBCYwYb2O9K4N3waC2nURP25NRwZlqR4PbDZy6JQMww,610
34
34
  pywebexec/static/images/success.svg,sha256=NVwezvVMplt46ElW798vqGfrL21Mw_DWHUp_qiD_FU8,489
35
35
  pywebexec/static/images/swagger-ui.svg,sha256=FR0yeOVwe4zCYKZAjCGcT_m0Mf25NexIVaSXifIkoU0,2117
36
- pywebexec/static/js/executables.js,sha256=jB4QqWi4Qeq1uJflTSqqjsZy-ec9iy5ZmFvfNWMzXq4,11913
36
+ pywebexec/static/js/executables.js,sha256=cTgCFHr_F9bFCirtfG_uR32vOY3vNUr4Ih3Wglj5lFc,11988
37
37
  pywebexec/static/js/popup.js,sha256=O3DEWnyb5yGW9tjODYycc-ujWndyAfnJMxulaQeogtc,9700
38
- pywebexec/static/js/schemaform.js,sha256=VEI6itnu9SKltIpfnCDDQKYglsAa8z1xvUP0FpSGe3E,9602
38
+ pywebexec/static/js/schemaform.js,sha256=NlFXFKJI53izxPXct3a5XiB1RhWGt0_EIp6o1HfsryU,9624
39
39
  pywebexec/static/js/script.js,sha256=yQP2dWwTObWbg6ROeKFxwYi1NfDd2gxEn2e3oL-FWL8,18379
40
40
  pywebexec/static/js/swagger-form.js,sha256=CLcSHMhk5P4-_2MIRBoJLgEnIj_9keDDSzUugXHZjio,4565
41
41
  pywebexec/static/js/js-yaml/LICENSE,sha256=oHvCRGi5ZUznalR9R6LbKC0HcztxXbTHOpi9Y5YflVA,1084
@@ -67,9 +67,9 @@ pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
67
67
  pywebexec/templates/index.html,sha256=w18O2plH_yS8bqlPsu5hwFFmCj9H2hWLSV8B6ADcSwU,3900
68
68
  pywebexec/templates/popup.html,sha256=3kpMccKD_OLLhJ4Y9KRw6Ny8wQWjVaRrUfV9y5-bDiQ,1580
69
69
  pywebexec/templates/swagger_ui.html,sha256=MAPr-z96VERAecDvX37V8q2Nxph-O0fNDBul1x2w9SI,1147
70
- pywebexec-2.2.8.dist-info/licenses/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
71
- pywebexec-2.2.8.dist-info/METADATA,sha256=Dr-b4gxKWPpV0us8pkVqOdFsitPKbGHMFQcZYgzkSYA,12832
72
- pywebexec-2.2.8.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
73
- pywebexec-2.2.8.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
74
- pywebexec-2.2.8.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
75
- pywebexec-2.2.8.dist-info/RECORD,,
70
+ pywebexec-2.3.0.dist-info/licenses/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
71
+ pywebexec-2.3.0.dist-info/METADATA,sha256=y1F2WI5mfVSDdt_GznPWBmK07NrToa8qOjSqls1r0kM,13015
72
+ pywebexec-2.3.0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
73
+ pywebexec-2.3.0.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
74
+ pywebexec-2.3.0.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
75
+ pywebexec-2.3.0.dist-info/RECORD,,