Laden...

FileSystemWatcher "blockt" Datei

Letzter Beitrag vor einem Jahr 10 Posts 675 Views
FileSystemWatcher "blockt" Datei

Moin,
ich möchte .dlls dynamisch laden und entladen und habe mich an der ersten Anwort in diesem Post orientiert.

Das Programm kopiert eine Dll aus dem Debugverzeichnis eines anderen Projekts (Plugin), läd die Dll und schaltet den FileSystemWatcher ein, der das bin/Debug-Verzeichnis des Plugin-Projekts überwacht. Wenn ich das Projekt builde und die Dll überschrieben wird, soll der Watcher feueren. Am Ende soll das Entladen, Kopieren und Laden der Dll aus dem Debug-Verzeichnis automatisch ablaufen. Momentan benutze ich noch für das LAden und Entladen Buttons.

Das ist der Code:


private void BtnLoadPlugin_Click(object sender, RoutedEventArgs e)
        {
            string fileFrom = "J:\\workspace\\_csharp\\Projects\\_test\\TestPlugIn\\bin\\Debug\\TestPlugIn.dll";
            string fileTo = "J:\\workspace\\_csharp\\Projects\\_test\\PlugInManager\\TestApp\\bin\\Debug\\PlugIns\\TestPlugIn.dll";
            File.Copy(fileFrom, fileTo, overwrite: true);

            _domain = AppDomain.CreateDomain("TestPlugIn", AppDomain.CurrentDomain.Evidence, new AppDomainSetup
            {
                ApplicationName = "TestPlugIn",
                ApplicationBase = "J:\\workspace\\_csharp\\Projects\\_test\\PlugInManager\\TestApp\\bin\\Debug\\PlugIns",
            });

            _plugin = (IPlugIn)_domain.CreateInstanceAndUnwrap("TestPlugIn",
                $"{"TestPlugIn"}.{"MyPlugIn"}");


            _plugin.DoSomething();
            string msg = _plugin.DoSomethingElse("Do Something else...");
            Console.WriteLine(msg);

            _watcher = new FileSystemWatcher("J:\\workspace\\_csharp\\Projects\\_test\\TestPlugIn\\bin\\Debug\\")
            {
                NotifyFilter = NotifyFilters.LastWrite,
            };
            _watcher.EnableRaisingEvents = true;
            _watcher.Changed += OnFolderChanged;
        }

        private void BtnUnloadPlugin_Click(object sender, RoutedEventArgs e)
        {
            AppDomain.Unload(_domain);
            _watcher.Changed -= OnFolderChanged;         
        }

        private void OnFolderChanged(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine($"File Changed: '{e.Name}'.");
        }

Das Laden und Entladen der AppDomain und der Dll funktioniert auch soweit. Leider führt das Überwachen des FileSystemWatchers irgendwie dazu, daß ich das Projekt, das vom Watcher überwacht wird, nicht builden kann.

Fehlermeldung beim Build-Versuch:

Fehlermeldung:
Cannot open 'J:\workspace_csharp\Projects_test\TestPlugIn\obj\Debug\TestPlugIn.pdb' for writing -- 'The process cannot access the file 'J:\workspace_csharp\Projects_test\TestPlugIn\obj\Debug\TestPlugIn.pdb' because it is being used by another process.'

Ich brauchte eine Weile bis ich die Fehlermeldung richtig gelesen habe. Es handelt sich nicht um bin/Debug, sondern um obj/Debug. Das Verzeichnis obj/Debug wird nirgends im Code benutzt. Die beiden Projekte kannen sich nicht. Die einzige Verbindung zwischen den beiden ist das Interface IPlugin, deren Dll beide Projekte als Referenz haben.

Ich habe im Zuge meiner Recherche bereits erfahren, daß der FileSystemWatcher weder Verzeichnisse noch Dateien blocken kann. ABER, lasse ich ihn weg, lade mit dem Programm die Dll und versuche dann das Plugin-Projekt zu builden, dann klappts. Auf der anderen Seite, wenn ich nur den Wachter in einem separatem Projekt starte, kann ich builden wie ich will, läuft auch ohne Probleme. Ich komme einfach nicht dahinter, woran es liegt, daß beides zusammen nicht funktionert, aber einzeln schon.

Das ist ein relativ üblicher Fehler bei der täglichen Arbeit mit Visual Studio im Debug-Modus, wenn man gewisse Dinge falsch verwendet. Der FileSystemWatcher blockiert nichts, sondern Deine (Debugging-)Umgebung.

Technischer Hintergrund: Du verwendest irgendwas, das eine unmanaged Referenz hat (zB ein FileStream) falsch, sodass das Handle offen bleibt, wenn das Debugging beendet wird.
Dadurch bleiben gewisse Files in Nutzung und Du kannst nicht mehr bauen, bis der Parent Prozess beendet wird und das System hart die Abhängigkeiten aufräumt. Es reicht bei unmanaged references niemals, wenn Du nur das Objekt de-referenzierst.
Basics dazu: Implement a Dispose method

Ohne dass wir hier den gesamten Code sehen liegt es nahe, dass Du den FileSystemWatcher nicht ordentlich aufräumst.
Denn der FileSystemWatcher verwendet unter der Haube ebenfalls unmanaged references (nichts anderes als Win32 Endpunkte, die Handles erstellen, die zum Überwachen und für die Events des FSW dienen), die nur aufgeräumt werden, wenn Du das mit Deinem Code sicher stellst; hier mit Dispose.

Ich habe Dispose() tatsächlich nicht benutzt. Habe es eingefügt und es ändert sich leicht etwas. Beim ersten Mal kann ich nicht builden, auch, wenn ich eine Minute lang warte, bevor ich builde. Drücke ich gleich danach nochmal F6, geht das. Entlade und lade ich die AppDomain, fängt das Spiel von vorn an. Er buildet beim zweiten Mal auch, wenn ich gleich nach dem Laden der dll zweimal builde.

Ich habe aber eben herausgefunden, daß wenn ich die .pdb-Datei im bin/Debug lösche, buildet er auch beim ersten Mal. Scheinbar wird die Blockade aufgehoben, wenn sich etwas im Verzeichnis ändert. Erstelle ich nach dem Aufrufen des Watchers eine leere Text-Datei im bin/Debug und warte paar Sekunden, kann ich beim ersten Mal builden oder ich builde zweimal. Aber nach dem Reload fängt auch das nochmal an. Wahrscheinlich braucht der Watcher etwas bis er disposet.


public partial class MainWindow : Window
    {
        private Contracts.IPlugIn _plugin;
        private AppDomain _domain;
        private FileSystemWatcher _watcher;

        public MainWindow()
        {
            // For english exceptions
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us");

            InitializeComponent();
        }

        private void BtnLoadPlugin_Click(object sender, RoutedEventArgs e)
        {
            string fileFrom = "J:\\workspace\\_csharp\\Projects\\_test\\TestPlugIn\\bin\\Debug\\TestPlugIn.dll";
            string fileTo = "J:\\workspace\\_csharp\\Projects\\_test\\PlugInManager\\TestApp\\bin\\Debug\\PlugIns\\TestPlugIn.dll";
            File.Copy(fileFrom, fileTo, overwrite: true);

            _domain = AppDomain.CreateDomain("TestPlugIn", AppDomain.CurrentDomain.Evidence, new AppDomainSetup
            {
                ApplicationName = "TestPlugIn",
                ApplicationBase = "J:\\workspace\\_csharp\\Projects\\_test\\PlugInManager\\TestApp\\bin\\Debug\\PlugIns",
            });

            _plugin = (IPlugIn)_domain.CreateInstanceAndUnwrap("TestPlugIn",
                $"{"TestPlugIn"}.{"MyPlugIn"}");


            _plugin.DoSomething();
            string msg = _plugin.DoSomethingElse("Do Something else...");
            Console.WriteLine(msg);

            _watcher = new FileSystemWatcher("J:\\workspace\\_csharp\\Projects\\_test\\TestPlugIn\\bin\\Debug\\")
            {
                NotifyFilter = NotifyFilters.LastWrite,
            };
            _watcher.EnableRaisingEvents = true;
            _watcher.Changed += OnFolderChanged;

            string workaround = "J:\\workspace\\_csharp\\Projects\\_test\\TestPlugIn\\bin\\Debug\\workaround.txt";
            File.Create(workaround).Dispose();
        }

        private void BtnUnloadPlugin_Click(object sender, RoutedEventArgs e)
        {
            AppDomain.Unload(_domain);
            _watcher.Changed -= OnFolderChanged;
            _watcher.Dispose();          
            _watcher = null;
        }

        private void OnFolderChanged(object sender, FileSystemEventArgs e)
        {
            Console.WriteLine($"File Changed: '{e.Name}'.");
        }
    }

Ich verstehe aber nicht, was das alles bin mit obj/Debug Verzeichnis zu tun hat und warum der FSW in einem eigenen Projekt, ohne das Laden und Entladen von AppDomains, keinen Einfluss auf VS hat und ich builden kann, obwohl er permanent das bin/Debug-Verzeichnis überwacht.

Wahrscheinlich braucht der Watcher etwas bis er disposet.

So funktionieren handles nicht. Deswegen hab ich dir die Links mit den Basics gegeben.
Es wird am Code liegen.

Das ist mir schon bewusst, daß es am Code liegt. Aber am welchen Teil des Codes? Am Dispose kann es nicht wirklich liegen, da FSW während des Build-Vorgangs noch läuft und laufen muss, damit er eben diesen registriert und feuert. Dispose geschieht erst danach. Das Builden läuft aber schon nach dem ersten Laden der Dll nicht rund, also bevor der FSW das erste Mal disposet wird.

Vermutlich eine Race Condition oder sowas wie ein Timing-Problem. Nicht ersichtlich.
Üblicherweise hat ein Plugin eine standardisierte LifeTime im Pluginframework; der oberste Knoten bekommt den Dispose-Call, den dann alle darunterliegenden Elemente nutzen können, um ihre unmanaged references aufzuräumen.
Ein Dependency Injection Framework übernimmt sowas automatisch. Das fehlt bei Dir komplett. Du behandelst alle Abhängigkeiten manuell und hast keinerlei Unterstützung beim Aufräumen, musst alles selbst machen.

Daher reine Vermutung aufgrund von Erfahrung: Timing Problem beim Aufräumen der Referenzen; oder hast irgendwo was vergessen.
Keine Ahnung ob das eine Rolle spielt, dass Du den FileSystemWatcher nach dem Domain Unload aufräumst. Liest sich erstmal komisch; aber hab seit 10 Jahren keine AppDomain mehr angefasst und weiß nicht, ob das so richtig ist, oder eine Quelle für Fehler sein kann.

Mit dem Build hat das hier nichts zutun. Der Build-Effekt ist ein Folgefehler.

Ich habe jetzt nochmal paar Ideen ausprobiert, z.B. habe ich den Watcher als erstes erstellt und dann die AppDomain. Hat nicht funktioniert. Darauf hin habe ich alles einzeln auskommentiert, spricht die Elemente von AppDomain, Copy und FSW, bis zu der Stelle, an der VS den Build ohne Probleme durchführt. Funktioniert hat haben diese Mal tatsächlich AppDomain mit dem Watcher, aber ohne File.Copy (es wurde die dll geladen, die bereits im Plugin-Verzeichnis war). Das ist wieder einer dieser Momente, an dem ich geneigt bin zu glauben, daß mein Code ein Eigenleben hat. Denn, es war beim Schreiben des Codes anders. Da war es noch der FSW. Und eigentlich sollte das Lesen einer Datei nichts locken. Wie auch immer.

Ich habe File.Copy durch FileStream ersetzt um zu schauen, ob sich etwas ändert.


//File.Copy(fileFrom, fileTo, overwrite: true);
            using(var streamFrom = new FileStream(
                fileFrom,
                FileMode.Open,
                FileAccess.Read,
                FileShare.ReadWrite))
            {
                using(var streamTo = new FileStream(fileTo, FileMode.Create))
                {
                    var buffer = new byte[0x10000];
                    int bytes;

                    while((bytes = streamFrom.Read(buffer, 0, buffer.Length)) > 0)
                    {
                        streamTo.Write(buffer, 0, bytes);
                    }
                }
            }

Leider ändert sich am Verhalten nichts.

Dann kam mir die Idee, das Kopieren async durchzuführen, um wirklich sich er zu sein, daß der Kopiervorgang auch abgeschlossen wurde.


//File.Copy(fileFrom, fileTo, overwrite: true);            
            CopyFileAsync(fileFrom, fileTo).Wait();

...

private async Task CopyFileAsync(string sourcePath, string destinationPath)
        {
            using(Stream source = File.OpenRead(sourcePath))
            {
                using(Stream destination = File.Create(destinationPath))
                {
                    await source.CopyToAsync(destination);
                }
            }
        }

Hehe, beim Klick auf den Load Button friert die GUI ein und es tut sich nichts (dachte immer, async + await sollen genau das verhindern). In der Konsole wird auch nichts mehr ausgegeben, d.h. das Programm kommt erst gar nicht so weit die dll zu laden. Ich habe vorher nicht versucht ein Build zu erstellen, die GUI friert immer und bei jedem Versuch ein.

Was das Plugin angeht, ja, es hat noch kein Dispose. Es besteht aus reinen TestMethoden. Alles andere kommt erst in den nächsten Schritten.


public class MyPlugIn : MarshalByRefObject, IPlugIn
    {
        public void DoSomething()
        {
            Console.WriteLine("working...");
        }

        string IPlugIn.DoSomethingElse(string message)
        {
            Console.WriteLine(message);
            return "Something else done...";
        }
    }

Jetzt fängst an zu stochern statt strukturiert vorzugehen, das macht kein Sinn.

Ich habe File.Copy durch FileStream ersetzt um zu schauen, ob sich etwas ändert.

Macht null sinn, weil File Copy keine Handles zurücklässt.

Dann kam mir die Idee, das Kopieren async durchzuführen, um wirklich sich er zu sein, daß der Kopiervorgang auch abgeschlossen wurde.

async hat mit "Kopiervorgang abgeschlossen" absolut nichts, also wirklich gar nichts zutun. Das ist nicht die Aufgabe von async.

friert die GUI ein und es tut sich nichts (dachte immer, async + await sollen genau das verhindern)

Wenn Du das wirklich so dachtest, dann stimmt da vielleicht das Verständnis nicht, was async/await tut und wofür es da ist. Das ist aller höchstens in gewissen Situationen ein Nebeneffekt, aber nicht die Hauptaufgabe.
Erklärt auch die async-Idee davor. Schau Dir das unbedingt an, weil ohne async/await heutzutage in .NET fast nicht programmiert werden kann.

Was das Plugin angeht, ja, es hat noch kein Dispose, da es noch kein Dispose braucht.

Wenn irgendein Element des Plugins ein Dispose braucht, braucht rein aus .NET sicht Dein Plugin eine Schnittstelle das Dispose zu ermöglichen.
Dispose ist der Standard-Pattern für das Abfrühstücken von long-existing resources. Hab Dir die Basic Links gegeben, les sie durch.

Ich habs raus. War das eine schwere Geburt! Es liegt nicht am Code! Und ich bin irgendwie froh drüber. Aber dein Verweis auf VS war auch richtig. Ich habe VS zweimal offen. Einmal mit der TestApp und einmal mit dem TestPlugin. Schließe ich das VS mit der TestApp und starte das Programm aus dem bin/Debug-Ordner, funktioniert der Build vom TestPlugin anstandslos. Puuuuuh! Blöderweise kommt der Fehler auch, wenn ich zwei verschiedene VS Versionen für die beiden Projekte nutze. 2019 und 2022 beißen sich auch.

Jetzt fängst an zu stochern statt strukturiert vorzugehen, das macht kein Sinn.

Du, das ist ein Hobby. Ich mache das nicht professionell. Paarmal im Jahr überkommt es mich und ich habe plötzlich eine Idee, die ich programmieren will, und dann artet es schon öfter in einer Trail & Error-Orgie aus. 🙂 Googlen, Code-Beispiele ausprobieren, noch mehr Beispiele ausprobieren, grübeln, was ich anders machen könnte, frei nach dem Motto: wenn ich nich so rum, dann andersrum..., ausprobieren, usw. Aber nichts gegen Weiterentwicklung, wenn du mir einen Hinweis gibts, wie man strukturiert Fehler sucht, versuche ich es gerne.

Hier eventuell Hilfreich:
[Tutorial] Vertrackte Fehler durch Vergleich von echtem Projekt mit minimalem Testprojekt finden

Siehe auch die Links unten in dem Artikel.