@@ -23,20 +23,19 @@ namespace SecondStageUpdater;
23
23
using System . Reflection ;
24
24
using System . Runtime . InteropServices ;
25
25
using System . Threading ;
26
+ using System . Threading . Tasks ;
26
27
using Rampastring . Tools ;
27
28
28
29
internal sealed class Program
29
30
{
30
31
private const int MutexTimeoutInSeconds = 30 ;
32
+ private const int MaxCopyAttempts = 5 ;
33
+ private const int CopyRetryWaitMilliseconds = 500 ;
31
34
32
- private static ConsoleColor defaultColor ;
33
- private static bool hasHandle ;
34
- private static Mutex clientMutex ;
35
+ private static readonly object consoleMessageLock = new ( ) ;
35
36
36
- private static void Main ( string [ ] args )
37
+ private static async Task Main ( string [ ] args )
37
38
{
38
- defaultColor = Console . ForegroundColor ;
39
-
40
39
try
41
40
{
42
41
Write ( "CnCNet Client Second-Stage Updater" , ConsoleColor . Green ) ;
@@ -61,7 +60,8 @@ private static void Main(string[] args)
61
60
62
61
string clientMutexId = FormattableString . Invariant ( $ "Global{ Guid . Parse ( "1CC9F8E7-9F69-4BBC-B045-E734204027A9" ) } ") ;
63
62
64
- clientMutex = new ( false , clientMutexId , out _ ) ;
63
+ Mutex clientMutex = new ( false , clientMutexId , out _ ) ;
64
+ bool hasHandle ;
65
65
66
66
try
67
67
{
@@ -78,6 +78,12 @@ private static void Main(string[] args)
78
78
Exit ( false ) ;
79
79
}
80
80
81
+ clientMutex . ReleaseMutex ( ) ;
82
+ clientMutex . Dispose ( ) ;
83
+
84
+ // This is occasionally necessary to prevent DLLs from being locked at the time that this update is attempting to overwrite them
85
+ await Task . Delay ( 1000 ) . ConfigureAwait ( false ) ;
86
+
81
87
DirectoryInfo updaterDirectory = SafePath . GetDirectory ( baseDirectory . FullName , "Updater" ) ;
82
88
83
89
if ( ! updaterDirectory . Exists )
@@ -95,6 +101,9 @@ private static void Main(string[] args)
95
101
96
102
Write ( $ "{ nameof ( SecondStageUpdater ) } : { relativeExecutableFile } ") ;
97
103
104
+ var copyTasks = new List < Task > ( ) ;
105
+ var failedFiles = new List < FileInfo > ( ) ;
106
+
98
107
foreach ( FileInfo fileInfo in files )
99
108
{
100
109
FileInfo relativeFileInfo = SafePath . GetFile ( fileInfo . FullName [ updaterDirectory . FullName . Length ..] ) ;
@@ -116,22 +125,19 @@ private static void Main(string[] args)
116
125
}
117
126
else
118
127
{
119
- try
120
- {
121
- FileInfo copiedFile = SafePath . GetFile ( baseDirectory . FullName , relativeFileInfo . ToString ( ) ) ;
122
-
123
- Write ( $ "Updating { relativeFileInfo } ") ;
124
- fileInfo . CopyTo ( copiedFile . FullName , true ) ;
125
- }
126
- catch ( Exception ex )
127
- {
128
- Write ( $ "Updating file failed! Returned error message: { ex } ", ConsoleColor . Yellow ) ;
129
- Write ( "If the problem persists, try to move the content of the \" Updater\" directory to the main directory manually or contact the staff for support." ) ;
130
- Exit ( false ) ;
131
- }
128
+ copyTasks . Add ( CopyFileTaskAsync ( baseDirectory , fileInfo , relativeFileInfo , failedFiles ) ) ;
132
129
}
133
130
}
134
131
132
+ await Task . WhenAll ( copyTasks . ToArray ( ) ) . ConfigureAwait ( false ) ;
133
+
134
+ if ( failedFiles . Any ( ) )
135
+ {
136
+ Write ( "Updating file(s) failed!" , ConsoleColor . Yellow ) ;
137
+ Write ( "If the problem persists, try to move the content of the \" Updater\" directory to the main directory manually or contact the staff for support." ) ;
138
+ Exit ( false ) ;
139
+ }
140
+
135
141
FileInfo versionFile = SafePath . GetFile ( updaterDirectory . FullName , versionFileName ) ;
136
142
137
143
if ( versionFile . Exists )
@@ -150,7 +156,7 @@ private static void Main(string[] args)
150
156
{
151
157
Write ( "Checking ClientDefinitions.ini for launcher executable filename." ) ;
152
158
153
- string [ ] lines = File . ReadAllLines ( SafePath . CombineFilePath ( resourceDirectory . FullName , "ClientDefinitions.ini" ) ) ;
159
+ string [ ] lines = await File . ReadAllLinesAsync ( SafePath . CombineFilePath ( resourceDirectory . FullName , "ClientDefinitions.ini" ) ) . ConfigureAwait ( false ) ;
154
160
string launcherPropertyName = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ? "LauncherExe" : "UnixLauncherExe" ;
155
161
string line = lines . Single ( q => q . Trim ( ) . StartsWith ( launcherPropertyName , StringComparison . OrdinalIgnoreCase ) && q . Contains ( '=' , StringComparison . OrdinalIgnoreCase ) ) ;
156
162
int commentStart = line . IndexOf ( ';' , StringComparison . OrdinalIgnoreCase ) ;
@@ -197,32 +203,89 @@ private static void Main(string[] args)
197
203
}
198
204
}
199
205
200
- private static void Exit ( bool success )
206
+ /// <summary>
207
+ /// This attempts to copy a file for the update with the ability to retry up to <see cref="MaxCopyAttempts"/> times.
208
+ /// There are instances where DLLs or other files may be locked and are unable to be overwritten by the update.
209
+ ///
210
+ /// TODO:
211
+ /// Make a backup of all files that are attempted. When we check for any failed files outside this function, restore all backups
212
+ /// if any failures occurred. This will prevent the user from being in a partially updated state.
213
+ ///
214
+ /// </summary>
215
+ /// <param name="baseDirectory">The absolute path of the game installation.</param>
216
+ /// <param name="sourceFileInfo">The file to be copied.</param>
217
+ /// <param name="relativeFileInfo">The relative file info for the destination of the file to be copied.</param>
218
+ /// <param name="failedFiles">If the copy fails too many times, the file should be added to this list.</param>
219
+ /// <returns>A Task.</returns>
220
+ private static async Task CopyFileTaskAsync ( DirectoryInfo baseDirectory , FileInfo sourceFileInfo , FileInfo relativeFileInfo , List < FileInfo > failedFiles )
201
221
{
202
- if ( hasHandle )
222
+ for ( int attempt = 1 ; ; attempt ++ )
203
223
{
204
- clientMutex . ReleaseMutex ( ) ;
205
- clientMutex . Dispose ( ) ;
206
- }
224
+ try
225
+ {
226
+ FileInfo destinationFile = SafePath . GetFile ( baseDirectory . FullName , relativeFileInfo . ToString ( ) ) ;
227
+ FileStream sourceFileStream = sourceFileInfo . Open ( new FileStreamOptions
228
+ {
229
+ Access = FileAccess . Read ,
230
+ Mode = FileMode . Open ,
231
+ Options = FileOptions . Asynchronous ,
232
+ Share = FileShare . None
233
+ } ) ;
234
+ await using ( sourceFileStream . ConfigureAwait ( false ) )
235
+ {
236
+ FileStream destinationFileStream = destinationFile . Open ( new FileStreamOptions
237
+ {
238
+ Access = FileAccess . Write ,
239
+ Mode = FileMode . Create ,
240
+ Options = FileOptions . Asynchronous ,
241
+ Share = FileShare . None
242
+ } ) ;
243
+ await using ( destinationFileStream . ConfigureAwait ( false ) )
244
+ {
245
+ await sourceFileStream . CopyToAsync ( destinationFileStream ) . ConfigureAwait ( false ) ;
246
+ }
247
+ }
207
248
208
- if ( ! success )
209
- {
210
- Write ( "Press any key to exit." ) ;
211
- Console . ReadKey ( ) ;
212
- Environment . Exit ( 1 ) ;
249
+ Write ( $ "Updated { relativeFileInfo } ") ;
250
+
251
+ // File was succesfully copied. Return from the function.
252
+ return ;
253
+ }
254
+ catch ( IOException ex )
255
+ {
256
+ if ( attempt >= MaxCopyAttempts )
257
+ {
258
+ // We tried too many times and need to bail.
259
+ failedFiles . Add ( sourceFileInfo ) ;
260
+ Write ( $ "Updating file failed too many times! Returned error message: { ex } ", ConsoleColor . Yellow ) ;
261
+ return ;
262
+ }
263
+
264
+ // We failed to copy the file, but can try again.
265
+ Write ( $ "Updating file attempt { attempt } failed! Returned error message: { ex . Message } ", ConsoleColor . Yellow ) ;
266
+ await Task . Delay ( CopyRetryWaitMilliseconds ) . ConfigureAwait ( false ) ;
267
+ }
213
268
}
214
269
}
215
270
216
- private static void Write ( string text )
271
+ private static void Exit ( bool success )
217
272
{
218
- Console . ForegroundColor = defaultColor ;
219
- Console . WriteLine ( text ) ;
273
+ if ( success )
274
+ return ;
275
+
276
+ Write ( "Press any key to exit." ) ;
277
+ Console . ReadKey ( ) ;
278
+ Environment . Exit ( 1 ) ;
220
279
}
221
280
222
- private static void Write ( string text , ConsoleColor color )
281
+ private static void Write ( string text , ConsoleColor ? color = null )
223
282
{
224
- Console . ForegroundColor = color ;
225
- Console . WriteLine ( text ) ;
226
- Console . ForegroundColor = defaultColor ;
283
+ // This is necessary, because console is written to from the copy file task
284
+ lock ( consoleMessageLock )
285
+ {
286
+ Console . ForegroundColor = color ?? Console . ForegroundColor ;
287
+ Console . WriteLine ( text ) ;
288
+ Console . ResetColor ( ) ;
289
+ }
227
290
}
228
291
}
0 commit comments