Loading noctua/api/fits_image.py +13 −5 Original line number Diff line number Diff line Loading @@ -104,11 +104,19 @@ def _load_data(cam_id: str) -> np.ndarray | None: path = _fits_path(cam_id) if path is None or not path.exists(): return None try: with fits.open(path) as hdul: data = hdul[0].data if data is None: return None return data.astype(np.float32) except OSError: # Reader lost a race with a writer that isn't using an atomic # rename yet, or hit a genuinely corrupt file — either way, "no # data this round" is the right degradation, not an unhandled # exception (this fed straight into an uncaught OSError that used # to kill the whole /web/socket connection via _handle_tile_request). return None def get_cached_data(cam_id: str) -> np.ndarray | None: Loading noctua/devices/atik.py +7 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ sudo cp ../include/Atik* /usr/include/ # System modules import ctypes import os import threading import time import numpy as np Loading Loading @@ -466,7 +467,11 @@ class Camera(BaseDevice): "Image type") log.debug(f"Writing on disk") hdu.writeto(filepath, overwrite=True) # Atomic write: temp file + os.replace() so a concurrent reader never # observes a truncated file mid-write ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) log.debug(f"Written") return filepath Loading noctua/devices/atik2.py +6 −1 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ """Interface with an Atik camera device using the vendor's AtikSDK Python package.""" # System modules import os import threading import time from ctypes import byref, c_int, string_at Loading Loading @@ -408,7 +409,11 @@ class Camera(BaseDevice): "Image type") log.debug(f"Writing on disk") hdu.writeto(filepath, overwrite=True) # Atomic write: temp file + os.replace() so a concurrent reader never # observes a truncated file mid-write ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) log.debug(f"Written") return filepath Loading noctua/devices/mako.py +9 −1 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ Camera ID is the IP address (e.g. 10.185.119.111). """ # System modules import os import threading import time from datetime import datetime as dt Loading Loading @@ -491,7 +492,14 @@ class Guider(Mako): hdr['YBINNING'] = (by, 'Y binning factor') log.debug(f"Writing on disk") hdu.writeto(filepath, overwrite=True) # Write to a sibling temp file then rename atomically — os.replace() # is a single directory-entry swap, so a concurrent reader (guider, # tile-request websocket, preview broadcaster) always sees either # the previous complete file or the new one, never a truncated / # half-written one ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) log.debug(f"Written") return filepath noctua/devices/stl.py +6 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ single "get frame" call. # System modules import ctypes import ctypes.util import os import threading import time from datetime import datetime as dt Loading Loading @@ -758,7 +759,11 @@ class Camera(STL): ft = self._last_imagetype hdr['IMAGETYP'] = (_frame_type.get(ft, str(ft)) if ft is not None else "", "Image type") hdu.writeto(filepath, overwrite=True) # Atomic write: temp file + os.replace() so a concurrent reader never # observes a truncated file mid-write ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) return filepath Loading Loading
noctua/api/fits_image.py +13 −5 Original line number Diff line number Diff line Loading @@ -104,11 +104,19 @@ def _load_data(cam_id: str) -> np.ndarray | None: path = _fits_path(cam_id) if path is None or not path.exists(): return None try: with fits.open(path) as hdul: data = hdul[0].data if data is None: return None return data.astype(np.float32) except OSError: # Reader lost a race with a writer that isn't using an atomic # rename yet, or hit a genuinely corrupt file — either way, "no # data this round" is the right degradation, not an unhandled # exception (this fed straight into an uncaught OSError that used # to kill the whole /web/socket connection via _handle_tile_request). return None def get_cached_data(cam_id: str) -> np.ndarray | None: Loading
noctua/devices/atik.py +7 −2 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ sudo cp ../include/Atik* /usr/include/ # System modules import ctypes import os import threading import time import numpy as np Loading Loading @@ -466,7 +467,11 @@ class Camera(BaseDevice): "Image type") log.debug(f"Writing on disk") hdu.writeto(filepath, overwrite=True) # Atomic write: temp file + os.replace() so a concurrent reader never # observes a truncated file mid-write ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) log.debug(f"Written") return filepath Loading
noctua/devices/atik2.py +6 −1 Original line number Diff line number Diff line Loading @@ -4,6 +4,7 @@ """Interface with an Atik camera device using the vendor's AtikSDK Python package.""" # System modules import os import threading import time from ctypes import byref, c_int, string_at Loading Loading @@ -408,7 +409,11 @@ class Camera(BaseDevice): "Image type") log.debug(f"Writing on disk") hdu.writeto(filepath, overwrite=True) # Atomic write: temp file + os.replace() so a concurrent reader never # observes a truncated file mid-write ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) log.debug(f"Written") return filepath Loading
noctua/devices/mako.py +9 −1 Original line number Diff line number Diff line Loading @@ -7,6 +7,7 @@ Camera ID is the IP address (e.g. 10.185.119.111). """ # System modules import os import threading import time from datetime import datetime as dt Loading Loading @@ -491,7 +492,14 @@ class Guider(Mako): hdr['YBINNING'] = (by, 'Y binning factor') log.debug(f"Writing on disk") hdu.writeto(filepath, overwrite=True) # Write to a sibling temp file then rename atomically — os.replace() # is a single directory-entry swap, so a concurrent reader (guider, # tile-request websocket, preview broadcaster) always sees either # the previous complete file or the new one, never a truncated / # half-written one ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) log.debug(f"Written") return filepath
noctua/devices/stl.py +6 −1 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ single "get frame" call. # System modules import ctypes import ctypes.util import os import threading import time from datetime import datetime as dt Loading Loading @@ -758,7 +759,11 @@ class Camera(STL): ft = self._last_imagetype hdr['IMAGETYP'] = (_frame_type.get(ft, str(ft)) if ft is not None else "", "Image type") hdu.writeto(filepath, overwrite=True) # Atomic write: temp file + os.replace() so a concurrent reader never # observes a truncated file mid-write ("Empty or corrupt FITS file"). tmp_path = f"{filepath}.tmp" hdu.writeto(tmp_path, overwrite=True) os.replace(tmp_path, filepath) return filepath Loading