pywebexec 0.0.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/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """ package pywebexec """
2
+
3
+ __author__ = "Franck Jouvanceau"
4
+
5
+ from .pywebexec import start_gunicorn
pywebexec/pywebexec.py ADDED
@@ -0,0 +1,284 @@
1
+ from flask import Flask, request, jsonify, render_template
2
+ from flask_httpauth import HTTPBasicAuth
3
+ import subprocess
4
+ import threading
5
+ import os
6
+ import json
7
+ import uuid
8
+ import argparse
9
+ import random
10
+ import string
11
+ from datetime import datetime
12
+ import shlex
13
+ from gunicorn.app.base import BaseApplication
14
+
15
+ app = Flask(__name__)
16
+ auth = HTTPBasicAuth()
17
+
18
+ # Directory to store the script status and output
19
+ SCRIPT_STATUS_DIR = 'script_status'
20
+
21
+ if not os.path.exists(SCRIPT_STATUS_DIR):
22
+ os.makedirs(SCRIPT_STATUS_DIR)
23
+
24
+ def generate_random_password(length=12):
25
+ characters = string.ascii_letters + string.digits + string.punctuation
26
+ return ''.join(random.choice(characters) for i in range(length))
27
+
28
+ class StandaloneApplication(BaseApplication):
29
+
30
+ def __init__(self, app, options=None):
31
+ self.options = options or {}
32
+ self.application = app
33
+ super().__init__()
34
+
35
+ def load_config(self):
36
+ config = {
37
+ key: value for key, value in self.options.items()
38
+ if key in self.cfg.settings and value is not None
39
+ }
40
+ for key, value in config.items():
41
+ self.cfg.set(key.lower(), value)
42
+
43
+ def load(self):
44
+ return self.application
45
+
46
+
47
+ def start_gunicorn():
48
+ options = {
49
+ 'bind': '%s:%s' % (args.listen, args.port),
50
+ 'workers': 4,
51
+ 'certfile': args.cert,
52
+ 'keyfile': args.key,
53
+ }
54
+ StandaloneApplication(app, options=options).run()
55
+
56
+ def parseargs():
57
+ global app, args
58
+ parser = argparse.ArgumentParser(description='Run the script execution server.')
59
+ parser.add_argument('--user', help='Username for basic auth')
60
+ parser.add_argument('--password', help='Password for basic auth')
61
+ parser.add_argument(
62
+ "-l", "--listen", type=str, default="0.0.0.0", help="HTTP server listen address"
63
+ )
64
+ parser.add_argument(
65
+ "-p", "--port", type=int, default=8080, help="HTTP server listen port"
66
+ )
67
+ parser.add_argument(
68
+ "-d", "--dir", type=str, default=os.getcwd(), help="Serve target directory"
69
+ )
70
+ parser.add_argument(
71
+ "-t",
72
+ "--title",
73
+ type=str,
74
+ default="FileBrowser",
75
+ help="Web html title",
76
+ )
77
+ parser.add_argument("-c", "--cert", type=str, help="Path to https certificate")
78
+ parser.add_argument("-k", "--key", type=str, help="Path to https certificate key")
79
+
80
+ args = parser.parse_args()
81
+
82
+ if args.user:
83
+ app.config['USER'] = args.user
84
+ if args.password:
85
+ app.config['PASSWORD'] = args.password
86
+ else:
87
+ app.config['PASSWORD'] = generate_random_password()
88
+ print(f'Generated password for user {args.user}: {app.config["PASSWORD"]}')
89
+ else:
90
+ app.config['USER'] = None
91
+ app.config['PASSWORD'] = None
92
+ return args
93
+
94
+ parseargs()
95
+
96
+ def get_status_file_path(script_id):
97
+ return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}.json')
98
+
99
+ def get_output_file_path(script_id):
100
+ return os.path.join(SCRIPT_STATUS_DIR, f'{script_id}_output.txt')
101
+
102
+ def update_script_status(script_id, status, script_name=None, params=None, start_time=None, end_time=None, exit_code=None):
103
+ status_file_path = get_status_file_path(script_id)
104
+ status_data = read_script_status(script_id) or {}
105
+ status_data['status'] = status
106
+ if script_name is not None:
107
+ status_data['script_name'] = script_name
108
+ if params is not None:
109
+ status_data['params'] = params
110
+ if start_time is not None:
111
+ status_data['start_time'] = start_time
112
+ if end_time is not None:
113
+ status_data['end_time'] = end_time
114
+ if exit_code is not None:
115
+ status_data['exit_code'] = exit_code
116
+ with open(status_file_path, 'w') as f:
117
+ json.dump(status_data, f)
118
+
119
+ def read_script_status(script_id):
120
+ status_file_path = get_status_file_path(script_id)
121
+ if not os.path.exists(status_file_path):
122
+ return None
123
+ with open(status_file_path, 'r') as f:
124
+ return json.load(f)
125
+
126
+ # Dictionary to store the process objects
127
+ processes = {}
128
+
129
+ def run_script(script_name, params, script_id):
130
+ start_time = datetime.now().isoformat()
131
+ update_script_status(script_id, 'running', script_name=script_name, params=params, start_time=start_time)
132
+ try:
133
+ output_file_path = get_output_file_path(script_id)
134
+ with open(output_file_path, 'w') as output_file:
135
+ # Run the script with parameters and redirect stdout and stderr to the file
136
+ process = subprocess.Popen([script_name] + params, stdout=output_file, stderr=output_file, text=True)
137
+ processes[script_id] = process
138
+ process.wait()
139
+ processes.pop(script_id, None)
140
+
141
+ end_time = datetime.now().isoformat()
142
+ # Update the status based on the result
143
+ if process.returncode == 0:
144
+ update_script_status(script_id, 'success', end_time=end_time, exit_code=process.returncode)
145
+ elif process.returncode == -15:
146
+ update_script_status(script_id, 'aborted', end_time=end_time, exit_code=process.returncode)
147
+ else:
148
+ update_script_status(script_id, 'failed', end_time=end_time, exit_code=process.returncode)
149
+ except Exception as e:
150
+ end_time = datetime.now().isoformat()
151
+ update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
152
+ with open(get_output_file_path(script_id), 'a') as output_file:
153
+ output_file.write(str(e))
154
+
155
+ def auth_required(f):
156
+ if app.config.get('USER'):
157
+ return auth.login_required(f)
158
+ return f
159
+
160
+ @app.route('/run_script', methods=['POST'])
161
+ @auth_required
162
+ def run_script_endpoint():
163
+ data = request.json
164
+ script_name = data.get('script_name')
165
+ params = data.get('params', [])
166
+
167
+ if not script_name:
168
+ return jsonify({'error': 'script_name is required'}), 400
169
+
170
+ # Ensure the script is an executable in the current directory
171
+ script_path = os.path.join(".", os.path.basename(script_name))
172
+ if not os.path.isfile(script_path) or not os.access(script_path, os.X_OK):
173
+ return jsonify({'error': 'script_name must be an executable in the current directory'}), 400
174
+
175
+ # Split params using shell-like syntax
176
+ try:
177
+ params = shlex.split(' '.join(params))
178
+ except ValueError as e:
179
+ return jsonify({'error': str(e)}), 400
180
+
181
+ # Generate a unique script_id
182
+ script_id = str(uuid.uuid4())
183
+
184
+ # Set the initial status to running and save script details
185
+ update_script_status(script_id, 'running', script_name, params)
186
+
187
+ # Run the script in a separate thread
188
+ thread = threading.Thread(target=run_script, args=(script_path, params, script_id))
189
+ thread.start()
190
+
191
+ return jsonify({'message': 'Script is running', 'script_id': script_id})
192
+
193
+ @app.route('/stop_script/<script_id>', methods=['POST'])
194
+ @auth_required
195
+ def stop_script(script_id):
196
+ process = processes.get(script_id)
197
+ end_time = datetime.now().isoformat()
198
+ if process and process.poll() is None:
199
+ try:
200
+ process.terminate()
201
+ process.wait() # Ensure the process has terminated
202
+ return jsonify({'message': 'Script aborted'})
203
+ except Exception as e:
204
+ status_data = read_script_status(script_id) or {}
205
+ status_data['status'] = 'failed'
206
+ status_data['end_time'] = end_time
207
+ status_data['exit_code'] = 1
208
+ with open(get_status_file_path(script_id), 'w') as f:
209
+ json.dump(status_data, f)
210
+ with open(get_output_file_path(script_id), 'a') as output_file:
211
+ output_file.write(str(e))
212
+ return jsonify({'error': 'Failed to terminate script'}), 500
213
+ update_script_status(script_id, 'failed', end_time=end_time, exit_code=1)
214
+ return jsonify({'error': 'Invalid script_id or script not running'}), 400
215
+
216
+ @app.route('/script_status/<script_id>', methods=['GET'])
217
+ @auth_required
218
+ def get_script_status(script_id):
219
+ status = read_script_status(script_id)
220
+ if not status:
221
+ return jsonify({'error': 'Invalid script_id'}), 404
222
+
223
+ output_file_path = get_output_file_path(script_id)
224
+ if os.path.exists(output_file_path):
225
+ with open(output_file_path, 'r') as output_file:
226
+ output = output_file.read()
227
+ status['output'] = output
228
+
229
+ return jsonify(status)
230
+
231
+ @app.route('/')
232
+ @auth_required
233
+ def index():
234
+ return render_template('index.html')
235
+
236
+ @app.route('/scripts', methods=['GET'])
237
+ @auth_required
238
+ def list_scripts():
239
+ scripts = []
240
+ for filename in os.listdir(SCRIPT_STATUS_DIR):
241
+ if filename.endswith('.json'):
242
+ script_id = filename[:-5]
243
+ status = read_script_status(script_id)
244
+ if status:
245
+ command = status['script_name'] + ' ' + shlex.join(status['params'])
246
+ scripts.append({
247
+ 'script_id': script_id,
248
+ 'status': status['status'],
249
+ 'start_time': status.get('start_time', 'N/A'),
250
+ 'end_time': status.get('end_time', 'N/A'),
251
+ 'command': command,
252
+ 'exit_code': status.get('exit_code', 'N/A')
253
+ })
254
+ # Sort scripts by start_time in descending order
255
+ scripts.sort(key=lambda x: x['start_time'], reverse=True)
256
+ return jsonify(scripts)
257
+
258
+ @app.route('/script_output/<script_id>', methods=['GET'])
259
+ @auth_required
260
+ def get_script_output(script_id):
261
+ output_file_path = get_output_file_path(script_id)
262
+ if os.path.exists(output_file_path):
263
+ with open(output_file_path, 'r') as output_file:
264
+ output = output_file.read()
265
+ status_data = read_script_status(script_id) or {}
266
+ return jsonify({'output': output, 'status': status_data.get("status")})
267
+ return jsonify({'error': 'Invalid script_id'}), 404
268
+
269
+ @app.route('/executables', methods=['GET'])
270
+ @auth_required
271
+ def list_executables():
272
+ executables = [f for f in os.listdir('.') if os.path.isfile(f) and os.access(f, os.X_OK)]
273
+ return jsonify(executables)
274
+
275
+ @auth.verify_password
276
+ def verify_password(username, password):
277
+ return username == app.config['USER'] and password == app.config['PASSWORD']
278
+
279
+
280
+
281
+
282
+ if __name__ == '__main__':
283
+ start_gunicorn()
284
+ #app.run(host='0.0.0.0', port=5000)
@@ -0,0 +1 @@
1
+ <svg viewBox="-1 -1 13 13" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill-rule="evenodd" clip-rule="evenodd" d="M6 12A6 6 0 106 0a6 6 0 000 12zM3 5a1 1 0 000 2h6a1 1 0 100-2H3z" fill="#ff641a"></path></g></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="25" height="16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" viewBox="0 0 16 16" width="25" height="16" fill="currentColor" style="display: inline-block; user-select: none; vertical-align: text-bottom; overflow: visible;"><path fill="#118811" d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="-2 -2 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><title>cross-circle</title><desc>Created with Sketch Beta.</desc><defs></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"><g id="Icon-Set-Filled" sketch:type="MSLayerGroup" transform="translate(-570.000000, -1089.000000)" fill="#ca0000"><path d="M591.657,1109.24 C592.048,1109.63 592.048,1110.27 591.657,1110.66 C591.267,1111.05 590.633,1111.05 590.242,1110.66 L586.006,1106.42 L581.74,1110.69 C581.346,1111.08 580.708,1111.08 580.314,1110.69 C579.921,1110.29 579.921,1109.65 580.314,1109.26 L584.58,1104.99 L580.344,1100.76 C579.953,1100.37 579.953,1099.73 580.344,1099.34 C580.733,1098.95 581.367,1098.95 581.758,1099.34 L585.994,1103.58 L590.292,1099.28 C590.686,1098.89 591.323,1098.89 591.717,1099.28 C592.11,1099.68 592.11,1100.31 591.717,1100.71 L587.42,1105.01 L591.657,1109.24 L591.657,1109.24 Z M586,1089 C577.163,1089 570,1096.16 570,1105 C570,1113.84 577.163,1121 586,1121 C594.837,1121 602,1113.84 602,1105 C602,1096.16 594.837,1089 586,1089 L586,1089 Z" id="cross-circle" sketch:type="MSShapeGroup"></path></g></g></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path d="M661.333333 170.666667l253.866667 34.133333-209.066667 209.066667zM362.666667 853.333333L108.8 819.2l209.066667-209.066667zM170.666667 362.666667L204.8 108.8l209.066667 209.066667z" fill="#4949d1"></path><path d="M198.4 452.266667l-89.6 17.066666c-2.133333 14.933333-2.133333 27.733333-2.133333 42.666667 0 98.133333 34.133333 192 98.133333 264.533333l64-55.466666C219.733333 663.466667 192 588.8 192 512c0-19.2 2.133333-40.533333 6.4-59.733333zM512 106.666667c-115.2 0-217.6 49.066667-292.266667 125.866666l59.733334 59.733334C339.2 230.4 420.266667 192 512 192c19.2 0 40.533333 2.133333 59.733333 6.4l14.933334-83.2C563.2 108.8 537.6 106.666667 512 106.666667zM825.6 571.733333l89.6-17.066666c2.133333-14.933333 2.133333-27.733333 2.133333-42.666667 0-93.866667-32-185.6-91.733333-258.133333l-66.133333 53.333333c46.933333 57.6 72.533333 130.133333 72.533333 202.666667 0 21.333333-2.133333 42.666667-6.4 61.866666zM744.533333 731.733333C684.8 793.6 603.733333 832 512 832c-19.2 0-40.533333-2.133333-59.733333-6.4l-14.933334 83.2c25.6 4.266667 51.2 6.4 74.666667 6.4 115.2 0 217.6-49.066667 292.266667-125.866667l-59.733334-57.6z" fill="#4949d1"></path><path d="M853.333333 661.333333l-34.133333 253.866667-209.066667-209.066667z" fill="#4949d1"></path></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 75 949 949" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"><path fill="#00a600" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336L456.192 600.384z"></path></g></svg>
File without changes
@@ -0,0 +1,265 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>pywebexec</title>
6
+ <style>
7
+ body { font-family: Arial, sans-serif; }
8
+ .table-container { max-height: 385px; overflow-y: auto; }
9
+ table { width: 100%; border-collapse: collapse; }
10
+ th, td { padding: 8px; text-align: left; border-bottom: 1px solid #ddd; }
11
+ th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
12
+ .output {
13
+ white-space: pre-wrap;
14
+ background: #f0f0f0;
15
+ padding: 10px;
16
+ border: 1px solid #ccc;
17
+ font-family: monospace;
18
+ border-radius: 15px;
19
+ }
20
+ .copy-icon { cursor: pointer; }
21
+ .monospace { font-family: monospace; }
22
+ .copied { color: green; margin-left: 5px; }
23
+ button {
24
+ -webkit-appearance: none;
25
+ -webkit-border-radius: none;
26
+ appearance: none;
27
+ border-radius: 15px;
28
+ padding: 3px;
29
+ padding-right: 13px;
30
+ border: 1px #555 solid;
31
+ height: 22px;
32
+ font-size: 13px;
33
+ outline: none;
34
+ text-indent: 10px;
35
+ background-color: #eee;
36
+ display: inline-block;
37
+ vertical-align: middle;
38
+ }
39
+ form {
40
+ padding-bottom: 15px;
41
+ }
42
+ .status-icon {
43
+ display: inline-block;
44
+ width: 16px;
45
+ height: 16px;
46
+ margin-right: 5px;
47
+ background-size: contain;
48
+ background-repeat: no-repeat;
49
+ vertical-align: middle;
50
+ }
51
+ .status-running {
52
+ background-image: url("/static/images/running.svg")
53
+ }
54
+ .status-success {
55
+ background-image: url("/static/images/success.svg")
56
+ }
57
+ .status-failed {
58
+ background-image: url("/static/images/failed.svg")
59
+ }
60
+ .status-aborted {
61
+ background-image: url("/static/images/aborted.svg")
62
+ }
63
+ .copy_clip {
64
+ padding-right: 25px;
65
+ background-repeat: no-repeat;
66
+ background-position: right top;
67
+ background-size: 25px 16px;
68
+ white-space: nowrap;
69
+ }
70
+ .copy_clip_left {
71
+ padding-left: 25px;
72
+ padding-right: 0px;
73
+ background-position: left top;
74
+ }
75
+ .copy_clip:hover {
76
+ cursor: pointer;
77
+ background-image: url("/static/images/copy.svg");
78
+ }
79
+ .copy_clip_ok, .copy_clip_ok:hover {
80
+ background-image: url("/static/images/copy_ok.svg");
81
+ }
82
+
83
+ </style>
84
+ </head>
85
+ <body>
86
+ <h1>pywebexec</h1>
87
+ <form id="launchForm">
88
+ <label for="scriptName">Command:</label>
89
+ <select id="scriptName" name="scriptName"></select>
90
+ <label for="params">Params:</label>
91
+ <input type="text" id="params" name="params">
92
+ <button type="submit">Launch</button>
93
+ </form>
94
+ <div class="table-container">
95
+ <table>
96
+ <thead>
97
+ <tr>
98
+ <th>Script ID</th>
99
+ <th>Status</th>
100
+ <th>Start Time</th>
101
+ <th>Duration</th>
102
+ <th>Exit</th>
103
+ <th>Command</th>
104
+ <th>Actions</th>
105
+ </tr>
106
+ </thead>
107
+ <tbody id="scripts"></tbody>
108
+ </table>
109
+ </div>
110
+ <div id="output" class="output"></div>
111
+
112
+ <script>
113
+ let currentScriptId = null;
114
+ let outputInterval = null;
115
+
116
+ document.getElementById('launchForm').addEventListener('submit', async (event) => {
117
+ event.preventDefault();
118
+ const scriptName = document.getElementById('scriptName').value;
119
+ const params = document.getElementById('params').value.split(' ');
120
+ const response = await fetch('/run_script', {
121
+ method: 'POST',
122
+ headers: {
123
+ 'Content-Type': 'application/json'
124
+ },
125
+ body: JSON.stringify({ script_name: scriptName, params: params })
126
+ });
127
+ const data = await response.json();
128
+ fetchScripts();
129
+ });
130
+
131
+ async function fetchScripts() {
132
+ const response = await fetch('/scripts');
133
+ const scripts = await response.json();
134
+ scripts.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
135
+ const scriptsTbody = document.getElementById('scripts');
136
+ scriptsTbody.innerHTML = '';
137
+ scripts.forEach(script => {
138
+ const scriptRow = document.createElement('tr');
139
+ scriptRow.innerHTML = `
140
+ <td class="monospace">
141
+ <span class="copy_clip" onclick="copyToClipboard('${script.script_id}', this)">${script.script_id.slice(0, 8)}</span>
142
+ </td>
143
+ <td><span class="status-icon status-${script.status}"></span>${script.status}</td>
144
+ <td>${formatTime(script.start_time)}</td>
145
+ <td>${script.status === 'running' ? formatDuration(script.start_time, new Date().toISOString()) : formatDuration(script.start_time, script.end_time)}</td>
146
+ <td>${script.exit_code}</td>
147
+ <td>${script.command.replace(/^\.\//, '')}</td>
148
+ <td>
149
+ <button onclick="viewOutput('${script.script_id}')">Log</button>
150
+ <button onclick="relaunchScript('${script.script_id}')">Relaunch</button>
151
+ ${script.status === 'running' ? `<button onclick="stopScript('${script.script_id}')">Stop</button>` : ''}
152
+ </td>
153
+ `;
154
+ scriptsTbody.appendChild(scriptRow);
155
+ });
156
+ }
157
+
158
+ async function fetchExecutables() {
159
+ const response = await fetch('/executables');
160
+ const executables = await response.json();
161
+ const scriptNameSelect = document.getElementById('scriptName');
162
+ scriptNameSelect.innerHTML = '';
163
+ executables.forEach(executable => {
164
+ const option = document.createElement('option');
165
+ option.value = executable;
166
+ option.textContent = executable;
167
+ scriptNameSelect.appendChild(option);
168
+ });
169
+ }
170
+
171
+ async function fetchOutput(script_id) {
172
+ const outputDiv = document.getElementById('output');
173
+ const response = await fetch(`/script_output/${script_id}`);
174
+ const data = await response.json();
175
+ if (data.error) {
176
+ outputDiv.innerHTML = data.error;
177
+ clearInterval(outputInterval);
178
+ } else {
179
+ outputDiv.innerHTML = data.output;
180
+ if (data.status != 'running') {
181
+ clearInterval(outputInterval)
182
+ }
183
+ }
184
+ }
185
+
186
+ async function viewOutput(script_id) {
187
+ currentScriptId = script_id;
188
+ clearInterval(outputInterval);
189
+ const response = await fetch(`/script_status/${script_id}`);
190
+ const data = await response.json();
191
+ if (data.status === 'running') {
192
+ fetchOutput(script_id);
193
+ outputInterval = setInterval(() => fetchOutput(script_id), 1000);
194
+ } else {
195
+ fetchOutput(script_id);
196
+ }
197
+ }
198
+
199
+ async function relaunchScript(script_id) {
200
+ const response = await fetch(`/script_status/${script_id}`);
201
+ const data = await response.json();
202
+ if (data.error) {
203
+ alert(data.error);
204
+ return;
205
+ }
206
+ const relaunchResponse = await fetch('/run_script', {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/json'
210
+ },
211
+ body: JSON.stringify({
212
+ script_name: data.script_name,
213
+ params: data.params
214
+ })
215
+ });
216
+ const relaunchData = await relaunchResponse.json();
217
+ alert(relaunchData.message);
218
+ fetchScripts();
219
+ }
220
+
221
+ async function stopScript(script_id) {
222
+ const response = await fetch(`/stop_script/${script_id}`, {
223
+ method: 'POST'
224
+ });
225
+ const data = await response.json();
226
+ if (data.error) {
227
+ alert(data.error);
228
+ } else {
229
+ alert(data.message);
230
+ fetchScripts();
231
+ }
232
+ }
233
+
234
+ function formatTime(time) {
235
+ if (!time || time === 'N/A') return 'N/A';
236
+ const date = new Date(time);
237
+ return date.toISOString().slice(0, 16).replace('T', ' ');
238
+ }
239
+
240
+ function formatDuration(startTime, endTime) {
241
+ if (!startTime || !endTime) return 'N/A';
242
+ const start = new Date(startTime);
243
+ const end = new Date(endTime);
244
+ const duration = (end - start) / 1000;
245
+ const hours = Math.floor(duration / 3600);
246
+ const minutes = Math.floor((duration % 3600) / 60);
247
+ const seconds = Math.floor(duration % 60);
248
+ return `${hours}h ${minutes}m ${seconds}s`;
249
+ }
250
+
251
+ function copyToClipboard(text, element) {
252
+ navigator.clipboard.writeText(text).then(() => {
253
+ element.classList.add('copy_clip_ok')
254
+ setTimeout(() => {
255
+ element.classList.remove('copy_clip_ok');
256
+ }, 2000);
257
+ });
258
+ }
259
+
260
+ fetchScripts();
261
+ fetchExecutables();
262
+ setInterval(fetchScripts, 5000);
263
+ </script>
264
+ </body>
265
+ </html>
pywebexec/version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.0.3'
16
+ __version_tuple__ = version_tuple = (0, 0, 3)
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 joknarf
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.1
2
+ Name: pywebexec
3
+ Version: 0.0.3
4
+ Summary: Simple Python HTTP Exec Server
5
+ Home-page: https://github.com/joknarf/pywebexec
6
+ Author: Franck Jouvanceau
7
+ Maintainer: Franck Jouvanceau
8
+ License: MIT License
9
+
10
+ Copyright (c) 2025 joknarf
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
29
+
30
+ Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
+ Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
+ Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
+ Keywords: http,fileserver,browser,explorer
34
+ Classifier: Development Status :: 5 - Production/Stable
35
+ Classifier: Intended Audience :: System Administrators
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Operating System :: POSIX
38
+ Classifier: Operating System :: Unix
39
+ Classifier: Operating System :: Microsoft :: Windows
40
+ Classifier: Operating System :: MacOS
41
+ Classifier: Programming Language :: Python
42
+ Classifier: Programming Language :: Python :: 3
43
+ Classifier: Programming Language :: Python :: 3.6
44
+ Classifier: Programming Language :: Python :: 3.7
45
+ Classifier: Programming Language :: Python :: 3.8
46
+ Classifier: Programming Language :: Python :: 3.9
47
+ Classifier: Programming Language :: Python :: 3.10
48
+ Classifier: Programming Language :: Python :: 3.11
49
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
50
+ Classifier: Topic :: System :: Clustering
51
+ Classifier: Topic :: System :: Networking
52
+ Classifier: Topic :: System :: Systems Administration
53
+ Requires-Python: >=3.6
54
+ Description-Content-Type: text/markdown
55
+ License-File: LICENSE
56
+ Requires-Dist: cryptography>=40.0.2
57
+ Requires-Dist: Flask>=3.0.3
58
+ Requires-Dist: Flask-HTTPAuth>=4.8.0
59
+
60
+ [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
61
+ ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
62
+ [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
63
+ [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
64
+ [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
65
+
66
+ # pywebexec
67
+ Simple Python HTTP(S) API/Web Command Launcher
68
+
69
+ ## Install
70
+ ```
71
+ $ pip install pywebexec
72
+ ```
73
+
74
+ ## Quick start
75
+
76
+ * start http server serving current directory executables listening on 0.0.0.0 port 8080
77
+ ```
78
+ $ pywebexec
79
+ ```
80
+
81
+ * Launch commands with params/view live output/Status using browser `http://<yourserver>:8080`
82
+
83
+ ## features
84
+
85
+ * Serve executables in current directory
86
+ * Launch commands with params from web browser
87
+ * Follow live output
88
+ * Stop command
89
+ * Relaunch command
90
+ * HTTPS support
91
+ * HTTPS self-signed certificate generator
92
+ * Can be started as a daemon (POSIX)
93
+ * uses gunicorn to serve http/https
94
+
95
+ ## Customize server
96
+ ```
97
+ $ pywebexec --listen 0.0.0.0 --port 8080
98
+ $ pywebexec -l 0.0.0.0 -p 8080
99
+ ```
100
+
101
+ ## Basic auth user/password
102
+ ```
103
+ $ pywebexec --user myuser [--password mypass]
104
+ $ pywebfs -u myuser [-P mypass]
105
+ ```
106
+ Generated password is given if no `--pasword` option
107
+
108
+ ## HTTPS server
109
+
110
+ * Generate auto-signed certificate and start https server
111
+ ```
112
+ $ pywebfs --gencert
113
+ $ pywebfs --g
114
+ ```
115
+
116
+ * Start https server using existing certificate
117
+ ```
118
+ $ pywebfs --cert /pathto/host.cert --key /pathto/host.key
119
+ $ pywebfs -c /pathto/host.cert -k /pathto/host.key
120
+ ```
121
+
122
+ ## Launch server as a daemon (Linux)
123
+
124
+ ```
125
+ $ pywebexec start
126
+ $ pywebexec status
127
+ $ pywebexec stop
128
+ ```
129
+ * log of server are stored in current directory `.web_status/pwexec_<listen>:<port>.log`
130
+
@@ -0,0 +1,17 @@
1
+ pywebexec/__init__.py,sha256=4spIsVaF8RJt8S58AG_wWoORRNkws9Iwqprj27C3ljM,99
2
+ pywebexec/pywebexec.py,sha256=mnnlYkRg8HpWdfcVamqEw0zFqy-ZVNVEVXYttecRTyg,10212
3
+ pywebexec/version.py,sha256=hB095avW4HuDZxn8qPHRG1UMzSSonb8ZDAsLxt9hmk8,411
4
+ pywebexec/static/images/aborted.svg,sha256=_mP43hU5QdRLFZIknBgjx-dIXrHgQG23-QV27ApXK2A,381
5
+ pywebexec/static/images/copy.svg,sha256=d9OwtGh5GzzZHzYcDrLfNxZYLth1Q64x7bRyYxu4Px0,622
6
+ pywebexec/static/images/copy_ok.svg,sha256=mEqUVUhSq8xaJK2msQkxRawnz_KwlCZ-tok8QS6hJ3g,451
7
+ pywebexec/static/images/failed.svg,sha256=ADZ7IKrUyOXtqpivnz3VcH0-Wru-I5MOi3OJAkI3hxk,1439
8
+ pywebexec/static/images/running.svg,sha256=vBpiG6ClNUNCArkwsyqK7O-qhIKJX1NI7MSjclNSp_8,1537
9
+ pywebexec/static/images/success.svg,sha256=PJDcCSTevJh7rkfSFLtc7P0pbeh8PVQBS8DaOLQemmc,489
10
+ pywebexec/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pywebexec/templates/index.html,sha256=tS2uVjto2iPpx-0j3aVxAfE564dJJKMzY0_dBC-vGjE,9813
12
+ pywebexec-0.0.3.dist-info/LICENSE,sha256=gRJf0JPT_wsZJsUGlWPTS8Vypfl9vQ1qjp6sNbKykuA,1064
13
+ pywebexec-0.0.3.dist-info/METADATA,sha256=9CAAkMxOY5LxHf8boAIwPj2FPgu7G2r3PMda-4U8xrw,4623
14
+ pywebexec-0.0.3.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
15
+ pywebexec-0.0.3.dist-info/entry_points.txt,sha256=-6--c27U7RARJe0BiW5CkTuKljf6pRtnDzE0wfmD9TM,65
16
+ pywebexec-0.0.3.dist-info/top_level.txt,sha256=vHoHyzngrfGdm_nM7Xn_5iLmaCrf10XO1EhldgNLEQ8,10
17
+ pywebexec-0.0.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.7.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pywebexec = pywebexec.pywebexec:start_gunicorn
@@ -0,0 +1 @@
1
+ pywebexec