pywebexec 1.1.2__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 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,18 +9,25 @@ 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
+ 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
+
18
28
 
19
29
  if os.environ.get('PYWEBEXEC_LDAP_SERVER'):
20
- try:
21
- from ldap3 import Server, Connection, ALL, SIMPLE, SUBTREE, Tls
22
- except:
23
- print("Need to install ldap3: pip install ldap3", file=sys.stderr)
24
- sys.exit(1)
30
+ from ldap3 import Server, Connection, ALL, SIMPLE, SUBTREE, Tls
25
31
 
26
32
  app = Flask(__name__)
27
33
  app.secret_key = os.urandom(24) # Secret key for session management
@@ -36,14 +42,13 @@ app.config['LDAP_BIND_DN'] = os.environ.get('PYWEBEXEC_LDAP_BIND_DN')
36
42
  app.config['LDAP_BIND_PASSWORD'] = os.environ.get('PYWEBEXEC_LDAP_BIND_PASSWORD')
37
43
 
38
44
  # Directory to store the command status and output
45
+ CWD = os.getcwd()
39
46
  COMMAND_STATUS_DIR = '.web_status'
40
47
  CONFDIR = os.path.expanduser("~/")
41
48
  if os.path.isdir(f"{CONFDIR}/.config"):
42
49
  CONFDIR += '/.config'
43
50
  CONFDIR += "/.pywebexec"
44
51
 
45
- if not os.path.exists(COMMAND_STATUS_DIR):
46
- os.makedirs(COMMAND_STATUS_DIR)
47
52
 
48
53
  # In-memory cache for command statuses
49
54
  command_status_cache = {}
@@ -54,11 +59,38 @@ def generate_random_password(length=12):
54
59
 
55
60
 
56
61
  def resolve_hostname(host):
57
- """try get fqdn from DNS"""
62
+ """try get fqdn from DNS/hosts"""
58
63
  try:
59
- return gethostbyname_ex(host)[0]
64
+ hostinfo = gethostbyname_ex(host)
65
+ return (hostinfo[0].rstrip('.'), hostinfo[2][0])
60
66
  except OSError:
61
- 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)
62
94
 
63
95
 
64
96
  def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
@@ -124,7 +156,7 @@ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
124
156
 
125
157
 
126
158
 
127
- class StandaloneApplication(Application):
159
+ class PyWebExec(Application):
128
160
 
129
161
  def __init__(self, app, options=None):
130
162
  self.options = options or {}
@@ -143,10 +175,17 @@ class StandaloneApplication(Application):
143
175
  return self.application
144
176
 
145
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
+
180
+ def strip_ansi_control_chars(text):
181
+ """Remove ANSI and control characters from the text."""
182
+ return ANSI_ESCAPE.sub(b'', text)
183
+
184
+
146
185
  def decode_line(line: bytes) -> str:
147
186
  """try decode line exception on binary"""
148
187
  try:
149
- return line.decode()
188
+ return strip_ansi_control_chars(line).decode().strip(" ")
150
189
  except UnicodeDecodeError:
151
190
  return ""
152
191
 
@@ -156,8 +195,11 @@ def last_line(fd, maxline=1000):
156
195
  line = "\n"
157
196
  fd.seek(0, os.SEEK_END)
158
197
  size = 0
159
- while line in ["\n", "\r"] and size < maxline:
198
+ last_pos = 0
199
+ while line in ["", "\n", "\r"] and size < maxline:
160
200
  try: # catch if file empty / only empty lines
201
+ if last_pos:
202
+ fd.seek(last_pos-2, os.SEEK_SET)
161
203
  while fd.read(1) not in [b"\n", b"\r"]:
162
204
  fd.seek(-2, os.SEEK_CUR)
163
205
  size += 1
@@ -165,8 +207,8 @@ def last_line(fd, maxline=1000):
165
207
  fd.seek(0)
166
208
  line = decode_line(fd.readline())
167
209
  break
210
+ last_pos = fd.tell()
168
211
  line = decode_line(fd.readline())
169
- fd.seek(-4, os.SEEK_CUR)
170
212
  return line.strip()
171
213
 
172
214
 
@@ -176,31 +218,35 @@ def get_last_non_empty_line_of_file(file_path):
176
218
  return last_line(f)
177
219
 
178
220
 
179
- def start_gunicorn(daemon=False, baselog=None):
180
- if daemon:
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:
181
232
  errorlog = f"{baselog}.log"
182
233
  accesslog = None # f"{baselog}.access.log"
183
- pidfile = f"{baselog}.pid"
184
- else:
185
- errorlog = "-"
186
- accesslog = "-"
187
- pidfile = None
234
+
188
235
  options = {
189
236
  'bind': '%s:%s' % (args.listen, args.port),
190
237
  'workers': 4,
191
238
  'timeout': 600,
192
239
  'certfile': args.cert,
193
240
  'keyfile': args.key,
194
- 'daemon': daemon,
241
+ 'daemon': daemonized,
195
242
  'errorlog': errorlog,
196
243
  'accesslog': accesslog,
197
244
  'pidfile': pidfile,
198
245
  }
199
- StandaloneApplication(app, options=options).run()
246
+ PyWebExec(app, options=options).run()
200
247
 
201
- def daemon_d(action, pidfilepath, hostname=None, args=None):
248
+ def daemon_d(action, pidfilepath, silent=False, hostname=None, args=None):
202
249
  """start/stop daemon"""
203
- import signal
204
250
  import daemon, daemon.pidfile
205
251
 
206
252
  pidfile = daemon.pidfile.TimeoutPIDLockFile(pidfilepath+".pid", acquire_timeout=30)
@@ -218,10 +264,14 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
218
264
  if status:
219
265
  print(f"pywebexec running pid {pidfile.read_pid()}")
220
266
  return True
221
- print("pywebexec not running")
267
+ if not silent:
268
+ print("pywebexec not running")
222
269
  return False
223
270
  elif action == "start":
224
- print(f"Starting server")
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)
225
275
  log = open(pidfilepath + ".log", "ab+")
226
276
  daemon_context = daemon.DaemonContext(
227
277
  stderr=log,
@@ -235,8 +285,22 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
235
285
  except Exception as e:
236
286
  print(e)
237
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
+
238
301
  def parseargs():
239
- global app, args
302
+ global app, args, COMMAND_STATUS_DIR
303
+
240
304
  parser = argparse.ArgumentParser(description='Run the command execution server.')
241
305
  parser.add_argument('-u', '--user', help='Username for basic auth')
242
306
  parser.add_argument('-P', '--password', help='Password for basic auth')
@@ -253,13 +317,15 @@ def parseargs():
253
317
  "-t",
254
318
  "--title",
255
319
  type=str,
256
- default="pywebexec",
320
+ default="PyWebExec",
257
321
  help="Web html title",
258
322
  )
259
323
  parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
260
324
  parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
261
325
  parser.add_argument("-g", "--gencert", action="store_true", help="https server self signed cert")
262
- parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status", choices=["start","stop","restart","status"])
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"])
263
329
 
264
330
  args = parser.parse_args()
265
331
  if os.path.isdir(args.dir):
@@ -271,9 +337,23 @@ def parseargs():
271
337
  else:
272
338
  print(f"Error: {args.dir} not found", file=sys.stderr)
273
339
  sys.exit(1)
340
+ if not os.path.exists(COMMAND_STATUS_DIR):
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}"
274
355
 
275
356
  if args.gencert:
276
- hostname = resolve_hostname(gethostname())
277
357
  args.cert = args.cert or f"{CONFDIR}/pywebexec.crt"
278
358
  args.key = args.key or f"{CONFDIR}/pywebexec.key"
279
359
  if not os.path.exists(args.cert):
@@ -294,9 +374,13 @@ def parseargs():
294
374
  app.config['USER'] = None
295
375
  app.config['PASSWORD'] = None
296
376
 
297
- return args
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}")
298
382
 
299
- parseargs()
383
+ return args
300
384
 
301
385
  def get_status_file_path(command_id):
302
386
  return os.path.join(COMMAND_STATUS_DIR, f'{command_id}.json')
@@ -304,7 +388,7 @@ def get_status_file_path(command_id):
304
388
  def get_output_file_path(command_id):
305
389
  return os.path.join(COMMAND_STATUS_DIR, f'{command_id}_output.txt')
306
390
 
307
- 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):
308
392
  status_file_path = get_status_file_path(command_id)
309
393
  status_data = read_command_status(command_id) or {}
310
394
  status_data['status'] = status
@@ -320,6 +404,8 @@ def update_command_status(command_id, status, command=None, params=None, start_t
320
404
  status_data['exit_code'] = exit_code
321
405
  if pid is not None:
322
406
  status_data['pid'] = pid
407
+ if user is not None:
408
+ status_data['user'] = user
323
409
  if status != 'running':
324
410
  output_file_path = get_output_file_path(command_id)
325
411
  if os.path.exists(output_file_path):
@@ -342,7 +428,10 @@ def read_command_status(command_id):
342
428
  if not os.path.exists(status_file_path):
343
429
  return None
344
430
  with open(status_file_path, 'r') as f:
345
- status_data = json.load(f)
431
+ try:
432
+ status_data = json.load(f)
433
+ except json.JSONDecodeError:
434
+ return None
346
435
 
347
436
  # Cache the status if it is not "running"
348
437
  if status_data['status'] != 'running':
@@ -350,38 +439,93 @@ def read_command_status(command_id):
350
439
 
351
440
  return status_data
352
441
 
353
- # Dictionary to store the process objects
354
- processes = {}
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
+
355
460
 
356
- def run_command(command, params, command_id):
461
+ def run_command(command, params, command_id, user):
357
462
  start_time = datetime.now().isoformat()
358
- 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)
359
465
  try:
360
- output_file_path = get_output_file_path(command_id)
361
- with open(output_file_path, 'w') as output_file:
362
- # Run the command with parameters and redirect stdout and stderr to the file
363
- process = subprocess.Popen([command] + params, stdout=output_file, stderr=output_file, bufsize=0) #text=True)
364
- update_command_status(command_id, 'running', pid=process.pid)
365
- processes[command_id] = process
366
- process.wait()
367
- processes.pop(command_id, None)
368
-
369
- end_time = datetime.now().isoformat()
370
- # Update the status based on the result
371
- if process.returncode == 0:
372
- update_command_status(command_id, 'success', end_time=end_time, exit_code=process.returncode)
373
- elif process.returncode == -15:
374
- update_command_status(command_id, 'aborted', end_time=end_time, exit_code=process.returncode)
375
- else:
376
- update_command_status(command_id, 'failed', end_time=end_time, exit_code=process.returncode)
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)
377
485
  except Exception as e:
378
486
  end_time = datetime.now().isoformat()
379
- 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)
488
+ with open(get_output_file_path(command_id), 'a') as output_file:
489
+ output_file.write(str(e))
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)
380
510
  with open(get_output_file_path(command_id), 'a') as output_file:
381
511
  output_file.write(str(e))
512
+ return jsonify({'error': 'Failed to terminate command'}), 500
513
+
514
+ parseargs()
515
+
382
516
 
383
517
  @app.before_request
384
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
+
526
+ if not app.config['USER'] and not app.config['LDAP_SERVER']:
527
+ return
528
+
385
529
  if 'username' not in session and request.endpoint not in ['login', 'static']:
386
530
  return auth.login_required(lambda: None)()
387
531
 
@@ -456,38 +600,18 @@ def run_command_endpoint():
456
600
  # Generate a unique command_id
457
601
  command_id = str(uuid.uuid4())
458
602
 
603
+ # Get the user from the session
604
+ user = session.get('username', '-')
605
+
459
606
  # Set the initial status to running and save command details
460
- update_command_status(command_id, 'running', command, params)
607
+ update_command_status(command_id, 'running', command, params, user=user)
461
608
 
462
609
  # Run the command in a separate thread
463
- 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))
464
611
  thread.start()
465
612
 
466
613
  return jsonify({'message': 'Command is running', 'command_id': command_id})
467
614
 
468
- @app.route('/stop_command/<command_id>', methods=['POST'])
469
- def stop_command(command_id):
470
- status = read_command_status(command_id)
471
- if not status or 'pid' not in status:
472
- return jsonify({'error': 'Invalid command_id or command not running'}), 400
473
-
474
- pid = status['pid']
475
- end_time = datetime.now().isoformat()
476
- try:
477
- os.kill(pid, 15) # Send SIGTERM
478
- update_command_status(command_id, 'aborted', end_time=end_time, exit_code=-15)
479
- return jsonify({'message': 'Command aborted'})
480
- except Exception as e:
481
- status_data = read_command_status(command_id) or {}
482
- status_data['status'] = 'failed'
483
- status_data['end_time'] = end_time
484
- status_data['exit_code'] = 1
485
- with open(get_status_file_path(command_id), 'w') as f:
486
- json.dump(status_data, f)
487
- with open(get_output_file_path(command_id), 'a') as output_file:
488
- output_file.write(str(e))
489
- return jsonify({'error': 'Failed to terminate command'}), 500
490
-
491
615
  @app.route('/command_status/<command_id>', methods=['GET'])
492
616
  def get_command_status(command_id):
493
617
  status = read_command_status(command_id)
@@ -515,10 +639,15 @@ def list_commands():
515
639
  status = read_command_status(command_id)
516
640
  if status:
517
641
  try:
518
- params = shlex.join(status['params'])
642
+ params = shlex.join(status.get('params', []))
519
643
  except AttributeError:
520
644
  params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
521
- command = status['command'] + ' ' + params
645
+ command = status.get('command', '-') + ' ' + params
646
+ last_line = status.get('last_output_line')
647
+ if last_line is None:
648
+ output_file_path = get_output_file_path(command_id)
649
+ if os.path.exists(output_file_path):
650
+ last_line = get_last_non_empty_line_of_file(output_file_path)
522
651
  commands.append({
523
652
  'command_id': command_id,
524
653
  'status': status['status'],
@@ -526,7 +655,7 @@ def list_commands():
526
655
  'end_time': status.get('end_time', 'N/A'),
527
656
  'command': command,
528
657
  'exit_code': status.get('exit_code', 'N/A'),
529
- 'last_output_line': status.get('last_output_line', get_last_non_empty_line_of_file(get_output_file_path(command_id))),
658
+ 'last_output_line': last_line,
530
659
  })
531
660
  # Sort commands by start_time in descending order
532
661
  commands.sort(key=lambda x: x['start_time'], reverse=True)
@@ -534,28 +663,60 @@ def list_commands():
534
663
 
535
664
  @app.route('/command_output/<command_id>', methods=['GET'])
536
665
  def get_command_output(command_id):
666
+ offset = int(request.args.get('offset', 0))
667
+ maxsize = int(request.args.get('maxsize', 10485760))
537
668
  output_file_path = get_output_file_path(command_id)
538
669
  if os.path.exists(output_file_path):
539
- with open(output_file_path, 'r') as output_file:
540
- output = output_file.read()
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()
541
674
  status_data = read_command_status(command_id) or {}
542
- return jsonify({'output': output, 'status': status_data.get("status")})
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
+ }
684
+ if request.headers.get('Accept') == 'text/plain':
685
+ return f"{output}\nstatus: {status_data.get('status')}", 200, {'Content-Type': 'text/plain'}
686
+ return jsonify(response)
543
687
  return jsonify({'error': 'Invalid command_id'}), 404
544
688
 
545
689
  @app.route('/executables', methods=['GET'])
546
690
  def list_executables():
547
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
548
693
  return jsonify(executables)
549
694
 
695
+ @app.route('/popup/<command_id>')
696
+ def popup(command_id):
697
+ return render_template('popup.html', command_id=command_id)
698
+
550
699
  def main():
700
+ global COMMAND_STATUS_DIR
551
701
  basef = f"{CONFDIR}/pywebexec_{args.listen}:{args.port}"
552
- if not os.path.exists(CONFDIR):
553
- os.mkdir(CONFDIR, mode=0o700)
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"
554
714
  if args.action == "start":
555
- return start_gunicorn(daemon=True, baselog=basef)
715
+ return start_gunicorn(daemonized=True, baselog=basef)
556
716
  if args.action:
557
717
  return daemon_d(args.action, pidfilepath=basef)
558
- return start_gunicorn()
718
+ return start_gunicorn(baselog=basef)
719
+
559
720
 
560
721
  if __name__ == '__main__':
561
722
  main()
Binary file