@@ -147,12 +147,16 @@ private async Task InitializeServicesAsync()
147147
148148 var credentialManager = _services . GetRequiredService < ICredentialManager > ( ) ;
149149 var rpcController = _services . GetRequiredService < IRpcController > ( ) ;
150+ var settingsManager = _services . GetRequiredService < ISettingsManager < CoderConnectSettings > > ( ) ;
150151
151152 AppBootstrapLogger . Info ( "Initializing credentials and RPC connection..." ) ;
152153
153- using var credentialLoadCts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 15 ) ) ;
154+ var appStopping = _hostApplicationLifetime ? . ApplicationStopping ?? CancellationToken . None ;
155+ using var credentialLoadCts = CancellationTokenSource . CreateLinkedTokenSource ( appStopping ) ;
156+ credentialLoadCts . CancelAfter ( TimeSpan . FromSeconds ( 15 ) ) ;
157+
154158 var loadCredentialsTask = credentialManager . LoadCredentials ( credentialLoadCts . Token ) ;
155- var reconnectTask = rpcController . Reconnect ( ) ;
159+ var reconnectTask = ReconnectWithStartupRetryAsync ( rpcController , appStopping ) ;
156160
157161 try
158162 {
@@ -164,11 +168,91 @@ private async Task InitializeServicesAsync()
164168 if ( loadCredentialsTask . IsFaulted )
165169 AppBootstrapLogger . Error ( "Credential initialization failed" , loadCredentialsTask . Exception ? . GetBaseException ( ) ) ;
166170
171+ // reconnectTask logs its own errors internally; just note if it threw here
167172 if ( reconnectTask . IsFaulted )
168- AppBootstrapLogger . Error ( "RPC reconnect failed" , reconnectTask . Exception ? . GetBaseException ( ) ) ;
169- else if ( reconnectTask . IsCanceled )
170- AppBootstrapLogger . Warn ( "RPC reconnect canceled" ) ;
173+ AppBootstrapLogger . Error ( "Startup reconnect failed unexpectedly" , reconnectTask . Exception ? . GetBaseException ( ) ) ;
174+ }
175+
176+ var reconnectSucceeded = reconnectTask is { IsCompletedSuccessfully : true , Result : true } ;
177+
178+ if ( ! reconnectSucceeded )
179+ AppBootstrapLogger . Warn ( "Startup continuing in disconnected state after retry exhaustion" ) ;
180+
181+ try
182+ {
183+ await MaybeAutoStartVpnOnLaunchAsync ( settingsManager , credentialManager , rpcController , reconnectSucceeded , appStopping ) ;
184+ }
185+ catch ( Exception ex )
186+ {
187+ AppBootstrapLogger . Error ( "ConnectOnLaunch failed" , ex ) ;
188+ }
189+ }
190+
191+ private async Task < bool > ReconnectWithStartupRetryAsync ( IRpcController rpcController , CancellationToken ct )
192+ {
193+ TimeSpan [ ] delays =
194+ [
195+ TimeSpan . Zero ,
196+ TimeSpan . FromSeconds ( 1 ) ,
197+ TimeSpan . FromSeconds ( 2 ) ,
198+ TimeSpan . FromSeconds ( 4 ) ,
199+ TimeSpan . FromSeconds ( 8 ) ,
200+ ] ;
201+
202+ Exception ? lastError = null ;
203+
204+ for ( var attempt = 0 ; attempt < delays . Length ; attempt ++ )
205+ {
206+ if ( attempt > 0 )
207+ await Task . Delay ( delays [ attempt ] , ct ) ;
208+
209+ try
210+ {
211+ await rpcController . Reconnect ( ct ) ;
212+ AppBootstrapLogger . Info ( $ "RPC reconnect succeeded on attempt { attempt + 1 } /{ delays . Length } ") ;
213+ return true ;
214+ }
215+ catch ( Exception ex ) when ( ! ct . IsCancellationRequested )
216+ {
217+ lastError = ex ;
218+ AppBootstrapLogger . Warn ( $ "RPC reconnect attempt { attempt + 1 } /{ delays . Length } failed: { ex . Message } ") ;
219+ }
171220 }
221+
222+ AppBootstrapLogger . Error ( "RPC reconnect exhausted startup retries" , lastError ) ;
223+ return false ;
224+ }
225+
226+ private async Task MaybeAutoStartVpnOnLaunchAsync (
227+ ISettingsManager < CoderConnectSettings > settingsManager ,
228+ ICredentialManager credentialManager ,
229+ IRpcController rpcController ,
230+ bool reconnectSucceeded ,
231+ CancellationToken ct )
232+ {
233+ var settings = await settingsManager . Read ( ct ) ;
234+ if ( ! settings . ConnectOnLaunch )
235+ return ;
236+
237+ if ( ! reconnectSucceeded )
238+ {
239+ AppBootstrapLogger . Info ( "ConnectOnLaunch skipped because startup reconnect did not succeed" ) ;
240+ return ;
241+ }
242+
243+ var creds = credentialManager . GetCachedCredentials ( ) ;
244+ var rpc = rpcController . GetState ( ) ;
245+
246+ if ( creds . State != CredentialState . Valid ||
247+ rpc . RpcLifecycle != RpcLifecycle . Connected ||
248+ rpc . VpnLifecycle != VpnLifecycle . Stopped )
249+ {
250+ AppBootstrapLogger . Info ( $ "ConnectOnLaunch skipped (cred={ creds . State } , rpc={ rpc . RpcLifecycle } , vpn={ rpc . VpnLifecycle } )") ;
251+ return ;
252+ }
253+
254+ await rpcController . StartVpn ( ct ) ;
255+ AppBootstrapLogger . Info ( "ConnectOnLaunch started VPN successfully" ) ;
172256 }
173257
174258 private void ConfigureTrayIcons ( TrayIconViewModel trayIconViewModel )
0 commit comments