1

I'm writing a WinForms program that uses MEF to load assemblies. Those assemblies are not located in the same folder than the executable. As I need to perform some file maintenance, I implemented some code in the file Program.cs, before loading the actual WinForm, so the files (even if assemblies) are not loaded (or shouldn't if they are) by the program.

I'm performing two operations: - Moving a folder from one location to an other one - Unzipping files from an archive and overwrite dll files from the folder moved (if file from the archive is newer than the one moved)

The problem is that after moving the folder, files in it are locked and cannot be overwritten. I also tried to move files one by one by disposing them when the move is finished.

Can someone explain me why the files are blocked and how I could avoid that

Thanks

private static void InitializePluginsFolder()
    {
        if (!Directory.Exists(Paths.PluginsPath))
        {
            Directory.CreateDirectory(Paths.PluginsPath);
        }

        // Find archive that contains plugins to deploy
        var assembly = Assembly.GetExecutingAssembly();
        if (assembly.Location == null)
        {
            throw new NullReferenceException("Executing assembly is null!");
        }

        var currentDirectory = new FileInfo(assembly.Location).DirectoryName;
        if (currentDirectory == null)
        {
            throw new NullReferenceException("Current folder is null!");
        }

        // Check if previous installation contains a "Plugins" folder
        var currentPluginsPath = Path.Combine(currentDirectory, "Plugins");
        if (Directory.Exists(currentPluginsPath))
        {
            foreach (FileInfo fi in new DirectoryInfo(currentPluginsPath).GetFiles())
            {
                using (FileStream sourceStream = new FileStream(fi.FullName, FileMode.Open))
                {
                    using (FileStream destStream = new FileStream(Path.Combine(Paths.PluginsPath, fi.Name), FileMode.Create))
                    {
                        destStream.Lock(0, sourceStream.Length);
                        sourceStream.CopyTo(destStream);
                    }
                }
            }

            Directory.Delete(currentPluginsPath, true);
        }

        // Then updates plugins with latest version of plugins (zipped)
        var pluginsZipFilePath = Path.Combine(currentDirectory, "Plugins.zip");

        // Extract content of plugins archive to a temporary folder
        var tempPath = string.Format("{0}_Temp", Paths.PluginsPath);

        if (Directory.Exists(tempPath))
        {
            Directory.Delete(tempPath, true);
        }

        ZipFile.ExtractToDirectory(pluginsZipFilePath, tempPath);

        // Moves all plugins to appropriate folder if version is greater
        // to the version in place
        foreach (var fi in new DirectoryInfo(tempPath).GetFiles())
        {
            if (fi.Extension.ToLower() != ".dll")
            {
                continue;
            }

            var targetFile = Path.Combine(Paths.PluginsPath, fi.Name);
            if (File.Exists(targetFile))
            {
                if (fi.GetAssemblyVersion() > new FileInfo(targetFile).GetAssemblyVersion())
                {
                    // If version to deploy is newer than current version
                    // Delete current version and copy the new one

                    // FAILS HERE

                    File.Copy(fi.FullName, targetFile, true);
                }
            }
            else
            {
                File.Move(fi.FullName, targetFile);
            }
        }

        // Delete temporary folder
        Directory.Delete(tempPath, true);
    }
Guido Preite
  • 14,905
  • 4
  • 36
  • 65
  • 2
    Please share your code. – Amey Kamat Oct 20 '16 at 09:24
  • Sadly we cant really even speculate from that. – BugFinder Oct 20 '16 at 09:26
  • Sorry I didn't want to put lot of code but I guess it can help for sure. This method is the first one called on the Main method – Tanguy Touzard Oct 20 '16 at 09:35
  • Can you verify that your program is generating the lock? i.e. run one program to create, and another to extract/move? What is the error you're getting? – Daryl Oct 21 '16 at 19:25
  • Not related to your problem but you don't need to do the `if (!Directory.Exists(...)) { Directory.CreateDirectory(...); }` combo. If a directory already exists and you call CreateDirectory no error will be thrown. – Scott Chamberlain Oct 22 '16 at 19:06
  • I tried to reproduced the issue. For me it works fine. The newly created file is overwritten with the file extracted from the zip archive. No exception thrown. What type of exception do you get? – rachri Oct 22 '16 at 19:39
  • I have also tried to reproduce the issue but it works fine. I can only suppose that:1) your assemblies are already loaded (see this [pag](https://weblog.west-wind.com/posts/2012/Nov/03/Back-to-Basics-When-does-a-NET-Assembly-Dependency-get-loaded) to check if your assemblies are loaded) or 2) your application doesn't have the rights to overwrite a file – Tinwor Oct 22 '16 at 20:13
  • @Tinwor I think the reason of the problem is in `GetAssemblyVersion` extension method. Please check my answer for the details. – Nikita Oct 22 '16 at 20:30

5 Answers5

3

Check the implementation of the GetAssemblyVersion() method used in this part of code:

if (File.Exists(targetFile))
{
  if (fi.GetAssemblyVersion() > new FileInfo(targetFile).GetAssemblyVersion())
  {
    // If version to deploy is newer than current version
    // Delete current version and copy the new one

    // FAILS HERE

    File.Copy(fi.FullName, targetFile, true);
  }
}

fi variable has type FileInfo, GetAssemblyVersion() looks like an extension method. You should check how assembly version is retrieved from the file. If this method loads an assembly it should also unload it to release the file.

The separate AppDomain is helpful if you need to load the assembly, do the job and after that unload it. Here is the GetAssemblyVersion method implementation:

public static Version GetAssemblyVersion(this FileInfo fi)
{
  AppDomain checkFileDomain = AppDomain.CreateDomain("DomainToCheckFileVersion");
  Assembly assembly = checkFileDomain.Load(new AssemblyName {CodeBase = fi.FullName});
  Version fileVersion = assembly.GetName().Version;
  AppDomain.Unload(checkFileDomain);
  return fileVersion;
}

The following implementation of the GetAssemblyVersion() could retrieve the assembly version without loading assembly into your AppDomain. Thnx @usterdev for the hint. It also allows you to get the version without assembly references resolve:

public static Version GetAssemblyVersion(this FileInfo fi)
{
  return AssemblyName.GetAssemblyName(fi.FullName).Version;
}
Nikita
  • 6,270
  • 2
  • 24
  • 37
1

You have to make sure that you are not loading the Assembly into your domain to get the Version from it, otherwise the file gets locked.

By using the AssemblyName.GetAssemblyName() static method (see MSDN), the assembly file is loaded, version is read and then unloaded but not added to your domain.

Here an extension for FileInfo doing so:

public static Version GetAssemblyVersion(this FileInfo fi)
{
    AssemblyName an = AssemblyName.GetAssemblyName(fi.FullName);
    return an.Version;
}
rachri
  • 525
  • 6
  • 13
0

The below statement locks the file

destStream.Lock(0, sourceStream.Length);

but after that you havent unlocked the file. Perhaps that is the cause of your problem.

Amey Kamat
  • 181
  • 2
  • 12
  • Thanks but unfortunately, this is not the cause. Removing the Lock or adding an Unlock after the copy does not fix the problem. – Tanguy Touzard Oct 20 '16 at 09:48
0

I would start checking if you program has actually already loaded the assembly.

two suggestions:

1 - Call a method like this before calling your InitializePluginsFolder

static void DumpLoadedAssemblies()
{
    var ads = AppDomain.CurrentDomain.GetAssemblies();
    Console.WriteLine(ads.Length);
    foreach (var ad in ads)
    {
        Console.WriteLine(ad.FullName);
        // maybe this can be helpful as well
        foreach (var f in ad.GetFiles())
            Console.WriteLine(f.Name);
        Console.WriteLine("*******");
    }
}

2 - In the first line of Main, register for AssemblyLoad Event and dump Loaded Assembly in the event handler

public static void Main()
{
    AppDomain.CurrentDomain.AssemblyLoad += OnAssemlyLoad;
    ...
}
static void OnAssemlyLoad(object sender, AssemblyLoadEventArgs args) 
{
    Console.WriteLine("Assembly Loaded: " + args.LoadedAssembly.FullName);
}
Gian Paolo
  • 4,161
  • 4
  • 16
  • 34
0

You definitely load assembly using AssemblyName.GetAssemblyName, unfortunately .NET has no conventional ways of checking assembly metadata without loading assembly. To avoid this you can:

  1. Load assembly in separated AppDomain as Nikita suggested, I can add: load it with ReflectionOnlyLoad
  2. Or get assembly version using Mono.Cecil library as Reflector does
  3. Just for completeness: actually you can load assembly into same AppDomain without locking assembly file in two stage: read file contents into byte[] and using Assembly.Load(byte[] rawAssembly) but this way has serious "Loading Context" issues and what will you do with several loaded assemblies :)
olk
  • 199
  • 1
  • 2
  • 8