OpenOrchestrator 1.1.0__tar.gz → 1.2.0__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 (46) hide show
  1. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/db_util.py +1 -3
  2. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/application.py +18 -1
  3. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/queue_tab.py +1 -1
  4. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/trigger_tab.py +1 -1
  5. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/application.py +1 -1
  6. OpenOrchestrator-1.2.0/OpenOrchestrator/scheduler/run_tab.py +175 -0
  7. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/runner.py +28 -18
  8. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0/OpenOrchestrator.egg-info}/PKG-INFO +1 -1
  9. {OpenOrchestrator-1.1.0/OpenOrchestrator.egg-info → OpenOrchestrator-1.2.0}/PKG-INFO +1 -1
  10. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/pyproject.toml +1 -1
  11. OpenOrchestrator-1.1.0/OpenOrchestrator/scheduler/run_tab.py +0 -177
  12. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/LICENSE +0 -0
  13. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/__init__.py +0 -0
  14. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/__main__.py +0 -0
  15. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/__init__.py +0 -0
  16. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/connection_frame.py +0 -0
  17. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/crypto_util.py +0 -0
  18. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/common/datetime_util.py +0 -0
  19. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/__init__.py +0 -0
  20. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/constants.py +0 -0
  21. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/logs.py +0 -0
  22. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/queues.py +0 -0
  23. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/database/triggers.py +0 -0
  24. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/__init__.py +0 -0
  25. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/datetime_input.py +0 -0
  26. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/__init__.py +0 -0
  27. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/constant_popup.py +0 -0
  28. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/credential_popup.py +0 -0
  29. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/generic_popups.py +0 -0
  30. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/popups/trigger_popup.py +0 -0
  31. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/constants_tab.py +0 -0
  32. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/logging_tab.py +0 -0
  33. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator/tabs/settings_tab.py +0 -0
  34. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator_connection/__init__.py +0 -0
  35. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/orchestrator_connection/connection.py +0 -0
  36. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/__init__.py +0 -0
  37. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/connection_frame.py +0 -0
  38. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator/scheduler/settings_tab.py +0 -0
  39. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/SOURCES.txt +0 -0
  40. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/dependency_links.txt +0 -0
  41. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/requires.txt +0 -0
  42. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/OpenOrchestrator.egg-info/top_level.txt +0 -0
  43. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/README.md +0 -0
  44. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/setup.cfg +0 -0
  45. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/tests/test_db_util.py +0 -0
  46. {OpenOrchestrator-1.1.0 → OpenOrchestrator-1.2.0}/tests/test_orchestrator_connection.py +0 -0
@@ -601,9 +601,7 @@ def begin_scheduled_trigger(trigger_id: str) -> bool:
601
601
 
602
602
  trigger.process_status = TriggerStatus.RUNNING
603
603
  trigger.last_run = datetime.now()
604
-
605
- next_run = croniter(trigger.cron_expr, trigger.next_run).get_next(datetime)
606
- trigger.next_run = next_run
604
+ trigger.next_run = croniter(trigger.cron_expr, datetime.now()).get_next(datetime)
607
605
 
608
606
  session.commit()
609
607
  return True
@@ -1,6 +1,8 @@
1
1
  """This module is the entry point for the Orchestrator app. It contains a single class
2
2
  that when created starts the application."""
3
3
 
4
+ import socket
5
+
4
6
  from nicegui import ui, app
5
7
 
6
8
  from OpenOrchestrator.orchestrator.tabs.trigger_tab import TriggerTab
@@ -39,7 +41,7 @@ class Application():
39
41
  app.on_connect(self.update_loop)
40
42
  app.on_disconnect(app.shutdown)
41
43
  app.on_exception(lambda exc: ui.notify(exc, type='negative'))
42
- ui.run(title="Orchestrator", favicon='🤖', native=False, port=23406, reload=False)
44
+ ui.run(title="Orchestrator", favicon='🤖', native=False, port=get_free_port(), reload=False)
43
45
 
44
46
  def update_tab(self):
45
47
  """Update the date in the currently selected tab."""
@@ -73,5 +75,20 @@ class Application():
73
75
  ''')
74
76
 
75
77
 
78
+ def get_free_port():
79
+ """Get a free port by creating a new socket and bind it
80
+ on port 0 allowing the os to select the port.
81
+ https://docs.python.org/3/library/socket.html#socket.create_connection
82
+
83
+ Returns:
84
+ A port number that should be free to use.
85
+ """
86
+ with socket.socket() as sock:
87
+ sock.bind(("", 0))
88
+ port = sock.getsockname()[1]
89
+
90
+ return port
91
+
92
+
76
93
  if __name__ in {'__main__', '__mp_main__'}:
77
94
  Application()
@@ -33,7 +33,7 @@ class QueueTab():
33
33
  """The 'Queues' tab object. It contains tables and buttons for dealing with queues."""
34
34
  def __init__(self, tab_name: str) -> None:
35
35
  with ui.tab_panel(tab_name):
36
- self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination=10).classes("w-full")
36
+ self.queue_table = ui.table(title="Queues", columns=QUEUE_COLUMNS, rows=[], row_key='Queue Name', pagination=50).classes("w-full")
37
37
  self.queue_table.on("rowClick", self._row_click)
38
38
 
39
39
  def update(self):
@@ -30,7 +30,7 @@ class TriggerTab():
30
30
  ui.button("New Scheduled Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.SCHEDULED))
31
31
  ui.button("New Queue Trigger", icon="add", on_click=lambda e: TriggerPopup(self, TriggerType.QUEUE))
32
32
 
33
- self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination=10, row_key='ID').classes("w-full")
33
+ self.trigger_table = ui.table(COLUMNS, [], title="Triggers", pagination=50, row_key='ID').classes("w-full")
34
34
  self.trigger_table.on('rowClick', self._row_click)
35
35
 
36
36
  self.add_column_colors()
@@ -26,7 +26,7 @@ class Application(tkinter.Tk):
26
26
  notebook = ttk.Notebook(self)
27
27
  notebook.pack(expand=True, fill='both')
28
28
 
29
- run_tab_ = run_tab.create_tab(notebook, self)
29
+ run_tab_ = run_tab.RunTab(notebook, self)
30
30
  settings_tab_ = settings_tab.create_tab(notebook)
31
31
 
32
32
  notebook.add(run_tab_, text='Run')
@@ -0,0 +1,175 @@
1
+ """This module is responsible for the layout and functionality of the run tab
2
+ in Scheduler."""
3
+
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ import tkinter
8
+ from tkinter import ttk
9
+ import sys
10
+
11
+ from sqlalchemy import exc as alc_exc
12
+
13
+ from OpenOrchestrator.common import crypto_util
14
+ from OpenOrchestrator.database import db_util
15
+ from OpenOrchestrator.scheduler import runner
16
+
17
+ if TYPE_CHECKING:
18
+ from OpenOrchestrator.scheduler.application import Application
19
+
20
+
21
+ # pylint: disable-next=too-many-ancestors
22
+ class RunTab(ttk.Frame):
23
+ """A ttk.frame object containing the functionality of the run tab in Scheduler."""
24
+ def __init__(self, parent: ttk.Notebook, app: Application):
25
+ super().__init__(parent)
26
+ self.pack(fill='both', expand=True)
27
+
28
+ self.app = app
29
+
30
+ s = ttk.Style()
31
+ s.configure('my.TButton', font=('Helvetica Bold', 24))
32
+ self.button = ttk.Button(self, text="Run", command=self.button_click, style='my.TButton')
33
+ self.button.pack()
34
+
35
+ # Text area
36
+ text_frame = tkinter.Frame(self)
37
+ text_frame.pack()
38
+
39
+ self.text_area = tkinter.Text(text_frame, state='disabled', wrap='none')
40
+
41
+ # Redirect stdout to the text area instead of console
42
+ sys.stdout.write = self.print_text
43
+
44
+ # Add scroll bars to text area
45
+ text_yscroll = ttk.Scrollbar(text_frame, orient='vertical', command=self.text_area.yview)
46
+ text_yscroll.pack(side='right', fill='y')
47
+ self.text_area.configure(yscrollcommand=text_yscroll.set)
48
+
49
+ text_xscroll = ttk.Scrollbar(text_frame, orient='horizontal', command=self.text_area.xview)
50
+ text_xscroll.pack(side='bottom', fill='x')
51
+ self.text_area.configure(xscrollcommand=text_xscroll.set)
52
+
53
+ self.text_area.pack()
54
+
55
+ def button_click(self):
56
+ """Callback for when the run/pause button is clicked."""
57
+ if self.app.running:
58
+ self.pause()
59
+ else:
60
+ self.run()
61
+
62
+ def pause(self):
63
+ """Stops the Scheduler and sets the app's status to 'paused'."""
64
+ self.button.configure(text="Run")
65
+ print('Paused... Please wait for all processes to stop before closing the application\n')
66
+ self.app.running = False
67
+
68
+ def run(self):
69
+ """Starts the Scheduler and sets the app's status to 'running'."""
70
+ if db_util.get_conn_string() is None:
71
+ print("Can't start without a valid connection string. Go to the settings tab to configure the connection string")
72
+ return
73
+ if crypto_util.get_key() is None:
74
+ print("Can't start without a valid encryption key. Go to the settings tab to configure the encryption key")
75
+ return
76
+
77
+ self.button.configure(text="Pause")
78
+ print('Running...\n')
79
+ self.app.running = True
80
+
81
+ # Only start a new loop if it's not already running
82
+ if self.app.tk.call('after', 'info') == '':
83
+ self.app.after(0, loop, self.app)
84
+
85
+ def print_text(self, text: str) -> None:
86
+ """Appends text to the text area.
87
+ Is used to replace the functionality of sys.stdout.write (print).
88
+
89
+ Args:
90
+ string: The string to append.
91
+ """
92
+ # Insert text at the end
93
+ self.text_area.configure(state='normal')
94
+ self.text_area.insert('end', text)
95
+
96
+ # If the number of lines are above 1000 delete 10 lines from the top
97
+ num_lines = int(self.text_area.index('end').split('.', maxsplit=1)[0])
98
+ if num_lines > 1000:
99
+ self.text_area.delete("1.0", "10.0")
100
+
101
+ # Scroll to end
102
+ self.text_area.see('end')
103
+ self.text_area.configure(state='disabled')
104
+
105
+
106
+ def loop(app: Application) -> None:
107
+ """The main loop function of the Scheduler.
108
+ Checks heartbeats, check triggers, and schedules the next loop.
109
+
110
+ Args:
111
+ app: The Scheduler Application object.
112
+ """
113
+ try:
114
+ check_heartbeats(app)
115
+
116
+ if app.running:
117
+ check_triggers(app)
118
+
119
+ except alc_exc.OperationalError:
120
+ print("Couldn't connect to database.")
121
+
122
+ if len(app.running_jobs) == 0:
123
+ print("Doing cleanup...")
124
+ runner.clear_repo_folder()
125
+
126
+ # Schedule next loop
127
+ if app.running or len(app.running_jobs) > 0:
128
+ print('Waiting 6 seconds...\n')
129
+ app.after(6_000, loop, app)
130
+ else:
131
+ print("Scheduler is paused and no more processes are running.")
132
+
133
+
134
+ def check_heartbeats(app: Application) -> None:
135
+ """Check if any running jobs are still running, failed or done.
136
+
137
+ Args:
138
+ app: The Scheduler Application object.
139
+ """
140
+ print('Checking heartbeats...')
141
+ for job in app.running_jobs:
142
+ if job.process.poll() is not None:
143
+ if job.process.returncode == 0:
144
+ print(f"Process '{job.trigger.process_name}' is done")
145
+ runner.end_job(job)
146
+ else:
147
+ print(f"Process '{job.trigger.process_name}' failed. Check process log for more info.")
148
+ runner.fail_job(job)
149
+
150
+ app.running_jobs.remove(job)
151
+ else:
152
+ print(f"Process '{job.trigger.process_name}' is still running")
153
+
154
+
155
+ def check_triggers(app: Application) -> None:
156
+ """Checks any process is blocking
157
+ and if not checks if any trigger should be run.
158
+
159
+ Args:
160
+ app: The Scheduler Application object.
161
+ """
162
+ # Check if process is blocking
163
+ blocking = False
164
+ for job in app.running_jobs:
165
+ if job.trigger.is_blocking:
166
+ print(f"Process '{job.trigger.process_name}' is blocking\n")
167
+ blocking = True
168
+
169
+ # Check triggers
170
+ if not blocking:
171
+ print('Checking triggers...')
172
+ job = runner.poll_triggers(app)
173
+
174
+ if job is not None:
175
+ app.running_jobs.append(job)
@@ -16,6 +16,7 @@ class Job():
16
16
  """An object that holds information about a running job."""
17
17
  process: subprocess.Popen
18
18
  trigger: Trigger
19
+ process_folder: str
19
20
 
20
21
 
21
22
  def poll_triggers(app) -> Job | None:
@@ -66,10 +67,7 @@ def run_single_trigger(trigger: SingleTrigger) -> Job | None:
66
67
  print('Running trigger: ', trigger.trigger_name)
67
68
 
68
69
  if db_util.begin_single_trigger(trigger.id):
69
- process = run_process(trigger)
70
-
71
- if process is not None:
72
- return Job(process, trigger)
70
+ return run_process(trigger)
73
71
 
74
72
  return None
75
73
 
@@ -88,10 +86,7 @@ def run_scheduled_trigger(trigger: ScheduledTrigger) -> Job | None:
88
86
  print('Running trigger: ', trigger.trigger_name)
89
87
 
90
88
  if db_util.begin_scheduled_trigger(trigger.id):
91
- process = run_process(trigger)
92
-
93
- if process is not None:
94
- return Job(process, trigger)
89
+ return run_process(trigger)
95
90
 
96
91
  return None
97
92
 
@@ -109,10 +104,7 @@ def run_queue_trigger(trigger: QueueTrigger) -> Job | None:
109
104
  print('Running trigger: ', trigger.trigger_name)
110
105
 
111
106
  if db_util.begin_queue_trigger(trigger.id):
112
- process = run_process(trigger)
113
-
114
- if process is not None:
115
- return Job(process, trigger)
107
+ return run_process(trigger)
116
108
 
117
109
  return None
118
110
 
@@ -142,7 +134,7 @@ def clone_git_repo(repo_url: str) -> str:
142
134
  def clear_repo_folder() -> None:
143
135
  """Completely remove the repos folder."""
144
136
  repo_folder = get_repo_folder_path()
145
- subprocess.run(['rmdir', '/s', '/q', repo_folder], check=False, shell=True, capture_output=True)
137
+ clear_folder(repo_folder)
146
138
 
147
139
 
148
140
  def get_repo_folder_path() -> str:
@@ -156,6 +148,15 @@ def get_repo_folder_path() -> str:
156
148
  return repo_path
157
149
 
158
150
 
151
+ def clear_folder(folder_path: str) -> None:
152
+ """Clear a folder on the system.
153
+
154
+ Args:
155
+ folder_path: The folder to remove.
156
+ """
157
+ subprocess.run(['rmdir', '/s', '/q', folder_path], check=False, shell=True, capture_output=True)
158
+
159
+
159
160
  def find_main_file(folder_path: str) -> str:
160
161
  """Finds the file in the given folder with the name 'main.py'.
161
162
  The search checks subfolders recursively.
@@ -193,6 +194,9 @@ def end_job(job: Job) -> None:
193
194
  elif isinstance(job.trigger, QueueTrigger):
194
195
  db_util.set_trigger_status(job.trigger.id, TriggerStatus.IDLE)
195
196
 
197
+ if job.process_folder:
198
+ clear_folder(job.process_folder)
199
+
196
200
 
197
201
  def fail_job(job: Job) -> None:
198
202
  """Mark a job as failed in the triggers table in the database.
@@ -205,8 +209,11 @@ def fail_job(job: Job) -> None:
205
209
  error_msg = f"An uncaught error ocurred during the process:\n{error}"
206
210
  db_util.create_log(job.trigger.process_name, LogLevel.ERROR, error_msg)
207
211
 
212
+ if job.process_folder:
213
+ clear_folder(job.process_folder)
208
214
 
209
- def run_process(trigger: Trigger) -> subprocess.Popen | None:
215
+
216
+ def run_process(trigger: Trigger) -> Job | None:
210
217
  """Runs the process of the given trigger with the necessary inputs:
211
218
  Process name
212
219
  Connection string
@@ -224,14 +231,15 @@ def run_process(trigger: Trigger) -> subprocess.Popen | None:
224
231
  trigger: The trigger whose process to run.
225
232
 
226
233
  Returns:
227
- subprocess.Popen: The Popen instance of the process if successful.
234
+ Job: A Job object referencing the process if succesful.
228
235
  """
229
236
  process_path = trigger.process_path
237
+ folder_path = None
230
238
 
231
239
  try:
232
240
  if trigger.is_git_repo:
233
- git_folder_path = clone_git_repo(process_path)
234
- process_path = find_main_file(git_folder_path)
241
+ folder_path = clone_git_repo(process_path)
242
+ process_path = find_main_file(folder_path)
235
243
 
236
244
  if not os.path.isfile(process_path):
237
245
  raise ValueError(f"The process path didn't point to a file on the system. Path: '{process_path}'")
@@ -244,7 +252,9 @@ def run_process(trigger: Trigger) -> subprocess.Popen | None:
244
252
 
245
253
  command_args = ['python', process_path, trigger.process_name, conn_string, crypto_key, trigger.process_args]
246
254
 
247
- return subprocess.Popen(command_args, stderr=subprocess.PIPE, text=True)
255
+ process = subprocess.Popen(command_args, stderr=subprocess.PIPE, text=True) # pylint: disable=consider-using-with
256
+
257
+ return Job(process, trigger, folder_path)
248
258
 
249
259
  # We actually want to catch any exception here
250
260
  # pylint: disable=broad-exception-caught
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: OpenOrchestrator
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: A package containing OpenOrchestrator and OpenOrchestrator Scheduler
5
5
  Author-email: ITK Development <itk-rpa@mkb.aarhus.dk>
6
6
  Project-URL: Homepage, https://github.com/itk-dev-rpa/OpenOrchestrator
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: OpenOrchestrator
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: A package containing OpenOrchestrator and OpenOrchestrator Scheduler
5
5
  Author-email: ITK Development <itk-rpa@mkb.aarhus.dk>
6
6
  Project-URL: Homepage, https://github.com/itk-dev-rpa/OpenOrchestrator
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "OpenOrchestrator"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  authors = [
9
9
  { name="ITK Development", email="itk-rpa@mkb.aarhus.dk" },
10
10
  ]
@@ -1,177 +0,0 @@
1
- """This module is responsible for the layout and functionality of the run tab
2
- in Scheduler."""
3
-
4
- import tkinter
5
- from tkinter import ttk
6
- import sys
7
-
8
- from OpenOrchestrator.common import crypto_util
9
- from OpenOrchestrator.database import db_util
10
- from OpenOrchestrator.scheduler import runner
11
-
12
-
13
- def create_tab(parent: ttk.Notebook, app) -> ttk.Frame:
14
- """Create a new Run tab object.
15
-
16
- Args:
17
- parent: The ttk.Notebook object that this tab is a child of.
18
- app: The Scheduler application object.
19
-
20
- Returns:
21
- ttk.Frame: The created tab object as a ttk.Frame.
22
- """
23
- tab = ttk.Frame(parent)
24
- tab.pack(fill='both', expand=True)
25
-
26
- status_label = ttk.Label(tab, text="State: Paused")
27
- status_label.pack()
28
-
29
- ttk.Button(tab, text="Run", command=lambda: run(app, status_label)).pack()
30
- ttk.Button(tab, text="Pause", command=lambda: pause(app, status_label)).pack()
31
-
32
- # Text area
33
- text_frame = tkinter.Frame(tab)
34
- text_frame.pack()
35
-
36
- text_area = tkinter.Text(text_frame, state='disabled', wrap='none')
37
- sys.stdout.write = lambda s: print_text(text_area, s)
38
-
39
- text_yscroll = ttk.Scrollbar(text_frame, orient='vertical', command=text_area.yview)
40
- text_yscroll.pack(side='right', fill='y')
41
- text_area.configure(yscrollcommand=text_yscroll.set)
42
-
43
- text_xscroll = ttk.Scrollbar(text_frame, orient='horizontal', command=text_area.xview)
44
- text_xscroll.pack(side='bottom', fill='x')
45
- text_area.configure(xscrollcommand=text_xscroll.set)
46
-
47
- text_area.pack()
48
-
49
- return tab
50
-
51
-
52
- def run(app, status_label: ttk.Label) -> None:
53
- """Starts the Scheduler and sets the app's status to 'running'.
54
-
55
- Args:
56
- app: The Scheduler application object.
57
- status_label: The label showing the current status.
58
- """
59
- if db_util.get_conn_string() is None:
60
- print("Can't start without a valid connection string. Go to the settings tab to configure the connection string")
61
- return
62
- if crypto_util.get_key() is None:
63
- print("Can't start without a valid encryption key. Go to the settings tab to configure the encryption key")
64
- return
65
-
66
- if not app.running:
67
- status_label.configure(text='State: Running')
68
- print('Running...\n')
69
- app.running = True
70
-
71
- # Only start loop if it's not already running
72
- if app.tk.call('after', 'info') == '':
73
- app.after(0, loop, app)
74
-
75
-
76
- def pause(app, status_label: ttk.Label):
77
- """Stops the Scheduler and sets the app's status to 'paused'.
78
-
79
- Args:
80
- app: The Scheduler application object.
81
- status_label: The label showing the current status.
82
- """
83
- if app.running:
84
- status_label.configure(text="State: Paused")
85
- print('Paused... Please wait for all processes to stop before closing the application\n')
86
- app.running = False
87
-
88
-
89
- def print_text(text_widget: tkinter.Text, text: str) -> None:
90
- """Appends text to the text area.
91
- Is used to replace the functionality of sys.stdout.write (print).
92
-
93
- Args:
94
- print_text: The text area object.
95
- string: The string to append.
96
- """
97
- # Insert text at the end
98
- text_widget.configure(state='normal')
99
- text_widget.insert('end', text)
100
-
101
- # If the number of lines are above 1000 delete 10 lines from the top
102
- num_lines = int(text_widget.index('end').split('.')[0])
103
- if num_lines > 1000:
104
- text_widget.delete("1.0", "10.0")
105
-
106
- # Scroll to end
107
- text_widget.see('end')
108
- text_widget.configure(state='disabled')
109
-
110
-
111
- def loop(app) -> None:
112
- """The main loop function of the Scheduler.
113
- Checks heartbeats, check triggers, and schedules the next loop.
114
-
115
- Args:
116
- app: The Scheduler Application object.
117
- """
118
- check_heartbeats(app)
119
-
120
- if app.running:
121
- check_triggers(app)
122
-
123
- if len(app.running_jobs) == 0:
124
- print("Doing cleanup...")
125
- runner.clear_repo_folder()
126
-
127
- # Schedule next loop
128
- if app.running or len(app.running_jobs) > 0:
129
- print('Waiting 6 seconds...\n')
130
- app.after(6_000, loop, app)
131
- else:
132
- print("Scheduler is paused and no more processes are running.")
133
-
134
-
135
- def check_heartbeats(app) -> None:
136
- """Check if any running jobs are still running, failed or done.
137
-
138
- Args:
139
- app: The Scheduler Application object.
140
- """
141
- print('Checking heartbeats...')
142
- for job in app.running_jobs:
143
- if job.process.poll() is not None:
144
- app.running_jobs.remove(job)
145
-
146
- if job.process.returncode == 0:
147
- print(f"Process '{job.trigger.process_name}' is done")
148
- runner.end_job(job)
149
- else:
150
- print(f"Process '{job.trigger.process_name}' failed. Check process log for more info.")
151
- runner.fail_job(job)
152
-
153
- else:
154
- print(f"Process '{job.trigger.process_name}' is still running")
155
-
156
-
157
- def check_triggers(app) -> None:
158
- """Checks any process is blocking
159
- and if not checks if any trigger should be run.
160
-
161
- Args:
162
- app: The Scheduler Application object.
163
- """
164
- # Check if process is blocking
165
- blocking = False
166
- for job in app.running_jobs:
167
- if job.trigger.is_blocking:
168
- print(f"Process '{job.trigger.process_name}' is blocking\n")
169
- blocking = True
170
-
171
- # Check triggers
172
- if not blocking:
173
- print('Checking triggers...')
174
- job = runner.poll_triggers(app)
175
-
176
- if job is not None:
177
- app.running_jobs.append(job)