This serie first implements an evolution to programs/wineconsole in order to be able to run a given (CUI) program with a clear status on its console. It allows to pickup between: a newly created (graphical) console, a newly created window-less console (a real console object, but not displayed at all), or detached (no console at all).
As a reminder, Wine also provides two other kinds of console: - running a CUI program, from a unix env, without redirecting all the 3 std streams (ie at least one of the three needs to be mapped to the unix console). This uses a bridge with the current Unix tty. (nick name: console-shell) - running a CUI program, from a unix env, with the 3 std streams not being tied to a console. (nick name: console-shell-no-window) Wineconsole doesn't provide those two consoles as they are already available directly from command line.
Testing some of the Wine programs revealed some bugs in Wine programs/.
Quite a few of them, when outputing, try first to write in Unicode to the console, and fall back (in case output stream has been redirected) to WriteFile, after converting the Unicode string with GetConsoleOutputCP().
This will lead to wrong results when: - program isn't attached to any console - program is attached a console-shell-no-window.
Some others programs do the same, but forcing CP_ACP instead.
I think it's time to evolve a bit... For the two use cases, I propose to always convert output to UTF8 in case of redirection (done with on example of each).
Signed-off-by: Eric Pouech eric.pouech@gmail.com ---
Eric Pouech (4): programs/wineconsole: select console type and/or std handles for child process dlls/kernelbase: fix CreateProcess with CREATE_NO_WINDOW when no std handles are inherited programs/chcp.com: write redirected output in UTF8 programs/attrib: write redirected output in UTF8
dlls/kernelbase/process.c | 2 +- programs/attrib/attrib.c | 9 +- programs/chcp.com/main.c | 9 +- programs/wineconsole/wineconsole.c | 192 ++++++++++++++++++++---- programs/wineconsole/wineconsole.man.in | 40 ++++- programs/wineconsole/wineconsole.rc | 12 ++ programs/wineconsole/wineconsole_res.h | 2 + 7 files changed, 222 insertions(+), 44 deletions(-)
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- programs/wineconsole/wineconsole.c | 192 ++++++++++++++++++++++++++----- programs/wineconsole/wineconsole.man.in | 40 ++++++ programs/wineconsole/wineconsole.rc | 12 ++ programs/wineconsole/wineconsole_res.h | 2 4 files changed, 212 insertions(+), 34 deletions(-)
diff --git a/programs/wineconsole/wineconsole.c b/programs/wineconsole/wineconsole.c index 1924bf791e6..714332f6c24 100644 --- a/programs/wineconsole/wineconsole.c +++ b/programs/wineconsole/wineconsole.c @@ -32,50 +32,180 @@
WINE_DEFAULT_DEBUG_CHANNEL(console);
-int WINAPI wWinMain( HINSTANCE inst, HINSTANCE prev, WCHAR *cmdline, INT show ) +static const unsigned int EC_INTERNAL = 255; /* value of exit_code for internal errors */ + +static void usage(LPCWSTR option) { - STARTUPINFOW startup = { sizeof(startup) }; - PROCESS_INFORMATION info; - WCHAR *cmd = cmdline; - DWORD exit_code; + WCHAR tmp[1024]; + + if (option) + { + LoadStringW( GetModuleHandleW( NULL ), IDS_CMD_UNKNOWN_OPTION, tmp, ARRAY_SIZE(tmp) ); + fwprintf(stderr, tmp, option); + } + LoadStringW( GetModuleHandleW( NULL ), IDS_CMD_USAGE, tmp, ARRAY_SIZE(tmp) ); + fprintf(stderr, "%ls\n", tmp); + exit( EC_INTERNAL ); +} + +/*********************************************************************** + * build_command_line + * + * Build the command line of a process from the argv array. + * (copied from dlls/ntdll/unix/env.c) + * + * We must quote and escape characters so that the argv array can be rebuilt + * from the command line: + * - spaces and tabs must be quoted + * 'a b' -> '"a b"' + * - quotes must be escaped + * '"' -> '"' + * - if ''s are followed by a '"', they must be doubled and followed by '"', + * resulting in an odd number of '' followed by a '"' + * '"' -> '\"' + * '\"' -> '\\"' + * - ''s are followed by the closing '"' must be doubled, + * resulting in an even number of '' followed by a '"' + * ' ' -> '" \"' + * ' \' -> '" \\"' + * - ''s that are not followed by a '"' can be left as is + * 'a\b' == 'a\b' + * 'a\b' == 'a\b' + */ +static WCHAR *build_command_line( WCHAR **wargv ) +{ + int len; + WCHAR **arg, *ret; + LPWSTR p; + + len = 1; + for (arg = wargv; *arg; arg++) len += 3 + 2 * wcslen( *arg ); + if (!(ret = malloc( len * sizeof(WCHAR) ))) return NULL; + + p = ret; + for (arg = wargv; *arg; arg++) + { + BOOL has_space, has_quote; + int i, bcount; + WCHAR *a;
- static WCHAR default_cmd[] = L"cmd"; + /* check for quotes and spaces in this argument (first arg is always quoted) */ + has_space = (arg == wargv) || !**arg || wcschr( *arg, ' ' ) || wcschr( *arg, '\t' ); + has_quote = wcschr( *arg, '"' ) != NULL;
- FreeConsole(); /* make sure we're not connected to inherited console */ - if (!AllocConsole()) + /* now transfer it to the command line */ + if (has_space) *p++ = '"'; + if (has_quote || has_space) + { + bcount = 0; + for (a = *arg; *a; a++) + { + if (*a == '\') bcount++; + else + { + if (*a == '"') /* double all the '\' preceding this '"', plus one */ + for (i = 0; i <= bcount; i++) *p++ = '\'; + bcount = 0; + } + *p++ = *a; + } + } + else + { + wcscpy( p, *arg ); + p += wcslen( p ); + } + if (has_space) + { + /* Double all the '' preceding the closing quote */ + for (i = 0; i < bcount; i++) *p++ = '\'; + *p++ = '"'; + } + *p++ = ' '; + } + if (p > ret) p--; /* remove last space */ + *p = 0; + if (p - ret >= 32767) { - ERR( "failed to allocate console: %lu\n", GetLastError() ); - return 1; + ERR( "command line too long (%Iu)\n", p - ret ); + exit( EC_INTERNAL ); } + return ret; +} + +static int report_failure( STARTUPINFOW *si, LPCWSTR cmd ) +{ + WCHAR format[256], *buf; + DWORD len;
- if (!*cmd) cmd = default_cmd; + WARN( "CreateProcess failed: %lu\n", GetLastError() ); + LoadStringW( GetModuleHandleW( NULL ), IDS_CMD_LAUNCH_FAILED, format, ARRAY_SIZE(format) ); + len = wcslen( format ) + wcslen( cmd ); + if ((buf = malloc( len * sizeof(WCHAR) ))) + { + swprintf( buf, len, format, cmd ); + if (si) + { + INPUT_RECORD ir; + WriteConsoleW( si->hStdOutput, buf, wcslen(buf), &len, NULL); + while (ReadConsoleInputW( si->hStdInput, &ir, 1, &len ) && ir.EventType == MOUSE_EVENT); + } + else fprintf(stderr, "%ls\n", buf); + } + return EC_INTERNAL; +}
- startup.dwFlags = STARTF_USESTDHANDLES; - startup.hStdInput = CreateFileW( L"CONIN$", GENERIC_READ | GENERIC_WRITE, 0, NULL, - OPEN_EXISTING, 0, 0 ); - startup.hStdOutput = CreateFileW( L"CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL, - OPEN_EXISTING, 0, 0 ); - startup.hStdError = startup.hStdOutput; +int wmain( int argc, WCHAR *argv[] ) +{ + STARTUPINFOW startup = { sizeof(startup) }; + PROCESS_INFORMATION info; + DWORD cpflags = 0; + BOOL inherit = FALSE; + DWORD exit_code; + WCHAR *cmdline; + int i;
- if (!CreateProcessW( NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &startup, &info )) + for (i = 1; i < argc; i++) { - WCHAR format[256], *buf; - INPUT_RECORD ir; - DWORD len; - exit_code = GetLastError(); - WARN( "CreateProcess failed: %lu\n", exit_code ); - LoadStringW( GetModuleHandleW( NULL ), IDS_CMD_LAUNCH_FAILED, format, ARRAY_SIZE(format) ); - len = wcslen( format ) + wcslen( cmd ); - if ((buf = malloc( len * sizeof(WCHAR) ))) + if (argv[i][0] != '-' || argv[i][1] != '-') break; + if (!argv[i][2]) {i++; break;} + if ( !wcscmp(argv[i], L"--mode=detached")) cpflags = DETACHED_PROCESS; + else if (!wcscmp(argv[i], L"--mode=console")) cpflags = CREATE_NEW_CONSOLE; + else if (!wcscmp(argv[i], L"--mode=headless")) cpflags = CREATE_NO_WINDOW; + else if (!wcscmp(argv[i], L"--console-std")) inherit = FALSE; + else if (!wcscmp(argv[i], L"--inherit-std")) inherit = TRUE; + else usage(argv[i]); + } + cmdline = i < argc ? build_command_line(&argv[i]) : wcsdup(L"cmd.exe"); + /* if at least one option is passed, don't use old mode */ + if (i > 1 && !cpflags) cpflags = CREATE_NEW_CONSOLE; + if (!cpflags) /* keep old behavior in place */ + { + FreeConsole(); + if (!AllocConsole()) { - swprintf( buf, len, format, cmd ); - WriteConsoleW( startup.hStdOutput, buf, wcslen(buf), &len, NULL); - while (ReadConsoleInputW( startup.hStdInput, &ir, 1, &len ) && ir.EventType == MOUSE_EVENT); + ERR( "failed to allocate console: %lu\n", GetLastError() ); + return EC_INTERNAL; } - return exit_code; + + startup.dwFlags |= STARTF_USESTDHANDLES; + startup.hStdInput = CreateFileW( L"CONIN$", GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, 0, 0 ); + startup.hStdOutput = CreateFileW( L"CONOUT$", GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, 0, 0 ); + startup.hStdError = startup.hStdOutput; + } + else if (inherit) + { + startup.dwFlags |= STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle( STD_INPUT_HANDLE ); + startup.hStdOutput = GetStdHandle( STD_OUTPUT_HANDLE ); + startup.hStdError = GetStdHandle( STD_ERROR_HANDLE ); } + if (!CreateProcessW( NULL, cmdline, NULL, NULL, FALSE, cpflags, NULL, NULL, &startup, &info )) + return report_failure( cpflags ? NULL : &startup, cmdline );
CloseHandle( info.hThread ); WaitForSingleObject( info.hProcess, INFINITE ); - return GetExitCodeProcess( info.hProcess, &exit_code ) ? exit_code : GetLastError(); + return GetExitCodeProcess( info.hProcess, &exit_code ) ? exit_code : EC_INTERNAL; } diff --git a/programs/wineconsole/wineconsole.man.in b/programs/wineconsole/wineconsole.man.in index dac4e62f321..1eb60ec100f 100644 --- a/programs/wineconsole/wineconsole.man.in +++ b/programs/wineconsole/wineconsole.man.in @@ -3,11 +3,45 @@ wineconsole - The Wine console .SH SYNOPSIS .B wineconsole -.RI [ command "] " +.RI "[ " options " ] [ " command " ] " .SH DESCRIPTION .B wineconsole -is the Wine console manager, used to run console commands and applications. It allows running the -console in a newly made window. +is the Wine console manager, used to run console commands and applications. + +It allows to have fine grain management over console and standard I/O streams used when running an application. +.SH OPTIONS + +.IP \fB--mode=console\fR +\fBwineconsole\fR will execute the command in a newly created window. + +This is the default when none of the \fB--mode=\fR options is provided. +.IP \fB--mode=detached\fR +\fBwineconsole\fR will execute the \fIcommand\fR without being attached to any console. +.IP \fB--mode=headless\fR +\fBwineconsole\fR will execute the \fIcommand\fR attached to an invisible console. +.IP \fB--console-std\fR +The \fIcommand\fR's standard I/O streams will be mapped to the console designed by \fB--mode=\fR option. + +This is the default when neither \fB--console-std\fR nor \fB--inherit-std\fR is provided. +.IP \fB--inherit-std\fR +The \fIcommand\fR's standard I/O streams will be mapped to the standard Unix streams of \fBwineconsole\fR. + +.IP \fIcommand\fR +The name of the executable to run, potentially followed by its arguments, with same meaning and syntax than using \fBwine\fR. + +If this part is omitted, than \fBcmd.exe\fR is run without arguments. + +\fBwineconsole\fR waits for the \fIcommand\fR to terminate before exiting. + +The exit status of the \fBwineconsole\fR is the exit status for the \fIcommand\fR, except when an error internal to \fBwineconsole\fR occurs, and 255 is returned. + +.SH NOTES +Consoles are only of interest when the \fIcommand\fR executable belongs to the CUI subsystem. + +Using \fBwineconsole\fR overrides default Wine console creation when invoked from regular shell or script. + +This default console acts as a real console from the Windows environment, while inhering standard input streams. + .SH BUGS Bugs can be reported on the .UR https://bugs.winehq.org diff --git a/programs/wineconsole/wineconsole.rc b/programs/wineconsole/wineconsole.rc index 6a61dd31ede..bea205935e6 100644 --- a/programs/wineconsole/wineconsole.rc +++ b/programs/wineconsole/wineconsole.rc @@ -25,6 +25,18 @@ LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT STRINGTABLE BEGIN
+IDS_CMD_UNKNOWN_OPTION "Unknown option %s\n" +IDS_CMD_USAGE "Usage: wineconsole [options] [command]\n\ +\n\ +options:\n\ + --mode=detached start [command] not being attached to any console\n\ + --mode=console start [command] being attached to a newly created console (this is the default)\n\ + --mode=headless start [command] being attached to a newly created yet not visible console\n\ +\n\ + --console-std ensures standard I/O streams of command are mapped to the console (this is the default)\n\ + --inherit-std ensures standard I/O streams of command are mapped to the ones which wineconsole is run with\n\ +\n\ + [command]: executable (and optional arguments) to run\n" IDS_CMD_LAUNCH_FAILED "wineconsole: Starting program %s failed.\nThe command is invalid.\n"
END diff --git a/programs/wineconsole/wineconsole_res.h b/programs/wineconsole/wineconsole_res.h index cca60ac5857..e5a8cb39d86 100644 --- a/programs/wineconsole/wineconsole_res.h +++ b/programs/wineconsole/wineconsole_res.h @@ -22,4 +22,6 @@ #include <winuser.h> #include <commctrl.h>
+#define IDS_CMD_UNKNOWN_OPTION 0x302 +#define IDS_CMD_USAGE 0x303 #define IDS_CMD_LAUNCH_FAILED 0x304
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- dlls/kernelbase/process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dlls/kernelbase/process.c b/dlls/kernelbase/process.c index 32f622bdf3e..b87538c26a7 100644 --- a/dlls/kernelbase/process.c +++ b/dlls/kernelbase/process.c @@ -207,7 +207,7 @@ static RTL_USER_PROCESS_PARAMETERS *create_process_params( const WCHAR *filename params->hStdOutput = startup->hStdOutput; params->hStdError = startup->hStdError; } - else if (flags & (DETACHED_PROCESS | CREATE_NEW_CONSOLE)) + else if (flags & (DETACHED_PROCESS | CREATE_NEW_CONSOLE | CREATE_NO_WINDOW)) { params->hStdInput = INVALID_HANDLE_VALUE; params->hStdOutput = INVALID_HANDLE_VALUE;
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- programs/chcp.com/main.c | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/programs/chcp.com/main.c b/programs/chcp.com/main.c index 6c5864d0e19..b7299d05437 100644 --- a/programs/chcp.com/main.c +++ b/programs/chcp.com/main.c @@ -34,14 +34,13 @@ static void output_writeconsole(const WCHAR *str, DWORD wlen) DWORD len; char *msgA;
- /* On Windows WriteConsoleW() fails if the output is redirected. So fall - * back to WriteFile(), assuming the console encoding is still the right - * one in that case. + /* WriteConsoleW() fails if the output is redirected. So fall back to WriteFile(), + * using UTF8 encoding. */ - len = WideCharToMultiByte(GetConsoleOutputCP(), 0, str, wlen, NULL, 0, NULL, NULL); + len = WideCharToMultiByte(CP_UTF8, 0, str, wlen, NULL, 0, NULL, NULL); msgA = malloc(len);
- WideCharToMultiByte(GetConsoleOutputCP(), 0, str, wlen, msgA, len, NULL, NULL); + WideCharToMultiByte(CP_UTF8, 0, str, wlen, msgA, len, NULL, NULL); WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), msgA, len, &count, FALSE); free(msgA); }
Signed-off-by: Eric Pouech eric.pouech@gmail.com
--- programs/attrib/attrib.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/programs/attrib/attrib.c b/programs/attrib/attrib.c index 4344200b3ec..00c852b24f4 100644 --- a/programs/attrib/attrib.c +++ b/programs/attrib/attrib.c @@ -86,7 +86,8 @@ static int WINAPIV ATTRIB_wprintf(const WCHAR *format, ...) }
/* If writing to console has failed (ever) we assume it's file - i/o so convert to OEM codepage and output */ + * i/o so convert to UTF8 and output + */ if (!res) { BOOL usedDefaultChar = FALSE; DWORD convertedChars; @@ -103,10 +104,10 @@ static int WINAPIV ATTRIB_wprintf(const WCHAR *format, ...) return 0; }
- /* Convert to OEM, then output */ - convertedChars = WideCharToMultiByte(CP_ACP, 0, output_bufW, + /* Convert to UTF8, then output */ + convertedChars = WideCharToMultiByte(CP_UTF8, 0, output_bufW, len, output_bufA, MAX_WRITECONSOLE_SIZE, - "?", &usedDefaultChar); + NULL, &usedDefaultChar); WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), output_bufA, convertedChars, &nOut, FALSE); }
Eric Pouech eric.pouech@gmail.com writes:
Signed-off-by: Eric Pouech eric.pouech@gmail.com
programs/attrib/attrib.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-)
That doesn't seem to match the Windows behavior.
Le 18/04/2022 à 12:11, Alexandre Julliard a écrit :
Eric Pouecheric.pouech@gmail.com writes:
Signed-off-by: Eric Pouecheric.pouech@gmail.com
programs/attrib/attrib.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-)
That doesn't seem to match the Windows behavior.
well, the idea was to have a nicer integration when redirected to streams expressed from unix command line
if you really want to stick to pure windows behavior in all cases, GetConsoleCP() is wrong anyway... we need to use CP_ACP unconditionnaly AFAICS
A+
Eric Pouech eric.pouech@orange.fr writes:
Le 18/04/2022 à 12:11, Alexandre Julliard a écrit :
Eric Pouech eric.pouech@gmail.com writes:
Signed-off-by: Eric Pouech eric.pouech@gmail.com
programs/attrib/attrib.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-)
That doesn't seem to match the Windows behavior.
well, the idea was to have a nicer integration when redirected to streams expressed from unix command line
if you really want to stick to pure windows behavior in all cases, GetConsoleCP() is wrong anyway... we need to use CP_ACP unconditionnaly AFAICS
Maybe Windows doesn't use GetConsoleCP(), but at least here it's definitely using OEM codepage.
Maybe Windows doesn't use GetConsoleCP(), but at least here it's definitely using OEM codepage.
according to https://testbot.winehq.org/JobDetails.pl?Key=112920
W7 up to 10 do use OEM cp (tested with various locale settings)
however, running locally on W11 I get:
cpcp.c:75: Run on éric, cpflags=0 cpcp.c:76: OEM(437): ko cpcp.c:77: ACP(0): ok cpcp.c:79: Con(1252): ok cpcp.c:80: UFT7: ko cpcp.c:81: UTF8: ko cpcp.c:82: mbcp: ok cpcp.c:75: Run on éric, cpflags=8 cpcp.c:76: OEM(437): ko cpcp.c:77: ACP(0): ok cpcp.c:79: Con(0): ok cpcp.c:80: UFT7: ko cpcp.c:81: UTF8: ko cpcp.c:82: mbcp: ok 0bf4:cpcp: 10 tests executed (0 marked as todo, 0 failures), 0 skipped.
(note: when not attached to a console, GetConsole(Output)CP() returns 0 for failure...
if this is not tested for failure, and passed as code page for MB conversion, it's CP_ACP)
so do we stick to W7-W10 behavior and always use OEM CP?
A+