Thursday, August 21, 2008

InstallShield XML File Changes

InstallShield 2008 has an interesting quirk when editing XML File Changes. When changing the value of an XML tag by typing a new value in, it doesn't like to get saved. You need to do something else on that screen like check/uncheck a box before the new value is saved. Or you can select the new value from the list of string values.

One thing I don't do well is use the string values from InstallShield. I like to type in the values I want instead of just changing the value of a string. I had several XML files that needed the exact same change. If I had properly used the string values, I could have just made the change once.

Tuesday, August 5, 2008

InstallShield and Windows Services

To add a Windows Service to InstallShield, add the service file into a component. I have my executable as the only file in the component. Right click on the file and set it as the key file. The expand the Advanced Settings tab. There are two sections for NT Services.
In Install NT Services you set the properties. I set the User Name to [SERVICE_USER_NAME] and Password to [SERVICE_USER_PASSWORD]. Then you can set the user in InstallScript using MsiSetProperty.
In Control NT Services you do something very important. You allow the service to be uninstalled properly. You will need to add a service. Then add an event which I am not entirely sure means. But then you have a list of properties for installing and uninstalling. The ones I set to Yes are Uninstall Stop and Uninstall Delete. I also change Wait Type to "Wait for the event to complete".
This allows the service to be uninstalled. However, I did notice that if the services management console is up that the service won't get deleted there. It gets disabled and then gets deleted after the console is closed.

InstallShield and LogonAsService Right

I was somewhat perplexed by this one. I am installing a Windows NT Service (don't let the "NT" bother you, that is just the OS when services where introduced or something like that). I have configured the service in my components. I filled out the properties for NT Services and set properties for the user and password. Then I use the user dialog I mentioned in previous posts to get the user credentials for the service. This dialog can create a new user for this, which is typically the case for my project.
However, the installed service doesn't start. Instead I get an error that the service can't be started with the message being "Error 1069: The service did not start due to a logon failure." I went into the properties to the Log On tab to check things. The password is obscured, so I retyped it in and clicked Apply. Then I get the message "The account has been granted the Log On As A Service right." After that, the service works.
This is what I learned. Users, especially domain users, don't have this right typically. I am guessing that since the new user is specified in the format "DOMAIN\USER" it is considered a domain user even if the domain is the local system. The localsystem user already has this right granted to it.
For some reason, MSI or InstallShield doesn't allow you to grant this right when the user is created. There are some posts on the InstallShield forums that say this. There is also mention of the answer.
The answer is calling the utility NTRIGHTS.EXE. This is the Windows command-line utility for administrating rights for users. So, first you need to acquire the utility. It comes from the Windows Resource Kit. It is not installed as part of Windows, so you will need to download it and install it. After that, you have the utility. Next it needs to be included in your InstallShield project. I copied the utility for a folder where I have my support files (I have all my files together for easy source-control). I then added it to my support files inside my project as a language independent file. Then I run it using LaunchAppAndWait.

szProgName = SUPPORTDIR^"ntrights.exe";
szProgParams = " -u" + szUserName + " +r SeServiceLogonRight";
StatusBox("Executing '" + szProgName + szProgParams + "'", DEBUG);
if (LaunchAppAndWait(szProgName, szProgParams, WAIT) <>
MessageBox("Unable to launch " + szProgName + ". Retype the password for the the user in the workflow service properties.", WARNING);
endif;

BTW, I call it from a later event in my InstallScript, specifically OnFirstUIAfter. That may not be the best as it would also need to be in OnMaintUIAfter, but this is a first cut and I haven't got all the maintenance mode worked out yet. Also, I put the call in a function so that I can call it from wherever it needs. The point is that it needs to called AFTER the user is created which is after InstallShield's "Do Stuff" phase.
Problem Solved.

Monday, August 4, 2008

SdLogonUserInformation

I discovered a interesting tidbit today -- SdLogonUserInformation only works with one user. It has a single set of properties it uses to gather user information. If run this dialog more than once to get multiple users, the last set of credentials wins.

Friday, August 1, 2008

Quiet Install of an MSI

My project has AJAX Extensions as a prerequisite. So, it needs to run the APSAJAXExtSetup.msi. It needs to run it as a prereq for my install. Also, I don't want my users to see, I want it installed for all users and I don't want it to run if it exists.

First, I found this link which was helpful. That and running the MSI with /? to get the help list got me the following command line.

APSAJAXExtSetup.msi /quiet ALLUSERS=2

I guess I need to back up a moment. I had to create a prerequisite in IS before all this. There didn't seem to be a standard one for AJAX extensions. In the Redistributable view, I added one. I then added the MSI on the File To Include tab. Then on the Application to Run tab I selected the MSI. The parameters can then be put into command line box on the Application to Run tab.

In order to conditionally run the MSI, you need to set a condition on the conditions tab. You need to specifiy a registry key or a file to check for that shows the package exists. To find a something, I opened the AJAX MSI in InstallShield and started poking around. I looked at the Registry view and settled on the key

HKEY_CURRENT_USER\Software\Microsoft\ASP.NET\ASP.NET 2.0 AJAX Extensions

I set the condition that if the key does not exist, run the prerequisite.

*EDIT* I guess the install is not completely silent. InstallShield has a dialog with a status bar showing that a prerequisite is being installed and what the prereq is. But the actual dialogs from the MSI package don't show.

InstallShield LaunchAppAndWait and File Access

The Task: Grant access to the files I have installed to the users I have created.

In the Files & Folders view, you can right click on a folder and select properties to get access to the permissions for a file or folder. However, this doesn't work so well because you can't add permissions to a file, you can only replace them. I didn't want that as I didn't want to clear out the Administrators group access to my install folder. I just wanted to add a couple of users to the access list. The real answer is to use LaunchAppAndWait to run cacls.exe on the folder.

I have used LaunchAppAndWait in the past and have struggled with it. It is difficult to debug. So, there are some things I would recommend. First, add a MessageBox for debug that shows what you are going to run. If you are not getting desired results, copy exactly what is in your message into a console window on the target system with your files in place and see what happens. You might be surprised. LaunchAppAndWait tells you if it launched the program, but does not tell your if the program had errors.

Of course, that isn't the way I did it the first time. The first time I was convinced LaunchAppAndWait was messing up. So, I created a sanity-check app in Visual Studio and called it from LaunchAppAndWait.



It's very simple. Take the arguments from the command line and put them into a text file. This way, you can use whatever command parameters your real command needs. Just change the program to the test app.

To use LaunchAppAndWait, set up a variable for the program name and a variable for the parameters. Actually, here is my function.

function SetDirectoryPermissions(szDirectoryName, szUserName)
STRING szProgName;
STRING szProgParams;
STRING svTempString;
begin
// Remove any trailing slash from the directory name.
StrSub(svTempString, szDirectoryName, StrLength(szDirectoryName) - 1, 1);
if (svTempString = "\\") then
StrSub(svTempString, szDirectoryName, 0, StrLength(szDirectoryName) - 1);
szDirectoryName = svTempString;
endif;

szProgName = WINDIR^"system32\\cacls.exe";
szProgParams = " " + szDirectoryName + " /E /G " + szUserName + ":F";
if (LaunchAppAndWait(szProgName, szProgParams, WAIT) <>
MessageBox("Unable to launch cacls. File permissions will need to be set manually.", WARNING);
endif;
end;


With all that out there in the open, it shows the issue I had with cacls.exe. If you pass in INSTALLDIR, there is a trailing slash (IS always puts one there). cacls.exe throws an error saying that it can't find the file. So, I had to remove the trailing slash and IT WORKED!

BTW, the command it is running is

cacls.exe C:\InstallDir /e /g DOMAIN\USER:F

Which brings up my last point. You should include the domain with the user name. Windows needs that to properly locate the credentials for the user.

Ok, so my code samples are rough. I am still getting used to blogger.