1+ using System ;
2+ using System . IO ;
3+ using System . Linq ;
4+ using System . Linq . Expressions ;
5+ using System . Threading ;
6+ using System . Collections . Generic ;
7+ using PatchKit . Unity . Patcher . Cancellation ;
8+ using PatchKit . Unity . Patcher . Debug ;
9+ using PatchKit . Unity . Patcher . AppUpdater . Commands ;
10+ using PatchKit . Unity . Patcher . AppUpdater . Status ;
11+ using PatchKit . Api . Models . Main ;
12+
13+ namespace PatchKit . Unity . Patcher . AppUpdater
14+ {
15+ public class AppRepairer
16+ {
17+ private static readonly DebugLogger DebugLogger = new DebugLogger ( typeof ( AppRepairer ) ) ;
18+
19+ public readonly AppUpdaterContext Context ;
20+
21+ // set to true if you wish to check file hashes
22+ public bool CheckHashes = false ;
23+
24+ // how many times process will repeat until it ultimately fails
25+ public int RepeatCount = 3 ;
26+
27+ private UpdaterStatus _status ;
28+
29+ private AppUpdaterStrategyResolver _strategyResolver ;
30+
31+ private AppUpdaterCommandFactory _commandFactory ;
32+
33+ private int _lowestVersionWithContentId ;
34+
35+ private const double IncreaseRepairCost = 1.5d ;
36+
37+
38+ public AppRepairer ( AppUpdaterContext context , UpdaterStatus status )
39+ {
40+ DebugLogger . LogConstructor ( ) ;
41+
42+ Checks . ArgumentNotNull ( context , "context" ) ;
43+
44+ Context = context ;
45+ _status = status ;
46+
47+ _strategyResolver = new AppUpdaterStrategyResolver ( _status ) ;
48+ _commandFactory = new AppUpdaterCommandFactory ( ) ;
49+ }
50+
51+ // returns true if data is valid (was valid from the start or successfull repair was performed)
52+ public bool Perform ( PatchKit . Unity . Patcher . Cancellation . CancellationToken cancellationToken )
53+ {
54+ _lowestVersionWithContentId = Context . App . GetLowestVersionWithContentId ( cancellationToken ) ;
55+
56+ for ( int attempt = 1 ; attempt <= RepeatCount ; ++ attempt )
57+ {
58+ DebugLogger . Log ( "Running integrity check, attempt " + attempt + " of " + RepeatCount ) ;
59+
60+ if ( PerformInternal ( cancellationToken ) )
61+ {
62+ return true ;
63+ }
64+ }
65+
66+ // retry count reached, let's check for the last time if data is ok, but without repairing
67+ int installedVersionId = Context . App . GetInstalledVersionId ( ) ;
68+ VersionIntegrity results = CheckIntegrity ( cancellationToken , installedVersionId ) ;
69+ var filesNeedFixing = FilesNeedFixing ( results ) ;
70+
71+ if ( filesNeedFixing . Count ( ) == 0 )
72+ {
73+ DebugLogger . Log ( "No missing or invalid size files." ) ;
74+ return true ;
75+ }
76+
77+
78+ DebugLogger . LogError ( "Still have corrupted files after all fixing attempts" ) ;
79+ return false ;
80+ }
81+
82+ // returns true if there was no integrity errors, false if there was and repair was performed
83+ private bool PerformInternal ( PatchKit . Unity . Patcher . Cancellation . CancellationToken cancellationToken )
84+ {
85+ int installedVersionId = Context . App . GetInstalledVersionId ( ) ;
86+
87+ VersionIntegrity results = CheckIntegrity ( cancellationToken , installedVersionId ) ;
88+ var filesNeedFixing = FilesNeedFixing ( results ) ;
89+
90+ if ( filesNeedFixing . Count ( ) == 0 )
91+ {
92+ DebugLogger . Log ( "No missing or invalid size files." ) ;
93+ return true ;
94+ }
95+
96+ // need to collect some data about the application to calculate the repair cost and make decisions
97+
98+ int latestVersionId = Context . App . GetLatestVersionId ( true , cancellationToken ) ;
99+
100+ AppContentSummary installedVersionContentSummary
101+ = Context . App . RemoteMetaData . GetContentSummary ( installedVersionId , cancellationToken ) ;
102+
103+ AppContentSummary latestVersionContentSummary
104+ = Context . App . RemoteMetaData . GetContentSummary ( latestVersionId , cancellationToken ) ;
105+
106+ bool isNewVersionAvailable = installedVersionId < latestVersionId ;
107+
108+ long contentSize = isNewVersionAvailable
109+ ? latestVersionContentSummary . Files . Sum ( f => f . Size )
110+ : installedVersionContentSummary . Files . Sum ( f => f . Size ) ;
111+
112+ double repairCost = CalculateRepairCost ( installedVersionContentSummary , filesNeedFixing ) ;
113+
114+ // increasing repair costs that reinstallation will be done for 1/3 of the content size
115+ repairCost *= IncreaseRepairCost ;
116+
117+
118+ if ( _lowestVersionWithContentId > installedVersionId )
119+ {
120+ DebugLogger . Log (
121+ "Repair is impossible because lowest version with content id is "
122+ + _lowestVersionWithContentId +
123+ " and currently installed version id is "
124+ + installedVersionId +
125+ ". Reinstalling." ) ;
126+
127+ ReinstallContent ( cancellationToken ) ;
128+ }
129+ else if ( repairCost < contentSize )
130+ {
131+ DebugLogger . Log ( string . Format ( "Repair cost {0} is smaller than content cost {1}, repairing..." , repairCost , contentSize ) ) ;
132+ IAppUpdaterStrategy repairStrategy = _strategyResolver . Create ( StrategyType . Repair , Context ) ;
133+ repairStrategy . Update ( cancellationToken ) ;
134+ }
135+ else
136+ {
137+ DebugLogger . Log ( string . Format ( "Content cost {0} is smaller than repair {1}. Reinstalling." , contentSize , repairCost ) ) ;
138+ ReinstallContent ( cancellationToken ) ;
139+ }
140+
141+ return false ;
142+ }
143+
144+ private VersionIntegrity CheckIntegrity (
145+ PatchKit . Unity . Patcher . Cancellation . CancellationToken cancellationToken ,
146+ int installedVersionId
147+ )
148+ {
149+ ICheckVersionIntegrityCommand checkIntegrity = _commandFactory
150+ . CreateCheckVersionIntegrityCommand (
151+ versionId : installedVersionId ,
152+ context : Context ,
153+ isCheckingHash : CheckHashes ,
154+ isCheckingSize : true ,
155+ cancellationToken : cancellationToken ) ;
156+
157+ checkIntegrity . Prepare ( _status , cancellationToken ) ;
158+ checkIntegrity . Execute ( cancellationToken ) ;
159+
160+ return checkIntegrity . Results ;
161+ }
162+
163+ private IEnumerable < FileIntegrity > FilesNeedFixing ( VersionIntegrity results )
164+ {
165+ var missingFiles = results . Files . Where ( f => f . Status == FileIntegrityStatus . MissingData ) ;
166+ var invalidSizeFiles = results . Files . Where ( f => f . Status == FileIntegrityStatus . InvalidSize ) ;
167+
168+ return missingFiles . Concat ( invalidSizeFiles ) ;
169+ }
170+
171+ private long CalculateRepairCost ( AppContentSummary contentSummary , IEnumerable < FileIntegrity > filesToRepair )
172+ {
173+ return filesToRepair
174+ . Select ( f => contentSummary . Files . FirstOrDefault ( e => e . Path == f . FileName ) )
175+ . Sum ( f => f . Size ) ;
176+ }
177+
178+ private void ReinstallContent ( PatchKit . Unity . Patcher . Cancellation . CancellationToken cancellationToken )
179+ {
180+ IUninstallCommand uninstall = _commandFactory . CreateUninstallCommand ( Context ) ;
181+ uninstall . Prepare ( _status , cancellationToken ) ;
182+ uninstall . Execute ( cancellationToken ) ;
183+
184+ // not catching any exceptions here, because exception during content installation in this place should be fatal
185+ var contentStrategy = new AppUpdaterContentStrategy ( Context , _status ) ;
186+ contentStrategy . RepairOnError = false ; // do not attempt to repair content to not cause a loop
187+ contentStrategy . Update ( cancellationToken ) ;
188+ }
189+ }
190+ }
0 commit comments