/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include "loader.hxx" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { void fail() { LPWSTR buf = nullptr; FormatMessageW( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, GetLastError(), 0, reinterpret_cast< LPWSTR >(&buf), 0, nullptr); MessageBoxW(nullptr, buf, nullptr, MB_OK | MB_ICONERROR); HeapFree(GetProcessHeap(), 0, buf); TerminateProcess(GetCurrentProcess(), 255); } struct CommandArgs { LPWSTR* argv; int argc; CommandArgs() { argv = CommandLineToArgvW(GetCommandLineW(), &argc); } ~CommandArgs() { LocalFree(argv); } auto begin() const { return argv; } auto end() const { return begin() + argc; } }; // tdf#120249: quotes in arguments need to be escaped; backslashes before quotes need doubling. See // https://docs.microsoft.com/en-us/windows/desktop/api/shellapi/nf-shellapi-commandlinetoargvw std::wstring EscapeArg(std::wstring_view sArg) { std::wstring sResult(L"\""); for (size_t lastPosQuote = 0; lastPosQuote <= sArg.size();) { const size_t posQuote = std::min(sArg.find(L'"', lastPosQuote), sArg.size()); size_t posBackslash = posQuote; while (posBackslash != lastPosQuote && sArg[posBackslash - 1] == L'\\') --posBackslash; // 2n+1 '\' to escape internal '"'; 2n '\' before closing '"' const size_t nEscapes = (posQuote - posBackslash) * 2 + (posQuote < sArg.size() ? 1 : 0); sResult.append(sArg.begin() + lastPosQuote, sArg.begin() + posBackslash); sResult.append(nEscapes, L'\\'); sResult.append(1, L'"'); lastPosQuote = posQuote + 1; } return sResult; } std::wstring getCWDarg() { std::wstring s(L" \"-env:OOO_CWD="); DWORD cwdLen = GetCurrentDirectoryW(0, nullptr); std::vector cwd(cwdLen); cwdLen = GetCurrentDirectoryW(cwdLen, cwd.data()); if (cwdLen == 0 || cwdLen >= cwd.size()) { s += L'0'; } else { s += L'2'; size_t n = 0; // number of trailing backslashes for (auto* p = cwd.data(); *p; ++p) { WCHAR c = *p; if (c == L'$') { s += L"\\$"; n = 0; } else if (c == L'\\') { s += L"\\\\"; n += 2; } else { s += c; n = 0; } } // The command line will continue with a double quote, so double any // preceding backslashes as required by Windows: s.append(n, L'\\'); } s += L'"'; return s; } WCHAR* commandLineAppend(WCHAR* buffer, std::wstring_view text) { auto ret = std::copy_n(text.begin(), text.size(), buffer); *ret = 0; // trailing null return ret; } // Set the PATH environment variable in the current (loader) process, so that a // following CreateProcess has the necessary environment: // returns a pair of strings { binPath, iniDirectory } // * binPath is the full path to the "bin" file corresponding to the current executable. // * iniDirectory is the full directory path (ending in "\") to the "ini" file corresponding to the // current executable. [[nodiscard]] std::pair extendLoaderEnvironment() { std::vector executable_path(EXTENDED_MAX_PATH); DWORD exe_len; for (;;) { exe_len = GetModuleFileNameW(nullptr, executable_path.data(), executable_path.size()); if (!exe_len) fail(); if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { executable_path.resize(exe_len + 4); // to accommodate a possible ".bin" in the end break; } executable_path.resize(executable_path.size() * 2); } WCHAR* iniDirEnd = tools::filename(executable_path.data()); std::wstring_view iniDirView(executable_path.data(), iniDirEnd); WCHAR* nameEnd = executable_path.data() + exe_len; if (!(nameEnd - iniDirEnd >= 4 && nameEnd[-4] == L'.' && (((nameEnd[-3] == L'E' || nameEnd[-3] == L'e') && (nameEnd[-2] == L'X' || nameEnd[-2] == L'x') && (nameEnd[-1] == L'E' || nameEnd[-1] == L'e')) || ((nameEnd[-3] == L'C' || nameEnd[-3] == L'c') && (nameEnd[-2] == L'O' || nameEnd[-2] == L'o') && (nameEnd[-1] == L'M' || nameEnd[-1] == L'm'))))) { *nameEnd = L'.'; nameEnd += 4; } nameEnd[-3] = 'b'; nameEnd[-2] = 'i'; nameEnd[-1] = 'n'; std::wstring_view nameView(iniDirEnd, nameEnd); WCHAR env[32767]; DWORD n = GetEnvironmentVariableW(L"PATH", env, std::size(env)); if ((n >= std::size(env) || n == 0) && GetLastError() != ERROR_ENVVAR_NOT_FOUND) { fail(); } std::wstring_view envView(env, n); // must be first in PATH to override other entries assert(iniDirView.back() == L'\\'); // hence -1 below std::wstring_view iniDirView1(iniDirView.substr(0, iniDirView.size() - 1)); if (!envView.starts_with(iniDirView1) || env[iniDirView1.size()] != L';') { std::wstring pad(iniDirView1); if (n != 0) { pad += L';'; pad += envView; } if (!SetEnvironmentVariableW(L"PATH", pad.data())) { fail(); } } return { tools::buildPath(iniDirView, nameView), std::wstring(iniDirView) }; } } namespace desktop_win32 { int officeloader_impl(bool bAllowConsole) { const auto& [szTargetFileName, szIniDirectory] = extendLoaderEnvironment(); STARTUPINFOW aStartupInfo; ZeroMemory(&aStartupInfo, sizeof(aStartupInfo)); aStartupInfo.cb = sizeof(aStartupInfo); // Create process with same command line, environment and stdio handles which // are directed to the created pipes GetStartupInfoW(&aStartupInfo); DWORD dwExitCode = DWORD(-1); bool fSuccess = false; bool bFirst = true; // read limit values from fundamental.override.ini unsigned int nMaxMemoryInMB = 0; bool bExcludeChildProcesses = true; bool fallbackForMaxMemoryInMB = true; bool fallbackForExcludeChildProcesses = true; try { boost::property_tree::ptree pt; std::ifstream aFile(szIniDirectory + L"\\fundamental.override.ini"); boost::property_tree::ini_parser::read_ini(aFile, pt); nMaxMemoryInMB = pt.get("Bootstrap.LimitMaximumMemoryInMB", nMaxMemoryInMB); fallbackForMaxMemoryInMB = !pt.get_child_optional("Bootstrap.LimitMaximumMemoryInMB"); bExcludeChildProcesses = pt.get("Bootstrap.ExcludeChildProcessesFromLimit", bExcludeChildProcesses); fallbackForExcludeChildProcesses = !pt.get_child_optional("Bootstrap.ExcludeChildProcessesFromLimit"); } catch (...) { nMaxMemoryInMB = 0; } // For backwards compatibility, for now also try to read the values from bootstrap.ini if // fundamental.override.ini does not provide them: if (fallbackForMaxMemoryInMB || fallbackForExcludeChildProcesses) { try { boost::property_tree::ptree pt; std::ifstream aFile(szIniDirectory + L"\\bootstrap.ini"); boost::property_tree::ini_parser::read_ini(aFile, pt); if (fallbackForMaxMemoryInMB) { nMaxMemoryInMB = pt.get("Win32.LimitMaximumMemoryInMB", nMaxMemoryInMB); } if (fallbackForExcludeChildProcesses) { bExcludeChildProcesses = pt.get("Win32.ExcludeChildProcessesFromLimit", bExcludeChildProcesses); } } catch (...) { } } // create a Windows JobObject with a memory limit HANDLE hJobObject = nullptr; if (nMaxMemoryInMB > 0) { JOBOBJECT_EXTENDED_LIMIT_INFORMATION aJobLimit; aJobLimit.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_JOB_MEMORY; if (bExcludeChildProcesses) aJobLimit.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; aJobLimit.JobMemoryLimit = nMaxMemoryInMB * 1024 * 1024; hJobObject = CreateJobObjectW(nullptr, nullptr); if (hJobObject != nullptr) SetInformationJobObject(hJobObject, JobObjectExtendedLimitInformation, &aJobLimit, sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); } std::vector aEscapedArgs; bool bHeadlessMode = false; for (std::wstring_view arg : CommandArgs()) { // Check command line arguments for "--headless" parameter. We only set the environment // variable "ATTACHED_PARENT_PROCESSID" for the headless mode as self-destruction of the // soffice.bin process can lead to certain side-effects (log-off can result in data-loss, // ".lock" is not deleted). See 138244 for more information. if (arg == L"-headless" || arg == L"--headless") bHeadlessMode = true; // check for wildcards in arguments - Windows does not expand automatically else if (arg.find_first_of(L"*?") != std::wstring_view::npos) { WIN32_FIND_DATAW aFindData; HANDLE h = FindFirstFileW(arg.data(), &aFindData); if (h != INVALID_HANDLE_VALUE) { const int nPathSize = 32 * 1024; wchar_t drive[3]; wchar_t dir[nPathSize]; wchar_t path[nPathSize]; _wsplitpath_s(arg.data(), drive, std::size(drive), dir, std::size(dir), nullptr, 0, nullptr, 0); do { _wmakepath_s(path, std::size(path), drive, dir, aFindData.cFileName, nullptr); aEscapedArgs.push_back(EscapeArg(path)); } while (FindNextFileW(h, &aFindData)); FindClose(h); continue; } } aEscapedArgs.push_back(EscapeArg(arg)); } size_t n = std::accumulate(aEscapedArgs.begin(), aEscapedArgs.end(), aEscapedArgs.size(), [](size_t a, const std::wstring& s) { return a + s.size(); }); std::wstring sCWDarg = getCWDarg(); n += sCWDarg.size() + 1; LPWSTR lpCommandLine = new WCHAR[n]; if (bHeadlessMode) { WCHAR szParentProcessId[64]; // This is more than large enough for a 128 bit decimal value if (_ltow(static_cast(GetCurrentProcessId()), szParentProcessId, 10)) SetEnvironmentVariableW(L"ATTACHED_PARENT_PROCESSID", szParentProcessId); } do { WCHAR* p = commandLineAppend(lpCommandLine, aEscapedArgs[0]); for (size_t i = 1; i < aEscapedArgs.size(); ++i) { const std::wstring& rArg = aEscapedArgs[i]; if (bFirst || EXITHELPER_NORMAL_RESTART == dwExitCode || rArg.starts_with(L"\"-env:")) { p = commandLineAppend(p, L" "); p = commandLineAppend(p, rArg); } } commandLineAppend(p, sCWDarg); bFirst = false; PROCESS_INFORMATION aProcessInfo; fSuccess = CreateProcessW(szTargetFileName.data(), lpCommandLine, nullptr, nullptr, TRUE, bAllowConsole ? 0 : DETACHED_PROCESS, nullptr, szIniDirectory.data(), &aStartupInfo, &aProcessInfo); if (fSuccess) { DWORD dwWaitResult; if (hJobObject) AssignProcessToJobObject(hJobObject, aProcessInfo.hProcess); do { // On Windows XP it seems as the desktop calls WaitForInputIdle after "OpenWith" so // we have to do so as if we were processing any messages dwWaitResult = MsgWaitForMultipleObjects(1, &aProcessInfo.hProcess, FALSE, INFINITE, QS_ALLEVENTS); if (WAIT_OBJECT_0 + 1 == dwWaitResult) { MSG msg; PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE); } } while (WAIT_OBJECT_0 + 1 == dwWaitResult); dwExitCode = 0; GetExitCodeProcess(aProcessInfo.hProcess, &dwExitCode); CloseHandle(aProcessInfo.hProcess); CloseHandle(aProcessInfo.hThread); } } while (fSuccess && (EXITHELPER_CRASH_WITH_RESTART == dwExitCode || EXITHELPER_NORMAL_RESTART == dwExitCode)); if (hJobObject) CloseHandle(hJobObject); delete[] lpCommandLine; return fSuccess ? dwExitCode : -1; } int unopkgloader_impl(bool bAllowConsole) { const auto& [szTargetFileName, szIniDirectory] = extendLoaderEnvironment(); STARTUPINFOW aStartupInfo{}; aStartupInfo.cb = sizeof(aStartupInfo); GetStartupInfoW(&aStartupInfo); DWORD dwExitCode = DWORD(-1); std::wstring sCWDarg = getCWDarg(); DWORD dummy; std::wstring redirect = tools::buildPath(szIniDirectory, L"redirect.ini"); bool hasRedirect = !redirect.empty() && (GetBinaryTypeW(redirect.data(), &dummy) || // cheaper check for file existence? GetLastError() != ERROR_FILE_NOT_FOUND); LPWSTR cl1 = GetCommandLineW(); std::wstring cl2 = cl1; if (hasRedirect) cl2 += L" \"-env:INIFILENAME=vnd.sun.star.pathname:" + redirect + L"\""; cl2 += sCWDarg; PROCESS_INFORMATION aProcessInfo; bool fSuccess = CreateProcessW( szTargetFileName.data(), cl2.data(), nullptr, nullptr, TRUE, bAllowConsole ? 0 : DETACHED_PROCESS, nullptr, szIniDirectory.data(), &aStartupInfo, &aProcessInfo); if (fSuccess) { DWORD dwWaitResult; do { // On Windows XP it seems as the desktop calls WaitForInputIdle after "OpenWidth" so we have to do so // as if we were processing any messages dwWaitResult = MsgWaitForMultipleObjects(1, &aProcessInfo.hProcess, FALSE, INFINITE, QS_ALLEVENTS); if (WAIT_OBJECT_0 + 1 == dwWaitResult) { MSG msg; PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE); } } while (WAIT_OBJECT_0 + 1 == dwWaitResult); dwExitCode = 0; GetExitCodeProcess(aProcessInfo.hProcess, &dwExitCode); CloseHandle(aProcessInfo.hProcess); CloseHandle(aProcessInfo.hThread); } return dwExitCode; } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */