Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prompt user to overwrite manually installed files #3043

Merged
merged 1 commit into from
May 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 118 additions & 3 deletions Core/ModuleInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,13 @@ private void Install(CkanModule module, bool autoInstalled, Registry registry, s
ModuleVersion version = registry.InstalledVersion(module.identifier);

// TODO: This really should be handled by higher-up code.
if (version != null)
if (version != null && !(version is UnmanagedModuleVersion))
{
User.RaiseMessage(" {0} {1} already installed, skipped", module.identifier, version);
return;
}

// Find our in the cache if we don't already have it.
// Find ZIP in the cache if we don't already have it.
filename = filename ?? Cache.GetCachedZip(module);

// If we *still* don't have a file, then kraken bitterly.
Expand Down Expand Up @@ -348,6 +348,7 @@ private static void CheckKindInstallationKraken(CkanModule module)
/// Installs the module from the zipfile provided.
/// Returns a list of files installed.
/// Propagates a BadMetadataKraken if our install metadata is bad.
/// Propagates a CancelledActionKraken if the user decides not to overwite unowned files.
/// Propagates a FileExistsKraken if we were going to overwrite a file.
/// </summary>
private IEnumerable<string> InstallModule(CkanModule module, string zip_filename, Registry registry)
Expand All @@ -360,6 +361,32 @@ private IEnumerable<string> InstallModule(CkanModule module, string zip_filename

try
{
var dll = registry.DllPath(module.identifier);
if (dll != null && !files.Any(f => ksp.ToRelativeGameDir(f.destination) == dll))
{
throw new DllLocationMismatchKraken(dll, $"DLL for module {module.identifier} found at {dll}, but it's not where CKAN would install it. Aborting to prevent multiple copies of the same mod being installed. To install this module, uninstall it manually and try again.");
}

// Look for overwritable files if session is interactive
if (!User.Headless)
{
var conflicting = FindConflictingFiles(zipfile, files, registry).Memoize();
if (conflicting.Any())
{
var fileMsg = conflicting
.OrderBy(c => c.Value)
.Aggregate("", (a, b) =>
$"{a}\r\n- {ksp.ToRelativeGameDir(b.Key.destination)} ({(b.Value ? "same" : "DIFFERENT")})");
if (User.RaiseYesNoDialog($"Module {module.name} wants to overwrite the following manually installed files:\r\n{fileMsg}\r\n\r\nOverwrite?"))
{
DeleteConflictingFiles(conflicting.Select(f => f.Key));
}
else
{
throw new CancelledActionKraken($"Not overwriting manually installed files, can't install {module.name}.");
}
}
}
foreach (InstallableFile file in files)
{
log.DebugFormat("Copying {0}", file.source.Name);
Expand All @@ -380,6 +407,94 @@ private IEnumerable<string> InstallModule(CkanModule module, string zip_filename
}
}

/// <summary>
/// Find files in the given list that are already installed and unowned.
/// Note, this compares files on demand; Memoize for performance!
/// </summary>
/// <param name="files">Files that we want to install for a module</param>
/// <returns>
/// List of pairs: Key = file, Value = true if identical, false if different
/// </returns>
private IEnumerable<KeyValuePair<InstallableFile, bool>> FindConflictingFiles(ZipFile zip, IEnumerable<InstallableFile> files, Registry registry)
{
foreach (InstallableFile file in files)
{
if (File.Exists(file.destination)
&& registry.FileOwner(ksp.ToRelativeGameDir(file.destination)) == null)
{
log.DebugFormat("Comparing {0}", file.destination);
using (Stream zipStream = zip.GetInputStream(file.source))
using (FileStream curFile = new FileStream(file.destination, FileMode.Open, FileAccess.Read))
{
yield return new KeyValuePair<InstallableFile, bool>(
file,
file.source.Size == curFile.Length
&& StreamsEqual(zipStream, curFile)
);
}
}
}
}

/// <summary>
/// Compare the contents of two streams
/// </summary>
/// <param name="s1">First stream to compare</param>
/// <param name="s2">Second stream to compare</param>
/// <returns>
/// true if both streams contain same bytes, false otherwise
/// </returns>
private bool StreamsEqual(Stream s1, Stream s2)
{
const int bufLen = 1024;
byte[] bytes1 = new byte[bufLen];
byte[] bytes2 = new byte[bufLen];
for (int bytesChecked = 0; bytesChecked < s1.Length; )
{
int bytesFrom1 = s1.Read(bytes1, 0, bufLen);
int bytesFrom2 = s2.Read(bytes2, 0, bufLen);
if (bytesFrom1 != bytesFrom2)
{
// One ended early, not equal.
log.DebugFormat("Read {0} bytes from stream1 and {1} bytes from stream2", bytesFrom1, bytesFrom2);
return false;
}
for (int i = 0; i < bytesFrom1; ++i)
{
if (bytes1[i] != bytes2[i])
{
log.DebugFormat("Byte {0} doesn't match", bytesChecked + i);
// Bytes don't match, not equal.
return false;
}
}
bytesChecked += bytesFrom1;
}
// Same bytes, they're equal.
return true;
}

/// <summary>
/// Remove files that the user chose to overwrite, so
/// the installer can replace them.
/// Uses a transaction so they can be undeleted if the install
/// fails at a later stage.
/// </summary>
/// <param name="files">The files to overwrite</param>
private void DeleteConflictingFiles(IEnumerable<InstallableFile> files)
{
TxFileManager file_transaction = new TxFileManager();
using (var transaction = CkanTransaction.CreateTransactionScope())
{
foreach (InstallableFile file in files)
{
log.DebugFormat("Trying to delete {0}", file.destination);
file_transaction.Delete(file.destination);
}
transaction.Complete();
}
}

/// <summary>
/// Checks the path against a list of reserved game directories
/// </summary>
Expand Down Expand Up @@ -698,7 +813,7 @@ internal static void CopyZipEntry(ZipFile zipfile, ZipEntry entry, string fullPa
}

// We don't allow for the overwriting of files. See #208.
if (File.Exists(fullPath))
if (file_transaction.FileExists(fullPath))
{
throw new FileExistsKraken(fullPath, string.Format("Trying to write {0} but it already exists.", fullPath));
}
Expand Down
9 changes: 9 additions & 0 deletions Core/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ [JsonIgnore] public IEnumerable<string> InstalledDlls
get { return installed_dlls.Keys; }
}

/// <summary>
/// Returns the file path of a DLL.
/// null if not found.
/// </summary>
public string DllPath(string identifier)
{
return installed_dlls.TryGetValue(identifier, out string path) ? path : null;
}

/// <summary>
/// A map between module identifiers and versions for official DLC that are installed.
/// </summary>
Expand Down
19 changes: 17 additions & 2 deletions Core/Types/Kraken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ public InstanceNameTakenKraken(string name, string reason = null)
this.instName = name;
}
}

public class ModuleIsDLCKraken : Kraken
{
/// <summary>
Expand All @@ -581,5 +581,20 @@ public ModuleIsDLCKraken(CkanModule module, string reason = null)
this.module = module;
}
}


/// <summary>
/// A manually installed mod is installed somewhere other than
/// where CKAN would install it, so we can't safely overwrite it.
/// </summary>
public class DllLocationMismatchKraken : Kraken
{
public readonly string path;

public DllLocationMismatchKraken(string path, string reason = null)
: base(reason)
{
this.path = path;
}
}

}
11 changes: 8 additions & 3 deletions GUI/Dialogs/YesNoDialog.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion GUI/Dialogs/YesNoDialog.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading.Tasks;
Expand All @@ -19,10 +20,20 @@ public DialogResult ShowYesNoDialog(Form parentForm, string text, string yesText

Util.Invoke(parentForm, () =>
{
var height = StringHeight(text, ClientSize.Width - 25) + 2 * 54;
DescriptionLabel.Text = text;
DescriptionLabel.TextAlign = text.Contains("\n")
? HorizontalAlignment.Left
: HorizontalAlignment.Center;
DescriptionLabel.ScrollBars = height < maxHeight
? ScrollBars.None
: ScrollBars.Vertical;
YesButton.Text = yesText ?? defaultYes;
NoButton.Text = noText ?? defaultNo;
ClientSize = new Size(ClientSize.Width, StringHeight(text, ClientSize.Width - 25) + 2 * 54);
ClientSize = new Size(
ClientSize.Width,
Math.Min(maxHeight, height)
);
task.SetResult(ShowDialog(parentForm));
});

Expand All @@ -47,6 +58,7 @@ public void HideYesNoDialog()
Util.Invoke(this, Close);
}

private const int maxHeight = 600;
private TaskCompletionSource<DialogResult> task;
private string defaultYes;
private string defaultNo;
Expand Down
11 changes: 8 additions & 3 deletions GUI/Main/MainInstall.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,10 @@ out Dictionary<CkanModule, HashSet<string>> supporters
currentUser.RaiseMessage(ex.InconsistenciesPretty);
return;
}
catch (CancelledActionKraken)
catch (CancelledActionKraken kraken)
{
currentUser.RaiseMessage(kraken.Message);
installCanceled = true;
return;
}
catch (MissingCertificateKraken kraken)
Expand Down Expand Up @@ -335,6 +337,11 @@ out Dictionary<CkanModule, HashSet<string>> supporters
currentUser.RaiseError(msg);
return;
}
catch (DllLocationMismatchKraken kraken)
{
currentUser.RaiseMessage(kraken.Message);
return;
}
}
}

Expand Down Expand Up @@ -410,8 +417,6 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e)
else if (installCanceled)
{
// User cancelled the installation
// Rebuilds the list of GUIMods
UpdateModsList(ChangeSet);
if (result.Key) {
FailWaitDialog(
Properties.Resources.MainInstallCancelTooLate,
Expand Down