3

I am opening a file using openfile dialog with the help of openas_rundll in c#.

Process.Start("rundll32.exe", string.Format("shell32.dll,OpenAs_RunDLL \"{0}\"", tempFilePath));

Now I want to detect which program is used to open the file. I want to trace the process.

My goal is to delete the file when user close the program.

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • 2
    There is no guarantee that a new process will be used to open the file. It may be opened by an existing process. Or the open could be handed to a third process that you don't know about. Or the user might close the file in the process and then use the process to open some other document (e.g. Notepad Open). – Raymond Chen Jul 30 '12 at 15:08
  • You are absolutely right Raymond. But let me explain you the scenario in detail. I have a file that I am opening using openas_rundll. Now when user is finish with the changes, I want my application to save a different copy of it and delete the existing one. Is this possible? – Dhaval Tawar Aug 03 '12 at 15:35
  • 1
    Not in the general case. It may be solvable in specific cases. – Raymond Chen Aug 03 '12 at 16:14
  • Can you please explain me in brief How can I achieve this? Right now I am using parent process and FileSystemWatcher together to achieve this. – Dhaval Tawar Aug 06 '12 at 05:37
  • 1
    As I already noted, this is not solvable in the general case. I may be solvable in specific cases. For example, if the file is an HTML document and the user opens it in Internet Explorer, then you can listen for the `DISPID_NAVIGATECOMPLETE` event (to detect that the user browsed to a different page) and the `DISPID_QUIT` event (to detect that the user closed the window). – Raymond Chen Aug 06 '12 at 13:33
  • It may not be HTML. But in general case it may be ms office files, txt, image files. – Dhaval Tawar Aug 07 '12 at 07:37
  • As I'm saying for the third time, there is no solution for the general case. It's possible that the file type handler sent the file name to another computer for remote editing. (Think [RAIL](http://download.microsoft.com/download/9/5/E/95EF66AF-9026-4BB0-A41D-A4F81802D92C/%5BMS-TSSO%5D.pdf).) Or the file was handed to an existing process (like Word). Or the user did a File.Open to view a different file (Notepad). – Raymond Chen Aug 07 '12 at 13:40
  • Thanks for the help. I think this will not be possible. Because I have the general case. The document can be of any type. – Dhaval Tawar Aug 07 '12 at 14:35

2 Answers2

0

You can try to catch the moment when actual app is closed by finding it py parent process id. If you found it, you can wait it to close as long as it is acceptable. Thanks jeremy-murray for GetAllProcessParentPids method:

public void StartProcessAndWathTillTerminated(string tempFilePath)
{
    // Show app selection dialog to user
    Process rundll32 = Process.Start("rundll32.exe", string.Format("shell32.dll,OpenAs_RunDLL {0}", tempFilePath));
    int rundll32id = rundll32.Id;

    // Wait till dialog is closed
    while (!rundll32.HasExited)
    {
        System.Threading.Thread.Sleep(50);
    }

    // Get all running processes with parent id
    Dictionary<int, int> allprocparents = GetAllProcessParentPids();

    int openedAppId = 0;
    // Loop throu all processes
    foreach (var allprocparent in allprocparents)
    {
        // Found child process, started by our rundll32.exe instance
        if (allprocparent.Value == rundll32id)
        {
            openedAppId = allprocparent.Key;
            break;
        }
    }

    // Check if we actually found any process. It can not be found in two situations:
    // 1) Process was closed too soon, while we was looking for it
    // 2) User clicked Cancel and no application was opened
    // Also it is possible that chesen application is already running. In this
    // case new instance will be opened by rundll32.exe for a very short period of 
    //time needed to pass file path to running instance. Anyway, this case falls into case 1).

   //If we ca not find process explicitly, we can try to find it by file lock, if one exists:
   //I'm using here a code snippet from https://stackoverflow.com/a/1263609/880156,
   //which assumes that there are possible more than one lock on this file. 
   //I just take first. 
   if (openedAppId==0)
    {
        Process handleExe = new Process();
        handleExe.StartInfo.FileName = "handle.exe";
        handleExe.StartInfo.Arguments = tempFilePath;
        handleExe.StartInfo.UseShellExecute = false;
        handleExe.StartInfo.RedirectStandardOutput = true;
        handleExe.Start();           
        handleExe.WaitForExit();
        string outputhandleExe = handleExe.StandardOutput.ReadToEnd();

        string matchPattern = @"(?<=\s+pid:\s+)\b(\d+)\b(?=\s+)";
        foreach(Match match in Regex.Matches(outputhandleExe, matchPattern))
        {
            openedAppId  = int.Parse(match.Value);
            break;
        }
    }


    if (openedAppId != 0)
    {
        Process openedApp = Process.GetProcessById(openedAppId);
        while (!openedApp.HasExited)
        {
            System.Threading.Thread.Sleep(50);
        }
    }
    // When we reach this position, App is already closed or was never started.
}


public static Dictionary<int, int> GetAllProcessParentPids()
{
    var childPidToParentPid = new Dictionary<int, int>();

    var processCounters = new SortedDictionary<string, PerformanceCounter[]>();
    var category = new PerformanceCounterCategory("Process");

    // As the base system always has more than one process running, 
    // don't special case a single instance return.
    var instanceNames = category.GetInstanceNames();
    foreach(string t in instanceNames)
    {
        try
        {
            processCounters[t] = category.GetCounters(t);
        }
        catch (InvalidOperationException)
        {
            // Transient processes may no longer exist between 
            // GetInstanceNames and when the counters are queried.
        }
    }

    foreach (var kvp in processCounters)
    {
        int childPid = -1;
        int parentPid = -1;

        foreach (var counter in kvp.Value)
        {
            if ("ID Process".CompareTo(counter.CounterName) == 0)
            {
                childPid = (int)(counter.NextValue());
            }
            else if ("Creating Process ID".CompareTo(counter.CounterName) == 0)
            {
                parentPid = (int)(counter.NextValue());
            }
        }

        if (childPid != -1 && parentPid != -1)
        {
            childPidToParentPid[childPid] = parentPid;
        }
    }

    return childPidToParentPid;
}

Update

It seems that there is no solution with 100% guarantee of success due to many reasons. I think that finding a process started by rundll32.exe is most solid among all other. If it fails, you still able to complete it with some other methods to determine process id.

As far as i know, there are several other ways to find that file is still used. Winword.exe, for example, creates some temp files in same directory and removes them when it closes. So if you able to catch a moment of temp files deleting then you may assume that program been closed.

Other programs may hold your file open by setting a lock on it. If so, you can find that program by finding lock owner. I used a solution with external program handle.exe from this answer https://stackoverflow.com/a/1263609/880156, so take a look.

I have to mention, that there may be no permanent file lock at all. It depend on program architecture. For example, if you open html file with Firefox, it reads file as fast as it can and closes it and does not leave file locked no more. In this case, even if you somehow find process name (e.g. "firefox.exe"), you will not able to find a moment when user closes a tab with your file.

If i were you, i would implement this solution, that still not ideal, and i would updgrade it later if it is necessary.

Community
  • 1
  • 1
Dmitriy Konovalov
  • 1,777
  • 14
  • 14
  • Thanks for the help. But I have tried this before. But the problem is, if user chooses program that is already running, openedAppId will be zero. Any other suggestions?? – Dhaval Tawar Aug 03 '12 at 15:37
  • Added some workaround for zero openedAppId. Please check updated code and read my thoughts. – Dmitriy Konovalov Aug 06 '12 at 07:38
0

Just a simple helper class which provides you with a method to open a file with the OpenWithDialog of windows and monitors the started processes with WMI to identify the choosen application.

for WMI, add System.Management.dll as reference

NOTICE: It doesn't recognice windows photo viewer - which is a dllhost.exe

Example call for your situation:

using (OpenFileDialog ofd = new OpenFileDialog())
{

    ofd.Filter = "All files(*.*)|*.*";
    if (ofd.ShowDialog() == System.Windows.Forms.DialogResult.OK)
    {
        using (Win.OpenWithDialogHelper helper = new Win.OpenWithDialogHelper())
        {
             helper.OpenFileAndWaitForExit(ofd.FileName);
             File.Delete(helper.Filepath);
        }
    }
}

The class:

namespace Win
{
    using System.Management;
    using System.Threading;
    using System.Diagnostics;
    using System.IO;

    public class OpenWithDialogHelper : IDisposable
    {
        #region members

        private Process openWithProcess;
        private ManagementEventWatcher monitor;

        public string Filepath { get; set; }
        public Process AppProcess { get; private set; }

        #endregion

        #region .ctor

        public OpenWithDialogHelper()
        {
        }

        public OpenWithDialogHelper(string filepath)
        {
            this.Filepath = filepath;
        }

        #endregion

        #region methods

        public void OpenFileAndWaitForExit(int milliseconds = 0)
        {
            OpenFileAndWaitForExit(this.Filepath, milliseconds);
        }

        public void OpenFileAndWaitForExit(string filepath, int milliseconds = 0)
        {
            this.Filepath = filepath;

            this.openWithProcess = new Process();
            this.openWithProcess.StartInfo.FileName = "rundll32.exe";
            this.openWithProcess.StartInfo.Arguments = String.Format("shell32.dll,OpenAs_RunDLL \"{0}\"", filepath);
            this.openWithProcess.Start();


            //using WMI, remarks to add System.Management.dll as reference!
            this.monitor = new ManagementEventWatcher(new WqlEventQuery("SELECT * FROM Win32_ProcessStartTrace"));
            this.monitor.EventArrived += new EventArrivedEventHandler(start_EventArrived);
            this.monitor.Start();

            this.openWithProcess.WaitForExit();

            //catching the app process...
            //it can't catched when the process was closed too soon
            //or the user clicked Cancel and no application was opened
            Thread.Sleep(1000);
            int i = 0;
            //wait max 5 secs...
            while (this.AppProcess == null && i < 3000)
            {
                Thread.Sleep(100); i++; 
            }

            if (this.AppProcess != null)
            {
                if (milliseconds > 0)
                    this.AppProcess.WaitForExit(milliseconds);
                else
                    this.AppProcess.WaitForExit();
            }
        }

        public void Dispose()
        {
            if (this.monitor != null)
            {
                this.monitor.EventArrived -= new EventArrivedEventHandler(start_EventArrived);
                this.monitor.Dispose();
            }

            if(this.openWithProcess != null)
                this.openWithProcess.Dispose();

            if (this.AppProcess != null)
                this.AppProcess.Dispose();
        }

        #endregion

        #region events

        private void start_EventArrived(object sender, EventArrivedEventArgs e)
        {
            int parentProcessID = Convert.ToInt32(e.NewEvent.Properties["ParentProcessID"].Value);
            //The ParentProcessID of the started process must be the OpenAs_RunDLL process
            //NOTICE: It doesn't recognice windows photo viewer
            // - which is a dllhost.exe that doesn't have the ParentProcessID
            if (parentProcessID == this.openWithProcess.Id)
            {
                this.AppProcess = Process.GetProcessById(Convert.ToInt32(
                    e.NewEvent.Properties["ProcessID"].Value));

                if (!this.AppProcess.HasExited)
                {
                    this.AppProcess.EnableRaisingEvents = true;
                }
            }
        }    

        #endregion
    }
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
codeteq
  • 1,502
  • 7
  • 13
  • There is a possibility that user will open file in windows photo viewer. The format of the file is not limited. It can be anything like MS Doc, Txt, JPEG, PNG, etc. So will this piece of code be helpful? – Dhaval Tawar Aug 03 '12 at 15:39