Skip to content

Conversation

@jonasschnelli
Copy link
Contributor

fixes #6078
Meant for 0.11 branch.

@laanwj
Copy link
Member

laanwj commented May 1, 2015

Good catch. I already fixed this once before, people really shouldn't be touching this 'magic' code. Maybe add a comment referring to the issue.

@laanwj
Copy link
Member

laanwj commented May 1, 2015

This will likely bring back issue #3136 (solved by #5877). You should imbue some locale on WIN32, I'm not sure which one, but indeed C is wrong.
Maybe the result of std::locale("")? what does boost lazily initialize it to?

@laanwj laanwj added this to the 0.10.0 milestone May 1, 2015
@jonasschnelli
Copy link
Contributor Author

Right. This is not the right approach.
Was trying to go down the rabbit hole of win/unicode handling...
Can't find a solution. It seems that it could also be a compiler wchar issue.
It looks like that the default locale is C?

I could also guess that it might be connected to https://github.com/bitcoin/bitcoin/blob/master/src/util.cpp#L672 SHGetSpecialFolderPathA. The A stands for ANSI. There is also a SHGetSpecialFolderPathW equivalent which supports WCHAR.

@laanwj
Copy link
Member

laanwj commented May 1, 2015

In windows the file system locale depends on the user locale. I think it only works if boost uses the same locale as windows. But I'm not sure anymore how this was determined. I got it to work once, hence it worked in 0.10...
(using the W* functions is out of the question unless you use them everywhere and that'd involve a lot of platform specific code as well as widechar to UTF-8 conversion)

@dexX7
Copy link
Contributor

dexX7 commented May 1, 2015

@jonasschnelli: using the "C" locale to imbue boost::filesystem::path indeed triggers the failure on Windows in this context.

There is more than one problem tackled by SetupEnvironment():

  1. On POSIX systems, messed up environment settings can cause a failure (Python test).
  2. During the deinitialization, an internal facet pointer of boost::path is deleted, and if boost::path was not initialized by the main thread, it causes failures such as [Qt] Do you want to rebuild the block database now? No -> crash #3136 (and related).

The current situation:

  1. As default, the "C" locale is used.
  2. On POSIX systems, the environment locale is used (via std::locale("")).
  3. On POSIX systems, to detect bad environment settings, an exception thrown by std::locale("") is caught.
  4. On POSIX systems, and if there are bad environment settings, the "C" locale is used as fallback.
  5. As workaround, to explicitly force the initialization of the internal facet pointer by the main thread, boost::filesystem::path::imbue() is called.
  6. Bad initialization of boost::filesystem::boost::path can result in exceptions, which are currently not caught.
  7. On Windows systems, the "C" locale appearingly does not cover the whole character set, which can result in (6), as shown by 0.10.1-win64 does not start #6078.

In practise, the current 0.10.1 can result in a failure on Russian (and other) Windows, and earlier versions are affected by the deinitialization issue, resulting in faulty Boost test executions, as well as other failures during the shutdown, such as the crash reported in the context of rebuilding the block database.

@laanwj
Copy link
Member

laanwj commented May 1, 2015

Arguably the new problem is worse, as it prevents people from running the client at all with some characters in their data directories, instead of a rare shutdown race. What about:

    std::locale loc("C");
    // On most POSIX systems (e.g. Linux, but not BSD) the environment's locale
    // may be invalid, in which case the "C" locale is used as fallback.
    try {
        loc = std::locale(""); // Raises a runtime error if current locale is invalid
    } catch (const std::runtime_error&) {
#if !defined(WIN32) && !defined(MAC_OSX) && !defined(__FreeBSD__) && !defined(__OpenBSD__)
        setenv("LC_ALL", "C", 1);
#endif
    }
    // The path locale is lazy initialized and to avoid deinitialization errors 
    // in multithreading environments, it is set explicitly by the main thread.
    boost::filesystem::path::imbue(loc);

This would imbue the system locale on all platforms and should avoid the race condition, as well as the data directory problems. Needs to be tested though..

@jonasschnelli
Copy link
Contributor Author

@laanwj i tried you proposed code and it does not fix the problem. Maybe it would be worth to revert #5877 and find a proper solution.

What really surprises me: At least since 0.9.2 (didn't try further down), bitcoind and Bitcoin-Qt does not run with a datadir-path containing special Chinese or other Asian chars (Windows only).

@dexX7
Copy link
Contributor

dexX7 commented May 1, 2015

boost::filesystem::path::imbue() returns the "previously used locale", which should be the "right one", if called in a pristine environment, or to put it another way: if imbue() wouldn't be called at all, then this appears to be the one fs::path would use.

In the following patch, a dummy locale is used to extract the internal locale, to use it explicitly, which seems to be a preferable choice, because the goal is not to modify the locale at all, but to prevent deinitialization issues, and the bad-environment-fallback also kicks in earlier.

diff --git a/src/util.cpp b/src/util.cpp
index 4fea18b..68fb326 100644
--- a/src/util.cpp
+++ b/src/util.cpp
@@ -713,18 +713,20 @@ void RenameThread(const char* name)

 void SetupEnvironment()
 {
-    std::locale loc("C");
     // On most POSIX systems (e.g. Linux, but not BSD) the environment's locale
     // may be invalid, in which case the "C" locale is used as fallback.
 #if !defined(WIN32) && !defined(MAC_OSX) && !defined(__FreeBSD__) && !defined(__OpenBSD__)
     try {
-        loc = std::locale(""); // Raises a runtime error if current locale is invalid
+        std::locale(""); // Raises a runtime error if current locale is invalid
     } catch (const std::runtime_error&) {
         setenv("LC_ALL", "C", 1);
     }
 #endif
     // The path locale is lazy initialized and to avoid deinitialization errors 
     // in multithreading environments, it is set explicitly by the main thread.
+    // A dummy locale is used to extract the internal default locale, used by
+    // boost::filesystem::path, which is then used to explicitly imbue the path.
+    std::locale loc = boost::filesystem::path::imbue(std::locale::classic());
     boost::filesystem::path::imbue(loc);
 }

I tested 0.9.3, 0.10.1, another patch and this one, and it's behavior appears to be similar to 0.9.3 (i.e. it can handle paths, which 0.10.1 can't handle).

Example paths and test results:

Neither the newly patched version, 0.9.3, or 0.10.0, are able to use "E:\LocaleTests\юзза" as datadir.

When choosing "E:\LocaleTests\юзза" as datadir via bitcoin-qt 0.9.3, then the error message is in German, while the patched version, as well as bitcoin-qt 0.10.0, shows an English message, even though the rest of the UI is actually translated.

@theuni
Copy link
Member

theuni commented May 1, 2015

I think that's a bit circuitous. It works, but not for obvious reasons.

The returned loc from boost is the classic locale plus custom boost facet for windows. Otherwise, you could just replace the first call with std::locale().

The below is greatly simplified and I think it does what we want. Note that std::locale::global("") does not work as intended on windows, presumably because we're statically linking libstdc++. The basic C setlocale() does though.

void SetupEnvironment()
{
    if (!setlocale(LC_ALL, ""))
    {
        setenv("LC_ALL", "C", 1);
        setlocale(LC_ALL, "C");
    }
    boost::filesystem::path::imbue(std::locale());
}

All we're really trying to accomplish here is to teach boost how to handle locale-specific char conversions for paths, right? Afaik we don't mess with c++ locales elsewhere.

The above accomplishes that, won't throw exceptions, and I believe it's portable. Tested and verified with a Chinese profile/username/locale on win7, and on Linux with a busted locale via LC_LANG=bad ./bitcoind

@dexX7
Copy link
Contributor

dexX7 commented May 1, 2015

@theuni: when building on Ubuntu 14.04, with HOST=x86_64-w64-mingw32, and the dependency packages provided via depends, I had to wrap setenv with #if !defined(WIN32) ..., because it would otherwise cause a build error.

Back on Windows 8.1 x64, when using bitcoin-qt.exe, the tricky paths, such as ..\œ and ..\€@ä®, are accepted and work very well, however, when using ..\юзза, a runtime error is raised.

I was not able to use that path with any version or patch, though 0.9.3, and the build with the dummy-workaround, respond with "Error: Specified data directory "E:\LocaleTests\????" does not exist." instead.

The behavior of bitcoind.exe 0.9.3, the dummy-workaround patch, and your simple patch appears to be similar.

@jonasschnelli
Copy link
Contributor Author

Updated this PR with @theunis solution (fixed: added a #if !defined(WIN32) for setenv()).
Built through gitian: https://builds.jonasschnelli.ch/pulls/6093/

Won't run on win7 with a username containing Chinese chars (as it was with 0.9.3, 0.10.0, probably it was always like this).
But won't also run on win7 with a username containing Russian chars (where 0.9.3 and 0.10.0 was running okay).

Short term we need a solution to fix #6078 without reintroducing #3136.
Long term we need a solution who can handle all possible windows charsets.

bildschirmfoto 2015-05-02 um 10 37 53

bildschirmfoto 2015-05-02 um 10 43 54

@theuni
Copy link
Member

theuni commented May 2, 2015

Ok, I've done more debugging here.
tl;dr: After poking some more with even more variables, I think @dexX7's change is the right one.

@jonasschnelli It works fine for me with a Chinese username. Turns out there's another variable: selected system "Format" in system location settings. Looks like that's what influences the default locale at runtime rather than the system locale.

Some testing using printf("locale %s:"setlocale(LC_ALL,NULL)):
With a Chinese format selected:locale: Chinese (Simplified)_People's Republic of China.936
With English selected: English_United States.1252

So.. makes sense. Locale is per-user.

When the Format is set to Chinese, a Chinese username works.
When the format is left (default?) in English, the Chinese username doesn't work.

This is the case because when we use the user's locale (Format), paths for that language should just work.

However.

Using @dexX7's trick, Chinese username with English Format does work. This is because of boost's usage of its own codecvt, which queries the Win API for the codepage type: https://github.com/boostorg/filesystem/blob/boost-1.55.0/src/windows_file_codecvt.cpp#L39

So it looks like that's actually the way to go.

@dexX7
Copy link
Contributor

dexX7 commented May 2, 2015

@theuni: my idea was basically to get access and capture the whole following block, which uses different facets, depending on the OS and build: boost-1.55.0/src/path.cpp#L792-L873, and then use it to properly "initialize" Boost path by the main thread with the correct locale, after taking care of bad environment settings.

Nevertheless, it would probably make sense to catch the exception, or handle failures related to the datadir. In your simpler solution I can trigger a runtime error, if I use the path "E:\LocaleTests\юзза" (which is a legit path) on a German localized Windows, while 0.9.3 and my patch results in "Error: Specified data directory "E:\LocaleTests\????" does not exist.". I'm not entirely sure, where this is coming from, but ideally there should be a more expressive user faced message, with clear instructions to use a "compatible" path, if possible

FWIW: dexX7@c9a3784

@laanwj's words are still ringing in my ear.. :/

This more and more looks like just some occult incantation to ward off the evil spirits of bad locales :)

@sipa
Copy link
Member

sipa commented May 2, 2015

Can't we all just agree to go back to EBCDIC?

@laanwj
Copy link
Member

laanwj commented May 4, 2015

I preferred PETSCII :)

Question: at least for the 0.10 branch, can you code this so that it doesn't affect behavior on non-Windows? I'd hate to release a 'fix' then break the Linux issue again, where an invalid locale prevents the application from starting.

Using @dexX7's trick, Chinese username with English Format does work. This is because of boost's usage of its own codecvt, which queries the Win API for the codepage type: https://github.com/boostorg/filesystem/blob/boost-1.55.0/src/windows_file_codecvt.cpp#L39

Phew -- that's suble. Thanks for shedding some light on this dark matter. I agree that @dexX7's swap-the-locale trick, though circuitous. seems to be the best way forward.

src/util.cpp Outdated
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please correct instead of remove the comments. Esoteric knowledge about locales is needed to understand this on reading, heck, we will have forgotten about the rationale for this in a few months.

@jonasschnelli jonasschnelli force-pushed the 2015/05/fix_win_boost_path branch 7 times, most recently from 430e459 to fda4da8 Compare May 4, 2015 11:39
@jonasschnelli jonasschnelli changed the title don't imbue boost::filesystem::path with locale "C" on windows fix WIN32 boost::filesystem::path issues when using special chars for datadir path May 4, 2015
@jonasschnelli
Copy link
Contributor Author

Updated to make use of @dexX7 solution (#6093 (comment)).
I think this is okay for 0.11.

If there are no complains, i'll open a PR for the 0.10 branch with the same solution but only touching Win32 behavior (some ifdefs).

@theuni
Copy link
Member

theuni commented May 4, 2015

@dexX7 Thanks for sticking with this and explaining the issue well. It looks like it took me a little while to figure out exactly what you already had, I just had to work through it myself for it to make sense. Nice detective work!

@laanwj Agreed on making it windows only for 0.10, and on adding comments.

Additionally, we're depending on internal boost workings here that may change in the future, I wonder if it'd be worthwhile to add a quick test to use './bitcoind.exe -datadir=c:\something\non-latin'. Sadly, I don't think we could trust wine to faithfully reproduce the issue, but at least we'd have a set of tests that could be run manually.

If that's possible, we'd probably want to bake it into the test_bitcoin.exe binary to rule out python/shell middle-man issues.

Edit: thinking on it a little more, using -datadir for a test is overkill. We could just add a quick unit-test to add a subdir inside the current profile and touch a file in there.

@dexX7
Copy link
Contributor

dexX7 commented May 4, 2015

@laanwj: Question: at least for the 0.10 branch, can you code this so that it doesn't affect behavior on non-Windows?

Is this the case? Travis is happy with the change applied on top of 0.10.1, and the bad-environment test passes as well. It does not bring back #3136, and at least to my understanding, it should restore the behavior of pre-0.10, but without the multithreading-deinitialization issue. But please don't give my understanding much weight here.

All this is based on the assumption that path::codecvt() is called via boost::filesystem::path at some point, which then undergoes a similar path as path::imbue() does, to return the original locale.

@theuni: Thanks for sticking with this and explaining the issue well.

It's the least I can do, given that my fix for #3136 (deinitialization issue), and the follow up #5950 (bad environment handling), ultimately led in this situation.

We could just add a quick unit-test to add a subdir inside the current profile and touch a file in there.

Having more tests for this would be incredibly useful.

There might be a twist though: the unit tests (via TestingSetup) appear to use pathTemp = GetTempPath() / ... for the temporary test datadir, and this probably throws, even before any tests are executed, if path can't handle paths inside the current profile, because GetTempPath() returns \Users\{username}\AppData\Local\Temp as per default on Windows Vista+.

Just brainstorming on this: what I tested manually might be wrapped into a Python test, which starts and stops nodes, using a bunch of datadirs with "exotic" characters. Unfortunally I'm not sure, which test data could be used for it, given that, appearingly, the system's localization plays a significant role here (e.g. œ™€® passes on my system, while юзза, which seems to work fine for the original poster of #6078, doesn't).

Tackling this from another perspective: ideally there would also be some form of mitigation in the context of GetDataDir() ([create datadir] and related), to a) catch potential exceptions, and b) provide some form of guidance for the user, or more telling messages, if it actually happens.

Edit: where I'm getting: there are probably other related issues in the context of the filesystem, which are not related to localization. For example, using a datadir without permission, results in a segfault.

@theuni
Copy link
Member

theuni commented May 4, 2015

@dexX7 If the test throws during initialization, then it will fail anyway, no? I don't see how that's a problem as it still indicates an error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated comment :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@jonasschnelli jonasschnelli force-pushed the 2015/05/fix_win_boost_path branch from fda4da8 to 3da7849 Compare May 10, 2015 08:06
@jonasschnelli
Copy link
Contributor Author

I lost track of this PR.
How we proceed here? @theuni @dexX7? Is this ready?

My tests still tell me that there are problems with special char datadir on windows (as it always was the case). Maybe this PR should concentrate on fixing #6078 without reintroducing #3136 (solved by #5877).
There will still be cases where bitcoind/qt won't start up. As example: if you use a Chinese username on a english windows.

@dexX7
Copy link
Contributor

dexX7 commented May 10, 2015

@jonasschnelli: As example: if you use a Chinese username on a english windows.

It's the best solution I've found, though this is still very unfortunate. As far as I can see, it restores the behavior of 0.9.3, without reintroducing #3136.

@jonasschnelli
Copy link
Contributor Author

Okay. Then i think this is merge ready and should probably get a ACK/NACK from @theuni.
@laanwj: Should we backport this (as it is) to 0.10 or should i create a PR where it only touches win32 codeflow?

@laanwj laanwj merged commit 3da7849 into bitcoin:master May 10, 2015
laanwj added a commit that referenced this pull request May 10, 2015
3da7849 [squashme] simplify SetupEnvironment() (by dexX7) (Jonas Schnelli)
b3ffcdf don't imbue boost::filesystem::path with locale "C" on windows (Jonas Schnelli)
@laanwj
Copy link
Member

laanwj commented May 10, 2015

Cherry-picked to 0.10 as 424ae66 (the change is now subtle enough that no special path for WIN32 is necessary)

@theuni
Copy link
Member

theuni commented May 10, 2015

post-merge ACK.

@dexX7
Copy link
Contributor

dexX7 commented May 10, 2015

@laanwj: the current 0.10 tip doesn't include the changes of this PR?

Edit: now I see it. :)

laanwj pushed a commit that referenced this pull request May 11, 2015
@mart2038
Copy link

post-merge comments. Kudos to you guys for sticking with a win32 issue, but... Why? 'They' say that coding for *nix is too human resource hungry due to intra-distribution incompatibilities... hmmmmm, Oooookay, if 'they' say so.

@sipa @laanwj EBCDIC, PETSCII You must have been rich! "When I were a lad" (olde Englysh comedy) we programmed our 4004 directly in binary using mechanical switches. Our output device was four LED's. Encoding? Who needs it? ;-)

reddink pushed a commit to reddcoin-project/reddcoin-3.10 that referenced this pull request May 27, 2020
@bitcoin bitcoin locked as resolved and limited conversation to collaborators Sep 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0.10.1-win64 does not start

6 participants