From 344f1bccdc990e685a000e2e709a51852ebe99fc Mon Sep 17 00:00:00 2001 From: Hilyxx Date: Fri, 30 May 2025 21:04:35 +0200 Subject: [PATCH 1/3] Simplified monitoring state and improve file operations - Remove the monitor pause/resume system and replaced it with _is_internal_update flag. Simplified monitoring state - Atomic file operations using temporary files - Safe file replacement with os.replace - Proper file syncing with os.fsync These changes introduce : - Reduced system overhead - Immediate response to changes - Better error handling - Fix CoolerChooser widget issue with real time update in UI in xlet-settings --- .../bin/JsonSettingsWidgets.py | 80 ++++++++++++------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py index 806813f812..ed85e6a52f 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py @@ -46,17 +46,27 @@ def __init__(self, filepath, notify_callback=None): self.resume_timeout = None self.notify_callback = notify_callback + self._monitor_active = True + self._is_internal_update = False self.filepath = filepath self.file_obj = Gio.File.new_for_path(self.filepath) - self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None) - self.file_monitor.connect("changed", self.check_settings) + self._setup_monitor() self.bindings = {} self.listeners = {} self.deps = {} self.settings = self.get_settings() + + def _setup_monitor(self): + """Set up initial file monitoring.""" + try: + self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None) + self.file_monitor.connect("changed", self.check_settings) + except GLib.Error as e: + print(f"Error initializing monitoring: {str(e)}") + self.file_monitor = None def bind(self, key, obj, prop, direction, map_get=None, map_set=None): if direction & (Gio.SettingsBindFlags.SET | Gio.SettingsBindFlags.GET) == 0: @@ -132,6 +142,9 @@ def set_object_value(self, info, value): info["obj"].set_property(info["prop"], value) def check_settings(self, *args): + """Check for settings changes.""" + if self._is_internal_update: + return old_settings = self.settings self.settings = self.get_settings() @@ -158,37 +171,46 @@ def get_settings(self): return settings def save_settings(self): - self.pause_monitor() - if os.path.exists(self.filepath): - os.remove(self.filepath) - raw_data = json.dumps(self.settings, indent=4, ensure_ascii=False) - new_file = open(self.filepath, 'w+') - new_file.write(raw_data) - new_file.close() - self.resume_monitor() - - def pause_monitor(self): - self.file_monitor.cancel() - self.handler = None - - def resume_monitor(self): - if self.resume_timeout: - GLib.source_remove(self.resume_timeout) - self.resume_timeout = GLib.timeout_add(2000, self.do_resume) - - def do_resume(self): - self.file_monitor = self.file_obj.monitor_file(Gio.FileMonitorFlags.SEND_MOVED, None) - self.handler = self.file_monitor.connect("changed", self.check_settings) - self.resume_timeout = None - return False + """Save settings with real-time UI updates.""" + self._is_internal_update = True + try: + temp_filepath = self.filepath + '.tmp' + + # Save to temporary file + raw_data = json.dumps(self.settings, indent=4, ensure_ascii=False) + with open(temp_filepath, 'w', encoding='utf-8') as temp_file: + temp_file.write(raw_data) + temp_file.flush() + os.fsync(temp_file.fileno()) + + # Atomically replace original file + os.replace(temp_filepath, self.filepath) + + except (IOError, OSError) as e: + print(f"Error saving settings: {str(e)}") + raise + finally: + self._is_internal_update = False def reset_to_defaults(self): + """Reset settings with real-time UI updates.""" + changed = False + for key in self.settings: if "value" in self.settings[key]: - self.settings[key]["value"] = self.settings[key]["default"] - self.do_key_update(key) - - self.save_settings() + old_value = self.settings[key]["value"] + new_value = self.settings[key]["default"] + if old_value != new_value: + self.settings[key]["value"] = new_value + self.do_key_update(key) + changed = True + + # Immediately notify callbacks + if self.notify_callback: + self.notify_callback(self, key, new_value) + + if changed: + self.save_settings() # Saving won't block UI updates anymore def do_key_update(self, key): if key in self.bindings: From 649d735cd205c6a13a4d7c71239df53aa6d3ef5e Mon Sep 17 00:00:00 2001 From: Hilyxx Date: Sun, 1 Jun 2025 19:22:49 +0200 Subject: [PATCH 2/3] Data management improvement Improved data management with cleanup of temporary files in the event of an error. --- .../bin/JsonSettingsWidgets.py | 94 +++++++++++++------ 1 file changed, 67 insertions(+), 27 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py index ed85e6a52f..d50ed19081 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py @@ -144,17 +144,24 @@ def set_object_value(self, info, value): def check_settings(self, *args): """Check for settings changes.""" if self._is_internal_update: - return + return + old_settings = self.settings self.settings = self.get_settings() for key in self.bindings: + # Skip keys that don't exist in both old and new settings to avoid KeyError + if key not in self.settings or key not in old_settings: + continue new_value = self.settings[key]["value"] if new_value != old_settings[key]["value"]: for info in self.bindings[key]: self.set_object_value(info, new_value) for key, callback_list in self.listeners.items(): + # Skip keys that don't exist in both old and new settings to avoid KeyError + if key not in self.settings or key not in old_settings: + continue new_value = self.settings[key]["value"] if new_value != old_settings[key]["value"]: for callback in callback_list: @@ -171,26 +178,40 @@ def get_settings(self): return settings def save_settings(self): - """Save settings with real-time UI updates.""" - self._is_internal_update = True - try: - temp_filepath = self.filepath + '.tmp' - - # Save to temporary file - raw_data = json.dumps(self.settings, indent=4, ensure_ascii=False) - with open(temp_filepath, 'w', encoding='utf-8') as temp_file: - temp_file.write(raw_data) - temp_file.flush() - os.fsync(temp_file.fileno()) - - # Atomically replace original file - os.replace(temp_filepath, self.filepath) - - except (IOError, OSError) as e: - print(f"Error saving settings: {str(e)}") - raise - finally: - self._is_internal_update = False + """Save settings with real-time UI updates and proper cleanup.""" + temp_filepath = self.filepath + '.tmp' + + with InternalUpdateContext(self): + try: + # Data serialization + raw_data = json.dumps(self.settings, indent=4, ensure_ascii=False) + + # Write to temporary file + with open(temp_filepath, 'w', encoding='utf-8') as temp_file: + temp_file.write(raw_data) + temp_file.flush() + os.fsync(temp_file.fileno()) + + # Atomic replacement + os.replace(temp_filepath, self.filepath) + + except (IOError, OSError, json.JSONEncodeError) as e: + print(f"Error while saving settings: {str(e)}") + # Cleanup temporary file in case of error + if os.path.exists(temp_filepath): + try: + os.remove(temp_filepath) + except OSError as cleanup_error: + print(f"Error while cleaning up temporary file: {str(cleanup_error)}") + raise + except Exception as e: + print(f"Unexpected error during save: {str(e)}") + if os.path.exists(temp_filepath): + try: + os.remove(temp_filepath) + except OSError: + pass + raise def reset_to_defaults(self): """Reset settings with real-time UI updates.""" @@ -241,12 +262,19 @@ def load_from_file(self, filepath): self.save_settings() def save_to_file(self, filepath): - if os.path.exists(filepath): - os.remove(filepath) - raw_data = json.dumps(self.settings, indent=4) - new_file = open(filepath, 'w+') - new_file.write(raw_data) - new_file.close() + temp_filepath = filepath + '.tmp' + try: + with open(temp_filepath, 'w') as temp_file: + json.dump(self.settings, temp_file, indent=4) + temp_file.flush() + os.fsync(temp_file.fileno()) + os.replace(temp_filepath, filepath) + finally: + if os.path.exists(temp_filepath): + try: + os.remove(temp_filepath) + except OSError: + pass class JSONSettingsRevealer(Gtk.Revealer): def __init__(self, settings, key): @@ -355,3 +383,15 @@ def __init__(self, key, settings, properties): for widget in can_backend: globals()["JSONSettings"+widget] = json_settings_factory(widget) + +class InternalUpdateContext: + """Context manager for internal updates""" + def __init__(self, handler): + self.handler = handler + + def __enter__(self): + self.handler._is_internal_update = True + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.handler._is_internal_update = False From f869375d769e893ee6951e78a7ebfa326201ca8f Mon Sep 17 00:00:00 2001 From: Hilyxx Date: Mon, 9 Jun 2025 15:01:57 +0200 Subject: [PATCH 3/3] Remove dead variables --- .../share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py index d50ed19081..02fc04dd68 100644 --- a/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py +++ b/files/usr/share/cinnamon/cinnamon-settings/bin/JsonSettingsWidgets.py @@ -44,9 +44,7 @@ class JSONSettingsHandler(object): def __init__(self, filepath, notify_callback=None): super(JSONSettingsHandler, self).__init__() - self.resume_timeout = None self.notify_callback = notify_callback - self._monitor_active = True self._is_internal_update = False self.filepath = filepath