Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ singularity_core_sources = files(
'src/components/sidebar/pages/performance_page.vala',
'src/core/display_manager.vala',
'src/core/hot_corner_manager.vala',
'src/core/safe_mode.vala',
'src/core/hand_control_manager.vala',
'src/core/layer_window.vala',
'src/core/monitor_osd.vala',
Expand Down Expand Up @@ -435,3 +436,9 @@ bar_layout_test = executable('bar-layout-test',
dependencies: [dependency('gobject-2.0'), gee_dep],
)
test('bar-layout', bar_layout_test)

safe_mode_test = executable('safe-mode-test',
sources: ['src/core/safe_mode.vala', 'tests/safe_mode_test.vala'],
dependencies: [dependency('gobject-2.0')],
)
test('safe-mode', safe_mode_test)
3 changes: 2 additions & 1 deletion src/components/dock/dock.vala
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,8 @@ namespace Singularity {
public signal void dock_visibility_changed(bool hidden);

private bool scrolling_tiling_active() {
return _settings.get_boolean("tiling-enabled")
return SafeMode.get_default().allows(SafeFeature.TILING)
&& _settings.get_boolean("tiling-enabled")
&& _settings.get_string("tiling-layout") == "scrolling";
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/overview/app_launcher_grid.vala
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ namespace Singularity {
// Listen to widget registry changes (in case a plugin or manifest
// arrives after the overview is built).
OverviewWidgetRegistry.get_default().changed.connect(on_apps_changed);
OverviewWidgetRegistry.get_default().load_manifests();
if (SafeMode.get_default().allows(SafeFeature.CUSTOM_WIDGETS))
OverviewWidgetRegistry.get_default().load_manifests();

// Container-level drop: dragged item is reordered relative to
// whatever child is under the pointer.
Expand Down
40 changes: 39 additions & 1 deletion src/components/sidebar/pages/desktop_page.vala
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,40 @@ namespace Singularity {
back_clicked.connect(() => {
view.go_home();
});

var safe_mode = SafeMode.get_default();
if (safe_mode.active) {
var recovery_group = new PreferencesGroup(
_("Safe Mode"),
_("The desktop recovered from repeated startup crashes. Your settings were not erased; optional startup features are temporarily inactive."));
var recovery_row = new ActionRow(
_("Recovery session active"),
safe_mode.reason + ". " + _("Click to report a crash on GitHub if this seems relevant."),
"dialog-warning-symbolic");
recovery_row.activated.connect(() => {
try {
AppInfo.launch_default_for_uri(
"https://github.com/singularityos-lab/singularity-desktop/issues",
null);
} catch (Error e) {
warning("Could not open the issue tracker: %s", e.message);
}
});
recovery_group.add_row(recovery_row);
var restart_normal = new ActionRow(
_("Restart Normal Session"),
_("Use your repaired settings on the next login"),
"view-refresh-symbolic");
restart_normal.activated.connect(() => {
if (safe_mode.clear_marker()) {
SessionManager.get_default().logout();
} else {
restart_normal.subtitle = _("Could not clear the Safe Mode marker; see the desktop log");
}
});
recovery_group.add_row(restart_normal);
add_group(recovery_group);
}
var reset_btn = new Button.from_icon_name("edit-undo-symbolic");
reset_btn.has_frame = false;
reset_btn.tooltip_text = _("Reset to Default");
Expand Down Expand Up @@ -950,7 +984,11 @@ namespace Singularity {
});
wm_group.add_row(rounded_row);

var tile_row = new SwitchRow(_("Tiling"), _("Automatically arrange windows using the selected layout"), settings.get_boolean("tiling-enabled"));
string tiling_description = safe_mode.active
? _("Configured value is shown here, but tiling is inactive until you restart normally")
: _("Automatically arrange windows using the selected layout");
var tile_row = new SwitchRow(_("Tiling"), tiling_description,
settings.get_boolean("tiling-enabled"));
tile_row.switch_btn.notify["active"].connect(() => {
settings.set_boolean("tiling-enabled", tile_row.switch_btn.active);
});
Expand Down
10 changes: 9 additions & 1 deletion src/core/hand_control_manager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace Singularity {
}

public void calibrate() {
if (!SafeMode.get_default().allows(SafeFeature.HAND_CONTROL)) return;
if (!available) return;
launch_command({ resolve_binary(), "--calibrate" });
}
Expand All @@ -47,6 +48,12 @@ namespace Singularity {
}

private void sync() {
if (!SafeMode.get_default().allows(SafeFeature.HAND_CONTROL)) {
if (process != null) process.force_exit();
process = null;
availability_changed();
return;
}
if (settings.get_boolean("hand-control-enabled")) {
start();
} else {
Expand Down Expand Up @@ -79,7 +86,8 @@ namespace Singularity {
started.get_exit_status() != 0);
bool was_primary = GLib.get_monotonic_time() - started_at >
1000000;
if (settings.get_boolean("hand-control-enabled") &&
if (SafeMode.get_default().allows(SafeFeature.HAND_CONTROL)
&& settings.get_boolean("hand-control-enabled") &&
(failed || was_primary)) {
Timeout.add_seconds(1, () => {
start();
Expand Down
36 changes: 29 additions & 7 deletions src/core/main.vala
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,13 @@ public class SingularityApp : Singularity.ShellApplication, Singularity.Shell.Sh
on_desktop_gesture, this);
// Launch the user's autostart entries once the session has settled
// (bus, portals); nothing else in the shell did this before (#170).
Timeout.add_seconds(1, () => { launch_autostart_apps(); return Source.REMOVE; });
if (Singularity.SafeMode.get_default().allows(
Singularity.SafeFeature.AUTOSTART)) {
Timeout.add_seconds(1, () => {
launch_autostart_apps();
return Source.REMOVE;
});
}
var cal_manager = Singularity.Calendar.CalendarManager.get_default();
cal_manager.register_provider(new Singularity.Calendar.LocalProvider());
Bus.own_name(
Expand Down Expand Up @@ -279,11 +285,14 @@ public class SingularityApp : Singularity.ShellApplication, Singularity.Shell.Sh

// Session recovery: snapshot windows on session end; offer to reopen
// them (via a dialog) at the next login.
session_recovery = new Singularity.SessionRecovery();
Singularity.SessionManager.get_default().session_ending.connect(() => {
session_recovery.capture();
});
maybe_offer_session_restore();
if (Singularity.SafeMode.get_default().allows(
Singularity.SafeFeature.SESSION_RESTORE)) {
session_recovery = new Singularity.SessionRecovery();
Singularity.SessionManager.get_default().session_ending.connect(() => {
session_recovery.capture();
});
maybe_offer_session_restore();
}

// Deferred non-critical module creation
Idle.add(() => {
Expand All @@ -298,7 +307,9 @@ public class SingularityApp : Singularity.ShellApplication, Singularity.Shell.Sh
_resources.start();
// Pre-warm widget modules (dlopen) at login so the first overview
// open isn't stalled loading them.
Singularity.OverviewWidgetRegistry.get_default().load_manifests();
if (Singularity.SafeMode.get_default().allows(
Singularity.SafeFeature.CUSTOM_WIDGETS))
Singularity.OverviewWidgetRegistry.get_default().load_manifests();
// Pre-create the search manager so the file provider's Tracker
// connection is established at login, not on the first search.
Singularity.SearchManager.get_default();
Expand Down Expand Up @@ -499,6 +510,17 @@ public class SingularityApp : Singularity.ShellApplication, Singularity.Shell.Sh
return Source.REMOVE;
});

if (Singularity.SafeMode.get_default().active) {
warning("Safe mode active: %s",
Singularity.SafeMode.get_default().reason);
// Open the repair surface once the panel/sidebar has settled. This
// is non-modal and remains available through the Settings entry.
Timeout.add(800, () => {
open_settings_page("desktop");
return Source.REMOVE;
});
}

}

private async void init_goa() {
Expand Down
6 changes: 5 additions & 1 deletion src/core/plugin_manager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ namespace Singularity {
context = new PluginContext();
settings = new GLib.Settings("dev.sinty.desktop");
settings.changed["enabled-plugins"].connect(() => {
enable_configured_plugins();
if (SafeMode.get_default().allows(SafeFeature.PLUGINS))
enable_configured_plugins();
});
}

Expand Down Expand Up @@ -78,6 +79,7 @@ namespace Singularity {
*/

public void load_plugins() {
if (!SafeMode.get_default().allows(SafeFeature.PLUGINS)) return;
if (settings.get_strv("enabled-plugins").length == 0 &&
Environment.get_variable("SINGULARITY_PLUGIN_PATH") == null) {
return;
Expand Down Expand Up @@ -180,6 +182,7 @@ namespace Singularity {
}

private void enable_configured_plugins() {
if (!SafeMode.get_default().allows(SafeFeature.PLUGINS)) return;
if (settings.get_strv("enabled-plugins").length == 0 && !engine_ready) return;
ensure_engine();
var model = (GLib.ListModel)engine;
Expand All @@ -204,6 +207,7 @@ namespace Singularity {
}

private void update_plugin_state(string module_name, bool enabled) {
if (!SafeMode.get_default().allows(SafeFeature.PLUGINS)) return;
ensure_engine();
var info = engine.get_plugin_info(module_name);
if (info == null) return;
Expand Down
64 changes: 64 additions & 0 deletions src/core/safe_mode.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Singularity {

/** Optional startup features suppressed while recovering from a crash loop. */
public enum SafeFeature {
TILING,
PLUGINS,
CUSTOM_WIDGETS,
HAND_CONTROL,
SESSION_RESTORE,
AUTOSTART
}

/**
* Process-wide recovery policy.
*
* Safe mode deliberately leaves the normal GSettings backend in place so
* Settings can repair persisted values. Callers use allows() for runtime
* activation rather than replacing or rewriting the configured value.
*/
public class SafeMode : Object {
private static SafeMode? instance;

public bool active { get; private set; }
public string marker_path { get; private set; }
public string reason { get; private set; }

public static SafeMode get_default() {
if (instance == null) {
string? marker = Environment.get_variable(
"SINGULARITY_SAFE_MODE_MARKER");
if (marker == null || marker == "") {
marker = Path.build_filename(Environment.get_user_state_dir(),
"singularity", "safe-mode");
}
string? configured_reason = Environment.get_variable(
"SINGULARITY_SAFE_MODE_REASON");
instance = new SafeMode(
Environment.get_variable("SINGULARITY_SAFE_MODE") == "1",
marker,
configured_reason ?? "Repeated startup failures");
}
return instance;
}

public SafeMode(bool active, string marker_path,
string reason = "Repeated startup failures") {
this.active = active;
this.marker_path = marker_path;
this.reason = reason;
}

public bool allows(SafeFeature feature) {
return !active;
}

/** Remove the persistent recovery marker before ending this session. */
public bool clear_marker() {
if (!FileUtils.test(marker_path, FileTest.EXISTS)) return true;
if (FileUtils.unlink(marker_path) == 0) return true;
warning("SafeMode: could not remove recovery marker %s", marker_path);
return false;
}
}
}
16 changes: 13 additions & 3 deletions src/core/tiling_manager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ namespace Singularity {
private static TilingManager? instance;
private AppSystem app_system;
private GLib.Settings settings;
private SafeMode safe_mode;
private bool enabled = true;
private bool shell_overview_active = false;
private uint apply_timeout_id = 0;
Expand Down Expand Up @@ -140,9 +141,11 @@ namespace Singularity {
public TilingManager(AppSystem app_system) {
instance = this;
this.app_system = app_system;
safe_mode = SafeMode.get_default();
settings = new GLib.Settings("dev.sinty.desktop");
setup_close_gesture_indicator();
enabled = settings.get_boolean("tiling-enabled");
enabled = settings.get_boolean("tiling-enabled")
&& safe_mode.allows(SafeFeature.TILING);
if (scrolling_active()) {
foreach (var win in app_system.get_windows())
startup_windows.add(win);
Expand Down Expand Up @@ -180,7 +183,7 @@ namespace Singularity {
on_tiling_interaction, this);
Singularity.wayland_set_cursor_position_callback(
on_cursor_position, this);
sync_compositor_mode();
if (safe_mode.allows(SafeFeature.TILING)) sync_compositor_mode();
if (enabled) schedule_apply_layout();
}

Expand Down Expand Up @@ -238,11 +241,16 @@ namespace Singularity {
}

private void sync_compositor_mode() {
if (!safe_mode.allows(SafeFeature.TILING)) return;
Singularity.wayland_set_scrolling_mode(scrolling_active() ? 1u : 0u);
}

private void on_mode_changed() {
enabled = settings.get_boolean("tiling-enabled");
enabled = settings.get_boolean("tiling-enabled")
&& safe_mode.allows(SafeFeature.TILING);
// Persist configuration changes in recovery mode, but do not send
// any tiling protocol requests while the feature is blocked.
if (!safe_mode.allows(SafeFeature.TILING)) return;
bool is_scrolling = scrolling_active();
sync_compositor_mode();
hide_drop_preview();
Expand Down Expand Up @@ -395,6 +403,7 @@ namespace Singularity {
}

private void hide_drop_preview() {
if (!safe_mode.allows(SafeFeature.TILING)) return;
Singularity.wayland_set_tiling_drop_preview(0, 0, 0, 0, 0);
}

Expand Down Expand Up @@ -1709,6 +1718,7 @@ namespace Singularity {
}

public void apply_layout() {
if (!safe_mode.allows(SafeFeature.TILING)) return;
if (scrolling_active() && shell_overview_active) return;
var tileable = get_tileable_windows();
if (scrolling_active()) {
Expand Down
47 changes: 47 additions & 0 deletions tests/safe_mode_test.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Singularity;

private void test_policy() {
var normal = new SafeMode(false, "/tmp/not-used");
var safe = new SafeMode(true, "/tmp/not-used");
SafeFeature[] features = {
SafeFeature.TILING,
SafeFeature.PLUGINS,
SafeFeature.CUSTOM_WIDGETS,
SafeFeature.HAND_CONTROL,
SafeFeature.SESSION_RESTORE,
SafeFeature.AUTOSTART
};

foreach (var feature in features) {
assert(normal.allows(feature));
assert(!safe.allows(feature));
}
}

private void test_marker_clear() {
string dir;
try {
dir = DirUtils.make_tmp("singularity-safe-mode-test-XXXXXX");
} catch (FileError e) {
assert_not_reached();
}
string marker = Path.build_filename(dir, "safe-mode");
try {
FileUtils.set_contents(marker, "version=1\n");
} catch (FileError e) {
assert_not_reached();
}

var safe = new SafeMode(true, marker);
assert(safe.clear_marker());
assert(!FileUtils.test(marker, FileTest.EXISTS));
assert(safe.clear_marker());
DirUtils.remove(dir);
}

public int main(string[] args) {
Test.init(ref args);
Test.add_func("/safe-mode/policy", test_policy);
Test.add_func("/safe-mode/marker-clear", test_marker_clear);
return Test.run();
}