-
-
Notifications
You must be signed in to change notification settings - Fork 345
/
Copy pathRepo.cs
599 lines (519 loc) · 23.9 KB
/
Repo.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Autofac;
using ChinhDo.Transactions;
using CKAN.GameVersionProviders;
using CKAN.Versioning;
using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Tar;
using ICSharpCode.SharpZipLib.Zip;
using log4net;
using Newtonsoft.Json;
namespace CKAN
{
public enum RepoUpdateResult
{
Failed,
Updated,
NoChanges
}
/// <summary>
/// Class for downloading the CKAN meta-info itself.
/// </summary>
public static class Repo
{
private static readonly ILog log = LogManager.GetLogger(typeof (Repo));
/// <summary>
/// Download and update the local CKAN meta-info.
/// Optionally takes a URL to the zipfile repo to download.
/// </summary>
public static RepoUpdateResult UpdateAllRepositories(RegistryManager registry_manager, KSP ksp, NetModuleCache cache, IUser user)
{
SortedDictionary<string, Repository> sortedRepositories = registry_manager.registry.Repositories;
if (sortedRepositories.Values.All(repo => !string.IsNullOrEmpty(repo.last_server_etag) && repo.last_server_etag == Net.CurrentETag(repo.uri)))
{
user?.RaiseMessage("No changes since last update");
return RepoUpdateResult.NoChanges;
}
List<CkanModule> allAvail = new List<CkanModule>();
foreach (KeyValuePair<string, Repository> repository in sortedRepositories)
{
log.InfoFormat("About to update {0}", repository.Value.name);
SortedDictionary<string, int> downloadCounts;
string newETag;
List<CkanModule> avail = UpdateRegistry(repository.Value.uri, ksp, user, out downloadCounts, out newETag);
registry_manager.registry.SetDownloadCounts(downloadCounts);
if (avail == null)
{
// Report failure if any repo fails, rather than losing half the list.
// UpdateRegistry will have alerted the user to specific errors already.
return RepoUpdateResult.Failed;
}
else
{
log.InfoFormat("Updated {0}", repository.Value.name);
// Merge all the lists
allAvail.AddRange(avail);
repository.Value.last_server_etag = newETag;
}
}
// Save allAvail to the registry if we found anything
if (allAvail.Count > 0)
{
registry_manager.registry.SetAllAvailable(allAvail);
// Save our changes.
registry_manager.Save(enforce_consistency: false);
ShowUserInconsistencies(registry_manager.registry, user);
List<CkanModule> metadataChanges = GetChangedInstalledModules(registry_manager.registry);
if (metadataChanges.Count > 0)
{
HandleModuleChanges(metadataChanges, user, ksp, cache);
}
// Registry.Available is slow, just return success,
// caller can check it if it's really needed
return RepoUpdateResult.Updated;
}
else
{
// Return failure
return RepoUpdateResult.Failed;
}
}
/// <summary>
/// Retrieve available modules from the URL given.
/// </summary>
private static List<CkanModule> UpdateRegistry(Uri repo, KSP ksp, IUser user, out SortedDictionary<string, int> downloadCounts, out string currentETag)
{
TxFileManager file_transaction = new TxFileManager();
downloadCounts = null;
// Use this opportunity to also update the build mappings... kind of hacky
ServiceLocator.Container.Resolve<IKspBuildMap>().Refresh();
log.InfoFormat("Downloading {0}", repo);
string repo_file = String.Empty;
try
{
repo_file = Net.Download(repo, out currentETag);
}
catch (System.Net.WebException ex)
{
user.RaiseMessage("Failed to download {0}: {1}", repo, ex.ToString());
currentETag = null;
return null;
}
// Check the filetype.
FileType type = FileIdentifier.IdentifyFile(repo_file);
List<CkanModule> newAvailable = null;
switch (type)
{
case FileType.TarGz:
newAvailable = UpdateRegistryFromTarGz(repo_file, out downloadCounts);
break;
case FileType.Zip:
newAvailable = UpdateRegistryFromZip(repo_file);
break;
}
// Remove our downloaded meta-data now we've processed it.
// Seems weird to do this as part of a transaction, but Net.Download uses them, so let's be consistent.
file_transaction.Delete(repo_file);
return newAvailable;
}
/// <summary>
/// Find installed modules that have different metadata in their equivalent available module
/// </summary>
/// <param name="registry">Registry to scan</param>
/// <returns>
/// List of CkanModules that are available and have changed metadata
/// </returns>
private static List<CkanModule> GetChangedInstalledModules(Registry registry)
{
List<CkanModule> metadataChanges = new List<CkanModule>();
foreach (InstalledModule installedModule in registry.InstalledModules)
{
string identifier = installedModule.identifier;
ModuleVersion installedVersion = registry.InstalledVersion(identifier);
if (!(registry.available_modules.ContainsKey(identifier)))
{
log.InfoFormat("UpdateRegistry, module {0}, version {1} not in registry", identifier, installedVersion);
continue;
}
if (!registry.available_modules[identifier].module_version.ContainsKey(installedVersion))
{
continue;
}
// if the mod is installed and the metadata is different we have to reinstall it
CkanModule metadata = registry.available_modules[identifier].module_version[installedVersion];
CkanModule oldMetadata = registry.InstalledModule(identifier).Module;
if (!MetadataEquals(metadata, oldMetadata))
{
metadataChanges.Add(registry.available_modules[identifier].module_version[installedVersion]);
}
}
return metadataChanges;
}
/// <summary>
/// Resolve differences between installed and available metadata for given ModuleInstaller
/// </summary>
/// <param name="metadataChanges">List of modules that changed</param>
/// <param name="user">Object for user interaction callbacks</param>
/// <param name="ksp">Game instance</param>
private static void HandleModuleChanges(List<CkanModule> metadataChanges, IUser user, KSP ksp, NetModuleCache cache)
{
StringBuilder sb = new StringBuilder();
for (int i = 0; i < metadataChanges.Count; i++)
{
CkanModule module = metadataChanges[i];
sb.AppendLine(string.Format("- {0} {1}", module.identifier, module.version));
}
if (user.RaiseYesNoDialog(string.Format(@"The following mods have had their metadata changed since last update:
{0}
You should reinstall them in order to preserve consistency with the repository.
Do you wish to reinstall now?", sb)))
{
ModuleInstaller installer = ModuleInstaller.GetInstance(ksp, cache, new NullUser());
// New upstream metadata may break the consistency of already installed modules
// e.g. if user installs modules A and B and then later up A is made to conflict with B
// This is perfectly normal and shouldn't produce an error, therefore we skip enforcing
// consistency. However, we will show the user any inconsistencies later on.
// Use the identifiers so we use the overload that actually resolves relationships
// Do each changed module one at a time so a failure of one doesn't cause all the others to fail
foreach (string changedIdentifier in metadataChanges.Select(i => i.identifier))
{
try
{
installer.Upgrade(
new[] { changedIdentifier },
new NetAsyncModulesDownloader(new NullUser()),
enforceConsistency: false
);
}
// Thrown when a dependency couldn't be satisfied
catch (ModuleNotFoundKraken)
{
log.WarnFormat("Skipping installation of {0} due to relationship error.", changedIdentifier);
user.RaiseMessage("Skipping installation of {0} due to relationship error.", changedIdentifier);
}
// Thrown when a conflicts relationship is violated
catch (InconsistentKraken)
{
log.WarnFormat("Skipping installation of {0} due to relationship error.", changedIdentifier);
user.RaiseMessage("Skipping installation of {0} due to relationship error.", changedIdentifier);
}
}
}
}
private static bool MetadataEquals(CkanModule metadata, CkanModule oldMetadata)
{
if ((metadata.install == null) != (oldMetadata.install == null)
|| (metadata.install != null
&& metadata.install.Length != oldMetadata.install.Length))
{
return false;
}
else if (metadata.install != null)
{
for (int i = 0; i < metadata.install.Length; i++)
{
if (!InstallStanzaEquals(metadata.install[i], oldMetadata.install[i]))
return false;
}
}
if (!RelationshipsAreEquivalent(metadata.conflicts, oldMetadata.conflicts))
return false;
if (!RelationshipsAreEquivalent(metadata.depends, oldMetadata.depends))
return false;
if (!RelationshipsAreEquivalent(metadata.recommends, oldMetadata.recommends))
return false;
if (metadata.provides != oldMetadata.provides)
{
if (metadata.provides == null || oldMetadata.provides == null)
return false;
else if (!metadata.provides.OrderBy(i => i).SequenceEqual(oldMetadata.provides.OrderBy(i => i)))
return false;
}
return true;
}
private static bool InstallStanzaEquals(ModuleInstallDescriptor newInst, ModuleInstallDescriptor oldInst)
{
if (newInst.file != oldInst.file)
return false;
if (newInst.install_to != oldInst.install_to)
return false;
if (newInst.@as != oldInst.@as)
return false;
if ((newInst.filter == null) != (oldInst.filter == null))
return false;
if (newInst.filter != null
&& !newInst.filter.SequenceEqual(oldInst.filter))
return false;
if ((newInst.filter_regexp == null) != (oldInst.filter_regexp == null))
return false;
if (newInst.filter_regexp != null
&& !newInst.filter_regexp.SequenceEqual(oldInst.filter_regexp))
return false;
if (newInst.find_matches_files != oldInst.find_matches_files)
return false;
if ((newInst.include_only == null) != (oldInst.include_only == null))
return false;
if (newInst.include_only != null
&& !newInst.include_only.SequenceEqual(oldInst.include_only))
return false;
if ((newInst.include_only_regexp == null) != (oldInst.include_only_regexp == null))
return false;
if (newInst.include_only_regexp != null
&& !newInst.include_only_regexp.SequenceEqual(oldInst.include_only_regexp))
return false;
return true;
}
private static bool RelationshipsAreEquivalent(List<RelationshipDescriptor> a, List<RelationshipDescriptor> b)
{
if (a == b)
// If they're the same exact object they must be equivalent
return true;
if (a == null || b == null)
// If they're not the same exact object and either is null then must not be equivalent
return false;
if (a.Count != b.Count)
// If their counts different they must not be equivalent
return false;
// Sort the lists so we can compare each relationship
var aSorted = a.OrderBy(i => i.ToString()).ToList();
var bSorted = b.OrderBy(i => i.ToString()).ToList();
for (var i = 0; i < a.Count; i++)
{
var aRel = aSorted[i];
var bRel = bSorted[i];
if (!aRel.Same(bRel))
{
return false;
}
}
// If we couldn't find any differences they must be equivalent
return true;
}
/// <summary>
/// Set a registry's available modules to the list from just one repo
/// </summary>
/// <param name="registry_manager">Manager of the regisry of interest</param>
/// <param name="ksp">Game instance</param>
/// <param name="user">Object for user interaction callbacks</param>
/// <param name="repo">Repository to check</param>
/// <returns>
/// Number of modules found in repo
/// </returns>
public static bool Update(RegistryManager registry_manager, KSP ksp, IUser user, string repo = null)
{
if (repo == null)
{
return Update(registry_manager, ksp, user, (Uri)null);
}
return Update(registry_manager, ksp, user, new Uri(repo));
}
// Same as above, just with a Uri instead of string for the repo
public static bool Update(RegistryManager registry_manager, KSP ksp, IUser user, Uri repo = null)
{
// Use our default repo, unless we've been told otherwise.
if (repo == null)
{
repo = CKAN.Repository.default_ckan_repo_uri;
}
SortedDictionary<string, int> downloadCounts;
string newETag;
List<CkanModule> newAvail = UpdateRegistry(repo, ksp, user, out downloadCounts, out newETag);
registry_manager.registry.SetDownloadCounts(downloadCounts);
if (newAvail != null && newAvail.Count > 0)
{
registry_manager.registry.SetAllAvailable(newAvail);
// Save our changes!
registry_manager.Save(enforce_consistency: false);
}
ShowUserInconsistencies(registry_manager.registry, user);
// Registry.Available is slow, just return success,
// caller can check it if it's really needed
return true;
}
/// <summary>
/// Returns available modules from the supplied tar.gz file.
/// </summary>
private static List<CkanModule> UpdateRegistryFromTarGz(string path, out SortedDictionary<string, int> downloadCounts)
{
log.DebugFormat("Starting registry update from tar.gz file: \"{0}\".", path);
downloadCounts = null;
List<CkanModule> modules = new List<CkanModule>();
// Open the gzip'ed file.
using (Stream inputStream = File.OpenRead(path))
{
// Create a gzip stream.
using (GZipInputStream gzipStream = new GZipInputStream(inputStream))
{
// Create a handle for the tar stream.
using (TarInputStream tarStream = new TarInputStream(gzipStream))
{
// Walk the archive, looking for .ckan files.
const string filter = @"\.ckan$";
while (true)
{
TarEntry entry = tarStream.GetNextEntry();
// Check for EOF.
if (entry == null)
{
break;
}
string filename = entry.Name;
if (filename.EndsWith("download_counts.json"))
{
downloadCounts = JsonConvert.DeserializeObject<SortedDictionary<string, int>>(
tarStreamString(tarStream, entry)
);
}
else if (!Regex.IsMatch(filename, filter))
{
// Skip things we don't want.
log.DebugFormat("Skipping archive entry {0}", filename);
continue;
}
log.DebugFormat("Reading CKAN data from {0}", filename);
// Read each file into a buffer.
string metadata_json = tarStreamString(tarStream, entry);
CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename);
if (module != null)
{
modules.Add(module);
}
}
}
}
}
return modules;
}
private static string tarStreamString(TarInputStream stream, TarEntry entry)
{
// Read each file into a buffer.
int buffer_size;
try
{
buffer_size = Convert.ToInt32(entry.Size);
}
catch (OverflowException)
{
log.ErrorFormat("Error processing {0}: Metadata size too large.", entry.Name);
return null;
}
byte[] buffer = new byte[buffer_size];
stream.Read(buffer, 0, buffer_size);
// Convert the buffer data to a string.
return Encoding.ASCII.GetString(buffer);
}
/// <summary>
/// Returns available modules from the supplied zip file.
/// </summary>
private static List<CkanModule> UpdateRegistryFromZip(string path)
{
log.DebugFormat("Starting registry update from zip file: \"{0}\".", path);
List<CkanModule> modules = new List<CkanModule>();
using (var zipfile = new ZipFile(path))
{
// Walk the archive, looking for .ckan files.
const string filter = @"\.ckan$";
foreach (ZipEntry entry in zipfile)
{
string filename = entry.Name;
// Skip things we don't want.
if (! Regex.IsMatch(filename, filter))
{
log.DebugFormat("Skipping archive entry {0}", filename);
continue;
}
log.DebugFormat("Reading CKAN data from {0}", filename);
// Read each file into a string.
string metadata_json;
using (var stream = new StreamReader(zipfile.GetInputStream(entry)))
{
metadata_json = stream.ReadToEnd();
stream.Close();
}
CkanModule module = ProcessRegistryMetadataFromJSON(metadata_json, filename);
if (module != null)
{
modules.Add(module);
}
}
zipfile.Close();
}
return modules;
}
private static CkanModule ProcessRegistryMetadataFromJSON(string metadata, string filename)
{
log.DebugFormat("Converting metadata from JSON.");
try
{
CkanModule module = CkanModule.FromJson(metadata);
// FromJson can return null for the empty string
if (module != null)
{
log.DebugFormat("Found {0} version {1}", module.identifier, module.version);
}
return module;
}
catch (Exception exception)
{
// Alas, we can get exceptions which *wrap* our exceptions,
// because json.net seems to enjoy wrapping rather than propagating.
// See KSP-CKAN/CKAN-meta#182 as to why we need to walk the whole
// exception stack.
bool handled = false;
while (exception != null)
{
if (exception is UnsupportedKraken || exception is BadMetadataKraken)
{
// Either of these can be caused by data meant for future
// clients, so they're not really warnings, they're just
// informational.
log.InfoFormat("Skipping {0} : {1}", filename, exception.Message);
// I'd *love a way to "return" from the catch block.
handled = true;
break;
}
// Look further down the stack.
exception = exception.InnerException;
}
// If we haven't handled our exception, then it really was exceptional.
if (handled == false)
{
if (exception == null)
{
// Had exception, walked exception tree, reached leaf, got stuck.
log.ErrorFormat("Error processing {0} (exception tree leaf)", filename);
}
else
{
// In case whatever's calling us is lazy in error reporting, we'll
// report that we've got an issue here.
log.ErrorFormat("Error processing {0} : {1}", filename, exception.Message);
}
throw;
}
return null;
}
}
private static void ShowUserInconsistencies(Registry registry, IUser user)
{
// However, if there are any sanity errors let's show them to the user so at least they're aware
var sanityErrors = registry.GetSanityErrors();
if (sanityErrors.Any())
{
var sanityMessage = new StringBuilder();
sanityMessage.AppendLine("The following inconsistencies were found:");
foreach (var sanityError in sanityErrors)
{
sanityMessage.Append("- ");
sanityMessage.AppendLine(sanityError);
}
user.RaiseMessage(sanityMessage.ToString());
}
}
}
}