kapsikkum-unmanic – Blame information for rev 1
?pathlinks?
Rev | Author | Line No. | Line |
---|---|---|---|
1 | office | 1 | #!/usr/bin/env python3 |
2 | # -*- coding: utf-8 -*- |
||
3 | |||
4 | """ |
||
5 | unmanic.service.py |
||
6 | |||
7 | Written by: Josh.5 <jsunnex@gmail.com> |
||
8 | Date: 06 Dec 2018, (7:21 AM) |
||
9 | |||
10 | Copyright: |
||
11 | Copyright (C) Josh Sunnex - All Rights Reserved |
||
12 | |||
13 | Permission is hereby granted, free of charge, to any person obtaining a copy |
||
14 | of this software and associated documentation files (the "Software"), to deal |
||
15 | in the Software without restriction, including without limitation the rights |
||
16 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||
17 | copies of the Software, and to permit persons to whom the Software is |
||
18 | furnished to do so, subject to the following conditions: |
||
19 | |||
20 | The above copyright notice and this permission notice shall be included in all |
||
21 | copies or substantial portions of the Software. |
||
22 | |||
23 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, |
||
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. |
||
26 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, |
||
27 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR |
||
28 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE |
||
29 | OR OTHER DEALINGS IN THE SOFTWARE. |
||
30 | |||
31 | """ |
||
32 | import argparse |
||
33 | import os |
||
34 | import time |
||
35 | import queue |
||
36 | import signal |
||
37 | import threading |
||
38 | |||
39 | from unmanic import config, metadata |
||
40 | from unmanic.libs import libraryscanner, unlogger, common, eventmonitor |
||
41 | from unmanic.libs.db_migrate import Migrations |
||
42 | from unmanic.libs.scheduler import ScheduledTasksManager |
||
43 | from unmanic.libs.taskqueue import TaskQueue |
||
44 | from unmanic.libs.postprocessor import PostProcessor |
||
45 | from unmanic.libs.taskhandler import TaskHandler |
||
46 | from unmanic.libs.uiserver import FrontendPushMessages, UIServer |
||
47 | from unmanic.libs.foreman import Foreman |
||
48 | |||
49 | unmanic_logging = unlogger.UnmanicLogger.__call__() |
||
50 | main_logger = unmanic_logging.get_logger() |
||
51 | |||
52 | |||
53 | def init_db(config_path): |
||
54 | # Set paths |
||
55 | app_dir = os.path.dirname(os.path.abspath(__file__)) |
||
56 | |||
57 | # Set database connection settings |
||
58 | database_settings = { |
||
59 | "TYPE": "SQLITE", |
||
60 | "FILE": os.path.join(config_path, 'unmanic.db'), |
||
61 | "MIGRATIONS_DIR": os.path.join(app_dir, 'migrations_v1'), |
||
62 | "MIGRATIONS_HISTORY_VERSION": 'v1', |
||
63 | } |
||
64 | |||
65 | # Ensure the config path exists |
||
66 | if not os.path.exists(config_path): |
||
67 | os.makedirs(config_path) |
||
68 | |||
69 | # Create database connection |
||
70 | from unmanic.libs.unmodels.lib import Database |
||
71 | db_connection = Database.select_database(database_settings) |
||
72 | |||
73 | # Run database migrations |
||
74 | migrations = Migrations(database_settings) |
||
75 | migrations.update_schema() |
||
76 | |||
77 | # Return the database connection |
||
78 | return db_connection |
||
79 | |||
80 | |||
81 | class Service: |
||
82 | |||
83 | def __init__(self): |
||
84 | self.threads = [] |
||
85 | self.run_threads = True |
||
86 | self.db_connection = None |
||
87 | |||
88 | self.developer = None |
||
89 | self.dev_local_api = None |
||
90 | |||
91 | self.event = threading.Event() |
||
92 | |||
93 | def start_handler(self, data_queues, task_queue): |
||
94 | main_logger.info("Starting TaskHandler") |
||
95 | handler = TaskHandler(data_queues, task_queue, self.event) |
||
96 | handler.daemon = True |
||
97 | handler.start() |
||
98 | self.threads.append({ |
||
99 | 'name': 'TaskHandler', |
||
100 | 'thread': handler |
||
101 | }) |
||
102 | return handler |
||
103 | |||
104 | def start_post_processor(self, data_queues, task_queue): |
||
105 | main_logger.info("Starting PostProcessor") |
||
106 | postprocessor = PostProcessor(data_queues, task_queue, self.event) |
||
107 | postprocessor.daemon = True |
||
108 | postprocessor.start() |
||
109 | self.threads.append({ |
||
110 | 'name': 'PostProcessor', |
||
111 | 'thread': postprocessor |
||
112 | }) |
||
113 | return postprocessor |
||
114 | |||
115 | def start_foreman(self, data_queues, settings, task_queue): |
||
116 | main_logger.info("Starting Foreman") |
||
117 | foreman = Foreman(data_queues, settings, task_queue, self.event) |
||
118 | foreman.daemon = True |
||
119 | foreman.start() |
||
120 | self.threads.append({ |
||
121 | 'name': 'Foreman', |
||
122 | 'thread': foreman |
||
123 | }) |
||
124 | return foreman |
||
125 | |||
126 | def start_library_scanner_manager(self, data_queues): |
||
127 | main_logger.info("Starting LibraryScannerManager") |
||
128 | library_scanner_manager = libraryscanner.LibraryScannerManager(data_queues, self.event) |
||
129 | library_scanner_manager.daemon = True |
||
130 | library_scanner_manager.start() |
||
131 | self.threads.append({ |
||
132 | 'name': 'LibraryScannerManager', |
||
133 | 'thread': library_scanner_manager |
||
134 | }) |
||
135 | return library_scanner_manager |
||
136 | |||
137 | def start_inotify_watch_manager(self, data_queues, settings): |
||
138 | if eventmonitor.event_monitor_module: |
||
139 | main_logger.info("Starting EventMonitorManager") |
||
140 | event_monitor_manager = eventmonitor.EventMonitorManager(data_queues, self.event) |
||
141 | event_monitor_manager.daemon = True |
||
142 | event_monitor_manager.start() |
||
143 | self.threads.append({ |
||
144 | 'name': 'EventMonitorManager', |
||
145 | 'thread': event_monitor_manager |
||
146 | }) |
||
147 | return event_monitor_manager |
||
148 | else: |
||
149 | main_logger.warn("Unable to start EventMonitorManager as no event monitor module was found") |
||
150 | |||
151 | def start_ui_server(self, data_queues, foreman): |
||
152 | main_logger.info("Starting UIServer") |
||
153 | uiserver = UIServer(data_queues, foreman, self.developer) |
||
154 | uiserver.daemon = True |
||
155 | uiserver.start() |
||
156 | self.threads.append({ |
||
157 | 'name': 'UIServer', |
||
158 | 'thread': uiserver |
||
159 | }) |
||
160 | return uiserver |
||
161 | |||
162 | def start_scheduled_tasks_manager(self): |
||
163 | main_logger.info("Starting ScheduledTasksManager") |
||
164 | scheduled_tasks_manager = ScheduledTasksManager(self.event) |
||
165 | scheduled_tasks_manager.daemon = True |
||
166 | scheduled_tasks_manager.start() |
||
167 | self.threads.append({ |
||
168 | 'name': 'ScheduledTasksManager', |
||
169 | 'thread': scheduled_tasks_manager |
||
170 | }) |
||
171 | return scheduled_tasks_manager |
||
172 | |||
173 | @staticmethod |
||
174 | def initial_register_unmanic(dev_local_api): |
||
175 | from unmanic.libs import session |
||
176 | s = session.Session(dev_local_api=dev_local_api) |
||
177 | s.register_unmanic(s.get_installation_uuid()) |
||
178 | |||
179 | def start_threads(self, settings): |
||
180 | # Create our data queues |
||
181 | data_queues = { |
||
182 | "library_scanner_triggers": queue.Queue(maxsize=1), |
||
183 | "scheduledtasks": queue.Queue(), |
||
184 | "inotifytasks": queue.Queue(), |
||
185 | "progress_reports": queue.Queue(), |
||
186 | "frontend_messages": FrontendPushMessages(), |
||
187 | "logging": unmanic_logging |
||
188 | } |
||
189 | |||
190 | # Clear cache directory |
||
191 | main_logger.info("Clearing previous cache") |
||
192 | common.clean_files_in_cache_dir(settings.get_cache_path()) |
||
193 | |||
194 | main_logger.info("Starting all threads") |
||
195 | |||
196 | # Register installation |
||
197 | self.initial_register_unmanic(self.dev_local_api) |
||
198 | |||
199 | # Setup job queue |
||
200 | task_queue = TaskQueue(data_queues) |
||
201 | |||
202 | # Setup post-processor thread |
||
203 | self.start_post_processor(data_queues, task_queue) |
||
204 | |||
205 | # Start the foreman thread |
||
206 | foreman = self.start_foreman(data_queues, settings, task_queue) |
||
207 | |||
208 | # Start new thread to handle messages from service |
||
209 | self.start_handler(data_queues, task_queue) |
||
210 | |||
211 | # Start scheduled thread |
||
212 | self.start_library_scanner_manager(data_queues) |
||
213 | |||
214 | # Start inotify watch manager |
||
215 | self.start_inotify_watch_manager(data_queues, settings) |
||
216 | |||
217 | # Start new thread to run the web UI |
||
218 | self.start_ui_server(data_queues, foreman) |
||
219 | |||
220 | # Start new thread to run the scheduled tasks manager |
||
221 | self.start_scheduled_tasks_manager() |
||
222 | |||
223 | def stop_threads(self): |
||
224 | main_logger.info("Stopping all threads") |
||
225 | self.event.set() |
||
226 | for thread in self.threads: |
||
227 | main_logger.info("Sending thread {} abort signal".format(thread['name'])) |
||
228 | thread['thread'].stop() |
||
229 | for thread in self.threads: |
||
230 | main_logger.info("Waiting for thread {} to stop".format(thread['name'])) |
||
231 | thread['thread'].join(10) |
||
232 | main_logger.info("Thread {} has successfully stopped".format(thread['name'])) |
||
233 | self.threads = [] |
||
234 | |||
235 | def sig_handle(self, signum, frame): |
||
236 | main_logger.info("Received {}".format(signum)) |
||
237 | self.stop() |
||
238 | |||
239 | def stop(self): |
||
240 | self.run_threads = False |
||
241 | |||
242 | def run(self): |
||
243 | # Init the configuration |
||
244 | settings = config.Config() |
||
245 | |||
246 | # Init the database |
||
247 | self.db_connection = init_db(settings.get_config_path()) |
||
248 | |||
249 | # Start all threads |
||
250 | self.start_threads(settings) |
||
251 | |||
252 | # Watch for the term signal |
||
253 | if os.name == "nt": |
||
254 | while self.run_threads: |
||
255 | try: |
||
256 | time.sleep(1) |
||
257 | except (KeyboardInterrupt, SystemExit) as e: |
||
258 | break |
||
259 | else: |
||
260 | signal.signal(signal.SIGINT, self.sig_handle) |
||
261 | signal.signal(signal.SIGTERM, self.sig_handle) |
||
262 | while self.run_threads: |
||
263 | signal.pause() |
||
264 | time.sleep(.5) |
||
265 | |||
266 | # Received term signal. Stop everything |
||
267 | self.stop_threads() |
||
268 | self.db_connection.stop() |
||
269 | while not self.db_connection.is_stopped(): |
||
270 | time.sleep(.5) |
||
271 | continue |
||
272 | main_logger.info("Exit Unmanic") |
||
273 | |||
274 | |||
275 | def main(): |
||
276 | parser = argparse.ArgumentParser(description='Unmanic') |
||
277 | parser.add_argument('--version', action='version', |
||
278 | version='%(prog)s {version}'.format(version=metadata.read_version_string('long'))) |
||
279 | parser.add_argument('--manage_plugins', action='store_true', |
||
280 | help='manage installed plugins') |
||
281 | parser.add_argument('--dev', |
||
282 | action='store_true', |
||
283 | help='Enable developer mode') |
||
284 | parser.add_argument('--dev-local-api', |
||
285 | action='store_true', |
||
286 | help='Enable development against local unmanic support api') |
||
287 | parser.add_argument('--port', nargs='?', |
||
288 | help='Specify the port to run the webserver on') |
||
289 | # parser.add_argument('--unmanic_path', nargs='?', |
||
290 | # help='Specify the unmanic configuration path instead of ~/.unmanic') |
||
291 | args = parser.parse_args() |
||
292 | |||
293 | # Configure application from args |
||
294 | settings = config.Config( |
||
295 | port=args.port, |
||
296 | unmanic_path=None |
||
297 | ) |
||
298 | |||
299 | if args.manage_plugins: |
||
300 | # Init the DB connection |
||
301 | db_connection = init_db(settings.get_config_path()) |
||
302 | |||
303 | # Run the plugin manager CLI |
||
304 | from unmanic.libs.unplugins.pluginscli import PluginsCLI |
||
305 | plugin_cli = PluginsCLI() |
||
306 | plugin_cli.run() |
||
307 | |||
308 | # Stop the DB connection |
||
309 | db_connection.stop() |
||
310 | while not db_connection.is_stopped(): |
||
311 | time.sleep(.2) |
||
312 | continue |
||
313 | else: |
||
314 | # Run the main Unmanic service |
||
315 | service = Service() |
||
316 | service.developer = args.dev |
||
317 | service.dev_local_api = args.dev_local_api |
||
318 | service.run() |
||
319 | |||
320 | |||
321 | if __name__ == "__main__": |
||
322 | main() |