Skip to content

Commit e7a9d70

Browse files
committed
added StartupManager to handle auto-start
changed order of CredentialManager loading
1 parent c2c3d51 commit e7a9d70

File tree

7 files changed

+120
-44
lines changed

7 files changed

+120
-44
lines changed

App/App.xaml.cs

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,25 @@ public async Task ExitApplication()
138138
protected override void OnLaunched(LaunchActivatedEventArgs args)
139139
{
140140
_logger.LogInformation("new instance launched");
141+
142+
// Load the credentials in the background.
143+
var credentialManagerCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
144+
var credentialManager = _services.GetRequiredService<ICredentialManager>();
145+
credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t =>
146+
{
147+
if (t.Exception != null)
148+
{
149+
_logger.LogError(t.Exception, "failed to load credentials");
150+
#if DEBUG
151+
Debug.WriteLine(t.Exception);
152+
Debugger.Break();
153+
#endif
154+
}
155+
156+
credentialManagerCts.Dispose();
157+
});
158+
159+
141160
// Start connecting to the manager in the background.
142161
var rpcController = _services.GetRequiredService<IRpcController>();
143162
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
@@ -155,7 +174,7 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
155174
#endif
156175
} else
157176
{
158-
if (rpcController.GetState().RpcLifecycle == RpcLifecycle.Disconnected)
177+
if (rpcController.GetState().VpnLifecycle == VpnLifecycle.Stopped)
159178
{
160179
if (_settingsManager.Read(SettingsManager.ConnectOnLaunchKey, false))
161180
{
@@ -172,23 +191,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
172191
}
173192
});
174193

175-
// Load the credentials in the background.
176-
var credentialManagerCts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
177-
var credentialManager = _services.GetRequiredService<ICredentialManager>();
178-
_ = credentialManager.LoadCredentials(credentialManagerCts.Token).ContinueWith(t =>
179-
{
180-
if (t.Exception != null)
181-
{
182-
_logger.LogError(t.Exception, "failed to load credentials");
183-
#if DEBUG
184-
Debug.WriteLine(t.Exception);
185-
Debugger.Break();
186-
#endif
187-
}
188-
189-
credentialManagerCts.Dispose();
190-
}, CancellationToken.None);
191-
192194
// Initialize file sync.
193195
var syncSessionCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
194196
var syncSessionController = _services.GetRequiredService<ISyncSessionController>();

App/Services/SettingsManager.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4-
using System.Linq;
5-
using System.Text;
64
using System.Text.Json;
7-
using System.Threading.Tasks;
85

96
namespace Coder.Desktop.App.Services;
107
/// <summary>

App/Services/StartupManager.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Microsoft.Win32;
2+
using System;
3+
using System.Diagnostics;
4+
using System.Security;
5+
6+
namespace Coder.Desktop.App.Services;
7+
public static class StartupManager
8+
{
9+
private const string RunKey = @"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
10+
private const string PoliciesExplorerUser = @"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer";
11+
private const string PoliciesExplorerMachine = @"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer";
12+
private const string DisableCurrentUserRun = "DisableCurrentUserRun";
13+
private const string DisableLocalMachineRun = "DisableLocalMachineRun";
14+
15+
private const string _defaultValueName = "CoderDesktopApp";
16+
17+
/// <summary>
18+
/// Adds the current executable to the per‑user Run key. Returns <c>true</c> if successful.
19+
/// Fails (returns <c>false</c>) when blocked by policy or lack of permissions.
20+
/// </summary>
21+
public static bool Enable()
22+
{
23+
if (IsDisabledByPolicy())
24+
return false;
25+
26+
string exe = Process.GetCurrentProcess().MainModule!.FileName;
27+
try
28+
{
29+
using var key = Registry.CurrentUser.OpenSubKey(RunKey, writable: true)
30+
?? Registry.CurrentUser.CreateSubKey(RunKey)!;
31+
key.SetValue(_defaultValueName, $"\"{exe}\"");
32+
return true;
33+
}
34+
catch (UnauthorizedAccessException) { return false; }
35+
catch (SecurityException) { return false; }
36+
}
37+
38+
/// <summary>Removes the value from the Run key (no-op if missing).</summary>
39+
public static void Disable()
40+
{
41+
using var key = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
42+
key?.DeleteValue(_defaultValueName, throwOnMissingValue: false);
43+
}
44+
45+
/// <summary>Checks whether the value exists in the Run key.</summary>
46+
public static bool IsEnabled()
47+
{
48+
using var key = Registry.CurrentUser.OpenSubKey(RunKey);
49+
return key?.GetValue(_defaultValueName) != null;
50+
}
51+
52+
/// <summary>
53+
/// Detects whether group policy disables per‑user startup programs.
54+
/// Mirrors <see cref="Windows.ApplicationModel.StartupTaskState.DisabledByPolicy"/>.
55+
/// </summary>
56+
public static bool IsDisabledByPolicy()
57+
{
58+
// User policy – HKCU
59+
using (var keyUser = Registry.CurrentUser.OpenSubKey(PoliciesExplorerUser))
60+
{
61+
if ((int?)keyUser?.GetValue(DisableCurrentUserRun) == 1) return true;
62+
}
63+
// Machine policy – HKLM
64+
using (var keyMachine = Registry.LocalMachine.OpenSubKey(PoliciesExplorerMachine))
65+
{
66+
if ((int?)keyMachine?.GetValue(DisableLocalMachineRun) == 1) return true;
67+
}
68+
69+
// Some non‑desktop SKUs report DisabledByPolicy implicitly
70+
return false;
71+
}
72+
}
73+

App/ViewModels/SettingsViewModel.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,22 @@
11
using Coder.Desktop.App.Services;
22
using CommunityToolkit.Mvvm.ComponentModel;
3-
using CommunityToolkit.Mvvm.Input;
43
using Microsoft.Extensions.Logging;
54
using Microsoft.UI.Dispatching;
65
using Microsoft.UI.Xaml;
7-
using Microsoft.UI.Xaml.Controls;
86
using System;
97

108
namespace Coder.Desktop.App.ViewModels;
119

1210
public partial class SettingsViewModel : ObservableObject
1311
{
14-
private Window? _window;
15-
private DispatcherQueue? _dispatcherQueue;
16-
1712
private readonly ILogger<SettingsViewModel> _logger;
1813

1914
[ObservableProperty]
2015
public partial bool ConnectOnLaunch { get; set; } = false;
2116

17+
[ObservableProperty]
18+
public partial bool StartOnLoginDisabled { get; set; } = false;
19+
2220
[ObservableProperty]
2321
public partial bool StartOnLogin { get; set; } = false;
2422

@@ -31,6 +29,10 @@ public SettingsViewModel(ILogger<SettingsViewModel> logger, ISettingsManager set
3129
ConnectOnLaunch = _settingsManager.Read(SettingsManager.ConnectOnLaunchKey, false);
3230
StartOnLogin = _settingsManager.Read(SettingsManager.StartOnLoginKey, false);
3331

32+
// Various policies can disable the "Start on login" option.
33+
// We disable the option in the UI if the policy is set.
34+
StartOnLoginDisabled = StartupManager.IsDisabledByPolicy();
35+
3436
this.PropertyChanged += (_, args) =>
3537
{
3638
if (args.PropertyName == nameof(ConnectOnLaunch))
@@ -41,28 +43,34 @@ public SettingsViewModel(ILogger<SettingsViewModel> logger, ISettingsManager set
4143
}
4244
catch (Exception ex)
4345
{
44-
Console.WriteLine($"Error saving {SettingsManager.ConnectOnLaunchKey} setting: {ex.Message}");
46+
_logger.LogError($"Error saving {SettingsManager.ConnectOnLaunchKey} setting: {ex.Message}");
4547
}
4648
}
4749
else if (args.PropertyName == nameof(StartOnLogin))
4850
{
4951
try
5052
{
5153
_settingsManager.Save(SettingsManager.StartOnLoginKey, StartOnLogin);
54+
if (StartOnLogin)
55+
{
56+
StartupManager.Enable();
57+
}
58+
else
59+
{
60+
StartupManager.Disable();
61+
}
5262
}
5363
catch (Exception ex)
5464
{
55-
Console.WriteLine($"Error saving {SettingsManager.StartOnLoginKey} setting: {ex.Message}");
65+
_logger.LogError($"Error saving {SettingsManager.StartOnLoginKey} setting: {ex.Message}");
5666
}
5767
}
5868
};
59-
}
6069

61-
public void Initialize(Window window, DispatcherQueue dispatcherQueue)
62-
{
63-
_window = window;
64-
_dispatcherQueue = dispatcherQueue;
65-
if (!_dispatcherQueue.HasThreadAccess)
66-
throw new InvalidOperationException("Initialize must be called from the UI thread");
70+
// Ensure the StartOnLogin property matches the current startup state.
71+
if (StartOnLogin != StartupManager.IsEnabled())
72+
{
73+
StartOnLogin = StartupManager.IsEnabled();
74+
}
6775
}
6876
}

App/Views/Pages/SettingsMainPage.xaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,16 @@
3939

4040
<controls:SettingsCard Description="This setting controls whether the Coder Desktop app starts on Windows startup."
4141
Header="Start on login"
42-
HeaderIcon="{ui:FontIcon Glyph=&#xE819;}">
42+
HeaderIcon="{ui:FontIcon Glyph=&#xE819;}"
43+
IsEnabled="{x:Bind ViewModel.StartOnLoginDisabled, Converter={StaticResource InverseBoolConverter}, Mode=OneWay}">
4344
<ToggleSwitch IsOn="{x:Bind ViewModel.StartOnLogin, Mode=TwoWay}" />
4445
</controls:SettingsCard>
4546

4647
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="Coder Connect" />
47-
<controls:SettingsCard Description="This setting controls whether Coder Connect automatically starts with Coder Desktop "
48+
<controls:SettingsCard Description="This setting controls whether Coder Connect automatically starts with Coder Desktop. "
4849
Header="Connect on launch"
49-
HeaderIcon="{ui:FontIcon Glyph=&#xE8AF;}">
50+
HeaderIcon="{ui:FontIcon Glyph=&#xE8AF;}"
51+
>
5052
<ToggleSwitch IsOn="{x:Bind ViewModel.ConnectOnLaunch, Mode=TwoWay}" />
5153
</controls:SettingsCard>
5254
</StackPanel>

App/Views/SettingsWindow.xaml.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ public SettingsWindow(SettingsViewModel viewModel)
1818

1919
SystemBackdrop = new DesktopAcrylicBackdrop();
2020

21-
ViewModel.Initialize(this, DispatcherQueue);
2221
RootFrame.Content = new SettingsMainPage(ViewModel);
2322

2423
this.CenterOnScreen();

Tests.App/Services/SettingsManagerTest.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
using Coder.Desktop.App.Services;
2-
using System;
3-
using System.Collections.Generic;
4-
using System.Linq;
5-
using System.Text;
6-
using System.Threading.Tasks;
72

83
namespace Coder.Desktop.Tests.App.Services
94
{

0 commit comments

Comments
 (0)