From 38bab59c1afa850a7fc532540607517274970c75 Mon Sep 17 00:00:00 2001 From: Richard Harrison Date: Wed, 21 Sep 2016 00:50:13 +0200 Subject: [PATCH] Revised Windows console handling - When started from the console use the console (when no --console) - When started from the GUI (with --console) open a new console window - When started from the GUI (without --console) don't open a new console window; stdout/stderr will not appear (except in logfiles as they do now). This opens stderr/stdout on the NUL device to alleviate any potential issues - When started from the Console (with --console) open a new console window - Ensure that IO redirection still works when started from the console. When redirecting stdout stderr will also be redirected (providing it wasn't already via 2>&1) - otherwise output from stderr will be lost. - When using redirection from the command prompt --console will produce an error message box. Notes: - fgfs needs to be a linked as Win32 GUI subsystem app - which it already is - What can't be done is to make the cmd prompt run fgfs synchronously; this is only something that can be done via "start /wait fgfs" Basically the way that Win32 works is quite sensible, but slightly at odds with the unix nature of the C-RTL; so the standard streams sort of get lost for GUI apps. AttachConsole and AllocConsole are provided to address this - but they do slightly different things. AttachConsole will attach to the cmd.exe (or any console related to the parent process), whereas AllocConsole will open a new one. Depending on where the application was launched from it makes sense to use AttachConsole for a cmd.exe launch and do nothing (unless --console is given) for a GUI launch. Redirection is not available from the GUI (unless set in the Process Create block) - so really only available when launched from the command line. If any stream is redirected then all must be otherwise it appears that AttachConsole will undo the redirection by changing the standard handles. --- simgear/debug/logstream.cxx | 163 ++++++++++++++++++++++++++++++++---- 1 file changed, 145 insertions(+), 18 deletions(-) diff --git a/simgear/debug/logstream.cxx b/simgear/debug/logstream.cxx index 2dea0fba..d1159cd1 100644 --- a/simgear/debug/logstream.cxx +++ b/simgear/debug/logstream.cxx @@ -42,6 +42,8 @@ #if defined (SG_WINDOWS) // for AllocConsole, OutputDebugString #include + #include + #include #endif const char* debugClassToString(sgDebugClass c) @@ -222,26 +224,109 @@ public: LogStreamPrivate() : m_logClass(SG_ALL), m_logPriority(SG_ALERT), +#if defined (SG_WINDOWS) + m_stdout_isRedirectedAlready(false), + m_stderr_isRedirectedAlready(false), +#endif m_isRunning(false) { - bool addStderr = true; #if defined (SG_WINDOWS) - // Check for stream redirection, has to be done before we call - // Attach / AllocConsole - const bool isFile = (GetFileType(GetStdHandle(STD_ERROR_HANDLE)) == FILE_TYPE_DISK); // Redirect to file? - if (AttachConsole(ATTACH_PARENT_PROCESS) == 0) { - // attach failed, don't install the callback - addStderr = false; - } else if (!isFile) { - // No - OK! now set streams to attached console - freopen("conout$", "w", stdout); - freopen("conout$", "w", stderr); - } -#endif - if (addStderr) { - m_callbacks.push_back(new StderrLogCallback(m_logClass, m_logPriority)); - m_consoleCallbacks.push_back(m_callbacks.back()); + /* + * 2016-09-20(RJH) - Reworked console handling + * 1) When started from the console use the console (when no --console) + * 2) When started from the GUI (with --console) open a new console window + * 3) When started from the GUI (without --console) don't open a new console + * window; stdout/stderr will not appear (except in logfiles as they do now) + * 4) When started from the Console (with --console) open a new console window + * 5) Ensure that IO redirection still works when started from the console + * + * Notes: + * 1) fgfs needs to be a GUI subsystem app - which it already is + * 2) What can't be done is to make the cmd prompt run fgfs synchronously; + * this is only something that can be done via "start /wait fgfs". + */ + + int stderr_handle_type = GetFileType(GetStdHandle(STD_ERROR_HANDLE)); + int stdout_handle_type = GetFileType(GetStdHandle(STD_OUTPUT_HANDLE)); + int stdout_isNull = 0; + int stderr_isNull = 0; + + m_stderr_isRedirectedAlready = stderr_handle_type == FILE_TYPE_DISK || stderr_handle_type == FILE_TYPE_PIPE || stderr_handle_type == FILE_TYPE_CHAR; + m_stdout_isRedirectedAlready = stdout_handle_type == FILE_TYPE_DISK || stdout_handle_type == FILE_TYPE_PIPE || stdout_handle_type == FILE_TYPE_CHAR; + + /* + * We don't want to attach to the console if either stream has been redirected - so in this case ensure that both streams + * are redirected as otherwise something will be lost (as Alloc or Attach Console will cause the handles that were bound + * to disappear) + */ + if (m_stdout_isRedirectedAlready){ + if (!m_stderr_isRedirectedAlready) + *stderr = *_fdopen(_open_osfhandle((intptr_t) GetStdHandle(STD_OUTPUT_HANDLE), _O_WRONLY), "a"); + else + *stderr = *_fdopen(_open_osfhandle((intptr_t) GetStdHandle(STD_ERROR_HANDLE), _O_WRONLY), "a"); + + *stdout = *_fdopen(_open_osfhandle((intptr_t) GetStdHandle(STD_OUTPUT_HANDLE), _O_WRONLY), "a"); + + } else { + /* + * Attempt to attach to the console process of the parent process; when launched from cmd.exe this should be the console, + * when launched via the RUN menu explorer, or another GUI app that wasn't started from the console this will fail. + * When it fails we will redirect to the NUL device. This is to ensure that we have valid streams. + * Later on in the initialisation sequence the --console option will be processed and this will cause the requestConsole() to + * always open a new console, except for streams that are redirected. The same rules apply there, if both streams are redirected + * the console will be opened, and it will contain a message to indicate that no output will be present because the streams are redirected + */ + if (AttachConsole(ATTACH_PARENT_PROCESS) == 0) { + /* + * attach failed - so ensure that the streams are bound to the null device - but only when not already redirected + */ + if (!m_stdout_isRedirectedAlready) + { + stdout_isNull = true; + freopen("NUL$", "w", stdout); + } + + if (!m_stderr_isRedirectedAlready) + { + stderr_isNull = true; + freopen("NUL$", "w", stderr); + } + } + /* + * providing that AttachConsole succeeded - we can then either reopen the stream onto the console, or use + * _fdopen to attached to the currently redirected (and open stream) + */ + if (!stdout_isNull){ + if (!m_stdout_isRedirectedAlready) + freopen("conout$", "w", stdout); + else + /* + * for already redirected streams we need to attach the stream to the OS handle that is open. + * - this comes from part of the answer http://stackoverflow.com/a/13841522 + * _open_osfhandle returns an FD for the Win32 Handle, which is then opened using fdopen and + * hopefully safely assigned to the stream (although it does look wrong to me it works) + * Removing this bit will stop pipes and command line redirection (> 2> and 2>&1 from working) + */ + *stdout = *_fdopen(_open_osfhandle((intptr_t) GetStdHandle(STD_OUTPUT_HANDLE), _O_WRONLY), "a"); + } + + if (!stderr_isNull){ + if (!m_stderr_isRedirectedAlready) + freopen("conout$", "w", stderr); + else + *stderr = *_fdopen(_open_osfhandle((intptr_t) GetStdHandle(STD_ERROR_HANDLE), _O_WRONLY), "a"); + } } + //http://stackoverflow.com/a/25927081 + //Clear the error state for each of the C++ standard stream objects. + std::wcout.clear(); + std::cout.clear(); + std::wcerr.clear(); + std::cerr.clear(); +#endif + + m_callbacks.push_back(new StderrLogCallback(m_logClass, m_logPriority)); + m_consoleCallbacks.push_back(m_callbacks.back()); #if defined (SG_WINDOWS) && !defined(NDEBUG) m_callbacks.push_back(new WinDebugLogCallback(m_logClass, m_logPriority)); m_consoleCallbacks.push_back(m_callbacks.back()); @@ -267,6 +352,11 @@ public: sgDebugClass m_logClass; sgDebugPriority m_logPriority; bool m_isRunning; +#if defined (SG_WINDOWS) + // track whether the console was redirected on launch (in the constructor, which is called early on) + bool m_stderr_isRedirectedAlready; + bool m_stdout_isRedirectedAlready; +#endif void startLog() { @@ -474,10 +564,47 @@ namespace simgear { void requestConsole() -{ - // this is a no-op now, stub exists for compatability for the moment. +{ +#if defined (SG_WINDOWS) + /* + * 2016-09-20(RJH) - Reworked console handling + * This is part of the reworked console handling for Win32. This is for building as a Win32 GUI Subsystem where no + * console is allocated on launch. If building as a console app then the startup will ensure that a console is created - but + * we don't need to handle that. + * The new handling is quite simple: + * 1. The constructor will ensure that these streams exists. It will attach to the + * parent command prompt if started from the command prompt, otherwise the + * stdout/stderr will be bound to the NUL device. + * 2. with --console a window will always appear regardless of where the process was + * started from. Any non redirected streams will be redirected + * 3. You cannot use --console and either redirected stream. + * + * This is called after the Private Log Stream constructor so we need to undo any console that it has attached to. + */ + + if (!global_privateLogstream->m_stderr_isRedirectedAlready && !global_privateLogstream->m_stdout_isRedirectedAlready) { + FreeConsole(); + if (AllocConsole()) { + if (!global_privateLogstream->m_stdout_isRedirectedAlready) + freopen("conout$", "w", stdout); + + if (!global_privateLogstream->m_stderr_isRedirectedAlready) + freopen("conout$", "w", stderr); + + //http://stackoverflow.com/a/25927081 + //Clear the error state for each of the C++ standard stream objects. + std::wcout.clear(); + std::cout.clear(); + std::wcerr.clear(); + std::cerr.clear(); + } + } else { + MessageBox(0, "--console ignored because stdout or stderr redirected with > or 2>", "Simgear Error", MB_OK | MB_ICONERROR); + } +#endif } + void shutdownLogging() { SGGuard g(global_logStreamLock);