How to prevent GetOpenFileName from changing the current directory while the dialog is shown?
Asked Answered
S

3

1

GetOpenFileName (for questionable reasons) changes the current directory of an application while the dialog is shown. This can be reset on dialog closure by specifying OFN_NOCHANGEDIR as dialog initialization flag:

OFN_NOCHANGEDIR Restores the current directory to its original value if the user changed the directory while searching for files.

Setting this flag, however, doesn't prevent the function from changing the current directory while the explorer dialog is shown.

This is an issue in multithreaded environments where other threads rely on the current directory to remain being the executable's path.

Is there a way to prevent GetOpenFileName from changing the current directory of an application while the explorer dialog is shown and the user browses through folders?

Sporting answered 22/5, 2018 at 12:43 Comment(8)
Not to helpful, but isn't "where other threads rely on the current directory to remain" an (threading) issue already? Can't you make those other threads to not require this? (related entertainment)Jacobson
As it seems this cannot be avoided, the only thing you can do is to have all threads use a variable with the/their current directory and make all file accesses absolute accesses using this variable (the variable can be a parameter, can be thread-global or can be just global if all threads use the same current directory).Siouan
When you say "This is an issue in multithreaded environments where other threads rely on the current directory to remain being the executable's path", they can never rely on that. It's whatever it happens to be when the process is launched, no guarantees whatsoever. So I'm with Christan: if you possibly can, fix the app.Broughton
@Jacobson I don't understand how it's a threading issue when a function that's supposed to show folder content decides to change the current directory of a whole application. And I'm afraid it's not that easy on a game where the resource factory is built on the current directory logic (not written by me). "Current directory hell" is something I can certainly relate to.Sporting
Alright, understanding the concerns about that. While it's not ideal how it is, it would require a ridiculous amount of effort to change the resource loading system "pathing" just for this "dialog problem". I'll post a cheap fix I have found as an answer to this, but will mention that it's not the best solution. Thanks so far!Sporting
If you're really, really, really stuck with relying on the current directory never changing, perhaps you can invoke GetOpenFileName in the context of a helper process, see: #3460374. It might even be enough to disable your main window while the open file dialog is open, maybe you don't need to resort to the level of trickery in that post.Broughton
Don't you just use IFileDialog and avoid the problem completely? Or am I missing something.Madriene
Arguably, the real bug is relying on the current working directory. It's a process-wide property, and any thread can change it at any time. You cannot reliably control the current working directory in a multithreaded application. Always construct fully qualified path names. You get to the executable's directory calling GetModuleFileName or friends.Cheryl
S
4

You have several options here:

  1. Use detours or MinHook to hook SetCurrentDirectory and make it do nothing for your process (ugly)
  2. Use a custom file chooser that does not change current directory. (better)
  3. Remove dependencies on current directory in your code, as you're likely to run into other bugs related to it, especially in multi-threaded environment. (best)
Storax answered 22/5, 2018 at 14:1 Comment(1)
I doubt there is a clean solution for this issue. As others mentioned, ideally, I would have to rewrite a lot of the code base to make sure there is no direct dependency on the current directory for file operations. Thanks for the incentives. There is certainly more than these 3 options though. I've posted another solution I'm sticking with as answer. @PaulSanders mentioned a helper process. So there's two more.Sporting
S
0

As others in the comments mentioned, it's not ideal to rely on the current directory to remain the same at all times. I would've changed it if it didn't require rewriting a resource loading factory (not my code).

However, one, admittedly cheap and messy, solution for this problem is to hook the window procedure of the OpenFileDialog and react to the folder change notification. From there, a previously backed up directory path can be "forced" as the current directory. This is what I did:

Backup:

// Well outside of the OpenFileDialog logic
static TCHAR g_BackupDir[MAX_PATH];
...
GetCurrentDirectory(ARRAYSIZE(g_BackupDir), g_BackupDir);

Hook:

// Hooking procedure specified as lpfnHook parameter to the GetOpenFileName function
// Requires the flags OFN_EXPLORER and OFN_ENABLEHOOK to be set as well
UINT_PTR CALLBACK OFNHookProc(_In_ HWND hdlg, _In_ UINT uiMsg, _In_ WPARAM wParam, _In_ LPARAM lParam)
{
    switch (uiMsg)
    {
        case WM_NOTIFY:
        {
            LPNMHDR pNotify = reinterpret_cast<LPNMHDR>(lParam);
            switch (pNotify->code)
            {
                case CDN_FOLDERCHANGE:
                    // Force back initial current dir
                    SetCurrentDirectory(g_BackupDir);

                    // No further processing on dialog
                    return 1;
            }
        }
        break;
    }

    // Default processing
    return 0;
}

The dialog logic itself doesn't seem to depend on the current directory in any way, shape or form. It works perfectly even when the current directory is being forced back on folder changes.

Goes into the category "Does the trick" and inferior to using absolute paths.

Sporting answered 22/5, 2018 at 14:15 Comment(3)
I considered posting this as an option in my answer but I didn't think it was a good one considering you said you worked in a multi-threaded environment. This will leave a tiny window of opportunity for another thread to invoke GetCurrentDirectory before you invoke SetCurrentDirectory and set it back, so it may lead to those hard-to-track bugs. But, if it works for you...Storax
Yes indeed, good spot! With hook proc being invoked on the same stack call of the folder change, it's almost instantly being called after the problematic directory change. So I think this will work almost all the time and is good enough a solution for me considering the circumstances. Thanks :)Sporting
Is 'almost all the time' really good enough? If this leads to sporadic, unpredictable and subtle bugs in your end product you might come to regret this. I personally would never even consider it. Solve the problem properly. People have posted at least two possible approaches but you seem dead set on doing it 'your way'.Broughton
B
0

I decided to post what ought to be a workable solution to this. It involves fixing the code 'properly' to use absolute paths but bear with me. It's not that hard. The answer posted by the OP is just oh so dangerous. It's not 'cheap' at all. On the contrary, it is likely to prove very expensive in the long run.

Now I assume that there are function / method calls scattered throughout the code that access files or folders via relative pathnames (most likely unqualified filenames). There may well be a lot of these but they probably all boil down to this:

do_something_with_this_file ("somefile.foo");

Now if do_something_with_this_file is a function defined within the application the solution is obvious: change the implementation of that function to convert the parameter passed in to an absolute path name, if necessary, before doing anything with it.

But life is not so easy if do_something_with_this_file is something like a call to CreateFile or perhaps fopen. Then we are stuck with the existing behaviour for that function. Or are we? Actually, no. There's a simple solution involving just a macro and a small amount of implementation work.

Time for an example. I will use fopen since it has a nice simple API, but it applies equally well to CreateFile or indeed anything else.

Firstly, create a header file, lets call it AbsolutePathHelpers.h, which redefines fopen, and whatever else you need, like so:

// AbsolutePathHelpers.h

FILE *APHfopen (const char *filename, const char *mode);
#define fopen APHfopen
// ...

Make sure that this header file gets included in all your compilation units, probably by #includeing it in some common header file that they all use.

Now write the implementation of APHfopen in a new .cpp file (let's call it AbsolutePathHelpers.cpp) which you will need link in with your project:

// AbsolutePathHelpers.cpp
#include <stdio.h>

#undef fopen
FILE *APHfopen (const char *filename, const char *mode)
{
    printf ("Opening %s\n", filename);      // diagnostic message for testing
    // Convert filename to absolute path here (if necessary) before calling the 'real' fopen
    return fopen (filename, mode);
}

// ...

And finally a simple test program:

// Test program
#include <stdio.h>

int main ()
{
    FILE *f = fopen ("myfile", "r");
    if (f)
        fclose (f);

    printf ("Finished.  Press any key...");
    getchar ();
    return 0;
}

Output:

Opening myfile
Finished.  Press any key...

Run it at Wandbox.

I'll leave the details of converting relative paths to absolute paths to the OP. All you need is a global variable containing the current directory in effect when the program starts up. Or, I strongly suspect, that should actually be the directory containing the executable.

Broughton answered 27/5, 2018 at 9:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.