meerschaum 2.2.6__py3-none-any.whl → 2.3.0.dev1__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.
Files changed (61) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/__main__.py +10 -5
  3. meerschaum/_internal/arguments/_parser.py +44 -15
  4. meerschaum/_internal/entry.py +35 -14
  5. meerschaum/_internal/shell/Shell.py +155 -53
  6. meerschaum/_internal/shell/updates.py +175 -0
  7. meerschaum/actions/api.py +12 -12
  8. meerschaum/actions/attach.py +95 -0
  9. meerschaum/actions/delete.py +35 -26
  10. meerschaum/actions/register.py +19 -5
  11. meerschaum/actions/show.py +119 -148
  12. meerschaum/actions/start.py +85 -75
  13. meerschaum/actions/stop.py +68 -39
  14. meerschaum/actions/sync.py +3 -3
  15. meerschaum/actions/upgrade.py +28 -36
  16. meerschaum/api/_events.py +18 -1
  17. meerschaum/api/_oauth2.py +2 -0
  18. meerschaum/api/_websockets.py +2 -2
  19. meerschaum/api/dash/jobs.py +5 -2
  20. meerschaum/api/routes/__init__.py +1 -0
  21. meerschaum/api/routes/_actions.py +122 -44
  22. meerschaum/api/routes/_jobs.py +340 -0
  23. meerschaum/api/routes/_pipes.py +25 -25
  24. meerschaum/config/_default.py +1 -0
  25. meerschaum/config/_formatting.py +1 -0
  26. meerschaum/config/_paths.py +5 -0
  27. meerschaum/config/_shell.py +84 -67
  28. meerschaum/config/_version.py +1 -1
  29. meerschaum/config/static/__init__.py +9 -0
  30. meerschaum/connectors/__init__.py +9 -11
  31. meerschaum/connectors/api/APIConnector.py +18 -1
  32. meerschaum/connectors/api/_actions.py +60 -71
  33. meerschaum/connectors/api/_jobs.py +260 -0
  34. meerschaum/connectors/api/_misc.py +1 -1
  35. meerschaum/connectors/api/_request.py +13 -9
  36. meerschaum/connectors/parse.py +23 -7
  37. meerschaum/core/Pipe/_sync.py +3 -0
  38. meerschaum/plugins/__init__.py +89 -5
  39. meerschaum/utils/daemon/Daemon.py +333 -149
  40. meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
  41. meerschaum/utils/daemon/RotatingFile.py +18 -7
  42. meerschaum/utils/daemon/StdinFile.py +110 -0
  43. meerschaum/utils/daemon/__init__.py +40 -27
  44. meerschaum/utils/formatting/__init__.py +83 -37
  45. meerschaum/utils/formatting/_jobs.py +118 -51
  46. meerschaum/utils/formatting/_shell.py +6 -0
  47. meerschaum/utils/jobs/_Job.py +684 -0
  48. meerschaum/utils/jobs/__init__.py +245 -0
  49. meerschaum/utils/misc.py +18 -17
  50. meerschaum/utils/packages/__init__.py +21 -15
  51. meerschaum/utils/packages/_packages.py +2 -2
  52. meerschaum/utils/prompt.py +20 -7
  53. meerschaum/utils/schedule.py +21 -15
  54. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/METADATA +9 -9
  55. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/RECORD +61 -54
  56. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/WHEEL +1 -1
  57. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/LICENSE +0 -0
  58. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/NOTICE +0 -0
  59. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/entry_points.txt +0 -0
  60. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/top_level.txt +0 -0
  61. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/zip-safe +0 -0
@@ -50,11 +50,13 @@ def _complete_stop(
50
50
 
51
51
  def _stop_jobs(
52
52
  action: Optional[List[str]] = None,
53
+ executor_keys: Optional[str] = None,
53
54
  timeout_seconds: Optional[int] = None,
54
55
  noask: bool = False,
55
56
  force: bool = False,
56
57
  yes: bool = False,
57
58
  nopretty: bool = False,
59
+ debug: bool = False,
58
60
  **kw
59
61
  ) -> SuccessTuple:
60
62
  """
@@ -62,66 +64,93 @@ def _stop_jobs(
62
64
 
63
65
  To see running processes, run `show jobs`.
64
66
  """
67
+ from meerschaum.utils.jobs import (
68
+ get_filtered_jobs,
69
+ get_running_jobs,
70
+ get_paused_jobs,
71
+ get_stopped_jobs,
72
+ get_restart_jobs,
73
+ )
65
74
  from meerschaum.utils.formatting._jobs import pprint_jobs
66
75
  from meerschaum.utils.daemon import (
67
76
  get_filtered_daemons, get_running_daemons, get_stopped_daemons, get_paused_daemons,
68
77
  )
69
78
  from meerschaum.utils.warnings import warn
70
79
  from meerschaum.utils.prompt import yes_no
71
- daemons = get_filtered_daemons(action, warn=(not nopretty))
72
- _running_daemons = get_running_daemons(daemons)
73
- _paused_daemons = get_paused_daemons(daemons)
74
- _stopped_daemons = get_stopped_daemons(daemons)
75
- if action and _stopped_daemons and not nopretty:
80
+
81
+ jobs = get_filtered_jobs(executor_keys, action, warn=(not nopretty))
82
+ running_jobs = get_running_jobs(executor_keys, jobs)
83
+ paused_jobs = get_paused_jobs(executor_keys, jobs)
84
+ restart_jobs = get_restart_jobs(executor_keys, jobs)
85
+ stopped_jobs = {
86
+ name: job
87
+ for name, job in get_stopped_jobs(executor_keys, jobs).items()
88
+ if name not in restart_jobs
89
+ }
90
+
91
+ jobs_to_stop = {
92
+ **running_jobs,
93
+ **paused_jobs,
94
+ **restart_jobs,
95
+ }
96
+
97
+ if action and stopped_jobs and not nopretty:
76
98
  warn(
77
- f"Skipping stopped job" + ("s" if len(_stopped_daemons) > 1 else '') + " '" +
78
- ("', '".join(d.daemon_id for d in _stopped_daemons)) + "'.",
79
- stack = False
99
+ "Skipping stopped job"
100
+ + ("s" if len(stopped_jobs) != 1 else '')
101
+ + " '"
102
+ + ("', '".join(name for name in stopped_jobs)) + "'.",
103
+ stack=False,
80
104
  )
81
105
 
82
- daemons_to_stop = _running_daemons + _paused_daemons
83
- if not daemons_to_stop:
84
- return False, "No running or paused jobs to stop."
106
+ if not jobs_to_stop:
107
+ return False, "No running, paused or restarting jobs to stop."
85
108
 
86
109
  if not action:
87
110
  if not force:
88
- pprint_jobs(daemons_to_stop)
111
+ pprint_jobs(jobs_to_stop)
89
112
  if not yes_no(
90
113
  "Stop the above jobs?",
91
114
  noask=noask, yes=yes, default='n'
92
115
  ):
93
116
  return False, "No jobs were stopped."
94
117
 
95
- _quit_daemons, _kill_daemons = [], []
96
- for d in daemons_to_stop:
97
- quit_success, quit_msg = d.quit(timeout=timeout_seconds)
98
- if quit_success:
99
- _quit_daemons.append(d)
100
- continue
101
- else:
102
- warn(
103
- f"Failed to gracefully quit job '{d.daemon_id}', attempting to terminate:\n "
104
- + f"{quit_msg}",
105
- stack = False,
106
- )
107
-
108
- kill_success, kill_msg = d.kill(timeout=timeout_seconds)
109
- if kill_success:
110
- _kill_daemons.append(d)
111
- continue
112
- if not nopretty:
113
- warn(f"Failed to kill job '{d.daemon_id}' (PID {d.pid}):\n{kill_msg}", stack=False)
118
+ job_success_tuples = {}
119
+ for name, job in jobs_to_stop.items():
120
+ stop_success, stop_msg = job.stop(
121
+ timeout_seconds=timeout_seconds,
122
+ debug=debug,
123
+ )
124
+ job_success_tuples[name] = (stop_success, stop_msg)
114
125
 
126
+ num_success = sum(
127
+ (
128
+ 1
129
+ for name, (stop_success, stop_msg) in job_success_tuples.items()
130
+ if stop_success
131
+ )
132
+ )
133
+ num_fail = sum(
134
+ (
135
+ 1
136
+ for name, (stop_success, stop_msg) in job_success_tuples.items()
137
+ if not stop_success
138
+ )
139
+ )
140
+ success = num_success > 0
115
141
  msg = (
116
- (("Stopped job" + ("s" if len(_quit_daemons) != 1 else '') +
117
- " '" + "', '".join([d.daemon_id for d in _quit_daemons]) + "'.")
118
- if _quit_daemons else '')
119
- + (("\n" if _quit_daemons else "")
120
- + ("Killed job" + ("s" if len(_kill_daemons) != 1 else '') +
121
- " '" + "', '".join([d.daemon_id for d in _kill_daemons]) + "'.")
122
- if _kill_daemons else '')
142
+ f"Stopped {num_success} job"
143
+ + ('s' if num_success != 1 else '')
144
+ + '.'
123
145
  )
124
- return (len(_quit_daemons + _kill_daemons) > 0), msg
146
+ if num_fail > 0:
147
+ msg += (
148
+ f"\nFailed to stop {num_fail} job"
149
+ + ('s' if num_fail != 1 else '')
150
+ + '.'
151
+ )
152
+
153
+ return success, msg
125
154
 
126
155
 
127
156
  ### NOTE: This must be the final statement of the module.
@@ -14,9 +14,9 @@ import meerschaum as mrsm
14
14
  from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Tuple, Union
15
15
 
16
16
  def sync(
17
- action: Optional[List[str]] = None,
18
- **kw: Any
19
- ) -> SuccessTuple:
17
+ action: Optional[List[str]] = None,
18
+ **kw: Any
19
+ ) -> SuccessTuple:
20
20
  """
21
21
  Fetch and sync data for pipes.
22
22
 
@@ -10,9 +10,9 @@ from __future__ import annotations
10
10
  from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Union
11
11
 
12
12
  def upgrade(
13
- action: Optional[List[str]] = None,
14
- **kw: Any
15
- ) -> SuccessTuple:
13
+ action: Optional[List[str]] = None,
14
+ **kw: Any
15
+ ) -> SuccessTuple:
16
16
  """
17
17
  Upgrade Meerschaum, plugins, or packages.
18
18
 
@@ -34,13 +34,13 @@ def upgrade(
34
34
 
35
35
 
36
36
  def _upgrade_meerschaum(
37
- action: Optional[List[str]] = None,
38
- yes: bool = False,
39
- force: bool = False,
40
- noask: bool = False,
41
- debug: bool = False,
42
- **kw: Any
43
- ) -> SuccessTuple:
37
+ action: Optional[List[str]] = None,
38
+ yes: bool = False,
39
+ force: bool = False,
40
+ noask: bool = False,
41
+ debug: bool = False,
42
+ **kw: Any
43
+ ) -> SuccessTuple:
44
44
  """
45
45
  Upgrade the current Meerschaum instance.
46
46
  Optionally specify dependency groups.
@@ -109,26 +109,21 @@ class NoVenv:
109
109
  pass
110
110
 
111
111
  def _upgrade_packages(
112
- action: Optional[List[str]] = None,
113
- venv: Union[str, None, NoVenv] = NoVenv,
114
- yes: bool = False,
115
- force: bool = False,
116
- noask: bool = False,
117
- debug: bool = False,
118
- **kw: Any
119
- ) -> SuccessTuple:
112
+ action: Optional[List[str]] = None,
113
+ venv: Union[str, None, NoVenv] = NoVenv,
114
+ yes: bool = False,
115
+ force: bool = False,
116
+ noask: bool = False,
117
+ debug: bool = False,
118
+ **kw: Any
119
+ ) -> SuccessTuple:
120
120
  """
121
121
  Upgrade and install dependencies.
122
122
  If provided, upgrade only a dependency group, otherwise default to `full`.
123
123
 
124
124
  Examples:
125
- ```
126
125
  upgrade packages
127
- ```
128
-
129
- ```
130
- upgrade packages docs
131
- ```
126
+ upgrade packages full
132
127
  """
133
128
  from meerschaum.utils.packages import packages, pip_install, get_prerelease_dependencies
134
129
  from meerschaum.utils.warnings import info, warn
@@ -178,14 +173,15 @@ def _upgrade_packages(
178
173
  )
179
174
  return success, msg
180
175
 
176
+
181
177
  def _upgrade_plugins(
182
- action: Optional[List[str]] = None,
183
- yes: bool = False,
184
- force: bool = False,
185
- noask: bool = False,
186
- debug: bool = False,
187
- **kw: Any
188
- ) -> SuccessTuple:
178
+ action: Optional[List[str]] = None,
179
+ yes: bool = False,
180
+ force: bool = False,
181
+ noask: bool = False,
182
+ debug: bool = False,
183
+ **kw: Any
184
+ ) -> SuccessTuple:
189
185
  """
190
186
  Upgrade all installed plugins to the latest versions.
191
187
  If no plugins are specified, attempt to upgrade all,
@@ -193,12 +189,8 @@ def _upgrade_plugins(
193
189
 
194
190
  Examples:
195
191
 
196
- ```
197
192
  upgrade plugins
198
- ```
199
- ```
200
- upgrade plugins testing
201
- ```
193
+ upgrade plugins noaa
202
194
  """
203
195
  from meerschaum.actions import actions
204
196
  from meerschaum.plugins import get_plugins_names
meerschaum/api/_events.py CHANGED
@@ -19,14 +19,20 @@ from meerschaum.utils.debug import dprint
19
19
  from meerschaum.connectors.poll import retry_connect
20
20
  from meerschaum.utils.warnings import warn
21
21
  from meerschaum._internal.term.tools import is_webterm_running
22
+ from meerschaum.utils.jobs import start_check_jobs_thread, stop_check_jobs_thread
23
+
24
+ _check_jobs_thread = None
22
25
 
23
26
  @app.on_event("startup")
24
27
  async def startup():
25
- conn = get_api_connector()
28
+ """
29
+ Connect to the instance database and begin monitoring jobs.
30
+ """
26
31
  try:
27
32
  if not no_dash:
28
33
  from meerschaum.api.dash.webterm import start_webterm
29
34
  start_webterm()
35
+
30
36
  connected = retry_connect(
31
37
  get_api_connector(),
32
38
  workers = get_uvicorn_config().get('workers', None),
@@ -36,18 +42,29 @@ async def startup():
36
42
  import traceback
37
43
  traceback.print_exc()
38
44
  connected = False
45
+
39
46
  if not connected:
40
47
  await shutdown()
41
48
  os._exit(1)
42
49
 
50
+ start_check_jobs_thread()
51
+
43
52
 
44
53
  @app.on_event("shutdown")
45
54
  async def shutdown():
55
+ """
56
+ Close the database connection and stop monitoring jobs.
57
+ """
46
58
  if debug:
47
59
  dprint("Closing connection...")
48
60
  if get_api_connector().type == 'sql':
49
61
  get_api_connector().engine.dispose()
50
62
 
63
+ stop_check_jobs_thread()
64
+ from meerschaum.api.routes._actions import _temp_jobs
65
+ for name, job in _temp_jobs.items():
66
+ job.delete()
67
+
51
68
  ### Terminate any running jobs left over.
52
69
  if 'meerschaum.api.dash' in sys.modules:
53
70
  from meerschaum.api.dash.actions import running_jobs, stop_action
meerschaum/api/_oauth2.py CHANGED
@@ -30,6 +30,7 @@ class CustomOAuth2PasswordRequestForm:
30
30
  self.client_id = client_id
31
31
  self.client_secret = client_secret
32
32
 
33
+
33
34
  LoginManager = fastapi_login.LoginManager
34
35
  def generate_secret_key() -> str:
35
36
  """
@@ -46,5 +47,6 @@ def generate_secret_key() -> str:
46
47
 
47
48
  return secret_key.encode('utf-8')
48
49
 
50
+
49
51
  SECRET = generate_secret_key()
50
52
  manager = LoginManager(SECRET, token_url=endpoints['login'])
@@ -23,8 +23,8 @@ sessions = {}
23
23
  @app.websocket('/dash/ws')
24
24
  @app.websocket(_websocket_endpoint)
25
25
  async def websocket_endpoint(
26
- websocket: fastapi.WebSocket,
27
- ):
26
+ websocket: fastapi.WebSocket,
27
+ ):
28
28
  """
29
29
  Communicate with the Web Interface over a websocket.
30
30
  """
@@ -16,6 +16,7 @@ from meerschaum.api import CHECK_UPDATE
16
16
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
17
17
  html, dcc = import_html(), import_dcc()
18
18
  dateutil_parser = attempt_import('dateutil.parser', check_update=CHECK_UPDATE)
19
+ from meerschaum.utils.jobs import get_jobs, Job
19
20
  from meerschaum.utils.daemon import (
20
21
  get_daemons,
21
22
  get_running_daemons,
@@ -38,13 +39,15 @@ def get_jobs_cards(state: WebState):
38
39
  Build cards and alerts lists for jobs.
39
40
  """
40
41
  daemons = get_daemons()
42
+ jobs = get_jobs()
41
43
  session_id = state['session-store.data'].get('session-id', None)
42
44
  is_authenticated = is_session_authenticated(session_id)
43
45
 
44
46
  alert = alert_from_success_tuple(daemons)
45
47
  cards = []
46
48
 
47
- for d in daemons:
49
+ for name, job in jobs.items():
50
+ d = job.daemon
48
51
  footer_children = html.Div(
49
52
  build_process_timestamps_children(d),
50
53
  id = {'type': 'process-timestamps-div', 'index': d.daemon_id},
@@ -80,7 +83,7 @@ def get_jobs_cards(state: WebState):
80
83
  ]
81
84
 
82
85
  body_children = [
83
- html.H4(html.B(d.daemon_id), className="card-title"),
86
+ html.H4(html.B(name), className="card-title"),
84
87
  html.Div(
85
88
  html.P(
86
89
  d.label,
@@ -8,6 +8,7 @@ Import all routes from other modules in package
8
8
 
9
9
  from meerschaum.api.routes._login import *
10
10
  from meerschaum.api.routes._actions import *
11
+ from meerschaum.api.routes._jobs import *
11
12
  from meerschaum.api.routes._connectors import *
12
13
  from meerschaum.api.routes._index import *
13
14
  from meerschaum.api.routes._misc import *
@@ -7,65 +7,143 @@ Execute Meerschaum Actions via the API
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import SuccessTuple, Union
10
+ import asyncio
11
+ import traceback
12
+ import shlex
13
+ from functools import partial
14
+ from datetime import datetime, timezone
11
15
 
16
+ from fastapi import WebSocket, WebSocketDisconnect
17
+
18
+ from meerschaum.utils.misc import generate_password
19
+ from meerschaum.utils.jobs import Job
20
+ from meerschaum.utils.warnings import warn
21
+ from meerschaum.utils.typing import SuccessTuple, Union, List, Dict
12
22
  from meerschaum.api import (
13
23
  fastapi, app, endpoints, get_api_connector, debug, manager, private, no_auth
14
24
  )
15
25
  from meerschaum.actions import actions
16
26
  import meerschaum.core
27
+ from meerschaum.config import get_config
28
+ from meerschaum._internal.arguments._parse_arguments import parse_dict_to_sysargs, parse_arguments
29
+
17
30
  actions_endpoint = endpoints['actions']
18
31
 
32
+ def is_user_allowed_to_execute(user) -> SuccessTuple:
33
+ if user is None:
34
+ return False, "Could not load user."
35
+
36
+ if user.type == 'admin':
37
+ return True, "Success"
38
+
39
+ allow_non_admin = get_config(
40
+ 'system', 'api', 'permissions', 'actions', 'non_admin', patch=True
41
+ )
42
+ if not allow_non_admin:
43
+ return False, (
44
+ "The administrator for this server has not allowed users to perform actions.\n\n"
45
+ + "Please contact the system administrator, or if you are running this server, "
46
+ + "open the configuration file with `edit config system` "
47
+ + "and search for 'permissions'. "
48
+ + "\nUnder the keys 'api:permissions:actions', "
49
+ + "you can allow non-admin users to perform actions."
50
+ )
51
+
52
+ return True, "Success"
53
+
54
+
19
55
  @app.get(actions_endpoint, tags=['Actions'])
20
56
  def get_actions(
21
- curr_user = (
22
- fastapi.Depends(manager) if private else None
23
- ),
24
- ) -> list:
57
+ curr_user = (
58
+ fastapi.Depends(manager) if private else None
59
+ ),
60
+ ) -> List[str]:
61
+ """
62
+ Return a list of the available actions.
63
+ """
25
64
  return list(actions)
26
65
 
27
- @app.post(actions_endpoint + "/{action}", tags=['Actions'])
28
- def do_action(
29
- action: str,
30
- keywords: dict = fastapi.Body(...),
31
- curr_user = (
32
- fastapi.Depends(manager) if not no_auth else None
33
- ),
34
- ) -> SuccessTuple:
66
+
67
+ async def notify_client(client, content: str):
68
+ """
69
+ Send a line of text to a client.
35
70
  """
36
- Perform a Meerschaum action (if permissions allow it).
71
+ try:
72
+ await client.send_text(content)
73
+ except WebSocketDisconnect:
74
+ pass
37
75
 
38
- Parameters
39
- ----------
40
- action: str :
41
- The action to perform.
42
-
43
- keywords: dict :
44
- The keywords dictionary to pass to the action.
76
+ _temp_jobs = {}
77
+ @app.websocket(actions_endpoint + '/ws')
78
+ async def do_action_websocket(websocket: WebSocket):
79
+ """
80
+ Execute an action and stream the output to the client.
81
+ """
82
+ await websocket.accept()
45
83
 
46
- Returns
47
- -------
48
- A `SuccessTuple` of success, message.
84
+ stop_event = asyncio.Event()
49
85
 
50
- """
51
- if curr_user is not None and curr_user.type != 'admin':
52
- from meerschaum.config import get_config
53
- allow_non_admin = get_config(
54
- 'system', 'api', 'permissions', 'actions', 'non_admin', patch=True
55
- )
56
- if not allow_non_admin:
57
- return False, (
58
- "The administrator for this server has not allowed users to perform actions.\n\n"
59
- + "Please contact the system administrator, or if you are running this server, "
60
- + "open the configuration file with `edit config system` "
61
- + "and search for 'permissions'. "
62
- + "\nUnder the keys 'api:permissions:actions', "
63
- + "you can allow non-admin users to perform actions."
86
+ async def monitor_logs(job):
87
+ success, msg = job.start()
88
+ await job.monitor_logs_async(
89
+ partial(notify_client, websocket),
90
+ stop_event=stop_event,
91
+ stop_on_exit=True,
64
92
  )
65
93
 
66
- if action not in actions:
67
- return False, f"Invalid action '{action}'."
68
- keywords['mrsm_instance'] = keywords.get('mrsm_instance', str(get_api_connector()))
69
- _debug = keywords.get('debug', debug)
70
- keywords.pop('debug', None)
71
- return actions[action](debug=_debug, **keywords)
94
+ job = None
95
+ try:
96
+ token = await websocket.receive_text()
97
+ user = await manager.get_current_user(token) if not no_auth else None
98
+ if user is None and not no_auth:
99
+ raise fastapi.HTTPException(
100
+ status_code=401,
101
+ detail="Invalid credentials.",
102
+ )
103
+
104
+ auth_success, auth_msg = is_user_allowed_to_execute(user)
105
+ auth_payload = {
106
+ 'is_authenticated': auth_success,
107
+ 'timestamp': datetime.now(timezone.utc).isoformat(),
108
+ }
109
+ await websocket.send_json(auth_payload)
110
+ if not auth_success:
111
+ await websocket.close()
112
+
113
+ sysargs = await websocket.receive_json()
114
+ kwargs = parse_arguments(sysargs)
115
+ _ = kwargs.pop('executor_keys', None)
116
+ _ = kwargs.pop('shell', None)
117
+ sysargs = parse_dict_to_sysargs(kwargs)
118
+
119
+ job_name = '.' + generate_password(12)
120
+ job = Job(
121
+ job_name,
122
+ sysargs,
123
+ _properties={
124
+ 'logs': {
125
+ 'write_timestamps': False,
126
+ },
127
+ },
128
+ )
129
+ _temp_jobs[job_name] = job
130
+ monitor_task = asyncio.create_task(monitor_logs(job))
131
+ await monitor_task
132
+ try:
133
+ await websocket.close()
134
+ except RuntimeError:
135
+ pass
136
+ except fastapi.HTTPException:
137
+ await websocket.send_text("Invalid credentials.")
138
+ await websocket.close()
139
+ except WebSocketDisconnect:
140
+ pass
141
+ except asyncio.CancelledError:
142
+ pass
143
+ except Exception:
144
+ warn(f"Error in logs websocket:\n{traceback.format_exc()}")
145
+ finally:
146
+ if job is not None:
147
+ job.delete()
148
+ _ = _temp_jobs.pop(job_name, None)
149
+ stop_event.set()