11using System ;
22using System . Collections . Generic ;
3- using System . Diagnostics ;
43using System . IO ;
54using System . Threading ;
65using System . Threading . Tasks ;
@@ -44,6 +43,10 @@ public partial class App : Application
4443 private readonly ILogger < App > _logger ;
4544 private readonly IUriHandler _uriHandler ;
4645
46+ private readonly ISettingsManager < CoderConnectSettings > _settingsManager ;
47+
48+ private readonly IHostApplicationLifetime _appLifetime ;
49+
4750 public App ( )
4851 {
4952 var builder = Host . CreateApplicationBuilder ( ) ;
@@ -90,6 +93,13 @@ public App()
9093 // FileSyncListMainPage is created by FileSyncListWindow.
9194 services . AddTransient < FileSyncListWindow > ( ) ;
9295
96+ services . AddSingleton < ISettingsManager < CoderConnectSettings > , SettingsManager < CoderConnectSettings > > ( ) ;
97+ services . AddSingleton < IStartupManager , StartupManager > ( ) ;
98+ // SettingsWindow views and view models
99+ services . AddTransient < SettingsViewModel > ( ) ;
100+ // SettingsMainPage is created by SettingsWindow.
101+ services . AddTransient < SettingsWindow > ( ) ;
102+
93103 // DirectoryPickerWindow views and view models are created by FileSyncListViewModel.
94104
95105 // TrayWindow views and view models
@@ -107,8 +117,10 @@ public App()
107117 services . AddTransient < TrayWindow > ( ) ;
108118
109119 _services = services . BuildServiceProvider ( ) ;
110- _logger = ( ILogger < App > ) _services . GetService ( typeof ( ILogger < App > ) ) ! ;
111- _uriHandler = ( IUriHandler ) _services . GetService ( typeof ( IUriHandler ) ) ! ;
120+ _logger = _services . GetRequiredService < ILogger < App > > ( ) ;
121+ _uriHandler = _services . GetRequiredService < IUriHandler > ( ) ;
122+ _settingsManager = _services . GetRequiredService < ISettingsManager < CoderConnectSettings > > ( ) ;
123+ _appLifetime = _services . GetRequiredService < IHostApplicationLifetime > ( ) ;
112124
113125 InitializeComponent ( ) ;
114126 }
@@ -129,58 +141,8 @@ public async Task ExitApplication()
129141 protected override void OnLaunched ( LaunchActivatedEventArgs args )
130142 {
131143 _logger . LogInformation ( "new instance launched" ) ;
132- // Start connecting to the manager in the background.
133- var rpcController = _services . GetRequiredService < IRpcController > ( ) ;
134- if ( rpcController . GetState ( ) . RpcLifecycle == RpcLifecycle . Disconnected )
135- // Passing in a CT with no cancellation is desired here, because
136- // the named pipe open will block until the pipe comes up.
137- _logger . LogDebug ( "reconnecting with VPN service" ) ;
138- _ = rpcController . Reconnect ( CancellationToken . None ) . ContinueWith ( t =>
139- {
140- if ( t . Exception != null )
141- {
142- _logger . LogError ( t . Exception , "failed to connect to VPN service" ) ;
143- #if DEBUG
144- Debug . WriteLine ( t . Exception ) ;
145- Debugger . Break ( ) ;
146- #endif
147- }
148- } ) ;
149-
150- // Load the credentials in the background.
151- var credentialManagerCts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 15 ) ) ;
152- var credentialManager = _services . GetRequiredService < ICredentialManager > ( ) ;
153- _ = credentialManager . LoadCredentials ( credentialManagerCts . Token ) . ContinueWith ( t =>
154- {
155- if ( t . Exception != null )
156- {
157- _logger . LogError ( t . Exception , "failed to load credentials" ) ;
158- #if DEBUG
159- Debug . WriteLine ( t . Exception ) ;
160- Debugger . Break ( ) ;
161- #endif
162- }
163144
164- credentialManagerCts . Dispose ( ) ;
165- } , CancellationToken . None ) ;
166-
167- // Initialize file sync.
168- // We're adding a 5s delay here to avoid race conditions when loading the mutagen binary.
169-
170- _ = Task . Delay ( 5000 ) . ContinueWith ( ( _ ) =>
171- {
172- var syncSessionCts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 10 ) ) ;
173- var syncSessionController = _services . GetRequiredService < ISyncSessionController > ( ) ;
174- syncSessionController . RefreshState ( syncSessionCts . Token ) . ContinueWith (
175- t =>
176- {
177- if ( t . IsCanceled || t . Exception != null )
178- {
179- _logger . LogError ( t . Exception , "failed to refresh sync state (canceled = {canceled})" , t . IsCanceled ) ;
180- }
181- syncSessionCts . Dispose ( ) ;
182- } , CancellationToken . None ) ;
183- } ) ;
145+ _ = InitializeServicesAsync ( _appLifetime . ApplicationStopping ) ;
184146
185147 // Prevent the TrayWindow from closing, just hide it.
186148 var trayWindow = _services . GetRequiredService < TrayWindow > ( ) ;
@@ -192,6 +154,74 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
192154 } ;
193155 }
194156
157+ /// <summary>
158+ /// Loads stored VPN credentials, reconnects the RPC controller,
159+ /// and (optionally) starts the VPN tunnel on application launch.
160+ /// </summary>
161+ private async Task InitializeServicesAsync ( CancellationToken cancellationToken = default )
162+ {
163+ var credentialManager = _services . GetRequiredService < ICredentialManager > ( ) ;
164+ var rpcController = _services . GetRequiredService < IRpcController > ( ) ;
165+
166+ using var credsCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
167+ credsCts . CancelAfter ( TimeSpan . FromSeconds ( 15 ) ) ;
168+
169+ var loadCredsTask = credentialManager . LoadCredentials ( credsCts . Token ) ;
170+ var reconnectTask = rpcController . Reconnect ( cancellationToken ) ;
171+ var settingsTask = _settingsManager . Read ( cancellationToken ) ;
172+
173+ var dependenciesLoaded = true ;
174+
175+ try
176+ {
177+ await Task . WhenAll ( loadCredsTask , reconnectTask , settingsTask ) ;
178+ }
179+ catch ( Exception )
180+ {
181+ if ( loadCredsTask . IsFaulted )
182+ _logger . LogError ( loadCredsTask . Exception ! . GetBaseException ( ) ,
183+ "Failed to load credentials" ) ;
184+
185+ if ( reconnectTask . IsFaulted )
186+ _logger . LogError ( reconnectTask . Exception ! . GetBaseException ( ) ,
187+ "Failed to connect to VPN service" ) ;
188+
189+ if ( settingsTask . IsFaulted )
190+ _logger . LogError ( settingsTask . Exception ! . GetBaseException ( ) ,
191+ "Failed to fetch Coder Connect settings" ) ;
192+
193+ // Don't attempt to connect if we failed to load credentials or reconnect.
194+ // This will prevent the app from trying to connect to the VPN service.
195+ dependenciesLoaded = false ;
196+ }
197+
198+ var attemptCoderConnection = settingsTask . Result ? . ConnectOnLaunch ?? false ;
199+ if ( dependenciesLoaded && attemptCoderConnection )
200+ {
201+ try
202+ {
203+ await rpcController . StartVpn ( cancellationToken ) ;
204+ }
205+ catch ( Exception ex )
206+ {
207+ _logger . LogError ( ex , "Failed to connect on launch" ) ;
208+ }
209+ }
210+
211+ // Initialize file sync.
212+ using var syncSessionCts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
213+ syncSessionCts . CancelAfter ( TimeSpan . FromSeconds ( 10 ) ) ;
214+ var syncSessionController = _services . GetRequiredService < ISyncSessionController > ( ) ;
215+ try
216+ {
217+ await syncSessionController . RefreshState ( syncSessionCts . Token ) ;
218+ }
219+ catch ( Exception ex )
220+ {
221+ _logger . LogError ( $ "Failed to refresh sync session state { ex . Message } ", ex ) ;
222+ }
223+ }
224+
195225 public void OnActivated ( object ? sender , AppActivationArguments args )
196226 {
197227 switch ( args . Kind )
0 commit comments