pywebexec 1.1.2__tar.gz → 1.1.13__tar.gz

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.
Files changed (29) hide show
  1. {pywebexec-1.1.2/pywebexec.egg-info → pywebexec-1.1.13}/PKG-INFO +11 -9
  2. {pywebexec-1.1.2 → pywebexec-1.1.13}/README.md +9 -7
  3. {pywebexec-1.1.2 → pywebexec-1.1.13}/pyproject.toml +1 -1
  4. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/pywebexec.py +9 -2
  5. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/css/style.css +66 -11
  6. pywebexec-1.1.13/pywebexec/static/images/running.gif +0 -0
  7. pywebexec-1.1.13/pywebexec/static/js/script.js +256 -0
  8. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/templates/index.html +4 -1
  9. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/version.py +2 -2
  10. {pywebexec-1.1.2 → pywebexec-1.1.13/pywebexec.egg-info}/PKG-INFO +11 -9
  11. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec.egg-info/SOURCES.txt +1 -1
  12. pywebexec-1.1.2/pywebexec/static/images/running.svg +0 -1
  13. pywebexec-1.1.2/pywebexec/static/js/script.js +0 -197
  14. {pywebexec-1.1.2 → pywebexec-1.1.13}/.github/workflows/python-publish.yml +0 -0
  15. {pywebexec-1.1.2 → pywebexec-1.1.13}/.gitignore +0 -0
  16. {pywebexec-1.1.2 → pywebexec-1.1.13}/LICENSE +0 -0
  17. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/__init__.py +0 -0
  18. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/images/aborted.svg +0 -0
  19. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/images/copy.svg +0 -0
  20. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/images/copy_ok.svg +0 -0
  21. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/images/failed.svg +0 -0
  22. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/images/favicon.svg +0 -0
  23. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/static/images/success.svg +0 -0
  24. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec/templates/__init__.py +0 -0
  25. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec.egg-info/dependency_links.txt +0 -0
  26. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec.egg-info/entry_points.txt +0 -0
  27. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec.egg-info/requires.txt +0 -0
  28. {pywebexec-1.1.2 → pywebexec-1.1.13}/pywebexec.egg-info/top_level.txt +0 -0
  29. {pywebexec-1.1.2 → pywebexec-1.1.13}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.1.2
3
+ Version: 1.1.13
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -30,7 +30,7 @@ License: MIT License
30
30
  Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
31
  Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
32
  Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
- Keywords: http,fileserver,browser,explorer
33
+ Keywords: http,server,remote commands,api,website
34
34
  Classifier: Development Status :: 5 - Production/Stable
35
35
  Classifier: Intended Audience :: System Administrators
36
36
  Classifier: License :: OSI Approved :: MIT License
@@ -63,7 +63,7 @@ Requires-Dist: ldap3>=2.9.1
63
63
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
64
64
  ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
65
65
  [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
66
- [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
66
+ [![PyPI Downloads](https://static.pepy.tech/badge/pywebexec)](https://pepy.tech/projects/pywebexec)
67
67
  [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
68
68
 
69
69
  # pywebexec
@@ -83,7 +83,7 @@ $ pywebexec
83
83
  ```
84
84
 
85
85
  * Launch commands with params/view live output/Status using browser
86
- ![pywebexec](https://github.com/user-attachments/assets/d352cc23-1552-4b79-a6ff-f02f05cf328e)
86
+ ![pywebexecnew](https://github.com/user-attachments/assets/06290b5e-284e-4e41-b32a-0f5aad074658)
87
87
 
88
88
  all commands output / statuses are available in the executables directory in subdirectory `.web_status`
89
89
 
@@ -99,8 +99,8 @@ all commands output / statuses are available in the executables directory in sub
99
99
  * Basic Auth
100
100
  * LDAP(S)
101
101
  * Can be started as a daemon (POSIX)
102
- * uses gunicorn to serve http/https
103
- * compatible Linux/MacOS
102
+ * Uses gunicorn to serve http/https
103
+ * Linux/MacOS compatible
104
104
 
105
105
  ## Customize server
106
106
  ```shell
@@ -122,7 +122,7 @@ Generated password is given if no `--pasword` option
122
122
  $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
123
123
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
124
124
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
125
- $ export PYWEBEXEC_LDAP_GROUPS=ou=mathematicians,ou=scientists
125
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
126
126
  $ export PYWEBEXEC_LDAP_USER_ID="uid"
127
127
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
128
128
  $ pywebexec
@@ -148,12 +148,14 @@ $ pywebexec start
148
148
  $ pywebexec status
149
149
  $ pywebexec stop
150
150
  ```
151
- * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
151
+ * log of server are stored in directory `~/[.config/].pywebexec/pywebexec_<listen>:<port>.log`
152
152
 
153
153
  ## Launch command through API
154
154
 
155
155
  ```shell
156
- $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
156
+ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "params":["param1", ...]}
157
+ $ curl http://myhost:8080/command_status/<command_id>
158
+ $ curl http://myhost:8080/command_output/<command_id> -H "Accept: text/plain"
157
159
  ```
158
160
 
159
161
  ## API reference
@@ -1,7 +1,7 @@
1
1
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
2
2
  ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
3
3
  [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
4
- [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
4
+ [![PyPI Downloads](https://static.pepy.tech/badge/pywebexec)](https://pepy.tech/projects/pywebexec)
5
5
  [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
6
6
 
7
7
  # pywebexec
@@ -21,7 +21,7 @@ $ pywebexec
21
21
  ```
22
22
 
23
23
  * Launch commands with params/view live output/Status using browser
24
- ![pywebexec](https://github.com/user-attachments/assets/d352cc23-1552-4b79-a6ff-f02f05cf328e)
24
+ ![pywebexecnew](https://github.com/user-attachments/assets/06290b5e-284e-4e41-b32a-0f5aad074658)
25
25
 
26
26
  all commands output / statuses are available in the executables directory in subdirectory `.web_status`
27
27
 
@@ -37,8 +37,8 @@ all commands output / statuses are available in the executables directory in sub
37
37
  * Basic Auth
38
38
  * LDAP(S)
39
39
  * Can be started as a daemon (POSIX)
40
- * uses gunicorn to serve http/https
41
- * compatible Linux/MacOS
40
+ * Uses gunicorn to serve http/https
41
+ * Linux/MacOS compatible
42
42
 
43
43
  ## Customize server
44
44
  ```shell
@@ -60,7 +60,7 @@ Generated password is given if no `--pasword` option
60
60
  $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
61
61
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
62
62
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
63
- $ export PYWEBEXEC_LDAP_GROUPS=ou=mathematicians,ou=scientists
63
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
64
64
  $ export PYWEBEXEC_LDAP_USER_ID="uid"
65
65
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
66
66
  $ pywebexec
@@ -86,12 +86,14 @@ $ pywebexec start
86
86
  $ pywebexec status
87
87
  $ pywebexec stop
88
88
  ```
89
- * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
89
+ * log of server are stored in directory `~/[.config/].pywebexec/pywebexec_<listen>:<port>.log`
90
90
 
91
91
  ## Launch command through API
92
92
 
93
93
  ```shell
94
- $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
94
+ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "params":["param1", ...]}
95
+ $ curl http://myhost:8080/command_status/<command_id>
96
+ $ curl http://myhost:8080/command_output/<command_id> -H "Accept: text/plain"
95
97
  ```
96
98
 
97
99
  ## API reference
@@ -20,7 +20,7 @@ dynamic=["version"]
20
20
  readme = "README.md"
21
21
  license = {file = "LICENSE"}
22
22
  requires-python = ">= 3.6"
23
- keywords = ["http", "fileserver", "browser", "explorer"]
23
+ keywords = ["http", "server", "remote commands", "api", "website"]
24
24
  classifiers = [
25
25
  "Development Status :: 5 - Production/Stable",
26
26
  "Intended Audience :: System Administrators",
@@ -342,7 +342,10 @@ def read_command_status(command_id):
342
342
  if not os.path.exists(status_file_path):
343
343
  return None
344
344
  with open(status_file_path, 'r') as f:
345
- status_data = json.load(f)
345
+ try:
346
+ status_data = json.load(f)
347
+ except json.JSONDecodeError:
348
+ return None
346
349
 
347
350
  # Cache the status if it is not "running"
348
351
  if status_data['status'] != 'running':
@@ -382,6 +385,8 @@ def run_command(command, params, command_id):
382
385
 
383
386
  @app.before_request
384
387
  def check_authentication():
388
+ if not app.config['USER'] and not app.config['LDAP_SERVER']:
389
+ return
385
390
  if 'username' not in session and request.endpoint not in ['login', 'static']:
386
391
  return auth.login_required(lambda: None)()
387
392
 
@@ -518,7 +523,7 @@ def list_commands():
518
523
  params = shlex.join(status['params'])
519
524
  except AttributeError:
520
525
  params = " ".join([shlex.quote(p) if " " in p else p for p in status['params']])
521
- command = status['command'] + ' ' + params
526
+ command = status.get('command', '-') + ' ' + params
522
527
  commands.append({
523
528
  'command_id': command_id,
524
529
  'status': status['status'],
@@ -539,6 +544,8 @@ def get_command_output(command_id):
539
544
  with open(output_file_path, 'r') as output_file:
540
545
  output = output_file.read()
541
546
  status_data = read_command_status(command_id) or {}
547
+ if request.headers.get('Accept') == 'text/plain':
548
+ return f"{output}\nstatus: {status_data.get('status')}", 200, {'Content-Type': 'text/plain'}
542
549
  return jsonify({'output': output, 'status': status_data.get("status")})
543
550
  return jsonify({'error': 'Invalid command_id'}), 404
544
551
 
@@ -1,23 +1,46 @@
1
- body { font-family: Arial, sans-serif; }
2
- .table-container { height: 270px; overflow-y: auto; position: relative; }
3
- table { width: 100%; border-collapse: collapse; }
1
+ body {
2
+ font-family: Arial, sans-serif;
3
+ }
4
+ .table-container {
5
+ height: 270px;
6
+ overflow-y: auto;
7
+ position: relative;
8
+ border-radius: 10px;
9
+ border: 1px solid #aaa;
10
+ box-shadow: 0 0 10px rgba(0, 0, 0, 0.30);
11
+ }
12
+ table {
13
+ width: 100%;
14
+ border-collapse: collapse;
15
+ }
4
16
  th, td {
5
17
  padding: 8px;
6
18
  text-align: left;
7
19
  border-bottom: 1px solid #ddd;
8
20
  white-space: nowrap;
9
21
  }
10
- th { background-color: #f2f2f2; position: sticky; top: 0; z-index: 1; }
22
+ th {
23
+ background-color: #444;
24
+ color: #eee;
25
+ position: sticky;
26
+ top: 0;
27
+ z-index: 1;
28
+ }
11
29
  .outcol {
12
30
  width: 100%;
13
31
  }
32
+ select { /* Safari bug */
33
+ font-size: 15px;
34
+ border: #aaa solid 1px;
35
+ border-radius: 5px;
36
+ }
14
37
  .output {
15
38
  white-space: pre-wrap;
16
39
  background: #f0f0f0;
17
40
  padding: 10px;
18
41
  border: 1px solid #ccc;
19
42
  font-family: monospace;
20
- border-radius: 15px;
43
+ border-radius: 10px;
21
44
  overflow-y: auto;
22
45
  }
23
46
  .copy-icon { cursor: pointer; }
@@ -28,7 +51,6 @@ button {
28
51
  -webkit-border-radius: none;
29
52
  appearance: none;
30
53
  border-radius: 15px;
31
- padding: 3px;
32
54
  padding-right: 13px;
33
55
  border: 1px #555 solid;
34
56
  height: 22px;
@@ -49,8 +71,13 @@ form {
49
71
  background-repeat: no-repeat;
50
72
  vertical-align: middle;
51
73
  }
74
+ .title-icon {
75
+ width: 30px;
76
+ height: 30px;
77
+ background-image: url("/static/images/favicon.svg")
78
+ }
52
79
  .status-running {
53
- background-image: url("/static/images/running.svg")
80
+ background-image: url("/static/images/running.gif")
54
81
  }
55
82
  .status-success {
56
83
  background-image: url("/static/images/success.svg")
@@ -62,10 +89,10 @@ form {
62
89
  background-image: url("/static/images/aborted.svg")
63
90
  }
64
91
  .copy_clip {
65
- padding-right: 25px;
92
+ padding-right: 20px;
66
93
  background-repeat: no-repeat;
67
94
  background-position: right top;
68
- background-size: 25px 16px;
95
+ background-size: 20px 12px;
69
96
  white-space: nowrap;
70
97
  }
71
98
  .copy_clip:hover {
@@ -87,7 +114,7 @@ input {
87
114
  height: 15px;
88
115
  font-size: 15px;
89
116
  outline: none;
90
- text-indent: 10px;
117
+ text-indent: 5px;
91
118
  background-color: white;
92
119
  }
93
120
  .currentcommand {
@@ -96,6 +123,7 @@ input {
96
123
  .resizer {
97
124
  width: 100%;
98
125
  height: 5px;
126
+ border-radius: 5px;
99
127
  background: #aaa;
100
128
  cursor: ns-resize;
101
129
  position: absolute;
@@ -105,8 +133,35 @@ input {
105
133
  .resizer-container {
106
134
  position: relative;
107
135
  height: 5px;
108
- margin-bottom: 10px;
136
+ margin: 5px;
137
+ /*margin-bottom: 10px;*/
109
138
  }
110
139
  tr.clickable-row {
111
140
  cursor: pointer;
112
141
  }
142
+ body.dimmed {
143
+ background-color: rgba(0, 0, 0, 0.5);
144
+ pointer-events: none;
145
+ }
146
+ body.dimmed * {
147
+ pointer-events: none;
148
+ }
149
+ .dimmer {
150
+ display: none;
151
+ position: fixed;
152
+ top: 0;
153
+ left: 0;
154
+ width: 100%;
155
+ height: 100%;
156
+ background-color: rgba(0, 0, 0, 0.5);
157
+ z-index: 1000;
158
+ }
159
+ .dimmer-text {
160
+ color: white;
161
+ font-size: 24px;
162
+ text-align: center;
163
+ position: absolute;
164
+ top: 50%;
165
+ left: 50%;
166
+ transform: translate(-50%, -50%);
167
+ }
@@ -0,0 +1,256 @@
1
+ let currentCommandId = null;
2
+ let outputInterval = null;
3
+
4
+ document.getElementById('launchForm').addEventListener('submit', async (event) => {
5
+ event.preventDefault();
6
+ const commandName = document.getElementById('commandName').value;
7
+ const params = document.getElementById('params').value.split(' ');
8
+ try {
9
+ const response = await fetch('/run_command', {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json'
13
+ },
14
+ body: JSON.stringify({ command: commandName, params: params })
15
+ });
16
+ if (!response.ok) {
17
+ throw new Error('Failed to launch command');
18
+ }
19
+ const data = await response.json();
20
+ fetchCommands();
21
+ viewOutput(data.command_id);
22
+ } catch (error) {
23
+ console.log('Error running command:', error);
24
+ }
25
+ });
26
+
27
+ async function fetchCommands() {
28
+ try {
29
+ const response = await fetch('/commands');
30
+ if (!response.ok) {
31
+ document.getElementById('dimmer').style.display = 'block';
32
+ return;
33
+ }
34
+ const commands = await response.json();
35
+ commands.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
36
+ const commandsTbody = document.getElementById('commands');
37
+ commandsTbody.innerHTML = '';
38
+ if (!currentCommandId && commands.length) {
39
+ currentCommandId = commands[0].command_id;
40
+ viewOutput(currentCommandId);
41
+ }
42
+ commands.forEach(command => {
43
+ const commandRow = document.createElement('tr');
44
+ commandRow.className = `clickable-row ${command.command_id === currentCommandId ? 'currentcommand' : ''}`;
45
+ commandRow.onclick = () => viewOutput(command.command_id);
46
+ commandRow.innerHTML = `
47
+ <td class="monospace">
48
+ ${navigator.clipboard == undefined ? `${command.command_id}` : `<span class="copy_clip" onclick="copyToClipboard('${command.command_id.slice(0, 8)}', this, event)">${command.command_id.slice(0, 8)}</span>`}
49
+ </td>
50
+ <td><span class="status-icon status-${command.status}"></span>${command.status}</td>
51
+ <td>${formatTime(command.start_time)}</td>
52
+ <td>${command.status === 'running' ? formatDuration(command.start_time, new Date().toISOString()) : formatDuration(command.start_time, command.end_time)}</td>
53
+ <td>${command.exit_code}</td>
54
+ <td>${command.command.replace(/^\.\//, '')}</td>
55
+ <td>
56
+ ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : `<button onclick="relaunchCommand('${command.command_id}')">Run</button>`}
57
+ </td>
58
+ <td class="monospace outcol">${command.last_output_line || ''}</td>
59
+ `;
60
+ commandsTbody.appendChild(commandRow);
61
+ });
62
+ document.getElementById('dimmer').style.display = 'none';
63
+ } catch (error) {
64
+ console.log('Error fetching commands:', error);
65
+ document.getElementById('dimmer').style.display = 'block';
66
+ }
67
+ }
68
+
69
+ async function fetchExecutables() {
70
+ try {
71
+ const response = await fetch('/executables');
72
+ if (!response.ok) {
73
+ throw new Error('Failed to fetch command status');
74
+ }
75
+ const executables = await response.json();
76
+ const commandNameSelect = document.getElementById('commandName');
77
+ commandNameSelect.innerHTML = '';
78
+ executables.forEach(executable => {
79
+ const option = document.createElement('option');
80
+ option.value = executable;
81
+ option.textContent = executable;
82
+ commandNameSelect.appendChild(option);
83
+ });
84
+ } catch (error) {
85
+ console.log('Error fetching executables:', error);
86
+ alert("Failed to fetch executables");
87
+ }
88
+ }
89
+
90
+ async function fetchOutput(command_id) {
91
+ try {
92
+ const outputDiv = document.getElementById('output');
93
+ const response = await fetch(`/command_output/${command_id}`);
94
+ if (!response.ok) {
95
+ return;
96
+ }
97
+ const data = await response.json();
98
+ if (data.error) {
99
+ outputDiv.innerHTML = data.error;
100
+ clearInterval(outputInterval);
101
+ } else {
102
+ outputDiv.innerHTML = data.output;
103
+ outputDiv.scrollTop = outputDiv.scrollHeight;
104
+ if (data.status != 'running') {
105
+ clearInterval(outputInterval);
106
+ }
107
+ }
108
+ } catch (error) {
109
+ console.log('Error fetching output:', error);
110
+ }
111
+ }
112
+
113
+ async function viewOutput(command_id) {
114
+ adjustOutputHeight();
115
+ currentCommandId = command_id;
116
+ clearInterval(outputInterval);
117
+ try {
118
+ const response = await fetch(`/command_status/${command_id}`);
119
+ if (!response.ok) {
120
+ return;
121
+ }
122
+ const data = await response.json();
123
+ if (data.status === 'running') {
124
+ fetchOutput(command_id);
125
+ outputInterval = setInterval(() => fetchOutput(command_id), 1000);
126
+ } else {
127
+ fetchOutput(command_id);
128
+ }
129
+ fetchCommands(); // Refresh the command list to highlight the current command
130
+ } catch (error) {
131
+ console.log('Error viewing output:', error);
132
+ }
133
+ }
134
+
135
+ async function relaunchCommand(command_id) {
136
+ try {
137
+ const response = await fetch(`/command_status/${command_id}`);
138
+ if (!response.ok) {
139
+ throw new Error('Failed to fetch command status');
140
+ }
141
+ const data = await response.json();
142
+ if (data.error) {
143
+ alert(data.error);
144
+ return;
145
+ }
146
+ const relaunchResponse = await fetch('/run_command', {
147
+ method: 'POST',
148
+ headers: {
149
+ 'Content-Type': 'application/json'
150
+ },
151
+ body: JSON.stringify({
152
+ command: data.command,
153
+ params: data.params
154
+ })
155
+ });
156
+ if (!relaunchResponse.ok) {
157
+ throw new Error('Failed to relaunch command');
158
+ }
159
+ const relaunchData = await relaunchResponse.json();
160
+ fetchCommands();
161
+ viewOutput(relaunchData.command_id);
162
+ } catch (error) {
163
+ console.log('Error relaunching command:', error);
164
+ alert('Failed to relaunch command. Please try again.');
165
+ }
166
+ }
167
+
168
+ async function stopCommand(command_id) {
169
+ try {
170
+ const response = await fetch(`/stop_command/${command_id}`, {
171
+ method: 'POST'
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error('Failed to stop command');
175
+ }
176
+ const data = await response.json();
177
+ if (data.error) {
178
+ alert(data.error);
179
+ } else {
180
+ alert(data.message);
181
+ fetchCommands();
182
+ }
183
+ } catch (error) {
184
+ console.log('Error stopping command:', error);
185
+ alert('Failed to stop command. Please try again.');
186
+ }
187
+ }
188
+
189
+ function formatTime(time) {
190
+ if (!time || time === 'N/A') return 'N/A';
191
+ const date = new Date(time);
192
+ return date.toISOString().slice(0, 16).replace('T', ' ');
193
+ }
194
+
195
+ function formatDuration(startTime, endTime) {
196
+ if (!startTime || !endTime) return 'N/A';
197
+ const start = new Date(startTime);
198
+ const end = new Date(endTime);
199
+ const duration = (end - start) / 1000;
200
+ const hours = Math.floor(duration / 3600);
201
+ const minutes = Math.floor((duration % 3600) / 60);
202
+ const seconds = Math.floor(duration % 60);
203
+ return `${hours}h ${minutes}m ${seconds}s`;
204
+ }
205
+
206
+ function copyToClipboard(text, element, event) {
207
+ event.stopPropagation();
208
+ event.stopImmediatePropagation();
209
+ navigator.clipboard.writeText(text).then(() => {
210
+ element.classList.add('copy_clip_ok');
211
+ setTimeout(() => {
212
+ element.classList.remove('copy_clip_ok');
213
+ }, 1000);
214
+ });
215
+ }
216
+
217
+ function adjustOutputHeight() {
218
+ const outputDiv = document.getElementById('output');
219
+ const windowHeight = window.innerHeight;
220
+ const outputTop = outputDiv.getBoundingClientRect().top;
221
+ const maxHeight = windowHeight - outputTop - 30; // 20px for padding/margin
222
+ outputDiv.style.maxHeight = `${maxHeight}px`;
223
+ }
224
+
225
+ function initResizer() {
226
+ const resizer = document.getElementById('resizer');
227
+ const tableContainer = document.getElementById('tableContainer');
228
+ let startY, startHeight;
229
+
230
+ resizer.addEventListener('mousedown', (e) => {
231
+ startY = e.clientY;
232
+ startHeight = parseInt(document.defaultView.getComputedStyle(tableContainer).height, 10);
233
+ document.documentElement.addEventListener('mousemove', doDrag, false);
234
+ document.documentElement.addEventListener('mouseup', stopDrag, false);
235
+ });
236
+
237
+ function doDrag(e) {
238
+ tableContainer.style.height = `${startHeight + e.clientY - startY}px`;
239
+ adjustOutputHeight();
240
+ }
241
+
242
+ function stopDrag() {
243
+ document.documentElement.removeEventListener('mousemove', doDrag, false);
244
+ document.documentElement.removeEventListener('mouseup', stopDrag, false);
245
+ }
246
+ }
247
+
248
+ window.addEventListener('resize', adjustOutputHeight);
249
+ window.addEventListener('load', () => {
250
+ adjustOutputHeight();
251
+ initResizer();
252
+ });
253
+
254
+ fetchExecutables();
255
+ fetchCommands();
256
+ setInterval(fetchCommands, 5000);
@@ -7,7 +7,10 @@
7
7
  <link rel="stylesheet" href="/static/css/style.css">
8
8
  </head>
9
9
  <body>
10
- <h2>{{ title }}</h2>
10
+ <div id="dimmer" class="dimmer">
11
+ <div class="dimmer-text">Server not reachable</div>
12
+ </div>
13
+ <h2><span class="status-icon title-icon"></span>{{ title }}</h2>
11
14
  <form id="launchForm">
12
15
  <label for="commandName">Command</label>
13
16
  <select id="commandName" name="commandName"></select>
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '1.1.2'
16
- __version_tuple__ = version_tuple = (1, 1, 2)
15
+ __version__ = version = '1.1.13'
16
+ __version_tuple__ = version_tuple = (1, 1, 13)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pywebexec
3
- Version: 1.1.2
3
+ Version: 1.1.13
4
4
  Summary: Simple Python HTTP Exec Server
5
5
  Home-page: https://github.com/joknarf/pywebexec
6
6
  Author: Franck Jouvanceau
@@ -30,7 +30,7 @@ License: MIT License
30
30
  Project-URL: Homepage, https://github.com/joknarf/pywebexec
31
31
  Project-URL: Documentation, https://github.com/joknarf/pywebexec/blob/main/README.md
32
32
  Project-URL: Repository, https://github.com/joknarf/pywebexec.git
33
- Keywords: http,fileserver,browser,explorer
33
+ Keywords: http,server,remote commands,api,website
34
34
  Classifier: Development Status :: 5 - Production/Stable
35
35
  Classifier: Intended Audience :: System Administrators
36
36
  Classifier: License :: OSI Approved :: MIT License
@@ -63,7 +63,7 @@ Requires-Dist: ldap3>=2.9.1
63
63
  [![Pypi version](https://img.shields.io/pypi/v/pywebexec.svg)](https://pypi.org/project/pywebexec/)
64
64
  ![example](https://github.com/joknarf/pywebexec/actions/workflows/python-publish.yml/badge.svg)
65
65
  [![Licence](https://img.shields.io/badge/licence-MIT-blue.svg)](https://shields.io/)
66
- [![](https://pepy.tech/badge/pywebexec)](https://pepy.tech/project/pywebexec)
66
+ [![PyPI Downloads](https://static.pepy.tech/badge/pywebexec)](https://pepy.tech/projects/pywebexec)
67
67
  [![Python versions](https://img.shields.io/badge/python-3.6+-blue.svg)](https://shields.io/)
68
68
 
69
69
  # pywebexec
@@ -83,7 +83,7 @@ $ pywebexec
83
83
  ```
84
84
 
85
85
  * Launch commands with params/view live output/Status using browser
86
- ![pywebexec](https://github.com/user-attachments/assets/d352cc23-1552-4b79-a6ff-f02f05cf328e)
86
+ ![pywebexecnew](https://github.com/user-attachments/assets/06290b5e-284e-4e41-b32a-0f5aad074658)
87
87
 
88
88
  all commands output / statuses are available in the executables directory in subdirectory `.web_status`
89
89
 
@@ -99,8 +99,8 @@ all commands output / statuses are available in the executables directory in sub
99
99
  * Basic Auth
100
100
  * LDAP(S)
101
101
  * Can be started as a daemon (POSIX)
102
- * uses gunicorn to serve http/https
103
- * compatible Linux/MacOS
102
+ * Uses gunicorn to serve http/https
103
+ * Linux/MacOS compatible
104
104
 
105
105
  ## Customize server
106
106
  ```shell
@@ -122,7 +122,7 @@ Generated password is given if no `--pasword` option
122
122
  $ export PYWEBEXEC_LDAP_SERVER=ldap://ldap.forumsys.com:389
123
123
  $ export PYWEBEXEC_LDAP_BIND_DN="cn=read-only-admin,dc=example,dc=com"
124
124
  $ export PYWEBEXEC_LDAP_BIND_PASSWORD="password"
125
- $ export PYWEBEXEC_LDAP_GROUPS=ou=mathematicians,ou=scientists
125
+ $ export PYWEBEXEC_LDAP_GROUPS="ou=mathematicians,ou=scientists"
126
126
  $ export PYWEBEXEC_LDAP_USER_ID="uid"
127
127
  $ export PYWEBEXEC_LDAP_BASE_DN="dc=example,dc=com"
128
128
  $ pywebexec
@@ -148,12 +148,14 @@ $ pywebexec start
148
148
  $ pywebexec status
149
149
  $ pywebexec stop
150
150
  ```
151
- * log of server are stored in directory `[.config/].pywebexec/pywebexec_<listen>:<port>.log`
151
+ * log of server are stored in directory `~/[.config/].pywebexec/pywebexec_<listen>:<port>.log`
152
152
 
153
153
  ## Launch command through API
154
154
 
155
155
  ```shell
156
- $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "param":["param1", ...]}
156
+ $ curl http://myhost:8080/run_script -H 'Content-Type: application/json' -X POST -d '{ "script_name":"myscript", "params":["param1", ...]}
157
+ $ curl http://myhost:8080/command_status/<command_id>
158
+ $ curl http://myhost:8080/command_output/<command_id> -H "Accept: text/plain"
157
159
  ```
158
160
 
159
161
  ## API reference
@@ -19,7 +19,7 @@ pywebexec/static/images/copy.svg
19
19
  pywebexec/static/images/copy_ok.svg
20
20
  pywebexec/static/images/failed.svg
21
21
  pywebexec/static/images/favicon.svg
22
- pywebexec/static/images/running.svg
22
+ pywebexec/static/images/running.gif
23
23
  pywebexec/static/images/success.svg
24
24
  pywebexec/static/js/script.js
25
25
  pywebexec/templates/__init__.py
@@ -1 +0,0 @@
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>
@@ -1,197 +0,0 @@
1
- let currentCommandId = null;
2
- let outputInterval = null;
3
-
4
- document.getElementById('launchForm').addEventListener('submit', async (event) => {
5
- event.preventDefault();
6
- const commandName = document.getElementById('commandName').value;
7
- const params = document.getElementById('params').value.split(' ');
8
- const response = await fetch('/run_command', {
9
- method: 'POST',
10
- headers: {
11
- 'Content-Type': 'application/json'
12
- },
13
- body: JSON.stringify({ command: commandName, params: params })
14
- });
15
- const data = await response.json();
16
- fetchCommands();
17
- viewOutput(data.command_id);
18
- });
19
-
20
- async function fetchCommands() {
21
- const response = await fetch('/commands');
22
- const commands = await response.json();
23
- commands.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
24
- const commandsTbody = document.getElementById('commands');
25
- commandsTbody.innerHTML = '';
26
- if (!currentCommandId && commands.length) {
27
- currentCommandId = commands[0].command_id;
28
- viewOutput(currentCommandId);
29
- }
30
- commands.forEach(command => {
31
- const commandRow = document.createElement('tr');
32
- commandRow.className = `clickable-row ${command.command_id === currentCommandId ? 'currentcommand' : ''}`;
33
- commandRow.onclick = () => viewOutput(command.command_id);
34
- commandRow.innerHTML = `
35
- <td class="monospace">
36
- <span class="copy_clip" onclick="copyToClipboard('${command.command_id}', this)">${command.command_id.slice(0, 8)}</span>
37
- </td>
38
- <td><span class="status-icon status-${command.status}"></span>${command.status}</td>
39
- <td>${formatTime(command.start_time)}</td>
40
- <td>${command.status === 'running' ? formatDuration(command.start_time, new Date().toISOString()) : formatDuration(command.start_time, command.end_time)}</td>
41
- <td>${command.exit_code}</td>
42
- <td>${command.command.replace(/^\.\//, '')}</td>
43
- <td>
44
- ${command.status === 'running' ? `<button onclick="stopCommand('${command.command_id}')">Stop</button>` : ` <button onclick="relaunchCommand('${command.command_id}')">Run</button>
45
- `}
46
- </td>
47
- <td class="monospace outcol">${command.last_output_line || ''}</td>
48
- `;
49
- commandsTbody.appendChild(commandRow);
50
- });
51
- }
52
-
53
- async function fetchExecutables() {
54
- const response = await fetch('/executables');
55
- const executables = await response.json();
56
- const commandNameSelect = document.getElementById('commandName');
57
- commandNameSelect.innerHTML = '';
58
- executables.forEach(executable => {
59
- const option = document.createElement('option');
60
- option.value = executable;
61
- option.textContent = executable;
62
- commandNameSelect.appendChild(option);
63
- });
64
- }
65
-
66
- async function fetchOutput(command_id) {
67
- const outputDiv = document.getElementById('output');
68
- const response = await fetch(`/command_output/${command_id}`);
69
- const data = await response.json();
70
- if (data.error) {
71
- outputDiv.innerHTML = data.error;
72
- clearInterval(outputInterval);
73
- } else {
74
- outputDiv.innerHTML = data.output;
75
- outputDiv.scrollTop = outputDiv.scrollHeight;
76
- if (data.status != 'running') {
77
- clearInterval(outputInterval);
78
- }
79
- }
80
- }
81
-
82
- async function viewOutput(command_id) {
83
- adjustOutputHeight();
84
- currentCommandId = command_id;
85
- clearInterval(outputInterval);
86
- const response = await fetch(`/command_status/${command_id}`);
87
- const data = await response.json();
88
- if (data.status === 'running') {
89
- fetchOutput(command_id);
90
- outputInterval = setInterval(() => fetchOutput(command_id), 1000);
91
- } else {
92
- fetchOutput(command_id);
93
- }
94
- fetchCommands(); // Refresh the command list to highlight the current command
95
- }
96
-
97
- async function relaunchCommand(command_id) {
98
- const response = await fetch(`/command_status/${command_id}`);
99
- const data = await response.json();
100
- if (data.error) {
101
- alert(data.error);
102
- return;
103
- }
104
- const relaunchResponse = await fetch('/run_command', {
105
- method: 'POST',
106
- headers: {
107
- 'Content-Type': 'application/json'
108
- },
109
- body: JSON.stringify({
110
- command: data.command,
111
- params: data.params
112
- })
113
- });
114
- const relaunchData = await relaunchResponse.json();
115
- fetchCommands();
116
- viewOutput(relaunchData.command_id);
117
- }
118
-
119
- async function stopCommand(command_id) {
120
- const response = await fetch(`/stop_command/${command_id}`, {
121
- method: 'POST'
122
- });
123
- const data = await response.json();
124
- if (data.error) {
125
- alert(data.error);
126
- } else {
127
- alert(data.message);
128
- fetchCommands();
129
- }
130
- }
131
-
132
- function formatTime(time) {
133
- if (!time || time === 'N/A') return 'N/A';
134
- const date = new Date(time);
135
- return date.toISOString().slice(0, 16).replace('T', ' ');
136
- }
137
-
138
- function formatDuration(startTime, endTime) {
139
- if (!startTime || !endTime) return 'N/A';
140
- const start = new Date(startTime);
141
- const end = new Date(endTime);
142
- const duration = (end - start) / 1000;
143
- const hours = Math.floor(duration / 3600);
144
- const minutes = Math.floor((duration % 3600) / 60);
145
- const seconds = Math.floor(duration % 60);
146
- return `${hours}h ${minutes}m ${seconds}s`;
147
- }
148
-
149
- function copyToClipboard(text, element) {
150
- navigator.clipboard.writeText(text).then(() => {
151
- element.classList.add('copy_clip_ok');
152
- setTimeout(() => {
153
- element.classList.remove('copy_clip_ok');
154
- }, 2000);
155
- });
156
- }
157
-
158
- function adjustOutputHeight() {
159
- const outputDiv = document.getElementById('output');
160
- const windowHeight = window.innerHeight;
161
- const outputTop = outputDiv.getBoundingClientRect().top;
162
- const maxHeight = windowHeight - outputTop - 30; // 20px for padding/margin
163
- outputDiv.style.maxHeight = `${maxHeight}px`;
164
- }
165
-
166
- function initResizer() {
167
- const resizer = document.getElementById('resizer');
168
- const tableContainer = document.getElementById('tableContainer');
169
- let startY, startHeight;
170
-
171
- resizer.addEventListener('mousedown', (e) => {
172
- startY = e.clientY;
173
- startHeight = parseInt(document.defaultView.getComputedStyle(tableContainer).height, 10);
174
- document.documentElement.addEventListener('mousemove', doDrag, false);
175
- document.documentElement.addEventListener('mouseup', stopDrag, false);
176
- });
177
-
178
- function doDrag(e) {
179
- tableContainer.style.height = `${startHeight + e.clientY - startY}px`;
180
- adjustOutputHeight();
181
- }
182
-
183
- function stopDrag() {
184
- document.documentElement.removeEventListener('mousemove', doDrag, false);
185
- document.documentElement.removeEventListener('mouseup', stopDrag, false);
186
- }
187
- }
188
-
189
- window.addEventListener('resize', adjustOutputHeight);
190
- window.addEventListener('load', () => {
191
- adjustOutputHeight();
192
- initResizer();
193
- });
194
-
195
- fetchExecutables();
196
- fetchCommands();
197
- setInterval(fetchCommands, 5000);
File without changes
File without changes
File without changes