|
1 | | -from __future__ import annotations |
2 | | - |
3 | | -import argparse |
4 | | -import configparser |
5 | | -import logging |
6 | | -import os |
7 | | -import platform |
8 | | -import shutil |
9 | | -import sys |
10 | | -import time |
11 | | -import webbrowser |
12 | | -from datetime import datetime |
13 | | -from pathlib import Path |
14 | | -from pprint import pprint |
15 | | -from queue import Queue |
16 | | -from typing import Literal |
17 | | -from urllib.parse import ParseResult, urlparse |
18 | | - |
19 | | -import requests |
20 | | -from rich.prompt import Confirm |
21 | | - |
22 | | -import murfey.client.update |
23 | | -import murfey.client.watchdir |
24 | | -import murfey.client.websocket |
25 | | -from murfey.client.customlogging import CustomHandler, DirectableRichHandler |
26 | | -from murfey.client.instance_environment import MurfeyInstanceEnvironment |
27 | | -from murfey.client.tui.app import MurfeyTUI |
28 | | -from murfey.client.tui.status_bar import StatusBar |
29 | | -from murfey.util.api import url_path_for |
30 | | -from murfey.util.client import authorised_requests, read_config |
31 | | -from murfey.util.models import Visit |
32 | | - |
33 | | -log = logging.getLogger("murfey.client") |
34 | | - |
35 | | -requests.get, requests.post, requests.put, requests.delete = authorised_requests() |
36 | | - |
37 | | - |
38 | | -def _get_visit_list(api_base: ParseResult, instrument_name: str): |
39 | | - proxy_path = api_base.path.rstrip("/") |
40 | | - get_visits_url = api_base._replace( |
41 | | - path=f"{proxy_path}{url_path_for('session_control.router', 'get_current_visits', instrument_name=instrument_name)}" |
42 | | - ) |
43 | | - server_reply = requests.get(get_visits_url.geturl()) |
44 | | - if server_reply.status_code != 200: |
45 | | - raise ValueError(f"Server unreachable ({server_reply.status_code})") |
46 | | - return [Visit.parse_obj(v) for v in server_reply.json()] |
47 | | - |
48 | | - |
49 | | -def write_config(config: configparser.ConfigParser): |
50 | | - mcch = os.environ.get("MURFEY_CLIENT_CONFIG_HOME") |
51 | | - murfey_client_config_home = Path(mcch) if mcch else Path.home() |
52 | | - with open(murfey_client_config_home / ".murfey", "w") as configfile: |
53 | | - config.write(configfile) |
54 | | - |
55 | | - |
56 | | -def main_loop( |
57 | | - source_watchers: list[murfey.client.watchdir.DirWatcher], |
58 | | - appearance_time: float, |
59 | | - transfer_all: bool, |
60 | | -): |
61 | | - log.info( |
62 | | - f"Murfey {murfey.__version__} on Python {'.'.join(map(str, sys.version_info[0:3]))} entering main loop" |
63 | | - ) |
64 | | - if appearance_time > 0: |
65 | | - modification_time: float | None = time.time() - appearance_time * 3600 |
66 | | - else: |
67 | | - modification_time = None |
68 | | - while True: |
69 | | - for sw in source_watchers: |
70 | | - sw.scan(modification_time=modification_time, transfer_all=transfer_all) |
71 | | - time.sleep(15) |
72 | | - |
73 | | - |
74 | | -def _enable_webbrowser_in_cygwin(): |
75 | | - """Helper function to make webbrowser.open() work in CygWin""" |
76 | | - if "cygwin" in platform.system().lower() and shutil.which("cygstart"): |
77 | | - webbrowser.register("cygstart", None, webbrowser.GenericBrowser("cygstart")) |
78 | | - |
79 | | - |
80 | | -def _check_for_updates( |
81 | | - server: ParseResult, install_version: None | Literal[True] | str |
82 | | -): |
83 | | - if install_version is True: |
84 | | - # User requested installation of the newest version |
85 | | - try: |
86 | | - murfey.client.update.check(server, force=True) |
87 | | - print("\nYou are already running the newest version of Murfey") |
88 | | - exit() |
89 | | - except Exception as e: |
90 | | - exit(f"Murfey update check failed with {e}") |
91 | | - |
92 | | - if install_version: |
93 | | - # User requested installation of a specific version |
94 | | - if murfey.client.update.install_murfey(server, install_version): |
95 | | - print(f"\nMurfey has been updated to version {install_version}") |
96 | | - exit() |
97 | | - else: |
98 | | - exit("Error occurred while updating Murfey") |
99 | | - |
100 | | - # Otherwise run a routine update check to ensure client and server are compatible |
101 | | - try: |
102 | | - murfey.client.update.check(server) |
103 | | - except Exception as e: |
104 | | - print(f"Murfey update check failed with {e}") |
105 | | - |
106 | | - |
107 | | -def run(): |
108 | | - # Load client config and server information |
109 | | - config = read_config() |
110 | | - instrument_name = config["Murfey"]["instrument_name"] |
111 | | - try: |
112 | | - server_routing = config["ServerRouter"] |
113 | | - except KeyError: |
114 | | - server_routing = {} |
115 | | - server_routing_prefix_found = False |
116 | | - if server_routing: |
117 | | - for path_prefix, server in server_routing.items(): |
118 | | - if str(Path.cwd()).startswith(path_prefix): |
119 | | - known_server = server |
120 | | - server_routing_prefix_found = True |
121 | | - break |
122 | | - else: |
123 | | - known_server = None |
124 | | - else: |
125 | | - known_server = config["Murfey"].get("server") |
126 | | - |
127 | | - # Set up argument parser with dynamic defaults based on client config |
128 | | - parser = argparse.ArgumentParser(description="Start the Murfey client") |
129 | | - parser.add_argument( |
130 | | - "--server", |
131 | | - metavar="HOST:PORT", |
132 | | - type=str, |
133 | | - help=f"Murfey server to connect to ({known_server})", |
134 | | - default=known_server, |
135 | | - ) |
136 | | - parser.add_argument("--visit", help="Name of visit") |
137 | | - parser.add_argument( |
138 | | - "--source", help="Directory to transfer files from", type=Path, default="." |
139 | | - ) |
140 | | - parser.add_argument( |
141 | | - "--destination", |
142 | | - help="Directory to transfer files to (syntax: 'data/2022/cm31093-2/tmp/murfey')", |
143 | | - ) |
144 | | - parser.add_argument( |
145 | | - "--update", |
146 | | - metavar="VERSION", |
147 | | - nargs="?", |
148 | | - default=None, |
149 | | - const=True, |
150 | | - help="Update Murfey to the newest or to a specific version", |
151 | | - ) |
152 | | - parser.add_argument( |
153 | | - "--demo", |
154 | | - action="store_true", |
155 | | - ) |
156 | | - parser.add_argument( |
157 | | - "--appearance-time", |
158 | | - type=float, |
159 | | - default=-1, |
160 | | - help="Only consider top level directories that have appeared more recently than this many hours ago", |
161 | | - ) |
162 | | - parser.add_argument( |
163 | | - "--fake-dc", |
164 | | - action="store_true", |
165 | | - default=False, |
166 | | - help="Do not perform data collection related calls to API (avoids database inserts)", |
167 | | - ) |
168 | | - parser.add_argument( |
169 | | - "--time-based-transfer", |
170 | | - action="store_true", |
171 | | - help="Transfer new files", |
172 | | - ) |
173 | | - parser.add_argument( |
174 | | - "--no-transfer", |
175 | | - action="store_true", |
176 | | - help="Avoid actually transferring files", |
177 | | - ) |
178 | | - parser.add_argument( |
179 | | - "--debug", |
180 | | - action="store_true", |
181 | | - help="Turn on debugging logs", |
182 | | - ) |
183 | | - parser.add_argument( |
184 | | - "--local", |
185 | | - action="store_true", |
186 | | - default=False, |
187 | | - help="Perform rsync transfers locally rather than remotely", |
188 | | - ) |
189 | | - parser.add_argument( |
190 | | - "--ignore-mdoc-metadata", |
191 | | - action="store_true", |
192 | | - default=False, |
193 | | - help="Do not attempt to read metadata from all mdoc files", |
194 | | - ) |
195 | | - parser.add_argument( |
196 | | - "--remove-files", |
197 | | - action="store_true", |
198 | | - default=False, |
199 | | - help="Remove source files immediately after their transfer", |
200 | | - ) |
201 | | - parser.add_argument( |
202 | | - "--name", |
203 | | - type=str, |
204 | | - default="", |
205 | | - help="Name of Murfey session to be created", |
206 | | - ) |
207 | | - parser.add_argument( |
208 | | - "--skip-existing-processing", |
209 | | - action="store_true", |
210 | | - default=False, |
211 | | - help="Do not trigger processing for any data directories currently on disk (you may have started processing for them in a previous murfey run)", |
212 | | - ) |
213 | | - args = parser.parse_args() |
214 | | - |
215 | | - # Logic to exit early based on parsed args |
216 | | - if not args.server: |
217 | | - exit("Murfey server not set. Please run with --server host:port") |
218 | | - if not args.server.startswith(("http://", "https://")): |
219 | | - if "://" in args.server: |
220 | | - exit("Unknown server protocol. Only http:// and https:// are allowed") |
221 | | - args.server = f"http://{args.server}" |
222 | | - if args.remove_files: |
223 | | - remove_prompt = Confirm.ask( |
224 | | - f"Are you sure you want to remove files from {args.source or Path('.').absolute()}?" |
225 | | - ) |
226 | | - if not remove_prompt: |
227 | | - exit("Exiting") |
228 | | - |
229 | | - # If a new server URL is provided, save info to config file |
230 | | - murfey_url = urlparse(args.server, allow_fragments=False) |
231 | | - if args.server != known_server: |
232 | | - # New server specified. Verify that it is real |
233 | | - print(f"Attempting to connect to new server {args.server}") |
234 | | - try: |
235 | | - murfey.client.update.check(murfey_url, install=False) |
236 | | - except Exception as e: |
237 | | - exit(f"Could not reach Murfey server at {args.server!r} - {e}") |
238 | | - |
239 | | - # If server is reachable then update the configuration |
240 | | - config["Murfey"]["server"] = args.server |
241 | | - write_config(config) |
242 | | - |
243 | | - # If user requested installation of a specific or a newer version then |
244 | | - # make that happen, otherwise ensure client and server are compatible and |
245 | | - # update if necessary. |
246 | | - _check_for_updates(server=murfey_url, install_version=args.update) |
247 | | - |
248 | | - if args.no_transfer: |
249 | | - log.info("No files will be transferred as --no-transfer flag was specified") |
250 | | - |
251 | | - # Check ISPyB (if set up) for ongoing visits |
252 | | - ongoing_visits = [] |
253 | | - if args.visit: |
254 | | - ongoing_visits = [args.visit] |
255 | | - elif server_routing_prefix_found: |
256 | | - for part in Path.cwd().parts: |
257 | | - if "-" in part: |
258 | | - ongoing_visits = [part] |
259 | | - break |
260 | | - if not ongoing_visits: |
261 | | - print("Ongoing visits:") |
262 | | - ongoing_visits = _get_visit_list(murfey_url, instrument_name) |
263 | | - pprint(ongoing_visits) |
264 | | - ongoing_visits = [v.name for v in ongoing_visits] |
265 | | - |
266 | | - _enable_webbrowser_in_cygwin() |
267 | | - |
268 | | - # Set up additional log handlers |
269 | | - log.setLevel(logging.DEBUG) |
270 | | - log_queue = Queue() |
271 | | - input_queue = Queue() |
272 | | - |
273 | | - # Rich-based console handler |
274 | | - rich_handler = DirectableRichHandler(enable_link_path=False) |
275 | | - rich_handler.setLevel(logging.DEBUG if args.debug else logging.INFO) |
276 | | - |
277 | | - # Set up websocket app and handler |
278 | | - client_id_response = requests.get( |
279 | | - f"{murfey_url.geturl()}{url_path_for('session_control.router', 'new_client_id')}" |
280 | | - ) |
281 | | - if client_id_response.status_code == 401: |
282 | | - exit( |
283 | | - "This instrument is not authorised to run the TUI app; please use the " |
284 | | - "Murfey web UI instead" |
285 | | - ) |
286 | | - elif client_id_response.status_code != 200: |
287 | | - exit( |
288 | | - "Unable to establish connection to Murfey server: \n" |
289 | | - f"{client_id_response.json()}" |
290 | | - ) |
291 | | - client_id: dict = client_id_response.json() |
292 | | - ws = murfey.client.websocket.WSApp( |
293 | | - server=args.server, |
294 | | - id=client_id["new_id"], |
295 | | - ) |
296 | | - ws_handler = CustomHandler(ws.send) |
297 | | - |
298 | | - # Add additional handlers and set logging levels |
299 | | - logging.getLogger().addHandler(rich_handler) |
300 | | - logging.getLogger().addHandler(ws_handler) |
301 | | - logging.getLogger("murfey").setLevel(logging.INFO) |
302 | | - logging.getLogger("websocket").setLevel(logging.WARNING) |
303 | | - |
304 | | - log.info("Starting Websocket connection") |
305 | | - |
306 | | - # Load machine data for subsequent sections |
307 | | - machine_data = requests.get( |
308 | | - f"{murfey_url.geturl()}{url_path_for('session_control.router', 'machine_info_by_instrument', instrument_name=instrument_name)}" |
309 | | - ).json() |
310 | | - gain_ref: Path | None = None |
311 | | - |
312 | | - # Set up Murfey environment instance and map it to websocket app |
313 | | - instance_environment = MurfeyInstanceEnvironment( |
314 | | - url=murfey_url, |
315 | | - client_id=ws.id, |
316 | | - instrument_name=instrument_name, |
317 | | - software_versions=machine_data.get("software_versions", {}), |
318 | | - # sources=[Path(args.source)], |
319 | | - # watchers=source_watchers, |
320 | | - default_destination=args.destination or str(datetime.now().year), |
321 | | - demo=args.demo, |
322 | | - processing_only_mode=server_routing_prefix_found, |
323 | | - rsync_url=( |
324 | | - urlparse(machine_data["rsync_url"]).hostname |
325 | | - if machine_data.get("rsync_url") |
326 | | - else "" |
327 | | - ), |
328 | | - ) |
329 | | - ws.environment = instance_environment |
330 | | - |
331 | | - # Set up and run Murfey TUI app |
332 | | - status_bar = StatusBar() |
333 | | - rich_handler.redirect = True |
334 | | - app = MurfeyTUI( |
335 | | - environment=instance_environment, |
336 | | - visits=ongoing_visits, |
337 | | - queues={"input": input_queue, "logs": log_queue}, |
338 | | - status_bar=status_bar, |
339 | | - dummy_dc=args.fake_dc, |
340 | | - do_transfer=not args.no_transfer, |
341 | | - gain_ref=gain_ref, |
342 | | - redirected_logger=rich_handler, |
343 | | - force_mdoc_metadata=not args.ignore_mdoc_metadata, |
344 | | - processing_enabled=machine_data.get("processing_enabled", True), |
345 | | - skip_existing_processing=args.skip_existing_processing, |
346 | | - ) |
347 | | - app.run() |
348 | | - rich_handler.redirect = False |
0 commit comments