Uninstall another MSI on install - installation

I have a Basic MSI project. I need to remove another MSI product on installation that is now integrated into our main application. I tried to use the upgrade scenarios and treat it as a major upgrade. However, this didn't work because of the upgrade codes not matching I believe.
Next, I also made a custom action that ran msiexec.exe after the CostFinalize (I think this was stated in the Installshield help.) This worked perfectly until I installed on a system that didn't have the installer I was looking to remove. My installer would fail if the other obsolete product was not installed. I tried to put a condition on the custom action set by the system search, but it seems the system search is limited in functionality. I can't just check a reg key and set a boolean property.
Any ideas?

A few things to consider
1) The UpgradeTable ( FindRelatedProducts / RemoveExisting Products ) can be used to remove ProductCodes associated with another product's UpgradeCode.
2) If memory serves, MSI won't remove a Per-User product during a Per-Machine install ( or the other way around ). The context has to be the same.
3) The UI Sequence doesn't run during silent installs.
4) You can't run msiexec from the execute sequence because there is a system wide mutex of only one execute sequence per machine.
5) If you schedule in UI ( I already told you that you shouldn't since it doesn't run during silent installs ) there is another mutex that says only 1 UI per process.
If you are going from per-user to per-user or per-machine to per-machine, I would think it's reasonaable you should be able to do what you want using Upgrade elements / table rows without writing custom actions. Otherwise you'll need a setup.exe style bootstrapper to handle the uninstall prior to entering the msiexec world.

I achieved this in InstallShield 2013 using custom InstallScript. The script is executed via a custom action in the UI sequence, but I placed it after the "SetupProgress" dialog, i.e. before "Execute Action" instead of after the CostFinalize (as the documentation does say). I added the condition "NOT Installed" to the action. If you place this in the suggested order, it will kick off the uninstallation as soon as the installer has initialized. If you move it to where I did, it doesn't kick off until the user has clicked the final install now button.
The reason to put this in the UI sequence is to get around the one msi installer (or uninstaller) at a time problem. This simply doesn't work in the execution sequence because of that restriction.
The primary weakness in this method is that as Christopher stated, this won't work in a silent install (that is also found in the IS documentation). That is the official means for achieving this though. (check out: http://helpnet.installshield.com/installshield16helplib/IHelpCustomActionMSIExec.htm) If you can live with that (since silent install is generally as special case scenario), then this works just fine.
As Chris also said, you can't launch the uninstaller ui while the primary ui is running, but that's not a problem with my script because it adds a command line switch to run the uninstaller without the ui (i.e. silently).
My script also avoids having to know the guid of the application you want to uninstall. Here's the script to bind to the custom action (UninstallPriorVersions is the entry point function):
////////////////////////////////////////////////////////////////////////////////
//
// This template script provides the code necessary to build an entry-point
// function to be called in an InstallScript custom action.
//
//
// File Name: Setup.rul
//
// Description: InstallShield script
//
////////////////////////////////////////////////////////////////////////////////
// Include Ifx.h for built-in InstallScript function prototypes, for Windows
// Installer API function prototypes and constants, and to declare code for
// the OnBegin and OnEnd events.
#include "ifx.h"
// The keyword export identifies MyFunction() as an entry-point function.
// The argument it accepts must be a handle to the Installer database.
export prototype UninstallPriorVersions(HWND);
// To Do: Declare global variables, define constants, and prototype user-
// defined and DLL functions here.
prototype NUMBER UninstallApplicationByName( STRING );
prototype NUMBER GetUninstallCmdLine( STRING, BOOL, BYREF STRING );
prototype STRING GetUninstallKey( STRING );
prototype NUMBER RegDBGetSubKeyNameContainingValue( NUMBER, STRING, STRING, STRING, BYREF STRING );
// To Do: Create a custom action for this entry-point function:
// 1. Right-click on "Custom Actions" in the Sequences/Actions view.
// 2. Select "Custom Action Wizard" from the context menu.
// 3. Proceed through the wizard and give the custom action a unique name.
// 4. Select "Run InstallScript code" for the custom action type, and in
// the next panel select "MyFunction" (or the new name of the entry-
// point function) for the source.
// 5. Click Next, accepting the default selections until the wizard
// creates the custom action.
//
// Once you have made a custom action, you must execute it in your setup by
// inserting it into a sequence or making it the result of a dialog's
// control event.
///////////////////////////////////////////////////////////////////////////////
//
// Function: UninstallPriorVersions
//
// Purpose: Uninstall prior versions of this application
//
///////////////////////////////////////////////////////////////////////////////
function UninstallPriorVersions(hMSI)
begin
UninstallApplicationByName( "The Name Of Some App" );
end;
///////////////////////////////////////////////////////////////////////////////
//
// Function: UninstallApplicationByName
//
// Purpose: Uninstall an application (without knowing the guid)
//
// Returns: (UninstCmdLine is assigned a value by referrence)
// >= ISERR_SUCCESS The function successfully got the command line.
// < ISERR_SUCCESS The function failed to get the command line.
//
///////////////////////////////////////////////////////////////////////////////
function NUMBER UninstallApplicationByName( AppName )
NUMBER nReturn;
STRING UninstCmdLine;
begin
nReturn = GetUninstallCmdLine( AppName, TRUE, UninstCmdLine );
if( nReturn < ISERR_SUCCESS ) then
return nReturn;
endif;
if( LaunchAppAndWait( "", UninstCmdLine, LAAW_OPTION_WAIT) = 0 ) then
return ISERR_SUCCESS;
else
return ISERR_SUCCESS-1;
endif;
end;
///////////////////////////////////////////////////////////////////////////////
//
// Function: GetUninstallCmdLine
//
// Purpose: Get the command line statement to uninstall an application
//
// Returns: (UninstCmdLine is assigned a value by referrence)
// >= ISERR_SUCCESS The function successfully got the command line.
// < ISERR_SUCCESS The function failed to get the command line.
//
///////////////////////////////////////////////////////////////////////////////
function NUMBER GetUninstallCmdLine( AppName, Silent, UninstCmdLine )
NUMBER nReturn;
begin
nReturn = RegDBGetUninstCmdLine ( GetUninstallKey( AppName ), UninstCmdLine );
if( nReturn < ISERR_SUCCESS ) then
return nReturn;
endif;
if( Silent && StrFind( UninstCmdLine, "MsiExec.exe" ) >= 0 )then
UninstCmdLine = UninstCmdLine + " /qn";
endif;
return nReturn;
end;
///////////////////////////////////////////////////////////////////////////////
//
// Function: GetUninstallKey
//
// Purpose: Find the uninstall key in the registry for an application looked up by name
//
// Returns: The uninstall key (i.e. the guid or a fall back value)
//
///////////////////////////////////////////////////////////////////////////////
function STRING GetUninstallKey( AppName )
STRING guid;
STRING Key64, Key32, ValueName;
begin
Key64 = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
Key32 = "SOFTWARE\\Wow6432Node\\\Microsoft\\Windows\\CurrentVersion\\Uninstall";
ValueName = "DisplayName";
if( RegDBGetSubKeyNameContainingValue( HKEY_LOCAL_MACHINE, Key64, ValueName, AppName, guid ) = 0 ) then
return guid; // return 64 bit GUID
endif;
if( RegDBGetSubKeyNameContainingValue( HKEY_LOCAL_MACHINE, Key32, ValueName, AppName, guid ) = 0 ) then
return guid; // return 32 bit GUID
endif;
return AppName; // return old style uninstall key (fall back value)
end;
///////////////////////////////////////////////////////////////////////////////
//
// Function: RegDBGetSubKeyNameContainingValue
//
// Purpose: Find a registry sub key containing a given value.
// Return the NAME of the subkey (NOT the entire key path)
//
// Returns: (SubKeyName is assigned a value by referrence)
// = 0 A sub key name was found with a matching value
// != 0 Failed to find a sub key with a matching value
//
///////////////////////////////////////////////////////////////////////////////
function NUMBER RegDBGetSubKeyNameContainingValue( nRootKey, Key, ValueName, Value, SubKeyName )
STRING SearchSubKey, SubKey, svValue;
NUMBER nResult, nType, nvSize;
LIST listSubKeys;
begin
SubKeyName = "";
listSubKeys = ListCreate(STRINGLIST);
if (listSubKeys = LIST_NULL) then
MessageBox ("Unable to create necessary list.", SEVERE);
abort;
endif;
RegDBSetDefaultRoot( nRootKey );
if (RegDBQueryKey( Key, REGDB_KEYS, listSubKeys ) = 0) then
nResult = ListGetFirstString (listSubKeys, SubKey);
while (nResult != END_OF_LIST)
SearchSubKey = Key + "\\" + SubKey;
nType = REGDB_STRING;
if (RegDBGetKeyValueEx (SearchSubKey, ValueName, nType, svValue, nvSize) = 0) then
if( svValue = Value ) then
SubKeyName = SubKey;
nResult = END_OF_LIST;
endif;
endif;
if( nResult != END_OF_LIST ) then
nResult = ListGetNextString (listSubKeys, SubKey);
endif;
endwhile;
endif;
ListDestroy (listSubKeys );
if ( SubKeyName = "" ) then
return 1;
else
return 0;
endif;
end;

Related

In MFC RegisterHotKey function don't respond

In MFC RegisterHotKey function don't respond from Dll but works fine if i put same code in exe.
i am using same code which works in exe and putting it in Dll.
Also making sure that Dll gets load and othere changes are reflecting when i run exe.
You are using the same code for registering a hotkey in your DLL as in your EXE. This is your problem. See documentation for RegisterHotKey:
An application must specify an id value in the range 0x0000 through 0xBFFF. A shared DLL must specify a value in the range 0xC000 through 0xFFFF (the range returned by the GlobalAddAtom function). To avoid conflicts with hot-key identifiers defined by other shared DLLs, a DLL should use the GlobalAddAtom function to obtain the hot-key identifier.
To use RegisterHotKey from a shared DLL, you need to generate an id value. To get a value in the correct range, call GlobalAddAtom with a string parameter (see About Atom Tables: String Atoms). To create a unique id, pass the string representation of a GUID.
// Putting in message map
ON_MESSAGE(WM_HOTKEY, OnHotKey)
//Demographic Bar HotKey (putting in create or init)
RegisterHotKey(GetSafeHwnd(), GlobalAddAtom("SHIFT+CTRL+ALT+D"), MOD_SHIFT | MOD_CONTROL | MOD_ALT, 'D');
//source file
LRESULT CMainFrame::OnHotKey(WPARAM wParam, LPARAM lParam)
{
int hotKeyId = (int)wParam;
CString atomName;
GlobalGetAtomName(hotKeyId, atomName.GetBuffer(20), 20);
atomName.ReleaseBuffer();
if (atomName.Compare("SHIFT+CTRL+ALT+D") == 0)
{
SetPaneFocused(1); //Hard-coding 1 & 2 as place holders. Actual pane id should be here
}
else if (atomName.Compare("SHIFT+CTRL+ALT+M") == 0)
{
SetPaneFocused(2);
}
return S_OK;
}

Creating N number of instances of MFC application

Can someone please tell me how to go about creating a maximum of N instances of an application in MFC?
Also, if N instances are running, and one instance gets closed, then one new instance can be created (but no more than N instances can run at any one time).
Thank you in advance.
a.
You can create a global semaphore that up to n process instances can enter. The n+1 th instance of your process will fail to enter the semaphore. Of course you should select a short timeout for the locking operation so you can present a meaningful feedback to the user.
For the semaphore stuff you can take a look at MSDN.
I'd use lock files. In your CMyApp::InitInstance() add:
CString Path;
// better get the path to the global app data or local user app data folder,
// depending on if you want to allow the three instances machine-wide or per user.
// Windows' file system virtualization will make GetModuleFileName() per user:
DWORD dw = GetModuleFileName(m_hInstance,
Path.GetBuffer(MAX_PATH), MAX_PATH);
Path.ReleaseBuffer();
// strip "exe" from filename and replace it with "lock"
Path = Path.Left(Path.GetLength()-3) + T("lock");
int i;
// better have the locking file in your class and do a clean Close on ExitInstance()!
CFile *pLockingFile = NULL;
for (i = 0; i < 3; i++) // restrict to three instances
{
CString Counter;
Counter.Format(T("%d"), i);
TRY
{
pLockingFile = new CFile(Path + Counter,
CFile::modeCreate|CFile::modeWrite|CFile::shareExclusive);
pLockingFile.Close();
break; // got an instance slot
}
CATCH( CFileException, e )
{
// maybe do something else here, if file open fails
}
END_CATCH
if (i >= 3)
return TRUE; // close instance, no slot available
}
Edit: To lock the software machine-wide, retrieve the common app folder using the following function instead of calling GetModuleFileName().
#pragma warning(disable: 4996) // no risk, no fun
BOOL GetCommonAppDataPath(char *path)
{
*path = '\0';
if (SHGetSpecialFolderPath(NULL, path, CSIDL_COMMON_APPDATA, TRUE))
{
strcat(path, T("\\MyApplication")); // usually found under C:\ProgramData\MyApplication
DWORD dwFileStat = GetFileAttributes(path);
if (dwFileStat == 0xffffffff) // no MyApplication directory yet?
CreateDirectory(path, NULL); // create it
dwFileStat = GetFileAttributes(path); // 2nd try, just to be sure
if (dwFileStat == 0xffffffff || !(dwFileStat & FILE_ATTRIBUTE_DIRECTORY))
return FALSE;
return TRUE;
}
return FALSE;
}
Note: This will only work from Vista on. If you have XP, writing to a global directory is an easy task, e.g. C:\Windows\Temp. I have put the function in a helper dll I only load if the OS is Vista or higher. Otherwise your software won't start because of unresolved references in system dlls.

SHBrowseForFolder not returning Selected folder string by reference

Hello there I'm working on a windows application using the Windows API with MS VC++ 2010 and I have the following code for selecting folders:
BOOL BrowseFolder(TCHAR *result)
{
BROWSEINFO brwinfo = { 0 };
brwinfo.lpszTitle = _T("Select Your Source Directory");
brwinfo.hwndOwner = hWnd;
LPITEMIDLIST pitemidl = SHBrowseForFolder (&brwinfo);
if (pitemidl == 0) return FALSE;
// get the full path of the folder
TCHAR path[MAX_PATH];
if (SHGetPathFromIDList (pitemidl, path)) result = path;
IMalloc *pMalloc = 0;
if (SUCCEEDED(SHGetMalloc(&pMalloc)))
{
pMalloc->Free(pitemidl);
pMalloc->Release();
}
::MessageBox(hWnd, result, "input", MB_OK);
::MessageBox(hWnd, inputFolder, "input", MB_OK); // Reference Test
return TRUE;
}
So, it opens up a browse folder dialog, saves the selected folder string in the reference parameter "result" and returns true if everything is ok.
Later I call:
BrowseFolder(inputFolder);
And when i try to print out the content of "inputFolder", it shows blank (inputFolder is a global variable TCHAR* inputFolder)
As you can see in the BrowseFolder definition I send two Message Boxes one for "result" and the other for "inputFolder" (this last one shows blank)
So my question is.. If I call: BrowseFolder(inputFolder); Shouldn't "inputFolder" be modified by reference? Why does it show empty?
Thanks in advance.
if (SHGetPathFromIDList (pitemidl, path)) result = path;
This line is your problem. result isn't a class like std::string, it's simply a pointer to a buffer of one or more TCHAR. Assigning it like that simply changes the pointer to point to path, it doesn't copy path into the buffer that result points to.
If you don't want to change to use a string class then you need to call a function to copy the string into the supplied buffer. For example:
StringCchCopy(result, MAX_PATH, path);
(this assumes the buffer is MAX_PATH characters in size).

Force GetKeyNameText to english

The Win32 function GetKeyNameText will provide the name of keyboard keys in the current input locale.
From MSDN:
The key name is translated according to the layout of the currently
installed keyboard, thus the function may give different results for
different input locales.
Is it possible to force the input locale for a short amount of time? Or is there another alternative to GetKeyNameText that will always return the name in English?
Update: This answer does not work. It actually modifies the keyboard settings of the user. This appear to be a behavior change between Windows versions.
CString csLangId;
csLangId.Format( L"%08X", MAKELANGID( LANG_INVARIANT, SUBLANG_NEUTRAL ) );
HKL hLocale = LoadKeyboardLayout( (LPCTSTR)csLangId, KLF_ACTIVATE );
HKL hPrevious = ActivateKeyboardLayout( hLocale, KLF_SETFORPROCESS );
// Call GetKeyNameText
ActivateKeyboardLayout( hPrevious, KLF_SETFORPROCESS );
UnloadKeyboardLayout( hLocale );
WARNING: GetKeyNameText is broken (it returns wrong A-Z key names for non-english keyboard layouts since it uses MapVirtualKey with MAPVK_VK_TO_CHAR that is broken), keyboard layout dlls pKeyNames and pKeyNamesExt text is bugged and outdated. I cannot recommend dealing with this stuff at all. :)
If you're really-really want to get this info - then you can load and parse it manually from keyboard layout dll file (kbdus.dll, kbdger.dll etc).
There is a bunch of undocumented stuff involved:
In order to get proper keyboard layout dll file name first you need to convert HKL to KLID string. You can do this via such code:
// Returns KLID string of size KL_NAMELENGTH
// Same as GetKeyboardLayoutName but for any HKL
// https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-language-pack-default-values
BOOL GetKLIDFromHKL(HKL hkl, _Out_writes_(KL_NAMELENGTH) LPWSTR pwszKLID)
{
bool succeded = false;
if ((HIWORD(hkl) & 0xf000) == 0xf000) // deviceId contains layoutId
{
WORD layoutId = HIWORD(hkl) & 0x0fff;
HKEY key;
CHECK_EQ(::RegOpenKeyW(HKEY_LOCAL_MACHINE, L"SYSTEM\\CurrentControlSet\\Control\\Keyboard Layouts", &key), ERROR_SUCCESS);
DWORD index = 0;
while (::RegEnumKeyW(key, index, pwszKLID, KL_NAMELENGTH) == ERROR_SUCCESS)
{
WCHAR layoutIdBuffer[MAX_PATH] = {};
DWORD layoutIdBufferSize = sizeof(layoutIdBuffer);
if (::RegGetValueW(key, pwszKLID, L"Layout Id", RRF_RT_REG_SZ, nullptr, layoutIdBuffer, &layoutIdBufferSize) == ERROR_SUCCESS)
{
if (layoutId == std::stoul(layoutIdBuffer, nullptr, 16))
{
succeded = true;
DBGPRINT("Found KLID 0x%ls by layoutId=0x%04x", pwszKLID, layoutId);
break;
}
}
++index;
}
CHECK_EQ(::RegCloseKey(key), ERROR_SUCCESS);
}
else
{
WORD langId = LOWORD(hkl);
// deviceId overrides langId if set
if (HIWORD(hkl) != 0)
langId = HIWORD(hkl);
std::swprintf(pwszKLID, KL_NAMELENGTH, L"%08X", langId);
succeded = true;
DBGPRINT("Found KLID 0x%ls by langId=0x%04x", pwszKLID, langId);
}
return succeded;
}
Then with KLID string you need to go to HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layouts\%KLID% registry path and read Layout File string from it.
Load this dll file from SHGetKnownFolderPath(FOLDERID_System, ...) (usually C:\Windows\System32) with LoadLibrary() call.
Next you need to do GetProcAddress(KbdDllHandle, "KbdLayerDescriptor") - you're receive pointer that can be casted to PKBDTABLES.
There is kbd.h header in Windows SDK that have KBDTABLES struct definition (there is some stuff involved to use proper KBD_LONG_POINTER size for x32 code running on x64 Windows. See my link to Gtk source at the end).
You have to look at pKeyNames and pKeyNamesExt in it to get scan code -> key name mapping.
Long story short: The GTK toolkit have the code that doing all this(see here and here). Actually they are building scan code -> printed chars tables from Windows keyboard layout dlls.

Can I suppress selected input before the application's main loop?

As part of my Visual Studio utilities add-in SamTools, I have a mouse input routine that catches Ctrl+MouseWheel and sends a pageup/pagedown command to the active text window. Visual Studio 2010 added a new "feature" that uses that gesture for zoom in/out (barf). Currently, my add-in does send the scrolling command, but Visual Studio still changes the font size because I'm not eating the input.
I set my hook with a call to SetWindowsHookEx. Here's the callback code. My question is: is the best way to prevent Visual Studio from handling the Ctrl+MouseWheel input as a zoom command to simply not call CallNextHookEx when I get a mouse wheel event with the Ctrl key down?
(Please bear in mind this is some old code of mine.) :)
private IntPtr MouseCallback(int code, UIntPtr wParam, ref MOUSEHOOKSTRUCTEX lParam)
{
try
{
// the callback runs twice for each action - this is the latch
if (enterHook)
{
enterHook = false;
if (code >= 0)
{
int x = lParam.mstruct.pt.X;
int y = lParam.mstruct.pt.Y;
uint action = wParam.ToUInt32();
switch (action)
{
case WM_MOUSEWHEEL:
OnMouseWheel(new MouseEventArgs(MouseButtons.None, 0, x, y, ((short)HIWORD(lParam.mouseData)) / (int)WHEEL_DELTA));
break;
default:
// don't do anything special
break;
}
}
}
else
{
enterHook = true;
}
}
catch
{
// can't let an exception get through or VS will crash
}
return CallNextHookEx(mouseHandle, code, wParam, ref lParam);
}
And here's the code that executes in response to the MouseWheel event:
void mouse_enhancer_MouseWheel( object sender, System.Windows.Forms.MouseEventArgs e )
{
try
{
if ( Keyboard.GetKeyState( System.Windows.Forms.Keys.ControlKey ).IsDown && Connect.ApplicationObject.ActiveWindow.Type == vsWindowType.vsWindowTypeDocument )
{
int clicks = e.Delta;
if (e.Delta < 0)
{
Connect.ApplicationObject.ExecuteCommand( "Edit.ScrollPageDown", "" );
}
else
{
Connect.ApplicationObject.ExecuteCommand( "Edit.ScrollPageUp", "" );
}
}
}
catch ( System.Runtime.InteropServices.COMException )
{
// this occurs if ctrl+wheel is activated on a drop-down list. just ignore it.
}
}
PS: SamTools is open source (GPL) - you can download it from the link and the source is in the installer.
PSS: Ctrl+[+] and Ctrl+[-] are better for zooming. Let Ctrl+MouseWheel scroll (the vastly more commonly used command).
According to MSDN, it's possible to toss mouse messages that you process. Here's the recommendation:
If nCode is less than zero, the hook
procedure must return the value
returned by CallNextHookEx.
If nCode is greater than or equal to
zero, and the hook procedure did not
process the message, it is highly
recommended that you call
CallNextHookEx and return the value it
returns; otherwise, other applications
that have installed WH_MOUSE hooks
will not receive hook notifications
and may behave incorrectly as a
result. If the hook procedure
processed the message, it may return a
nonzero value to prevent the system
from passing the message to the target
window procedure.
In other words, if your mouse callback ends up using the mouse message, you don't have to call the next CallNextHookEx -- just return a nonzero value and (in theory, at least) the mouse movement should get swallowed. If that doesn't work the way you want, comment and we can iterate.
BTW, another possible alternative: it's possible that VS's mapping to the mouse wheel shows up in the Tools...Customize... UI, just like key mappings do. In that case, you could simply remap your add-in's commands instead of working at the hook level. But it's also posible (likely?) that this gesture is hard-coded.

Resources