Elektroda.pl
Elektroda.pl
X
Proszę, dodaj wyjątek dla www.elektroda.pl do Adblock.
Dzięki temu, że oglądasz reklamy, wspierasz portal i użytkowników.

[C#] - Obsługa plików tymczasowych przez zewnętrzne aplikacje

20 Lut 2013 15:04 2016 6
  • Moderator Samochody
    Jest aplikacja kliencka z poziomu której użytkownik widzi listę plików załączoną do karty (mniejsza o szczegóły). Wszystko, włącznie z plikami (pdf) jest przechowywane w bazie MS-SQL. Problem wygląda następująco: jak najprościej zrobić, żeby użytkownik programu mógł otworzyć wiele plików - w momencie otwarcia pliku, obiekt jest pobierany z bazy i zapisywany na dysku w katalogu tymczasowym jako plik, następnie plik ten jest otwierany w skojarzonej z danym typem plików aplikacji, a po zamknięciu tej aplikacji plik jest usuwany.

    Próbowałem tak, ale nie działa - da się otworzyć tylko jeden plik, przy kolejnym występuje nieobsłużony wyjątek:

    Kod: csharp
    Zaloguj się, aby zobaczyć kod
  • Poziom 42  
    A jaki konkretnie wyjątek wyskakuje i w którym miejscu?

    A co do sposobu - to ten zaprezentowany wydaje się najsensowniejszym, choć trochę marnuje zasobów (ilość tworzonych wątków). Można by rozważyć dwie inne możliwości - cyklicznie próbujemy usunąć pliki (wszak wciąż otwarte po prostu nie dadzą się usunąć), albo usuwamy na koniec programu.

    A - Twoje rozwiązanie nie obsłuży prawdopodobnie przypadku, kiedy plik np. poprzez komunikat DDE wysłany z aplikacji do innej jej instancji, jest otwierany w istniejącym już oknie. Pomyśl też o takim przypadku ;)
  • Moderator Samochody
    Próbowałem jeszcze tak:
    Kod: csharp
    Zaloguj się, aby zobaczyć kod

    Niby jest ok, ale otwierając kolejny plik jest on pobierany i od razu usuwany, zanim jeszcze zostanie otwarty.
  • Poziom 2  
    Zakładając, że Folder jet w dalszym ciągu tymczasowy a Nazwa unikalna per plik, teraz wygląda całkowicie poprawnie.

    Nie sądzę, żeby para p.Start/p.WaitForExit puszczała wątek przedwcześnie, więc błąd musi być na zewnatrz tego kodu.

    Podejrzewam, że odpalasz w/w kod pojedynczo dla każdego pojedynczego rekordu z bazy. Sprawdź przede wszystkim czy nazwy plików się nie powtarzają. Jeden nurt wątku mógł by ruszyć żeby obsłużyć plik z nazwą XYZ, podczas gdy inny wątek mógłby dla tej samej nazwy próbować zrobić to samo, ułamek wcześniej, i usunąć plik zanim pierwszy by w ogóle dotarł do odpalenia procesu. Szybko to sprawdzisz dodając kilka Debug.WriteLine, żeby odnotowały jakie ID-wątku utworzyło, a jakie ID-wątku usunęło plik.

    O, a może poprostu współdzielisz zmienne Folder i Nazwa i wszystkie Twoje wątki korzystają defacto z tych samych wartości? Zwróć uwagę na DELEGATE. Jeżeli, closure schwytało referencję do $this, to wtedy będą współdzielone. Jeżeli Folder i Nazwa były zmiennymi lokalnymi metody, to wtedy zostaną złapane przez wartość i będą odseparowane. Porównaj:

    Kod: csharp
    Zaloguj się, aby zobaczyć kod


    Pierwsza i druga są identyczne. Jedyna różnica to czy explicite wskazujesz $this jako źródło, czy nie, jednak w zachowaniu są identyczne - closure schwyta $this i zapamięta że ::liczba i ::napis mają być odczytane z $this. Skonczy sie to współdzieleniem wartości.

    Trzecia jest inna. Closure tutaj schwyta dwie zmienne lokalne, i nie ma pojęcia o $this. Konkretne wartości 'liczby' i 'napisu' będą ujęte w closure i efektywnie, każdy taki delegat będzie miał swoją "kopię" danych.
  • Moderator Samochody
    Przepisałem tę funkcję dodałem Debug i to jego wynik:
    Kod: csharp
    Zaloguj się, aby zobaczyć kod


    Code:
    D/L: C:\Users\Tomek\AppData\Local\Temp\KW-Wpis_Jerzy.pdf
    
    The thread 0x1694 has exited with code 0 (0x0).
    START: 4256 : C:\Users\Tomek\AppData\Local\Temp\KW-Wpis_Jerzy.pdf
    D/L: C:\Users\Tomek\AppData\Local\Temp\KW-Wpis_Regina.pdf
    The thread 0x760 has exited with code 0 (0x0).
    START: 1368 : C:\Users\Tomek\AppData\Local\Temp\KW-Wpis_Regina.pdf
    FINISH: 1368 : C:\Users\Tomek\AppData\Local\Temp\KW-Wpis_Regina.pdf
    <Acrobat Reader się uruchamia i zgłasza, że plik nie istnieje>
    The thread 0x1044 has exited with code 0 (0x0).
    The thread 0x774 has exited with code 0 (0x0).
    The thread 0x1268 has exited with code 0 (0x0).
    <zamknięcie okna Acrobat Readera z pierwszym plikiem>
    FINISH: 4256 : C:\Users\Tomek\AppData\Local\Temp\KW-Wpis_Jerzy.pdf
    The thread 0xbb4 has exited with code 0 (0x0).

    D/L - pobranie pliku z bazy,
    START - otwarcie pliku w domyślnej aplikacji / id procesu
    FINISH - zakończenie procesu i usunięcie pliku / id procesu

    ***

    Wychodzi na to, że Acrobat Reader się w ten sposób zachowuje, bo jeśli jeden plik jest pdf, a drugi jpg to wszystko działa. Tak jakby otwierał plik w nowym procesie, po czym go kończył i próbował otworzyć w tym samym procesie, który już jest uruchomiony.
  • Poziom 2  
    Aj, cholender. Nie pomyślałem, o tym. To prawda, dokładnie tak się dzieje.

    Większość "dużych" programów obsługuje pracę wielodokumentową. Np. Excel. Jednak bardzo wiele z nich nie integruje się "kompletnie" z Shell'em Windowsów (czyli explorer.exe) i nie używa jego "zaawansowanych" funkcji, np. DDE/Open, tylko do otwierania rejestruje się po staremu "dosowemu/konsolowemu" jako program-i-parameter-uruchomienia, czyli sciezka do pliku.

    Konczy sie to tym, ze odpalasz taki program i otwierasz kilka dokumentow - i jest ok. Lecz gdy dwu-klikniesz w Explorerze na pliku i program sie otworzy, a potem dwu-klikniesz na inny plik --- otworzy się drugi program. Lipnie to wygląda z poziomu użytkownika. Przecież już miałem wielo-dokumentowy program otworzony, prawda?

    Jeżeli autorzy programu nie chcą używać "lepszych ficzerów" Explorera, to "tradycyjnie" robią to na około: Program na starcie nie pokazuje się natychmiast, tylko najpierw sprawdza czy jest już uruchomiony poprzednik. Jeżeli jest, to wysyła do niego przez IPC komendę otwarcia kolejnego pliku, a sam się natychmiast kończy. W ten sposób user widzi że "mam jeden program który otwiera wiele plików", a i oszczędza sie na czasie uruchamiania i ładowania kolenych kopii programu.

    Sprawdziłem u siebie w rejestrze mapping plików PDF:

    .pdf -> AcroExch.Document -> Shell/Open/Command = "C:\Program Files\Adobe\Reader 9.0\Reader\AcroRd32.exe" "%1"

    czyli dokladnie jest tak jak zaobserwowałeś.. Nowy Acrobat startuje za każdym razem, dostaje ścieżkę do klejnych plików, a potem najwyraźniej przekazuje wywołanie do poprzednika: możesz to błyskawicznie spradzić - otwórz kilka PDFów i sprawdź TaskManagerem, ile Acrobatów jest otworzone. U mnie zawsze jest co najwyżej jeden. To znaczy że Acrobat jest skonstruowany jako wielodokumentowy, mimo że pokazuje każdy dokument w osobnym oknie i udaje przed użytkownikiem że jest tych Acrobatów więcej uruchomionych..

    To znaczy, że dla Twojego zestawu typów plików jest o wiele więcej pracy do wykonania..

    Ogólnie, miałeś cztery wyjścia:
    1) założyć że każdy plik otworzy się w nowym procesie
    2) założyć że dane rozszerzenie obsługuje jeden typ programów, i że zawsze są to różne procesy per rozszerzenie
    3) zbudować minibazę określającą które rozszerzenia obsługiwane są wspólnie
    4) nie zakładać niczego..

    (1) już odpadło. Jeśli nie Acrobat, to zauważyłbyś to pewnie na Wordzie czy Excelu..
    (2) w miarę proste do wykonania, wystarczy przed otworzeniem pogrupować pliki wg rozszerzeń i otwierać-zamykać je grupami. tzn otworzyć wszystkie na raz, ale np. nie kasować PDFów dopóki choć jeden proces skojarzony z PDF'ami jeszcze pracuje
    (3) wykonywalne dokładnie jak (2), ale zabezpiecza przed faktem, że np. .rtf .doc i .docx są Wordem, a .pdf i .fdf Acrobatem otwierane. Implementacja (2) sobie z tym czasami nie poradzi. Minus jest taki, że musisz przeanalizować każde rozszerzenie pliku i jakoś je wstępnie ręcznie pogrupować, i to grupowanie gdzies w programie przechować.. albo w konfiguracji, albo w bazie..
    (4) nie zakładać żadnego grupowania. Otworzyć wszystkie pliki, i nie kasować żadnego, dopóki wszystkie procesy z tej serii się nie zakończą. O wiele szybsze w implementacji niż 2/3, ale wiadomo, nie dokładnie to samo. Pliki będą "wisieć" dopóki uzytkownik nie zamknie wszytkiego.

    Ale.. Niestety, jeśli chodzi o wielo-dokumentowe programy, nawet to nie uratuje sytyacji w 100%. Łatwo można sobie wyobrazić, że użytkownik już miał jakiegoś PDF otworzonego, a potem Twoj program puścił serię PDFow do otworzenia. Wszystkie mikro-procesy które tylko przekazują polecenia dalej natychmiast sie zakonczą i wtopka.

    Możesz iść na wojnę i próbować analizować który plik jest otwierany którym programem, nawet ręcznie - ostatecznie wiesz jakie pliki są w bazie trzymane - i wyryć w programie odpowiednie detektory i zachowania dla PDF, odpowiednie detektory i zachowania dla DOC, itp. Może to nawet się całkiem ładnie zgeneralizować i udać. Jednak jest to sporo więcej pracy.

    W ogólności, nie wiele da się zrobić z tym faktem, ponieważ Twój program, ani nikt poza systemem operacyjnym, nie ma zielonego pojęcia, co to znaczy "otworzyć plik typu XYZ". Nie wiadomo co system z tym zrobi. Możesz podglądac wpisy w rejestrze i śledzić czy otwiera dany plik Explorer Acrobat czy IrfanView, ale jest to już totalna przesada moim zdaniem.. Zresztą, "sprytnie" napisany mikroproces do obsługi pliku WTF mógłby przecież dla danego pliku otwierać raz przeglądarkę a raz edytor, zależnie od nagłówka pliku..

    Jeżeli typów plików masz mało i nie odstręcza Cię analiza ich "cyklu życia", to chyba już wszystko wiesz..

    IMHO, nie powinieneś jednak śledzić procesów. Spróbuj się skupić na samym inteligentnym ich usuwaniu. Spróbuj np. po otworzeniu pliku, uśpić wątek na 5-10 sekund i następnie skasowac plik w ciemno. Jeżeli przeglądarka tych plików jest inteligentna, to założy na ten plik blokadę i przy usuwaniu poleci wyjątek. Złapiesz go, i uspisz wątek na następne 10 sekund i sprobujesz potem ponownie, i tak dalej do skutku. Nieinteligentna przeglądarka plików nie zaloży blokady i usuniesz go od razu po pierwszych 5-10 sekundach, ALE będzie on już załadowany i wyświetlony, dopóki przeglądarki plików się nie wyłączy..

    Inna myśl: Czy użytkownik otwiera ich bardzo wiele tych plików? Ile one ważą przeciętnie w jednej sesji korzystania z Twojego programu? Może calkowicie rozsądne będzie nie kasowanie ich od razu, tylko hurtem w momencie wyłączania Twojego programu? Program udostępnił pliki, konczysz pracę z programem, pliki udostępnione wyparowują - całkowicie wytłumaczalne użytkownikowi: "Nie zamykaj mojego programu, póki pracujesz na jego plikach."

    Inna myśl: Jeżeli wszystko inne zawiedzie, system Windows ma mechanizm "usuwania plików na starcie systemu". Jeżeli otwieranych plików jest rozsądna ilość, możesz nie kasować ich nigdy, tylko ściągać z bazy, otwierać i dopisywać do tej listy do-usunięcia. Będą śmiecić przez chwilę, ale przy następnym włączeniu kompa zostaną usunięte. Niestety opóźni to start systemu w zależności od liczby plików, a i użytkownik pewnie kilka razy się zdziwi, że mu pliki zniknęły a przecież cały czas sobie leżały gdzieś tam przez kilka godzin.. Nie mówiąć o użytkownikach takich jak ja, którzy przez 95% hibernują system.

    Podsumowując, niełatwo. IMHO, najrozsądniejszą opcją wykonywalną w krótkim czasie jest zasypianie i próba kasowania co jakiś czas. Nieudane otwarcie (np. wyjątek z Process.Start) - to można kasować natychmiast. Po udanym otwarciu zaś od razu uśpij wątek na dłużej, np. 30 sekund, żeby mieć pewność że przeglądarka danych plików na pewno miała dość czasu, zeby się uruchomić. Jak będą problemy u kogoś na Pentium-333MHz zawsze można ten interwał zwiększyć. A następne próby kasowania już mogą być szybsze, np. co 5-10 sekund, zeby nie zarzynać systemu.. Można to wtedy wszystko zgromadzić w jednym (!) wątku, nie potrzeba już N wątków dla N plików. Jeden wątek plus lista plików do skasowania, i powinno chodzić cicho i sprawnie.

    Niestety, niektóre programy nie dość że nie zakładają locków na otwarte dokumenty, i nie dość, że wykrywają czy otwarty plik się np. zmienił (np - VisualStudio), to jeszcze wykrywają, czy może został skasowany z dysku i w takim przypadku zamykają jego podgląd. Część przeglądarek wbudowanych w WindowsXP/Vista/7 sie tak zachowuje na przyklad. Z nimi - nie wygrasz w żaden sensowny sposób i pozostaje kasowanie hurtowe na zamknięciu programu, lub zmianie sesji systemu operacyjnego (logoff uwaga na switch-user,sleep i hibernacje)..
  • Moderator Samochody
    Ostatecznie zdecydowałem się na wariant rozwiązania nr 4 - utrzymuję listę otwartych plików i mam licznik zewnętrznych procesów - w momencie zakończenia procesu sprawdzam licznik i jeśli wskazuje 0 to próbuję kasować wszystkie pliki z listy.