pywebexec 0.1.1__py3-none-any.whl → 1.0.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,24 +12,99 @@ import string
12
12
  from datetime import datetime
13
13
  import shlex
14
14
  from gunicorn.app.base import Application
15
+ from datetime import timezone, timedelta
16
+ import ipaddress
17
+ from socket import gethostname, gethostbyname_ex
15
18
 
16
19
  app = Flask(__name__)
17
20
  auth = HTTPBasicAuth()
18
21
 
19
- # Directory to store the script status and output
20
- SCRIPT_STATUS_DIR = '.web_status'
22
+ # Directory to store the command status and output
23
+ COMMAND_STATUS_DIR = '.web_status'
21
24
  CONFDIR = os.path.expanduser("~/")
22
25
  if os.path.isdir(f"{CONFDIR}/.config"):
23
26
  CONFDIR += '/.config'
24
27
  CONFDIR += "/.pywebexec"
25
28
 
26
- if not os.path.exists(SCRIPT_STATUS_DIR):
27
- os.makedirs(SCRIPT_STATUS_DIR)
29
+ if not os.path.exists(COMMAND_STATUS_DIR):
30
+ os.makedirs(COMMAND_STATUS_DIR)
28
31
 
29
32
  def generate_random_password(length=12):
30
33
  characters = string.ascii_letters + string.digits + string.punctuation
31
34
  return ''.join(random.choice(characters) for i in range(length))
32
35
 
36
+
37
+ def resolve_hostname(host):
38
+ """try get fqdn from DNS"""
39
+ try:
40
+ return gethostbyname_ex(host)[0]
41
+ except OSError:
42
+ return host
43
+
44
+
45
+ def generate_selfsigned_cert(hostname, ip_addresses=None, key=None):
46
+ """Generates self signed certificate for a hostname, and optional IP addresses.
47
+ from: https://gist.github.com/bloodearnest/9017111a313777b9cce5
48
+ """
49
+ from cryptography import x509
50
+ from cryptography.x509.oid import NameOID
51
+ from cryptography.hazmat.primitives import hashes
52
+ from cryptography.hazmat.backends import default_backend
53
+ from cryptography.hazmat.primitives import serialization
54
+ from cryptography.hazmat.primitives.asymmetric import rsa
55
+
56
+ # Generate our key
57
+ if key is None:
58
+ key = rsa.generate_private_key(
59
+ public_exponent=65537,
60
+ key_size=2048,
61
+ backend=default_backend(),
62
+ )
63
+
64
+ name = x509.Name([
65
+ x509.NameAttribute(NameOID.COMMON_NAME, hostname)
66
+ ])
67
+
68
+ # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored.
69
+ alt_names = [x509.DNSName(hostname)]
70
+ alt_names.append(x509.DNSName("localhost"))
71
+
72
+ # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios
73
+ if ip_addresses:
74
+ for addr in ip_addresses:
75
+ # openssl wants DNSnames for ips...
76
+ alt_names.append(x509.DNSName(addr))
77
+ # ... whereas golang's crypto/tls is stricter, and needs IPAddresses
78
+ # note: older versions of cryptography do not understand ip_address objects
79
+ alt_names.append(x509.IPAddress(ipaddress.ip_address(addr)))
80
+ san = x509.SubjectAlternativeName(alt_names)
81
+
82
+ # path_len=0 means this cert can only sign itself, not other certs.
83
+ basic_contraints = x509.BasicConstraints(ca=True, path_length=0)
84
+ now = datetime.now(timezone.utc)
85
+ cert = (
86
+ x509.CertificateBuilder()
87
+ .subject_name(name)
88
+ .issuer_name(name)
89
+ .public_key(key.public_key())
90
+ .serial_number(1000)
91
+ .not_valid_before(now)
92
+ .not_valid_after(now + timedelta(days=10*365))
93
+ .add_extension(basic_contraints, False)
94
+ .add_extension(san, False)
95
+ .sign(key, hashes.SHA256(), default_backend())
96
+ )
97
+ cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM)
98
+ key_pem = key.private_bytes(
99
+ encoding=serialization.Encoding.PEM,
100
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
101
+ encryption_algorithm=serialization.NoEncryption(),
102
+ )
103
+
104
+ return cert_pem, key_pem
105
+
106
+
107
+
33
108
  class StandaloneApplication(Application):
34
109
 
35
110
  def __init__(self, app, options=None):
@@ -110,7 +185,7 @@ def daemon_d(action, pidfilepath, hostname=None, args=None):
110
185
 
111
186
  def parseargs():
112
187
  global app, args
113
- parser = argparse.ArgumentParser(description='Run the script execution server.')
188
+ parser = argparse.ArgumentParser(description='Run the command execution server.')
114
189
  parser.add_argument('--user', help='Username for basic auth')
115
190
  parser.add_argument('--password', help='Password for basic auth')
116
191
  parser.add_argument(
@@ -126,11 +201,12 @@ def parseargs():
126
201
  "-t",
127
202
  "--title",
128
203
  type=str,
129
- default="FileBrowser",
204
+ default="pywebexec",
130
205
  help="Web html title",
131
206
  )
132
207
  parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
133
208
  parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
209
+ parser.add_argument("-g", "--gencert", action="store_true", help="https server self signed cert")
134
210
  parser.add_argument("action", nargs="?", help="daemon action start/stop/restart/status", choices=["start","stop","restart","status"])
135
211
 
136
212
  args = parser.parse_args()
@@ -144,6 +220,17 @@ def parseargs():
144
220
  print(f"Error: {args.dir} not found", file=sys.stderr)
145
221
  sys.exit(1)
146
222
 
223
+ if args.gencert:
224
+ hostname = resolve_hostname(gethostname())
225
+ args.cert = args.cert or f"{CONFDIR}/pywebexec.crt"
226
+ args.key = args.key or f"{CONFDIR}/pywebexec.key"
227
+ if not os.path.exists(args.cert):
228
+ (cert, key) = generate_selfsigned_cert(hostname)
229
+ with open(args.cert, "wb") as fd:
230
+ fd.write(cert)
231
+ with open(args.key, "wb") as fd:
232
+ fd.write(key)
233
+
147
234
  if args.user:
148
235
  app.config['USER'] = args.user
149
236
  if args.password:
@@ -158,18 +245,18 @@ def parseargs():
158
245
 
159
246
  parseargs()
160
247
 
161
- def get_status_file_path(script_id):
162
- return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}.json')
248
+ def get_status_file_path(command_id):
249
+ return os.path.join(COMMAND_STATUS_DIR, f'{command_id}.json')
163
250
 
164
- def get_output_file_path(script_id):
165
- return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}_output.txt')
251
+ def get_output_file_path(command_id):
252
+ return os.path.join(COMMAND_STATUS_DIR, f'{command_id}_output.txt')
166
253
 
167
- def update_script_status(script_id, status, script_name=None, params=None, start_time=None, end_time=None, exit_code=None, pid=None):
168
- status_file_path = get_status_file_path(script_id)
169
- status_data = read_script_status(script_id) or {}
254
+ def update_command_status(command_id, status, command=None, params=None, start_time=None, end_time=None, exit_code=None, pid=None):
255
+ status_file_path = get_status_file_path(command_id)
256
+ status_data = read_command_status(command_id) or {}
170
257
  status_data['status'] = status
171
- if script_name is not None:
172
- status_data['script_name'] = script_name
258
+ if command is not None:
259
+ status_data['command'] = command
173
260
  if params is not None:
174
261
  status_data['params'] = params
175
262
  if start_time is not None:
@@ -183,8 +270,8 @@ def update_script_status(script_id, status, script_name=None, params=None, start
183
270
  with open(status_file_path, 'w') as f:
184
271
  json.dump(status_data, f)
185
272
 
186
- def read_script_status(script_id):
187
- status_file_path = get_status_file_path(script_id)
273
+ def read_command_status(command_id):
274
+ status_file_path = get_status_file_path(command_id)
188
275
  if not os.path.exists(status_file_path):
189
276
  return None
190
277
  with open(status_file_path, 'r') as f:
@@ -193,31 +280,31 @@ def read_script_status(script_id):
193
280
  # Dictionary to store the process objects
194
281
  processes = {}
195
282
 
196
- def run_script(script_name, params, script_id):
283
+ def run_command(command, params, command_id):
197
284
  start_time = datetime.now().isoformat()
198
- update_script_status(script_id, 'running', script_name=script_name, params=params, start_time=start_time)
285
+ update_command_status(command_id, 'running', command=command, params=params, start_time=start_time)
199
286
  try:
200
- output_file_path = get_output_file_path(script_id)
287
+ output_file_path = get_output_file_path(command_id)
201
288
  with open(output_file_path, 'w') as output_file:
202
- # Run the script with parameters and redirect stdout and stderr to the file
203
- process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, bufsize=0) #text=True)
204
- update_script_status(script_id, 'running', pid=process.pid)
205
- processes[script_id] = process
289
+ # Run the command with parameters and redirect stdout and stderr to the file
290
+ process = subprocess.Popen([command] + params, stdout=output_file, stderr=output_file, bufsize=0) #text=True)
291
+ update_command_status(command_id, 'running', pid=process.pid)
292
+ processes[command_id] = process
206
293
  process.wait()
207
- processes.pop(script_id, None)
294
+ processes.pop(command_id, None)
208
295
 
209
296
  end_time = datetime.now().isoformat()
210
297
  # Update the status based on the result
211
298
  if process.returncode == 0:
212
- update_script_status(script_id, 'success', end_time=end_time, exit_code=process.returncode)
299
+ update_command_status(command_id, 'success', end_time=end_time, exit_code=process.returncode)
213
300
  elif process.returncode == -15:
214
- update_script_status(script_id, 'aborted', end_time=end_time, exit_code=process.returncode)
301
+ update_command_status(command_id, 'aborted', end_time=end_time, exit_code=process.returncode)
215
302
  else:
216
- update_script_status(script_id, 'failed', end_time=end_time, exit_code=process.returncode)
303
+ update_command_status(command_id, 'failed', end_time=end_time, exit_code=process.returncode)
217
304
  except Exception as e:
218
305
  end_time = datetime.now().isoformat()
219
- update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
220
- with open(get_output_file_path(script_id), 'a') as output_file:
306
+ update_command_status(command_id, 'failed', end_time=end_time, exit_code=1)
307
+ with open(get_output_file_path(command_id), 'a') as output_file:
221
308
  output_file.write(str(e))
222
309
 
223
310
  def auth_required(f):
@@ -225,20 +312,20 @@ def auth_required(f):
225
312
  return auth.login_required(f)
226
313
  return f
227
314
 
228
- @app.route('/run_script', methods=['POST'])
315
+ @app.route('/run_command', methods=['POST'])
229
316
  @auth_required
230
- def run_script_endpoint():
317
+ def run_command_endpoint():
231
318
  data = request.json
232
- script_name = data.get('script_name')
319
+ command = data.get('command')
233
320
  params = data.get('params', [])
234
321
 
235
- if not script_name:
236
- return jsonify({'error': 'script_name is required'}), 400
322
+ if not command:
323
+ return jsonify({'error': 'command is required'}), 400
237
324
 
238
- # Ensure the script is an executable in the current directory
239
- script_path = os.path.join(".", os.path.basename(script_name))
240
- if not os.path.isfile(script_path) or not os.access(script_path, os.X_OK):
241
- return jsonify({'error': 'script_name must be an executable in the current directory'}), 400
325
+ # Ensure the command is an executable in the current directory
326
+ command_path = os.path.join(".", os.path.basename(command))
327
+ if not os.path.isfile(command_path) or not os.access(command_path, os.X_OK):
328
+ return jsonify({'error': 'command must be an executable in the current directory'}), 400
242
329
 
243
330
  # Split params using shell-like syntax
244
331
  try:
@@ -246,98 +333,98 @@ def run_script_endpoint():
246
333
  except ValueError as e:
247
334
  return jsonify({'error': str(e)}), 400
248
335
 
249
- # Generate a unique script_id
250
- script_id = str(uuid.uuid4())
336
+ # Generate a unique command_id
337
+ command_id = str(uuid.uuid4())
251
338
 
252
- # Set the initial status to running and save script details
253
- update_script_status(script_id, 'running', script_name, params)
339
+ # Set the initial status to running and save command details
340
+ update_command_status(command_id, 'running', command, params)
254
341
 
255
- # Run the script in a separate thread
256
- thread = threading.Thread(target=run_script, args=(script_path, params, script_id))
342
+ # Run the command in a separate thread
343
+ thread = threading.Thread(target=run_command, args=(command_path, params, command_id))
257
344
  thread.start()
258
345
 
259
- return jsonify({'message': 'Script is running', 'script_id': script_id})
346
+ return jsonify({'message': 'Command is running', 'command_id': command_id})
260
347
 
261
- @app.route('/stop_script/<script_id>', methods=['POST'])
348
+ @app.route('/stop_command/<command_id>', methods=['POST'])
262
349
  @auth_required
263
- def stop_script(script_id):
264
- status = read_script_status(script_id)
350
+ def stop_command(command_id):
351
+ status = read_command_status(command_id)
265
352
  if not status or 'pid' not in status:
266
- return jsonify({'error': 'Invalid script_id or script not running'}), 400
353
+ return jsonify({'error': 'Invalid command_id or command not running'}), 400
267
354
 
268
355
  pid = status['pid']
269
356
  end_time = datetime.now().isoformat()
270
357
  try:
271
358
  os.kill(pid, 15) # Send SIGTERM
272
- update_script_status(script_id, 'aborted', end_time=end_time, exit_code=-15)
273
- return jsonify({'message': 'Script aborted'})
359
+ update_command_status(command_id, 'aborted', end_time=end_time, exit_code=-15)
360
+ return jsonify({'message': 'Command aborted'})
274
361
  except Exception as e:
275
- status_data = read_script_status(script_id) or {}
362
+ status_data = read_command_status(command_id) or {}
276
363
  status_data['status'] = 'failed'
277
364
  status_data['end_time'] = end_time
278
365
  status_data['exit_code'] = 1
279
- with open(get_status_file_path(script_id), 'w') as f:
366
+ with open(get_status_file_path(command_id), 'w') as f:
280
367
  json.dump(status_data, f)
281
- with open(get_output_file_path(script_id), 'a') as output_file:
368
+ with open(get_output_file_path(command_id), 'a') as output_file:
282
369
  output_file.write(str(e))
283
- return jsonify({'error': 'Failed to terminate script'}), 500
370
+ return jsonify({'error': 'Failed to terminate command'}), 500
284
371
 
285
- @app.route('/script_status/<script_id>', methods=['GET'])
372
+ @app.route('/command_status/<command_id>', methods=['GET'])
286
373
  @auth_required
287
- def get_script_status(script_id):
288
- status = read_script_status(script_id)
374
+ def get_command_status(command_id):
375
+ status = read_command_status(command_id)
289
376
  if not status:
290
- return jsonify({'error': 'Invalid script_id'}), 404
377
+ return jsonify({'error': 'Invalid command_id'}), 404
291
378
 
292
- output_file_path = get_output_file_path(script_id)
293
- if os.path.exists(output_file_path):
294
- with open(output_file_path, 'r') as output_file:
295
- output = output_file.read()
296
- status['output'] = output
379
+ # output_file_path = get_output_file_path(command_id)
380
+ # if os.path.exists(output_file_path):
381
+ # with open(output_file_path, 'r') as output_file:
382
+ # output = output_file.read()
383
+ # status['output'] = output
297
384
 
298
385
  return jsonify(status)
299
386
 
300
387
  @app.route('/')
301
388
  @auth_required
302
389
  def index():
303
- return render_template('index.html')
390
+ return render_template('index.html', title=args.title)
304
391
 
305
- @app.route('/scripts', methods=['GET'])
392
+ @app.route('/commands', methods=['GET'])
306
393
  @auth_required
307
- def list_scripts():
308
- scripts = []
309
- for filename in os.listdir(SCRIPT_STATUS_DIR):
394
+ def list_commands():
395
+ commands = []
396
+ for filename in os.listdir(COMMAND_STATUS_DIR):
310
397
  if filename.endswith('.json'):
311
- script_id = filename[:-5]
312
- status = read_script_status(script_id)
398
+ command_id = filename[:-5]
399
+ status = read_command_status(command_id)
313
400
  if status:
314
401
  try:
315
402
  params = shlex.join(status['params'])
316
403
  except AttributeError:
317
404
  params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
318
- command = status['script_name'] + ' ' + params
319
- scripts.append({
320
- 'script_id': script_id,
405
+ command = status['command'] + ' ' + params
406
+ commands.append({
407
+ 'command_id': command_id,
321
408
  'status': status['status'],
322
409
  'start_time': status.get('start_time', 'N/A'),
323
410
  'end_time': status.get('end_time', 'N/A'),
324
411
  'command': command,
325
412
  'exit_code': status.get('exit_code', 'N/A')
326
413
  })
327
- # Sort scripts by start_time in descending order
328
- scripts.sort(key=lambda x: x['start_time'], reverse=True)
329
- return jsonify(scripts)
414
+ # Sort commands by start_time in descending order
415
+ commands.sort(key=lambda x: x['start_time'], reverse=True)
416
+ return jsonify(commands)
330
417
 
331
- @app.route('/script_output/<script_id>', methods=['GET'])
418
+ @app.route('/command_output/<command_id>', methods=['GET'])
332
419
  @auth_required
333
- def get_script_output(script_id):
334
- output_file_path = get_output_file_path(script_id)
420
+ def get_command_output(command_id):
421
+ output_file_path = get_output_file_path(command_id)
335
422
  if os.path.exists(output_file_path):
336
423
  with open(output_file_path, 'r') as output_file:
337
424
  output = output_file.read()
338
- status_data = read_script_status(script_id) or {}
425
+ status_data = read_command_status(command_id) or {}
339
426
  return jsonify({'output': output, 'status': status_data.get("status")})
340
- return jsonify({'error': 'Invalid script_id'}), 404
427
+ return jsonify({'error': 'Invalid command_id'}), 404
341
428
 
342
429
  @app.route('/executables', methods=['GET'])
343
430
  @auth_required
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
- <title>pywebexec</title>
5
+ <title>{{ title }}</title>
6
6
  <style>
7
7
  body { font-family: Arial, sans-serif; }
8
8
  .table-container { height: 380px; overflow-y: auto; position: relative; }
@@ -83,7 +83,7 @@
83
83
  input {
84
84
  width: 50%
85
85
  }
86
- .currentscript {
86
+ .currentcommand {
87
87
  background-color: #eef;
88
88
  }
89
89
  .resizer {
@@ -103,11 +103,11 @@
103
103
  </style>
104
104
  </head>
105
105
  <body>
106
- <h1>pywebexec</h1>
106
+ <h1>{{ title }}</h1>
107
107
  <form id="launchForm">
108
- <label for="scriptName">Command:</label>
109
- <select id="scriptName" name="scriptName"></select>
110
- <label for="params">Params:</label>
108
+ <label for="commandName">Command</label>
109
+ <select id="commandName" name="commandName"></select>
110
+ <label for="params">Params</label>
111
111
  <input type="text" id="params" name="params">
112
112
  <button type="submit">Launch</button>
113
113
  </form>
@@ -115,7 +115,7 @@
115
115
  <table>
116
116
  <thead>
117
117
  <tr>
118
- <th>Script ID</th>
118
+ <th>Command ID</th>
119
119
  <th>Status</th>
120
120
  <th>Start Time</th>
121
121
  <th>Duration</th>
@@ -124,7 +124,7 @@
124
124
  <th>Actions</th>
125
125
  </tr>
126
126
  </thead>
127
- <tbody id="scripts"></tbody>
127
+ <tbody id="commands"></tbody>
128
128
  </table>
129
129
  </div>
130
130
  <div class="resizer-container">
@@ -133,69 +133,69 @@
133
133
  <div id="output" class="output"></div>
134
134
 
135
135
  <script>
136
- let currentScriptId = null;
136
+ let currentCommandId = null;
137
137
  let outputInterval = null;
138
138
 
139
139
  document.getElementById('launchForm').addEventListener('submit', async (event) => {
140
140
  event.preventDefault();
141
- const scriptName = document.getElementById('scriptName').value;
141
+ const commandName = document.getElementById('commandName').value;
142
142
  const params = document.getElementById('params').value.split(' ');
143
- const response = await fetch('/run_script', {
143
+ const response = await fetch('/run_command', {
144
144
  method: 'POST',
145
145
  headers: {
146
146
  'Content-Type': 'application/json'
147
147
  },
148
- body: JSON.stringify({ script_name: scriptName, params: params })
148
+ body: JSON.stringify({ command: commandName, params: params })
149
149
  });
150
150
  const data = await response.json();
151
- fetchScripts();
152
- viewOutput(data.script_id);
151
+ fetchCommands();
152
+ viewOutput(data.command_id);
153
153
  });
154
154
 
155
- async function fetchScripts() {
156
- const response = await fetch('/scripts');
157
- const scripts = await response.json();
158
- scripts.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
159
- const scriptsTbody = document.getElementById('scripts');
160
- scriptsTbody.innerHTML = '';
161
- scripts.forEach(script => {
162
- const scriptRow = document.createElement('tr');
163
- scriptRow.className = script.script_id === currentScriptId ? 'currentscript' : '';
164
- scriptRow.innerHTML = `
155
+ async function fetchCommands() {
156
+ const response = await fetch('/commands');
157
+ const commands = await response.json();
158
+ commands.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
159
+ const commandsTbody = document.getElementById('commands');
160
+ commandsTbody.innerHTML = '';
161
+ commands.forEach(command => {
162
+ const commandRow = document.createElement('tr');
163
+ commandRow.className = command.command_id === currentCommandId ? 'currentcommand' : '';
164
+ commandRow.innerHTML = `
165
165
  <td class="monospace">
166
- <span class="copy_clip" onclick="copyToClipboard('${script.script_id}', this)">${script.script_id.slice(0, 8)}</span>
166
+ <span class="copy_clip" onclick="copyToClipboard('${command.command_id}', this)">${command.command_id.slice(0, 8)}</span>
167
167
  </td>
168
- <td><span class="status-icon status-${script.status}"></span>${script.status}</td>
169
- <td>${formatTime(script.start_time)}</td>
170
- <td>${script.status === 'running' ? formatDuration(script.start_time, new Date().toISOString()) : formatDuration(script.start_time, script.end_time)}</td>
171
- <td>${script.exit_code}</td>
172
- <td>${script.command.replace(/^\.\//, '')}</td>
168
+ <td><span class="status-icon status-${command.status}"></span>${command.status}</td>
169
+ <td>${formatTime(command.start_time)}</td>
170
+ <td>${command.status === 'running' ? formatDuration(command.start_time, new Date().toISOString()) : formatDuration(command.start_time, command.end_time)}</td>
171
+ <td>${command.exit_code}</td>
172
+ <td>${command.command.replace(/^\.\//, '')}</td>
173
173
  <td>
174
- <button onclick="viewOutput('${script.script_id}')">Log</button>
175
- <button onclick="relaunchScript('${script.script_id}')">Relaunch</button>
176
- ${script.status === 'running' ? `<button onclick="stopScript('${script.script_id}')">Stop</button>` : ''}
174
+ <button onclick="viewOutput('${command.command_id}')">Log</button>
175
+ <button onclick="relaunchCommand('${command.command_id}')">Relaunch</button>
176
+ ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : ''}
177
177
  </td>
178
178
  `;
179
- scriptsTbody.appendChild(scriptRow);
179
+ commandsTbody.appendChild(commandRow);
180
180
  });
181
181
  }
182
182
 
183
183
  async function fetchExecutables() {
184
184
  const response = await fetch('/executables');
185
185
  const executables = await response.json();
186
- const scriptNameSelect = document.getElementById('scriptName');
187
- scriptNameSelect.innerHTML = '';
186
+ const commandNameSelect = document.getElementById('commandName');
187
+ commandNameSelect.innerHTML = '';
188
188
  executables.forEach(executable => {
189
189
  const option = document.createElement('option');
190
190
  option.value = executable;
191
191
  option.textContent = executable;
192
- scriptNameSelect.appendChild(option);
192
+ commandNameSelect.appendChild(option);
193
193
  });
194
194
  }
195
195
 
196
- async function fetchOutput(script_id) {
196
+ async function fetchOutput(command_id) {
197
197
  const outputDiv = document.getElementById('output');
198
- const response = await fetch(`/script_output/${script_id}`);
198
+ const response = await fetch(`/command_output/${command_id}`);
199
199
  const data = await response.json();
200
200
  if (data.error) {
201
201
  outputDiv.innerHTML = data.error;
@@ -209,45 +209,45 @@
209
209
  }
210
210
  }
211
211
 
212
- async function viewOutput(script_id) {
212
+ async function viewOutput(command_id) {
213
213
  adjustOutputHeight();
214
- currentScriptId = script_id;
214
+ currentCommandId = command_id;
215
215
  clearInterval(outputInterval);
216
- const response = await fetch(`/script_status/${script_id}`);
216
+ const response = await fetch(`/command_status/${command_id}`);
217
217
  const data = await response.json();
218
218
  if (data.status === 'running') {
219
- fetchOutput(script_id);
220
- outputInterval = setInterval(() => fetchOutput(script_id), 1000);
219
+ fetchOutput(command_id);
220
+ outputInterval = setInterval(() => fetchOutput(command_id), 1000);
221
221
  } else {
222
- fetchOutput(script_id);
222
+ fetchOutput(command_id);
223
223
  }
224
- fetchScripts(); // Refresh the script list to highlight the current script
224
+ fetchCommands(); // Refresh the command list to highlight the current command
225
225
  }
226
226
 
227
- async function relaunchScript(script_id) {
228
- const response = await fetch(`/script_status/${script_id}`);
227
+ async function relaunchCommand(command_id) {
228
+ const response = await fetch(`/command_status/${command_id}`);
229
229
  const data = await response.json();
230
230
  if (data.error) {
231
231
  alert(data.error);
232
232
  return;
233
233
  }
234
- const relaunchResponse = await fetch('/run_script', {
234
+ const relaunchResponse = await fetch('/run_command', {
235
235
  method: 'POST',
236
236
  headers: {
237
237
  'Content-Type': 'application/json'
238
238
  },
239
239
  body: JSON.stringify({
240
- script_name: data.script_name,
240
+ command: data.command,
241
241
  params: data.params
242
242
  })
243
243
  });
244
244
  const relaunchData = await relaunchResponse.json();
245
- fetchScripts();
246
- viewOutput(relaunchData.script_id);
245
+ fetchCommands();
246
+ viewOutput(relaunchData.command_id);
247
247
  }
248
248
 
249
- async function stopScript(script_id) {
250
- const response = await fetch(`/stop_script/${script_id}`, {
249
+ async function stopCommand(command_id) {
250
+ const response = await fetch(`/stop_command/${command_id}`, {
251
251
  method: 'POST'
252
252
  });
253
253
  const data = await response.json();
@@ -255,7 +255,7 @@
255
255
  alert(data.error);
256
256
  } else {
257
257
  alert(data.message);
258
- fetchScripts();
258
+ fetchCommands();
259
259
  }
260
260
  }
261
261
 
@@ -322,9 +322,9 @@
322
322
  initResizer();
323
323
  });
324
324
 
325
- fetchScripts();
325
+ fetchCommands();
326
326
  fetchExecutables();
327
- setInterval(fetchScripts, 5000);
327
+ setInterval(fetchCommands, 5000);
328
328
  </script>
329
329
  </body>
330
330
  </html>
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 = '0.1.1'
16
- __version_tuple__ = version_tuple = (0, 1, 1)
15
+ __version__ = version = '1.0.0'
16
+ __version_tuple__ = version_tuple = (1, 0, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 0.1.1
3
+ Version: 1.0.0
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -85,7 +85,7 @@ $ pywebexec
85
85
 
86
86
  ## features
87
87
 
88
- * Serve executables in current directory
88
+ * Serve executables in a directory
89
89
  * Launch commands with params from web browser or API call
90
90
  * Follow live output
91
91
  * Stop command
@@ -94,11 +94,12 @@ $ pywebexec
94
94
  * HTTPS self-signed certificate generator
95
95
  * Can be started as a daemon (POSIX)
96
96
  * uses gunicorn to serve http/https
97
+ * compatible Linux/MacOS
97
98
 
98
99
  ## Customize server
99
100
  ```
100
- $ pywebexec --listen 0.0.0.0 --port 8080
101
- $ pywebexec -l 0.0.0.0 -p 8080
101
+ $ pywebexec --dir ~/myscripts --listen 0.0.0.0 --port 8080
102
+ $ pywebexec -d ~/myscripts -l 0.0.0.0 -p 8080
102
103
  ```
103
104
 
104
105
  ## Basic auth user/password
@@ -122,7 +123,7 @@ $ pywebfs --cert /pathto/host.cert --key /pathto/host.key
122
123
  $ pywebfs -c /pathto/host.cert -k /pathto/host.key
123
124
  ```
124
125
 
125
- ## Launch server as a daemon (Linux)
126
+ ## Launch server as a daemon
126
127
 
127
128
  ```
128
129
  $ pywebexec start
@@ -131,3 +132,20 @@ $ pywebexec stop
131
132
  ```
132
133
  * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
133
134
 
135
+ ## Launch command through API
136
+
137
+ ```
138
+ # curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
139
+ ```
140
+
141
+ ## API reference
142
+
143
+
144
+ | method | route | params/payload | returns
145
+ |-----------|-----------------------------|--------------------|---------------------|
146
+ | POST | /run_command | command: str<br>params: array[str] | command_id: uuid<br>message: str |
147
+ | POST | /stop_command/command_id | | message: str |
148
+ | GET | /command_status/command_id | | command_id: uuid<br>command: str<br>params: array[str]<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
149
+ | GET | /command_output/command_id | | output: str<br>status: str |
150
+ | GET | /commands | | array of<br>command_id: uuid<br>command: str<br>start_time: isotime<br>end_time: isotime<br>status: str<br>exit_code: int |
151
+ | GET | /executables | | array of str |
@@ -1,6 +1,6 @@
1
1
  pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
2
- pywebexec/pywebexec.py,sha256=IyNVTDepd5UPfKBHqctIyNez9McNb7z9xN01rXGIK44,12928
3
- pywebexec/version.py,sha256=PKIMyjdUACH4-ONvtunQCnYE2UhlMfp9su83e3HXl5E,411
2
+ pywebexec/pywebexec.py,sha256=2lVnQd9BgaUunjUtgYKIwgIRllX11v64jZQgfweteFg,16281
3
+ pywebexec/version.py,sha256=DGJ4pj32xs3_DRJhSzQwCiRNnAQrMgo09USYpyMZsKc,411
4
4
  pywebexec/static/images/aborted.svg,sha256=_mP43hU5QdRLFZIknBgjx-dIXrHgQG23-QV27ApXK2A,381
5
5
  pywebexec/static/images/copy.svg,sha256=d9OwtGh5GzzZHzYcDrLfNxZYLth1Q64x7bRyYxu4Px0,622
6
6
  pywebexec/static/images/copy_ok.svg,sha256=mEqUVUhSq8xaJK2msQkxRawnz_KwlCZ-tok8QS6hJ3g,451
@@ -8,10 +8,10 @@ pywebexec/static/images/failed.svg,sha256=ADZ7IKrUyOXtqpivnz3VcH0-Wru-I5MOi3OJAk
8
8
  pywebexec/static/images/running.svg,sha256=vBpiG6ClNUNCArkwsyqK7O-qhIKJX1NI7MSjclNSp_8,1537
9
9
  pywebexec/static/images/success.svg,sha256=PJDcCSTevJh7rkfSFLtc7P0pbeh8PVQBS8DaOLQemmc,489
10
10
  pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- pywebexec/templates/index.html,sha256=0628YJ1TIH7WPA-UJPfYqEjBo8joYn9685R9Zm5Zz30,12356
12
- pywebexec-0.1.1.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
13
- pywebexec-0.1.1.dist-info/METADATA,sha256=83vtZn2DbTfVaOFejyAiKhef_rYEiE6-ko_LMi8yPhg,4770
14
- pywebexec-0.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
15
- pywebexec-0.1.1.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
16
- pywebexec-0.1.1.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
17
- pywebexec-0.1.1.dist-info/RECORD,,
11
+ pywebexec/templates/index.html,sha256=taMeaWiX7ijHI-kWVyjIBH0E0Fj5RY7-9ocOXOiTZCk,12431
12
+ pywebexec-1.0.0.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
13
+ pywebexec-1.0.0.dist-info/METADATA,sha256=CrooVWXHqkcV0PtBpWzHedlZVLvoR0b77O_MxojvK6E,5970
14
+ pywebexec-1.0.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
15
+ pywebexec-1.0.0.dist-info/entry_points.txt,sha256=l52GBkPCXRkmlHfEyoVauyfBdg8o-CAtC8qQpOIjJK0,55
16
+ pywebexec-1.0.0.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
17
+ pywebexec-1.0.0.dist-info/RECORD,,