/* * mptWine.cpp * ----------- * Purpose: Wine stuff. * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "mptWine.h" #include "mptOS.h" #include "../common/mptFileIO.h" #include #include #if MPT_OS_WINDOWS #include #endif OPENMPT_NAMESPACE_BEGIN #if MPT_OS_WINDOWS namespace mpt { namespace Wine { Context::Context(mpt::OS::Wine::VersionContext versionContext) : m_VersionContext(versionContext) , wine_get_dos_file_name(nullptr) , wine_get_unix_file_name(nullptr) { if(!mpt::OS::Windows::IsWine()) { throw mpt::Wine::Exception("Wine not detected."); } if(!m_VersionContext.Version().IsValid()) { throw mpt::Wine::Exception("Unknown Wine version detected."); } m_Kernel32 = std::make_shared>(mpt::library::load({ mpt::library::path_search::system, mpt::library::path_prefix::none, MPT_PATH("kernel32.dll"), mpt::library::path_suffix::none })); if(!m_Kernel32->has_value()) { throw mpt::Wine::Exception("Could not load Wine kernel32.dll."); } if(!(*m_Kernel32)->bind(wine_get_unix_file_name, "wine_get_unix_file_name")) { throw mpt::Wine::Exception("Could not bind Wine kernel32.dll:wine_get_unix_file_name."); } if(!(*m_Kernel32)->bind(wine_get_dos_file_name, "wine_get_dos_file_name")) { throw mpt::Wine::Exception("Could not bind Wine kernel32.dll:wine_get_dos_file_name."); } { std::string out; std::string err; try { if(ExecutePosixShellCommand("uname -m", out, err) != 0) { throw mpt::Wine::Exception("Wine 'uname -m' failed."); } if(!err.empty()) { throw mpt::Wine::Exception("Wine 'uname -m' failed."); } out = mpt::trim(out, std::string("\r\n")); m_Uname_m = out; } catch(const std::exception &) { m_Uname_m = std::string(); } } try { m_HOME = GetPosixEnvVar("HOME"); } catch(const std::exception &) { m_HOME = std::string(); } try { m_XDG_DATA_HOME = GetPosixEnvVar("XDG_DATA_HOME"); if(m_XDG_DATA_HOME.empty()) { m_XDG_DATA_HOME = m_HOME + "/.local/share"; } } catch(const std::exception &) { m_XDG_DATA_HOME = std::string(); } try { m_XDG_CACHE_HOME = GetPosixEnvVar("XDG_CACHE_HOME"); if(m_XDG_CACHE_HOME.empty()) { m_XDG_CACHE_HOME = m_HOME + "/.cache"; } } catch(const std::exception &) { m_XDG_CACHE_HOME = std::string(); } try { m_XDG_CONFIG_HOME = GetPosixEnvVar("XDG_CONFIG_HOME"); if(m_XDG_CONFIG_HOME.empty()) { m_XDG_CONFIG_HOME = m_HOME + "/.config"; } } catch(const std::exception &) { m_XDG_CONFIG_HOME = std::string(); } } std::string Context::PathToPosix(mpt::PathString windowsPath) { std::string result; if(windowsPath.empty()) { return result; } if(windowsPath.Length() >= 32000) { throw mpt::Wine::Exception("Path too long."); } LPSTR tmp = nullptr; tmp = wine_get_unix_file_name(windowsPath.ToWide().c_str()); if(!tmp) { throw mpt::Wine::Exception("Wine kernel32.dll:wine_get_unix_file_name failed."); } result = tmp; HeapFree(GetProcessHeap(), 0, tmp); tmp = nullptr; return result; } mpt::PathString Context::PathToWindows(std::string hostPath) { mpt::PathString result; if(hostPath.empty()) { return result; } if(hostPath.length() >= 32000) { throw mpt::Wine::Exception("Path too long."); } LPWSTR tmp = nullptr; tmp = wine_get_dos_file_name(hostPath.c_str()); if(!tmp) { throw mpt::Wine::Exception("Wine kernel32.dll:wine_get_dos_file_name failed."); } result = mpt::PathString::FromWide(tmp); HeapFree(GetProcessHeap(), 0, tmp); tmp = nullptr; return result; } std::string Context::PathToPosixCanonical(mpt::PathString windowsPath) { std::string result; std::string hostPath = PathToPosix(windowsPath); if(hostPath.empty()) { return result; } std::string output; std::string error; int exitcode = ExecutePosixShellCommand(std::string() + "readlink -f " + EscapePosixShell(hostPath), output, error); if(!error.empty()) { throw mpt::Wine::Exception("Wine readlink failed: " + error); } if(exitcode != 0 && exitcode != 1) { throw mpt::Wine::Exception("Wine readlink failed."); } std::string trimmedOutput = mpt::trim(output, std::string("\r\n")); result = trimmedOutput; return result; } static void ExecutePosixCommandProgressDefault(void * /*userdata*/ ) { ::Sleep(10); return; } static ExecuteProgressResult ExecutePosixShellScriptProgressDefault(void * /*userdata*/ ) { ::Sleep(10); return ExecuteProgressContinueWaiting; } std::string Context::EscapePosixShell(std::string line) { const char escape_chars [] = { '|', '&', ';', '<', '>', '(', ')', '$', '`', '"', '\'', ' ', '\t' }; const char maybe_escape_chars [] = { '*', '?', '[', '#', '~', '=', '%' }; line = mpt::replace(line, std::string("\\"), std::string("\\\\")); for(char c : escape_chars) { line = mpt::replace(line, std::string(1, c), std::string("\\") + std::string(1, c)); } for(char c : maybe_escape_chars) { line = mpt::replace(line, std::string(1, c), std::string("\\") + std::string(1, c)); } return line; } ExecResult Context::ExecutePosixShellScript(std::string script, FlagSet flags, std::map > filetree, std::string title, ExecutePosixCommandProgress progress, ExecutePosixShellScriptProgress progressCancel, void *userdata) { // Relevant documentation: // https://stackoverflow.com/questions/6004070/execute-shell-commands-from-program-running-in-wine // https://www.winehq.org/pipermail/wine-bugs/2014-January/374918.html // https://bugs.winehq.org/show_bug.cgi?id=34730 if(!progress) progress = &ExecutePosixCommandProgressDefault; if(!progressCancel) progressCancel = &ExecutePosixShellScriptProgressDefault; if(flags[ExecFlagInteractive]) flags.reset(ExecFlagSilent); if(flags[ExecFlagSplitOutput]) flags.set(ExecFlagSilent); std::vector tempfiles; progress(userdata); mpt::TempDirGuard dirWindowsTemp(mpt::CreateTempFileName()); if(dirWindowsTemp.GetDirname().empty()) { throw mpt::Wine::Exception("Creating temporary directoy failed."); } const std::string dirPosix = PathToPosix(dirWindowsTemp.GetDirname()); if(dirPosix.empty()) { throw mpt::Wine::Exception("mpt::Wine::ConvertWindowsPathToHost returned empty path."); } const std::string dirPosixEscape = EscapePosixShell(dirPosix); const mpt::PathString dirWindows = dirWindowsTemp.GetDirname(); progress(userdata); // write the script to disk mpt::PathString scriptFilenameWindows = dirWindows + P_("script.sh"); { mpt::ofstream tempfile(scriptFilenameWindows, std::ios::binary); tempfile << script; tempfile.flush(); if(!tempfile) { throw mpt::Wine::Exception("Error writing script.sh."); } } const std::string scriptFilenamePosix = PathToPosix(scriptFilenameWindows); if(scriptFilenamePosix.empty()) { throw mpt::Wine::Exception("Error converting script.sh path."); } const std::string scriptFilenamePosixEscape = EscapePosixShell(scriptFilenamePosix); progress(userdata); // create a wrapper that will call the script and gather result. mpt::PathString wrapperstarterFilenameWindows = dirWindows + P_("wrapperstarter.sh"); { mpt::ofstream tempfile(wrapperstarterFilenameWindows, std::ios::binary); std::string wrapperstarterscript; wrapperstarterscript += std::string() + "#!/usr/bin/env sh" "\n"; wrapperstarterscript += std::string() + "exec /usr/bin/env sh " + dirPosixEscape + "wrapper.sh" "\n"; tempfile << wrapperstarterscript; tempfile.flush(); if(!tempfile) { throw mpt::Wine::Exception("Error writing wrapper.sh."); } } mpt::PathString wrapperFilenameWindows = dirWindows + P_("wrapper.sh"); std::string cleanupscript; { mpt::ofstream tempfile(wrapperFilenameWindows, std::ios::binary); std::string wrapperscript; if(!flags[ExecFlagSilent]) { wrapperscript += "printf \"\\033]0;" + title + "\\a\"" "\n"; } wrapperscript += "chmod u+x " + scriptFilenamePosixEscape + "\n"; wrapperscript += "cd " + dirPosixEscape + "filetree" "\n"; if(flags[ExecFlagInteractive]) { // no stdout/stderr capturing for interactive scripts wrapperscript += scriptFilenamePosixEscape + "\n"; wrapperscript += "MPT_RESULT=$?" "\n"; wrapperscript += "echo ${MPT_RESULT} > " + dirPosixEscape + "exit" "\n"; } else if(flags[ExecFlagSplitOutput]) { wrapperscript += "(" + scriptFilenamePosixEscape + "; echo $? >&4) 4>" + dirPosixEscape + "exit 1>" + dirPosixEscape + "out 2>" + dirPosixEscape + "err" "\n"; } else { wrapperscript += "(" + scriptFilenamePosixEscape + "; echo $? >&4) 2>&1 4>" + dirPosixEscape + "exit | tee " + dirPosixEscape + "out" "\n"; } wrapperscript += "echo done > " + dirPosixEscape + "done" "\n"; cleanupscript += "rm " + dirPosixEscape + "done" "\n"; cleanupscript += "rm " + dirPosixEscape + "exit" "\n"; if(flags[ExecFlagInteractive]) { // nothing } else if(flags[ExecFlagSplitOutput]) { cleanupscript += "rm " + dirPosixEscape + "out" "\n"; cleanupscript += "rm " + dirPosixEscape + "err" "\n"; } else { cleanupscript += "rm " + dirPosixEscape + "out" "\n"; } cleanupscript += "rm -r " + dirPosixEscape + "filetree" "\n"; cleanupscript += "rm " + dirPosixEscape + "script.sh" "\n"; cleanupscript += "rm " + dirPosixEscape + "wrapper.sh" "\n"; cleanupscript += "rm " + dirPosixEscape + "wrapperstarter.sh" "\n"; cleanupscript += "rm " + dirPosixEscape + "terminal.sh" "\n"; if(flags[ExecFlagAsync]) { wrapperscript += cleanupscript; cleanupscript.clear(); } tempfile << wrapperscript; tempfile.flush(); if(!tempfile) { throw mpt::Wine::Exception("Error writing wrapper.sh."); } } progress(userdata); ::CreateDirectory((dirWindows + P_("filetree")).AsNative().c_str(), nullptr); for(const auto &file : filetree) { std::vector path = mpt::String::Split(mpt::ToUnicode(mpt::Charset::UTF8, file.first), U_("/")); mpt::PathString combinedPath = dirWindows + P_("filetree") + P_("\\"); if(path.size() > 1) { for(std::size_t singlepath = 0; singlepath < path.size() - 1; ++singlepath) { if(path[singlepath].empty()) { continue; } combinedPath += mpt::PathString::FromUnicode(path[singlepath]); if(!combinedPath.IsDirectory()) { if(::CreateDirectory(combinedPath.AsNative().c_str(), nullptr) == 0) { throw mpt::Wine::Exception("Error writing filetree."); } } combinedPath += P_("\\"); } } try { mpt::LazyFileRef out(dirWindows + P_("filetree") + P_("\\") + mpt::PathString::FromUTF8(mpt::replace(file.first, std::string("/"), std::string("\\")))); out = file.second; } catch(std::exception &) { throw mpt::Wine::Exception("Error writing filetree."); } } progress(userdata); // create a wrapper that will find a suitable terminal and run the wrapper script in the terminal window. mpt::PathString terminalWrapperFilenameWindows = dirWindows + P_("terminal.sh"); { mpt::ofstream tempfile(terminalWrapperFilenameWindows, std::ios::binary); // NOTE: // Modern terminals detach themselves from the invoking shell if another instance is already present. // This means we cannot rely on terminal invocation being syncronous. static constexpr const char * terminals[] = { "x-terminal-emulator", "konsole", "mate-terminal", "xfce4-terminal", "gnome-terminal", "uxterm", "xterm", "rxvt", }; std::string terminalscript = "\n"; for(const std::string terminal : terminals) { // mate-terminal on Debian 8 cannot execute commands with arguments, // thus we use a separate script that requires no arguments to execute. terminalscript += "if command -v " + terminal + " 2>/dev/null 1>/dev/null ; then" "\n"; terminalscript += " chmod u+x " + dirPosixEscape + "wrapperstarter.sh" "\n"; terminalscript += " exec `command -v " + terminal + "` -e \"" + dirPosixEscape + "wrapperstarter.sh\"" "\n"; terminalscript += "fi" "\n"; } tempfile << terminalscript; tempfile.flush(); if(!tempfile) { return ExecResult::Error(); } } progress(userdata); // build unix command line std::string unixcommand; bool createProcessSuccess = false; if(!createProcessSuccess) { if(flags[ExecFlagSilent]) { unixcommand = "/usr/bin/env sh \"" + dirPosixEscape + "wrapper.sh\""; } else { unixcommand = "/usr/bin/env sh \"" + dirPosixEscape + "terminal.sh\""; } progress(userdata); std::wstring unixcommandW = mpt::ToWide(mpt::Charset::UTF8, unixcommand); std::wstring titleW = mpt::ToWide(mpt::Charset::UTF8, title); STARTUPINFOW startupInfo = {}; startupInfo.lpTitle = titleW.data(); startupInfo.cb = sizeof(startupInfo); PROCESS_INFORMATION processInformation = {}; progress(userdata); BOOL success = FALSE; if(flags[ExecFlagSilent]) { success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &startupInfo, &processInformation); } else { success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &startupInfo, &processInformation); } progress(userdata); createProcessSuccess = (success != FALSE); progress(userdata); if(success) { if(!flags[ExecFlagAsync]) { // note: execution is not syncronous with all Wine versions, // we additionally explicitly wait for "done" later while(WaitForSingleObject(processInformation.hProcess, 0) == WAIT_TIMEOUT) { // wait if(progressCancel(userdata) != ExecuteProgressContinueWaiting) { CloseHandle(processInformation.hThread); CloseHandle(processInformation.hProcess); throw mpt::Wine::Exception("Canceled."); } } } progress(userdata); CloseHandle(processInformation.hThread); CloseHandle(processInformation.hProcess); } } progress(userdata); // Work around Wine being unable to execute PIE binaries on Debian 9. // Luckily, /bin/bash is still non-PIE on Debian 9. if(!createProcessSuccess) { if(flags[ExecFlagSilent]) { unixcommand = "/bin/bash \"" + dirPosixEscape + "wrapper.sh\""; } else { unixcommand = "/bin/bash \"" + dirPosixEscape + "terminal.sh\""; } progress(userdata); std::wstring unixcommandW = mpt::ToWide(mpt::Charset::UTF8, unixcommand); std::wstring titleW = mpt::ToWide(mpt::Charset::UTF8, title); STARTUPINFOW startupInfo = {}; startupInfo.lpTitle = titleW.data(); startupInfo.cb = sizeof(startupInfo); PROCESS_INFORMATION processInformation = {}; progress(userdata); BOOL success = FALSE; if(flags[ExecFlagSilent]) { success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &startupInfo, &processInformation); } else { success = CreateProcessW(NULL, unixcommandW.data(), NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &startupInfo, &processInformation); } progress(userdata); createProcessSuccess = (success != FALSE); progress(userdata); if(success) { if(!flags[ExecFlagAsync]) { // note: execution is not syncronous with all Wine versions, // we additionally explicitly wait for "done" later while(WaitForSingleObject(processInformation.hProcess, 0) == WAIT_TIMEOUT) { // wait if(progressCancel(userdata) != ExecuteProgressContinueWaiting) { CloseHandle(processInformation.hThread); CloseHandle(processInformation.hProcess); throw mpt::Wine::Exception("Canceled."); } } } progress(userdata); CloseHandle(processInformation.hThread); CloseHandle(processInformation.hProcess); } } progress(userdata); if(!createProcessSuccess) { throw mpt::Wine::Exception("CreateProcess failed."); } progress(userdata); if(flags[ExecFlagAsync]) { ExecResult result; result.exitcode = 0; return result; } while(!(dirWindows + P_("done")).IsFile()) { // wait if(progressCancel(userdata) != ExecuteProgressContinueWaiting) { throw mpt::Wine::Exception("Canceled."); } } progress(userdata); int exitCode = 0; { mpt::ifstream exitFile(dirWindows + P_("exit"), std::ios::binary); if(!exitFile) { throw mpt::Wine::Exception("Script .exit file not found."); } std::string exitString; exitFile >> exitString; if(exitString.empty()) { throw mpt::Wine::Exception("Script .exit file empty."); } exitCode = ConvertStrTo(exitString); } progress(userdata); std::string outputString; if(!flags[ExecFlagInteractive]) { mpt::ifstream outputFile(dirWindows + P_("out"), std::ios::binary); if(outputFile) { outputFile.seekg(0, std::ios::end); std::streampos outputFileSize = outputFile.tellg(); outputFile.seekg(0, std::ios::beg); std::vector outputFileBuf(mpt::saturate_cast(static_cast(outputFileSize))); outputFile.read(&outputFileBuf[0], outputFileBuf.size()); outputString = mpt::buffer_cast(outputFileBuf); } } progress(userdata); std::string errorString; if(flags[ExecFlagSplitOutput]) { mpt::ifstream errorFile(dirWindows + P_("err"), std::ios::binary); if(errorFile) { errorFile.seekg(0, std::ios::end); std::streampos errorFileSize = errorFile.tellg(); errorFile.seekg(0, std::ios::beg); std::vector errorFileBuf(mpt::saturate_cast(static_cast(errorFileSize))); errorFile.read(&errorFileBuf[0], errorFileBuf.size()); errorString = mpt::buffer_cast(errorFileBuf); } } progress(userdata); ExecResult result; result.exitcode = exitCode; result.output = outputString; result.error = errorString; std::deque paths; paths.push_back(dirWindows + P_("filetree")); mpt::PathString basePath = (dirWindows + P_("filetree")).EnsureTrailingSlash(); while(!paths.empty()) { mpt::PathString path = paths.front(); paths.pop_front(); path.EnsureTrailingSlash(); HANDLE hFind = NULL; WIN32_FIND_DATA wfd = {}; hFind = FindFirstFile((path + P_("*.*")).AsNative().c_str(), &wfd); if(hFind != NULL && hFind != INVALID_HANDLE_VALUE) { do { mpt::PathString filename = mpt::PathString::FromNative(wfd.cFileName); if(filename != P_(".") && filename != P_("..")) { filename = path + filename; filetree[filename.ToUTF8()] = std::vector(); if(filename.IsDirectory()) { paths.push_back(filename); } else if(filename.IsFile()) { try { mpt::LazyFileRef f(filename); std::vector buf = f; mpt::PathString treeFilename = mpt::PathString::FromNative(filename.AsNative().substr(basePath.AsNative().length())); result.filetree[treeFilename.ToUTF8()] = buf; } catch (std::exception &) { // nothing?! } } } } while(FindNextFile(hFind, &wfd)); FindClose(hFind); } } mpt::DeleteWholeDirectoryTree(dirWindows); return result; } int Context::ExecutePosixShellCommand(std::string command, std::string & output, std::string & error) { std::string script; script += "#!/usr/bin/env sh" "\n"; script += "exec " + command + "\n"; mpt::Wine::ExecResult execResult = ExecutePosixShellScript ( script , mpt::Wine::ExecFlagSilent | mpt::Wine::ExecFlagSplitOutput, std::map >() , std::string() , nullptr , nullptr , nullptr ); output = execResult.output; error = execResult.error; return execResult.exitcode; } std::string Context::GetPosixEnvVar(std::string var, std::string def) { // We cannot use std::getenv here because Wine overrides SOME env vars, // in particular, HOME is unset in the Wine environment. // Instead, we just spawn a shell that will catch up a sane environment on // its own. std::string output; std::string error; int exitcode = ExecutePosixShellCommand(std::string() + "echo $" + var, output, error); if(!error.empty()) { throw mpt::Wine::Exception("Wine echo $var failed: " + error); } if(exitcode != 0) { throw mpt::Wine::Exception("Wine echo $var failed."); } std::string result = mpt::trim_right(output, std::string("\r\n")); if(result.empty()) { result = def; } return result; } } // namespace Wine } // namespace mpt #else // !MPT_OS_WINDOWS MPT_MSVC_WORKAROUND_LNK4221(mptWine) #endif // MPT_OS_WINDOWS OPENMPT_NAMESPACE_END