@@ -479,6 +479,12 @@ private async Task ExecuteStandardWorkflowAsync()
479479 _configInfo . LastVersion = downloadPlan . Assets . LastOrDefault ( ) ? . Version ;
480480 GeneralTracer . Info ( $ "ClientStrategy: Scenario={ scenario } , AssetCount={ downloadPlan . Assets . Count } ") ;
481481
482+ // Store original assets and CVP flag for chain fallback.
483+ // If the CVP download/apply fails, we rebuild the plan from cached chain
484+ // packages without a second server request.
485+ var isCvpAttempt = downloadPlan . Assets . Any ( a => a . IsCrossVersion ) ;
486+ var originalAssets = sourceResult . Assets . ToList ( ) ;
487+
482488 // Dispatch update info event with populated version data (full GeneralSpacestation-compatible fields)
483489 var versionInfos = downloadPlan . Assets . Select ( a => new VersionEntry
484490 {
@@ -561,78 +567,109 @@ private async Task ExecuteStandardWorkflowAsync()
561567
562568 _osStrategy ! . Create ( _configInfo ) ;
563569
564- // Download ALL packages via orchestrator (requirement 6: client downloads everything
565- // regardless of whether client or upgrade needs updating)
570+ // ════════════════════════════════════════════════════════════════════
571+ // Download + Apply with CVP fallback to chain.
572+ // If the CVP download OR apply fails, retry with chain packages
573+ // from the cached original response without a second server request.
574+ // ════════════════════════════════════════════════════════════════════
566575 var orchOptions = Download . Models . DownloadOrchestratorOptions . From ( _configInfo ) ;
567- GeneralTracer . Info ( $ "ClientStrategy: downloading { downloadPlan . Assets . Count } asset(s)." ) ;
568- if ( _orchestrator != null )
576+
577+ async Task ExecuteDownloadAsync ( Download . Models . DownloadPlan plan )
569578 {
570- await _orchestrator . ExecuteAsync ( downloadPlan , _configInfo . TempPath ) . ConfigureAwait ( false ) ;
579+ if ( _orchestrator != null )
580+ {
581+ await _orchestrator . ExecuteAsync ( plan , _configInfo . TempPath ) . ConfigureAwait ( false ) ;
582+ }
583+ else
584+ {
585+ var httpClient = GeneralUpdate . Core . Network . HttpClientProvider . Shared ;
586+ var orchestrator = new Download . Orchestrators . DefaultDownloadOrchestrator (
587+ httpClient , orchOptions , _customDownloadPolicy ,
588+ _customDownloadExecutor , _customDownloadPipelineFactory ) ;
589+ await orchestrator . ExecuteAsync ( plan , _configInfo . TempPath ) . ConfigureAwait ( false ) ;
590+ }
571591 }
572- else
592+
593+ // Download + report + build version lists + scenario dispatch
594+ async Task DownloadAndApplyAsync ( Download . Models . DownloadPlan plan , UpdateScenario sc )
573595 {
574- var httpClient = GeneralUpdate . Core . Network . HttpClientProvider . Shared ;
575- var orchestrator = new Download . Orchestrators . DefaultDownloadOrchestrator (
576- httpClient , orchOptions , _customDownloadPolicy ,
577- _customDownloadExecutor , _customDownloadPipelineFactory ) ;
578- await orchestrator . ExecuteAsync ( downloadPlan , _configInfo . TempPath ) . ConfigureAwait ( false ) ;
579- }
596+ GeneralTracer . Info ( $ "ClientStrategy: downloading { plan . Assets . Count } asset(s).") ;
597+ await ExecuteDownloadAsync ( plan ) . ConfigureAwait ( false ) ;
580598
581- await SafeReportDownloadCompletedAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
582- await SafeOnDownloadCompletedAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
599+ await SafeReportDownloadCompletedAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
600+ await SafeOnDownloadCompletedAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
583601
584- // Build VersionEntry list with AppType preserved from server response.
585- var downloadVersions = downloadPlan . Assets . Select ( a => new VersionEntry
586- {
587- RecordId = a . RecordId ,
588- Name = a . Name ,
589- Hash = a . SHA256 ,
590- Url = a . Url ,
591- Version = a . Version ,
592- Format = _configInfo . Format . ToExtension ( ) ,
593- AppType = a . AppType ?? ( int ) AppType . Client
594- } ) . ToList ( ) ;
602+ // Build VersionEntry list with AppType preserved from server response.
603+ var dVersions = plan . Assets . Select ( a => new VersionEntry
604+ {
605+ RecordId = a . RecordId ,
606+ Name = a . Name ,
607+ Hash = a . SHA256 ,
608+ Url = a . Url ,
609+ Version = a . Version ,
610+ Format = _configInfo . Format . ToExtension ( ) ,
611+ AppType = a . AppType ?? ( int ) AppType . Client
612+ } ) . ToList ( ) ;
613+
614+ var uVersions = dVersions . Where ( v => v . AppType == ( int ) AppType . Upgrade ) . ToList ( ) ;
615+ var cVersions = dVersions . Where ( v => v . AppType == ( int ) AppType . Client ) . ToList ( ) ;
616+ GeneralTracer . Info (
617+ $ "ClientStrategy: Upgrade packages={ uVersions . Count } , MainApp packages={ cVersions . Count } ") ;
595618
596- var upgradeVersions = downloadVersions . Where ( v => v . AppType == ( int ) AppType . Upgrade ) . ToList ( ) ;
597- var clientVersions = downloadVersions . Where ( v => v . AppType == ( int ) AppType . Client ) . ToList ( ) ;
598- GeneralTracer . Info (
599- $ "ClientStrategy: Upgrade packages={ upgradeVersions . Count } , MainApp packages={ clientVersions . Count } ") ;
619+ // ── Dispatch by scenario ──
620+ switch ( sc )
621+ {
622+ case UpdateScenario . UpgradeOnly :
623+ await ApplyUpgradePackagesAsync ( uVersions ) . ConfigureAwait ( false ) ;
624+ await SafeOnAfterUpdateAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
625+ await SafeReportUpdateAppliedAsync ( hooksCtx , _upgradeRecordId ) . ConfigureAwait ( false ) ;
626+ GeneralTracer . Info ( "ClientStrategy: Upgrade-only update applied, client continues running." ) ;
627+ break ;
628+
629+ case UpdateScenario . MainOnly :
630+ SendProcessIpc ( cVersions ) ;
631+ await SafeOnAfterUpdateAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
632+ await SafeReportUpdateAppliedAsync ( hooksCtx , _mainRecordId ) . ConfigureAwait ( false ) ;
633+ if ( LaunchAfterPrepare )
634+ {
635+ await SafeOnBeforeStartAppAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
636+ await LaunchUpgradeProcessAsync ( ) . ConfigureAwait ( false ) ;
637+ }
638+ break ;
639+
640+ case UpdateScenario . Both :
641+ await ApplyUpgradePackagesAsync ( uVersions ) . ConfigureAwait ( false ) ;
642+ await SafeOnAfterUpdateAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
643+ await SafeReportUpdateAppliedAsync ( hooksCtx , _upgradeRecordId ) . ConfigureAwait ( false ) ;
644+ SendProcessIpc ( cVersions ) ;
645+ if ( LaunchAfterPrepare )
646+ {
647+ await SafeOnBeforeStartAppAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
648+ await LaunchUpgradeProcessAsync ( ) . ConfigureAwait ( false ) ;
649+ }
650+ break ;
651+ case UpdateScenario . None :
652+ default :
653+ throw new InvalidOperationException ( $ "Unhandled update scenario: { sc } ") ;
654+ }
655+ }
600656
601- // ── Dispatch by scenario — one switch, four states, zero nested if-else ──
602- switch ( scenario )
657+ try
603658 {
604- case UpdateScenario . UpgradeOnly :
605- await ApplyUpgradePackagesAsync ( upgradeVersions ) . ConfigureAwait ( false ) ;
606- await SafeOnAfterUpdateAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
607- await SafeReportUpdateAppliedAsync ( hooksCtx , _upgradeRecordId ) . ConfigureAwait ( false ) ;
608- GeneralTracer . Info ( "ClientStrategy: Upgrade-only update applied, client continues running." ) ;
609- break ;
610-
611- case UpdateScenario . MainOnly :
612- SendProcessIpc ( clientVersions ) ;
613- await SafeOnAfterUpdateAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
614- await SafeReportUpdateAppliedAsync ( hooksCtx , _mainRecordId ) . ConfigureAwait ( false ) ;
615- if ( LaunchAfterPrepare )
616- {
617- await SafeOnBeforeStartAppAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
618- await LaunchUpgradeProcessAsync ( ) . ConfigureAwait ( false ) ;
619- }
620- break ;
621-
622- case UpdateScenario . Both :
623- await ApplyUpgradePackagesAsync ( upgradeVersions ) . ConfigureAwait ( false ) ;
624- await SafeOnAfterUpdateAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
625- await SafeReportUpdateAppliedAsync ( hooksCtx , _upgradeRecordId ) . ConfigureAwait ( false ) ;
626- SendProcessIpc ( clientVersions ) ;
627- if ( LaunchAfterPrepare )
628- {
629- await SafeOnBeforeStartAppAsync ( hooksCtx ) . ConfigureAwait ( false ) ;
630- await LaunchUpgradeProcessAsync ( ) . ConfigureAwait ( false ) ;
631- }
632- break ;
633- case UpdateScenario . None :
634- default :
635- throw new InvalidOperationException ( $ "Unhandled update scenario: { scenario } ") ;
659+ await DownloadAndApplyAsync ( downloadPlan , scenario ) . ConfigureAwait ( false ) ;
660+ }
661+ catch ( Exception ex ) when ( isCvpAttempt && ex is not OperationCanceledException )
662+ {
663+ GeneralTracer . Warn ( $ "ClientStrategy: CVP attempt failed, falling back to chain packages. { ex . Message } ") ;
664+ var fallback = BuildChainFallback ( originalAssets , localClientVersion , resolvedUpgradeVersion ) ;
665+ if ( fallback == null )
666+ throw new InvalidOperationException (
667+ "CVP failed and no chain packages are available for fallback." , ex ) ;
668+
669+ ( downloadPlan , scenario ) = fallback . Value ;
670+ UpdateRecordIdsFromPlan ( downloadPlan ) ;
671+ GeneralTracer . Info ( $ "ClientStrategy: retrying with { downloadPlan . Assets . Count } chain asset(s), Scenario={ scenario } ") ;
672+ await DownloadAndApplyAsync ( downloadPlan , scenario ) . ConfigureAwait ( false ) ;
636673 }
637674 }
638675
@@ -1122,5 +1159,45 @@ await Reporter
11221159 }
11231160 }
11241161
1162+ /// <summary>
1163+ /// Builds a chain-only fallback plan from the cached original server response,
1164+ /// excluding any CVP assets. Returns null when no chain packages are available.
1165+ /// </summary>
1166+ private ( Download . Models . DownloadPlan Plan , UpdateScenario Scenario ) ? BuildChainFallback (
1167+ List < Download . Models . DownloadAsset > originalAssets ,
1168+ string localClientVersion ,
1169+ string ? resolvedUpgradeVersion )
1170+ {
1171+ var chainAssets = originalAssets . Where ( a => ! a . IsCrossVersion ) . ToList ( ) ;
1172+ var plan = DownloadPlanBuilder . Build ( chainAssets , localClientVersion , resolvedUpgradeVersion ) ;
1173+ if ( ! plan . HasAssets ) return null ;
1174+
1175+ _configInfo . LastVersion = plan . Assets . LastOrDefault ( ) ? . Version ;
1176+ _configInfo . IsMainUpdate = DownloadPlanBuilder . HasUpdate ( chainAssets , AppType . Client , localClientVersion ) ;
1177+ _configInfo . IsUpgradeUpdate = DownloadPlanBuilder . HasUpdate ( chainAssets , AppType . Upgrade , resolvedUpgradeVersion ) ;
1178+
1179+ var sc = ( _configInfo . IsMainUpdate , _configInfo . IsUpgradeUpdate ) switch
1180+ {
1181+ ( false , true ) => UpdateScenario . UpgradeOnly ,
1182+ ( true , false ) => UpdateScenario . MainOnly ,
1183+ ( true , true ) => UpdateScenario . Both ,
1184+ _ => UpdateScenario . None
1185+ } ;
1186+
1187+ return ( plan , sc ) ;
1188+ }
1189+
1190+ /// <summary>
1191+ /// Updates <see cref="_mainRecordId"/> and <see cref="_upgradeRecordId"/> from the
1192+ /// current download plan so status reports use correct record identifiers after fallback.
1193+ /// </summary>
1194+ private void UpdateRecordIdsFromPlan ( Download . Models . DownloadPlan plan )
1195+ {
1196+ _mainRecordId = plan . Assets
1197+ . FirstOrDefault ( a => ( a . AppType ?? ( int ) AppType . Client ) == ( int ) AppType . Client ) ? . RecordId ?? 0 ;
1198+ _upgradeRecordId = plan . Assets
1199+ . FirstOrDefault ( a => a . AppType == ( int ) AppType . Upgrade ) ? . RecordId ?? 0 ;
1200+ }
1201+
11251202 #endregion
11261203}
0 commit comments