Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add console wrapper app to handle console i/o redirection on Windows. #67434

Merged
merged 1 commit into from
Nov 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SConstruct
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,7 @@ if selected_platform in platform_list:

methods.generate_version_header(env.module_version_string)

env["PROGSUFFIX_WRAP"] = suffix + env.module_version_string + ".console" + env["PROGSUFFIX"]
env["PROGSUFFIX"] = suffix + env.module_version_string + env["PROGSUFFIX"]
env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
# (SH)LIBSUFFIX will be used for our own built libraries
Expand Down
7 changes: 7 additions & 0 deletions editor/export/editor_export_platform_pc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,16 @@ Error EditorExportPlatformPC::prepare_template(const Ref<EditorExportPreset> &p_
return ERR_FILE_NOT_FOUND;
}

String wrapper_template_path = template_path.get_basename() + "_console.exe";
int con_wrapper_mode = p_preset->get("debug/export_console_script");
bool copy_wrapper = (con_wrapper_mode == 1 && p_debug) || (con_wrapper_mode == 2);

Ref<DirAccess> da = DirAccess::create(DirAccess::ACCESS_FILESYSTEM);
da->make_dir_recursive(p_path.get_base_dir());
Error err = da->copy(template_path, p_path, get_chmod_flags());
if (err == OK && copy_wrapper && FileAccess::exists(wrapper_template_path)) {
err = da->copy(wrapper_template_path, p_path.get_basename() + ".console.exe", get_chmod_flags());
}
if (err != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Prepare Template"), TTR("Failed to copy export template."));
}
Expand Down
25 changes: 25 additions & 0 deletions platform/windows/SCsub
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,44 @@ common_win = [
"gl_manager_windows.cpp",
]

common_win_wrap = [
"console_wrapper_windows.cpp",
]

res_file = "godot_res.rc"
res_target = "godot_res" + env["OBJSUFFIX"]
res_obj = env.RES(res_target, res_file)

prog = env.add_program("#bin/godot", common_win + res_obj, PROGSUFFIX=env["PROGSUFFIX"])

# Build console wrapper app.
if env["windows_subsystem"] == "gui":
env_wrap = env.Clone()
res_wrap_file = "godot_res_wrap.rc"
res_wrap_target = "godot_res_wrap" + env["OBJSUFFIX"]
res_wrap_obj = env_wrap.RES(res_wrap_target, res_wrap_file)

if env.msvc:
env_wrap.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"])
env_wrap.Append(LINKFLAGS=["version.lib"])
else:
env_wrap.Append(LINKFLAGS=["-Wl,--subsystem,console"])
env_wrap.Append(LIBS=["version"])

prog_wrap = env_wrap.add_program("#bin/godot", common_win_wrap + res_wrap_obj, PROGSUFFIX=env["PROGSUFFIX_WRAP"])

# Microsoft Visual Studio Project Generation
if env["vsproj"]:
env.vs_srcs += ["platform/windows/" + res_file]
env.vs_srcs += ["platform/windows/godot.natvis"]
for x in common_win:
env.vs_srcs += ["platform/windows/" + str(x)]
if env["windows_subsystem"] == "gui":
for x in common_win_wrap:
env.vs_srcs += ["platform/windows/" + str(x)]

if not os.getenv("VCINSTALLDIR"):
if env["debug_symbols"] and env["separate_debug_symbols"]:
env.AddPostAction(prog, run_in_subprocess(platform_windows_builders.make_debug_mingw))
if env["windows_subsystem"] == "gui":
env.AddPostAction(prog_wrap, run_in_subprocess(platform_windows_builders.make_debug_mingw))
181 changes: 181 additions & 0 deletions platform/windows/console_wrapper_windows.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*************************************************************************/
/* console_wrapper_windows.cpp */
/*************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */
/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/

#include <windows.h>

#include <shlwapi.h>
#include <stdio.h>
#include <stdlib.h>

#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x4
#endif

int main(int argc, char *argv[]) {
// Get executable name.
WCHAR exe_name[MAX_PATH] = {};
if (!GetModuleFileNameW(nullptr, exe_name, MAX_PATH)) {
wprintf(L"GetModuleFileName failed, error %d\n", GetLastError());
return -1;
}

// Get product name from the resources and set console title.
DWORD ver_info_handle = 0;
DWORD ver_info_size = GetFileVersionInfoSizeW(exe_name, &ver_info_handle);
if (ver_info_size > 0) {
LPBYTE ver_info = (LPBYTE)malloc(ver_info_size);
if (ver_info) {
if (GetFileVersionInfoW(exe_name, ver_info_handle, ver_info_size, ver_info)) {
LPCWSTR text_ptr = nullptr;
UINT text_size = 0;
if (VerQueryValueW(ver_info, L"\\StringFileInfo\\040904b0\\ProductName", (void **)&text_ptr, &text_size) && (text_size > 0)) {
SetConsoleTitleW(text_ptr);
}
}
free(ver_info);
}
}

// Enable virtual termial sequences processing.
HANDLE stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD out_mode = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(stdout_handle, out_mode);

// Find main executable name and check if it exist.
static PCWSTR exe_renames[] = {
L".console.exe",
L"_console.exe",
L" console.exe",
L"console.exe",
nullptr,
};

bool rename_found = false;
for (int i = 0; exe_renames[i]; i++) {
PWSTR c = StrRStrIW(exe_name, nullptr, exe_renames[i]);
if (c) {
CopyMemory(c, L".exe", sizeof(WCHAR) * 5);
rename_found = true;
break;
}
}
if (!rename_found) {
wprintf(L"Invalid wrapper executable name.\n");
return -1;
}

DWORD file_attrib = GetFileAttributesW(exe_name);
if (file_attrib == INVALID_FILE_ATTRIBUTES || (file_attrib & FILE_ATTRIBUTE_DIRECTORY)) {
wprintf(L"Main executable %ls not found.\n", exe_name);
return -1;
}

// Create job to monitor process tree.
HANDLE job_handle = CreateJobObjectW(nullptr, nullptr);
if (!job_handle) {
wprintf(L"CreateJobObject failed, error %d\n", GetLastError());
return -1;
}

HANDLE io_port_handle = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1);
if (!io_port_handle) {
wprintf(L"CreateIoCompletionPort failed, error %d\n", GetLastError());
return -1;
}

JOBOBJECT_ASSOCIATE_COMPLETION_PORT compl_port;
ZeroMemory(&compl_port, sizeof(compl_port));
compl_port.CompletionKey = job_handle;
compl_port.CompletionPort = io_port_handle;

if (!SetInformationJobObject(job_handle, JobObjectAssociateCompletionPortInformation, &compl_port, sizeof(compl_port))) {
wprintf(L"SetInformationJobObject(AssociateCompletionPortInformation) failed, error %d\n", GetLastError());
return -1;
}

JOBOBJECT_EXTENDED_LIMIT_INFORMATION jeli;
ZeroMemory(&jeli, sizeof(jeli));
jeli.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

if (!SetInformationJobObject(job_handle, JobObjectExtendedLimitInformation, &jeli, sizeof(jeli))) {
wprintf(L"SetInformationJobObject(ExtendedLimitInformation) failed, error %d\n", GetLastError());
return -1;
}

// Start the main process.
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));

STARTUPINFOW si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);

WCHAR new_command_line[32767];
_snwprintf_s(new_command_line, 32767, _TRUNCATE, L"%ls %ls", exe_name, PathGetArgsW(GetCommandLineW()));

if (!CreateProcessW(nullptr, new_command_line, nullptr, nullptr, true, CREATE_SUSPENDED, nullptr, nullptr, &si, &pi)) {
wprintf(L"CreateProcess failed, error %d\n", GetLastError());
return -1;
}

if (!AssignProcessToJobObject(job_handle, pi.hProcess)) {
wprintf(L"AssignProcessToJobObject failed, error %d\n", GetLastError());
return -1;
}

ResumeThread(pi.hThread);
CloseHandle(pi.hThread);

// Wait until main process and all of its children are finished.
DWORD completion_code = 0;
ULONG_PTR completion_key = 0;
LPOVERLAPPED overlapped = nullptr;

while (GetQueuedCompletionStatus(io_port_handle, &completion_code, &completion_key, &overlapped, INFINITE)) {
if ((HANDLE)completion_key == job_handle && completion_code == JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO) {
break;
}
}

CloseHandle(job_handle);
CloseHandle(io_port_handle);

// Get exit code of the main process.
DWORD exit_code = 0;
GetExitCodeProcess(pi.hProcess, &exit_code);

CloseHandle(pi.hProcess);

return exit_code;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
return main(0, nullptr);
}
48 changes: 11 additions & 37 deletions platform/windows/export/export_plugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,13 @@ Error EditorExportPlatformWindows::sign_shared_object(const Ref<EditorExportPres
}
}

Error EditorExportPlatformWindows::_export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path) {
Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::WRITE);
if (f.is_null()) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), vformat(TTR("Could not open file \"%s\"."), p_path));
return ERR_CANT_CREATE;
}

f->store_line("@echo off");
f->store_line("title \"" + p_app_name + "\"");
f->store_line("\"%~dp0" + p_pkg_name + "\" \"%*\"");
f->store_line("pause > nul");

return OK;
}

Error EditorExportPlatformWindows::modify_template(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags) {
if (p_preset->get("application/modify_resources")) {
_rcedit_add_data(p_preset, p_path);
_rcedit_add_data(p_preset, p_path, true);
String wrapper_path = p_path.get_basename() + ".console.exe";
if (FileAccess::exists(wrapper_path)) {
_rcedit_add_data(p_preset, wrapper_path, false);
}
}
return OK;
}
Expand All @@ -71,6 +60,10 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset>
Error err = EditorExportPlatformPC::export_project(p_preset, p_debug, pck_path, p_flags);
if (p_preset->get("codesign/enable") && err == OK) {
_code_sign(p_preset, pck_path);
String wrapper_path = p_path.get_basename() + ".console.exe";
if (FileAccess::exists(wrapper_path)) {
_code_sign(p_preset, wrapper_path);
}
}

if (p_preset->get("binary_format/embed_pck") && err == OK) {
Expand All @@ -81,25 +74,6 @@ Error EditorExportPlatformWindows::export_project(const Ref<EditorExportPreset>
}
}

String app_name;
if (String(GLOBAL_GET("application/config/name")) != "") {
app_name = String(GLOBAL_GET("application/config/name"));
} else {
app_name = "Unnamed";
}
app_name = OS::get_singleton()->get_safe_dir_name(app_name);

// Save console script.
if (err == OK) {
int con_scr = p_preset->get("debug/export_console_script");
if ((con_scr == 1 && p_debug) || (con_scr == 2)) {
String scr_path = p_path.get_basename() + ".cmd";
if (_export_debug_script(p_preset, app_name, p_path.get_file(), scr_path) != OK) {
add_message(EXPORT_MESSAGE_ERROR, TTR("Debug Script Export"), TTR("Could not create console script."));
}
}
}

return err;
}

Expand Down Expand Up @@ -146,7 +120,7 @@ void EditorExportPlatformWindows::get_export_options(List<ExportOption> *r_optio
r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/trademarks"), ""));
}

Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path) {
Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path, bool p_set_icon) {
String rcedit_path = EDITOR_GET("export/windows/rcedit");

if (rcedit_path != String() && !FileAccess::exists(rcedit_path)) {
Expand Down Expand Up @@ -184,7 +158,7 @@ Error EditorExportPlatformWindows::_rcedit_add_data(const Ref<EditorExportPreset

List<String> args;
args.push_back(p_path);
if (!icon_path.is_empty()) {
if (!icon_path.is_empty() && p_set_icon) {
args.push_back("--set-icon");
args.push_back(icon_path);
}
Expand Down
3 changes: 1 addition & 2 deletions platform/windows/export/export_plugin.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@
#include "platform/windows/logo.gen.h"

class EditorExportPlatformWindows : public EditorExportPlatformPC {
Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path);
Error _rcedit_add_data(const Ref<EditorExportPreset> &p_preset, const String &p_path, bool p_set_icon);
Error _code_sign(const Ref<EditorExportPreset> &p_preset, const String &p_path);
Error _export_debug_script(const Ref<EditorExportPreset> &p_preset, const String &p_app_name, const String &p_pkg_name, const String &p_path);

public:
virtual Error export_project(const Ref<EditorExportPreset> &p_preset, bool p_debug, const String &p_path, int p_flags = 0) override;
Expand Down
31 changes: 31 additions & 0 deletions platform/windows/godot_res_wrap.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#include "core/version.h"
#ifndef _STR
#define _STR(m_x) #m_x
#define _MKSTR(m_x) _STR(m_x)
#endif

1 VERSIONINFO
FILEVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0
PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,VERSION_PATCH,0
FILEOS 4
FILETYPE 1
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Godot Engine"
VALUE "FileDescription", VERSION_NAME " (Console)"
VALUE "FileVersion", VERSION_NUMBER
VALUE "ProductName", VERSION_NAME " (Console)"
VALUE "Licence", "MIT"
VALUE "LegalCopyright", "Copyright (c) 2007-" _MKSTR(VERSION_YEAR) " Juan Linietsky, Ariel Manzur and contributors"
VALUE "Info", "https://godotengine.org"
VALUE "ProductVersion", VERSION_FULL_BUILD
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
2 changes: 0 additions & 2 deletions platform/windows/os_windows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ void RedirectIOToConsole() {
RedirectStream("CONIN$", "r", stdin, STD_INPUT_HANDLE);
RedirectStream("CONOUT$", "w", stdout, STD_OUTPUT_HANDLE);
RedirectStream("CONOUT$", "w", stderr, STD_ERROR_HANDLE);

printf("\n"); // Make sure our output is starting from the new line.
}
}

Expand Down
4 changes: 4 additions & 0 deletions platform/windows/os_windows.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@
#define WINDOWS_DEBUG_OUTPUT_ENABLED
#endif

#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x4
#endif

template <class T>
class ComAutoreleaseRef {
public:
Expand Down