/*
* Copyright (c) 2020, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
#include "MsiUtils.h"
#include "MsiDb.h"
#include "Resources.h"
#include "Dll.h"
#include "UniqueHandle.h"
#include "FileUtils.h"
#include "WinErrorHandling.h"
// Code in this file requires linking with msi.lib
namespace msi {
namespace {
template <
class Func,
class Arg1Type,
class Arg2Type>
tstring getProperty(Func func,
const LPCSTR funcName, Arg1Type arg1,
Arg2Type arg2) {
std::vector<TCHAR> buf(20);
DWORD size =
static_cast<DWORD>(buf.size());
UINT status = ERROR_MORE_DATA;
while (ERROR_MORE_DATA ==
(status = func(arg1, arg2, &*buf.begin(), &size))) {
buf.resize(buf.size() * 2);
size =
static_cast<DWORD>(buf.size());
}
if (status != ERROR_SUCCESS) {
JP_THROW(Error(tstrings::any() << funcName <<
"(" << arg1
<<
", " << arg2 <<
") failed", status));
}
return tstring(buf.begin(), buf.begin() + size);
}
template <
class Func,
class Arg1Type,
class Arg2Type>
tstring getProperty(
const std::nothrow_t&, Func func,
const LPCSTR funcName,
Arg1Type arg1, Arg2Type arg2) {
try {
return getProperty(func, funcName, arg1, arg2);
}
catch (
const std::exception&) {
}
return tstring();
}
tstring escapePropertyValue(
const tstring& value) {
// Escape quotes as described in
// http://msdn.microsoft.com/en-us/library/aa367988.aspx
tstring reply = tstrings::replace(value, _T(
"\""), _T("\
"\""));
if (reply.empty()) {
// MSDN: To clear a public property by using the command line,
// set its value to an empty string.
reply = _T(
"\"\
"");
}
if (reply.find_first_of(_T(
" \t")) != tstring::npos) {
reply = _T(
'"') + reply + _T(
'"');
}
return reply;
}
template <
class It>
tstring stringifyProperties(It b, It e) {
tostringstream buf;
for (; b != e; ++b) {
const tstring value = escapePropertyValue(b->second);
buf << _T(
" ") << b->first << _T(
"=") << value;
}
tstring reply = tstrings::trim(buf.str());
return reply;
}
class CallbackTrigger {
CallbackTrigger(
const CallbackTrigger&);
CallbackTrigger&
operator=(
const CallbackTrigger&);
enum { MESSAGE_FILTER = 0xffffffff };
static int WINAPI adapter(LPVOID ctx, UINT type, LPCWSTR msg) {
Callback* callback =
reinterpret_cast<Callback*>(ctx);
if (!callback) {
return 0;
}
JP_TRY;
// MSDN: Handling Progress Messages Using MsiSetExternalUI
// http://msdn.microsoft.com/en-us/library/aa368786(v=vs.85).aspx
const INSTALLMESSAGE mt = (INSTALLMESSAGE)(0xFF000000 & type);
const UINT flags = 0x00FFFFFF & type;
if (msg) {
callback->notify(mt, flags, tstrings::toWinString(msg));
}
JP_CATCH_ALL;
return 0;
}
public:
explicit CallbackTrigger(Callback& cb) {
MsiSetExternalUIW(adapter, DWORD(MESSAGE_FILTER), &cb);
}
~CallbackTrigger() {
// Not restoring the original callback.
// Just because the original message filter is unknown.
MsiSetExternalUIW(0, 0, 0);
}
};
class LogFileTrigger {
LogFileTrigger(
const LogFileTrigger&);
LogFileTrigger&
operator=(
const LogFileTrigger&);
public:
explicit LogFileTrigger(
const tstring& path) {
if (path.empty()) {
MsiEnableLog(0, NULL, 0);
}
else {
MsiEnableLog(INSTALLLOGMODE_VERBOSE, path.c_str(), 0);
}
}
~LogFileTrigger() {
// Disable log
MsiEnableLog(0, NULL, 0);
}
};
struct SuppressUI:
public OverrideUI {
SuppressUI(): OverrideUI(withoutUI()) {
}
};
class StateImpl:
public ActionData::State {
const OverrideUI overrideUi;
LogFileTrigger logGuard;
std::unique_ptr<CallbackTrigger> callbackGuard;
public:
explicit StateImpl(
const ActionData& data): overrideUi(data.uiMode),
logGuard(data.logFile) {
if (data.callback) {
callbackGuard = std::unique_ptr<CallbackTrigger>(
new CallbackTrigger(*data.callback));
}
}
};
}
// namespace
void closeMSIHANDLE(MSIHANDLE h) {
if (h) {
const auto status = MsiCloseHandle(h);
if (status != ERROR_SUCCESS) {
LOG_WARNING(tstrings::any() <<
"MsiCloseHandle("
<< h <<
") failed with error=" << status);
}
}
}
// DatabaseRecord::getString() should live in MsiDb.cpp.
// However it can't access handy msi::getProperty() from that location.
tstring DatabaseRecord::getString(
unsigned idx)
const {
return ::msi::getProperty(MsiRecordGetString,
"MsiRecordGetString",
handle, UINT(idx));
}
tstring getProductInfo(
const Guid& productCode,
const tstring& prop) {
const tstring id = productCode.toMsiString();
return getProperty(MsiGetProductInfo,
"MsiGetProductInfo", id.c_str(),
prop.c_str());
}
tstring getProductInfo(
const std::nothrow_t&,
const Guid& productCode,
const tstring& prop) {
const tstring id = productCode.toMsiString();
return getProperty(std::nothrow, MsiGetProductInfo,
"MsiGetProductInfo",
id.c_str(), prop.c_str());
}
tstring getPropertyFromCustomAction(MSIHANDLE h,
const tstring& prop) {
return getProperty(MsiGetProperty,
"MsiGetProperty", h, prop.c_str());
}
tstring getPropertyFromCustomAction(
const std::nothrow_t&, MSIHANDLE h,
const tstring& prop) {
return getProperty(std::nothrow, MsiGetProperty,
"MsiGetProperty", h,
prop.c_str());
}
namespace {
std::string makeMessage(
const std::string& msg, UINT errorCode) {
std::ostringstream err;
err <<
"MSI error [" << errorCode <<
"]";
const std::wstring msimsg_dll = tstrings::winStringToUtf16(FileUtils::combinePath(
SysInfo::getSystem32Dir(), _T(
"msimsg.dll")));
// Convert MSI Error Code to Description String
// http://msdn.microsoft.com/en-us/library/aa370315(v=vs.85).aspx
Dll::Handle lib(LoadLibraryExW(msimsg_dll.c_str(), NULL,
LOAD_LIBRARY_AS_DATAFILE));
if (!lib.get()) {
JP_THROW(SysError(tstrings::any() <<
"LoadLibraryExW(" <<
msimsg_dll <<
") failed", LoadLibraryExW));
}
else {
tstring descr;
try {
descr = StringResource(errorCode, lib.get()).string();
}
catch (
const std::exception &) {
descr = _T(
"No description");
}
err <<
"(" << descr <<
")";
}
return joinErrorMessages(msg, err.str());
}
}
// namespace
Error::Error(
const tstrings::any& msg, UINT ec): std::runtime_error(
makeMessage(msg.str(), ec)), errorCode(ec) {
}
Error::Error(
const std::string& msg, UINT ec): std::runtime_error(
makeMessage(msg, ec)), errorCode(ec) {
}
tstring ActionData::getCmdLineArgs()
const {
tstring raw = tstrings::trim(rawCmdLineArgs);
tstring strProperties = stringifyProperties(props.begin(), props.end());
if (!raw.empty() && !strProperties.empty()) {
raw += _T(
' ');
}
return raw + strProperties;
}
std::unique_ptr<ActionData::State> ActionData::createState()
const {
return std::unique_ptr<ActionData::State>(
new StateImpl(*
this));
}
namespace {
bool isMsiStatusSuccess(
const UINT status) {
switch (status) {
case ERROR_SUCCESS:
case ERROR_SUCCESS_REBOOT_INITIATED:
case ERROR_SUCCESS_REBOOT_REQUIRED:
return true;
default:
break;
}
return false;
}
ActionStatus handleMsiStatus(tstrings::any& logMsg,
const UINT status) {
if (!isMsiStatusSuccess(status)) {
logMsg <<
"failed [" << status <<
"]";
return ActionStatus(status, logMsg.str());
}
logMsg <<
"succeeded";
if (status != ERROR_SUCCESS) {
logMsg <<
" [" << status <<
"]";
}
LOG_INFO(logMsg);
return ActionStatus(status);
}
}
// namespace
ActionStatus::
operator bool()
const {
return isMsiStatusSuccess(value);
}
void ActionStatus::throwIt()
const {
JP_THROW(Error(comment, value));
}
namespace {
template <
class T>
ActionStatus msiAction(
const T& obj, INSTALLSTATE state,
const tstring& cmdLineArgs) {
const tstring id = obj.getProductCode().toMsiString();
const int level = INSTALLLEVEL_MAXIMUM;
const UINT status = MsiConfigureProductEx(id.c_str(), level, state,
cmdLineArgs.c_str());
tstrings::any logMsg;
logMsg <<
"MsiConfigureProductEx("
<< id
<<
", " << level
<<
", " << state
<<
", " << cmdLineArgs
<<
") ";
return handleMsiStatus(logMsg, status);
}
}
// namespace
template <>
ActionStatus action<uninstall>::execute(
const uninstall& obj,
const tstring& cmdLineArgs) {
return msiAction(obj, INSTALLSTATE_ABSENT, cmdLineArgs);
}
template <>
ActionStatus action<update>::execute(
const update& obj,
const tstring& cmdLineArgs) {
return msiAction(obj, INSTALLSTATE_LOCAL, cmdLineArgs);
}
template <>
ActionStatus action<install>::execute(
const install& obj,
const tstring& cmdLineArgs) {
const tstring& msiPath = obj.getMsiPath();
const UINT status = MsiInstallProduct(msiPath.c_str(),
cmdLineArgs.c_str());
tstrings::any logMsg;
logMsg <<
"MsiInstallProduct(" << msiPath <<
", " << cmdLineArgs <<
") ";
return handleMsiStatus(logMsg, status);
}
uninstall::uninstall() {
// Uninstall default behavior is to never reboot.
setProperty(_T(
"REBOOT"), _T(
"ReallySuppress"));
}
bool waitForInstallationCompletion(DWORD timeoutMS)
{
// "_MSIExecute" mutex is used by the MSI installer service to prevent multiple installations at the same time
// http://msdn.microsoft.com/en-us/library/aa372909(VS.85).aspx
LPCTSTR mutexName = _T(
"Global\\_MSIExecute");
UniqueHandle h(OpenMutex(SYNCHRONIZE,
FALSE, mutexName));
if (h.get() != NULL) {
DWORD res = WaitForSingleObject(h.get(), timeoutMS);
// log only if timeout != 0
if (timeoutMS != 0) {
LOG_INFO(tstrings::any() <<
"finish waiting for mutex: " << res);
}
if (res == WAIT_TIMEOUT) {
return false;
}
}
return true;
}
bool isProductInstalled(
const Guid& productCode) {
// Query any property. If product exists, query should succeed.
try {
getProductInfo(productCode, INSTALLPROPERTY_VERSIONSTRING);
}
catch (
const Error& e) {
switch (e.getReason()) {
case ERROR_UNKNOWN_PRODUCT:
// if the application being queried is advertised and not installed.
case ERROR_UNKNOWN_PROPERTY:
return false;
}
}
return true;
}
}
// namespace msi