Can't delete folder WIX installer - windows-services

I have a service that is installed by my WIX installer:
<Component Id="cmp_myService" Guid="{5FC8815E-33T8-4C3D-9654-849EE4CB1E22}">
<File Id="f_myService" Name="MyService.exe" Source="$(var.SourcePath)MyService.exe" KeyPath="yes" />
<ServiceInstall Id="si_myServiceInstall" Name="My Service" DisplayName="It is my service" Type="ownProcess" Interactive="yes" Start="auto" ErrorControl="normal" Description="Here is some description" />
<ServiceControl Id="sc_startStopMyService" Name="My Service" Start="install" Stop="both" Remove="uninstall" Wait="yes" />
</Component>
Also, I have a custom action ca1_removeInstallDirOnUnsinstall that deletes a folder with all its content (MyService.exe is inside this folder)
<Custom Action="ca1_removeInstallDirOnUnsinstall" After="DeleteServices"><![CDATA[REMOVE="ALL"]]></Custom>
I schedule this custom action to run right after DeleteServices action in the execution sequence. I assume that at this point MyService.exe should be stopped and deleted. But I get an exception Access to the path 'MyService.exe' is denied which means that the service isn't deleted yet. Why does that happen and where my custom action should be scheduled to be sure that the service is already deleted?
Related log files:
MSI (s) (64:3C) [08:42:32:597]: Doing action: StopServices
MSI (s) (64:3C) [08:42:32:597]: Note: 1: 2205 2: 3: ActionText
Action ended 8:42:32: UnpublishFeatures. Return value 1.
Action start 8:42:32: StopServices.
MSI (s) (64:3C) [08:42:32:597]: Doing action: DeleteServices
MSI (s) (64:3C) [08:42:32:597]: Note: 1: 2205 2: 3: ActionText
Action ended 8:42:32: StopServices. Return value 1.
Action start 8:42:32: DeleteServices.
MSI (s) (64:3C) [08:42:32:597]: Doing action: ca1_removeInstallDirOnUnsinstall
MSI (s) (64:3C) [08:42:32:597]: Note: 1: 2205 2: 3: ActionText
Action ended 8:42:32: DeleteServices. Return value 1.
MSI (s) (64:80) [08:42:32:597]: Invoking remote custom action. DLL: C:\windows\Installer\MSIA125.tmp, Entrypoint: RemoveInstallDirRecursively
MSI (s) (64:B8) [08:42:32:597]: Generating random cookie.
MSI (s) (64:B8) [08:42:32:597]: Created Custom Action Server with PID 6176 (0x1820).
MSI (s) (64:98) [08:42:32:644]: Running as a service.
MSI (s) (64:B4) [08:42:32:644]: Hello, I'm your 32bit Impersonated custom action server.
Action start 8:42:32: ca1_removeInstallDirOnUnsinstall.
SFXCA: Extracting custom action to temporary directory: C:\windows\Installer\MSIA125.tmp-\
SFXCA: Binding to CLR version v4.0.30319
Calling custom action CustomAction!CustomAction.FilesAndFoldersCustomAction.RemoveInstallDirRecursively
Access to the path 'MyService.exe' is denied.

InstallFinalize: DeleteServices happens before InstallFiles. I suppose msiexec.exe might have a lock on the folder for potential re-install as part of a major upgrade, but I am not sure (find locks using: perfmon.exe, procexp64.exe). You can put the delete action right before InstallFinalize (which ends elevated operations that change the system). This location should work, but no guarantees. It is not good practice to "nuke" whole folders.
Best Practice: Generally you should not delete whole folders (it is quite risky - mess something up and you could end up deleting half your computer. Seriously. I have seen it happen. Not with MSI but with a "cleanup EXE"). I would put log files in the user profile or some other location outside of %ProgramFiles%. Or maybe use the event log or even a database to upload to online to avoid too many files left behind.
Acceptable Remains: In my opinion log files and other user data should not be attempted uninstalled automagically. Why? They are user data - in other words: they belong to the user. You can't just delete them summarily? I would just leave the data in place in case they want to analyze them or re-install the application (the latter is particularly important for license keys - do you leave them behind?).
WiX: WiX has several built-in constructs to help remove files and folders. There are the built-in MSI varieties (RemoveFile, RemoveFolder) that just delete files by wildcard or name. Then there is the RemoveFileEx described by Bob Arnson (WiX developer team). Please read the blog post. I believe it can do the job recursively (all sub folders). As you understand I don't use these constructs much since I favor other cleanup approaches. Quite frankly I just try to document the cleanup procedure in a PDF or an online KDB article.
Links:
wix - how to delete non-empty folder
WIX - How to use RemoveFiles

Related

Are TFS Build Agent User Capabilities' Values Obtainable Within Build Steps?

I'm trying to write a build step within TFS that relies on knowing where the Build agent has nuget.exe stored (the standard nuget-install step mucks around with the order of arguments in a way that breaks build execution, so I want to run the exe myself using one of the batch/shell/ps steps).
It would seem that setting up a capability on the Build Agent with that path would make sense, but I cannot seem to reference the value in any of my build steps, and I cannot find anything helpful on MSDN.
I'm expecting it to be something like $(Env.MyUserCapability), but it never resolves to the value.
Is it possible to retrieve a capability value within a build step? And if so, how do you do it? And if not, what is a viable alternative?
The user-defined capabilities are metadata only. But you can set a global environment variable (e.g. NUGET) and set that to a path to a nuget.exe, when you restart the agent, the machine-wide environment is then discovered as capability and you can then use it.
If you are writing a custom task, you can also add a nuget.exe to the task that will be downloaded to the executing agent.
UPDATE: I made a public extension out of this.
UPDATE: this works in Azure DevOps 2019.
In TFS 2018u1, the following works:
Import-Module "Microsoft.TeamFoundation.DistributedTask.Task.Common"
Import-Module "Microsoft.TeamFoundation.DistributedTask.Task.Internal"
Add-Type -Assembly "Microsoft.TeamFoundation.DistributedTask.WebApi"
$VSS = Get-VssConnection -TaskContext $distributedTaskContext
$AgentCli = $VSS.GetClient([Microsoft.TeamFoundation.DistributedTask.WebApi.TaskAgentHttpClient])
$AgentConfig = Get-Content "$Env:AGENT_HOMEDIRECTORY\.agent" -Raw | ConvertFrom-Json
$Agent = $AgentCli.GetAgentAsync($AgentConfig.PoolId, $Env:AGENT_ID, $TRUE, $FALSE, $NULL, $NULL, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()
if($Agent.UserCapabilities.MyCapability)
{
Write-Host "Got the capability!";
}
The long string of default arguments ending with CancellationToken::None is for compatibility with Powershell 4. PS4 doesn't support default values for value-typed method parameters, PS5 does.
This snippet does something very questionable - it relies on the location and the structure of the agent configuration file. This is fragile. The problem is that the GetAgentAsync method requires both pool ID and the agent ID, and the former is not exposed in the environment variables. A slightly less hackish approach would check all pools and find the right one by the agent ID:
$Pools = $AgentCli.GetAgentPoolsAsync($NULL, $NULL, $NULL, $NULL, $NULL, [System.Threading.CancellationToken]::None).GetAwaiter().GetResult()
$Demands = New-Object 'System.Collections.Generic.List[string]'
foreach($Pool in $Pools)
{
$Agent = $AgentCli.GetAgentsAsync($Pool.ID, $Env:AGENT_NAME, $TRUE, $FALSE, $NULL, $Demands, $NULL, [System.Threading.CancellationToken]::None).Result
if($Agent -and $Agent.Id -eq $Env:AGENT_ID)
{
Break
}
}
This relies on another undocumented implementation detail, specifically that agent IDs are globally unique. This seems to hold as late as TFS 2018, but who knows.
When you employ the $distributedTaskContext, the task is connecting back to TFS with an artificial user identity, "Project Collection Build Service" (not with the agent service account). There's one user like that in each collection, they're distinct. In order to allow tasks running in releases in a collection to query the agent for user capabilities, you need to grant the Reader role to the relevant pool(s) (or all pools) to the user account called "Project Collection Build Service (TheCollectionName)" from that collection.
It also looks like some actions also grant an implicit Reader role on a pool to the task identity.
Alternatively, you can construct a VssConnection from scratch with Windows credentials, and grant the agent account(s) Reader role on the pool(s).

ClickOnce myApp.application generated with wrong PublishUrl

I have added a MSBuild target to update the PublishUrl and then call the 'Publish' target, passing the new value in. This has allowed me to build multiple branches and have the corresponding ClickOnce app dropped in a branch specific share location.
<PropertyGroup>
<PublishUrl >\\someNetworkShare\</PublishUrl>
</PropertyGroup>
<Target Name="PublishClickOnce">
<PublishUrl>$(PublishUrl)$(BranchName)\</PublishUrl>
<MSBuild Projects="$(ProjectPath)" Properties="PublishUrl=$(PublishUrl); PublishDir=$(PublishUrl); Platform=AnyCPU" Targets="Publish" />
</Target>
The problem is that this just doesn't work on one of my build machines. It works perfectly fine on 2 of them, but not the 3rd.
Looking at the myApp.application file i can see that the deploymentProvider codebase points to the unchanged PublishedUrl (but it has been updated at the time of calling the 'Publish' target:
<deploymentProvider codebase="file://someNetworkShare/myApp.application" />
The above should be:
<deploymentProvider codebase="file://someNetworkShare/branchName/myApp.application" />
A few things i have tried:
Update the 'InstallUrl' to match the 'PublishUrl'
Added logging to ensure that the 'PublishUrl' has updated before calling the 'Publish' target.
Added a condition to the 'PublishUrl' to only update if it is empty
Any help would be much appreciated.
Thanks
-------------------------------- UPDATE --------------------------------
So a couple of things:
The working build machines are on W7, the failing machines are on a mixture of W7 and W8.
It turns out that you can exclude the deploymentProvider element (ProjectProperties -> Publish -> Manifest -> Exclude deployment provider URL). If excluded the system attempts to figure it out by its self at run-time (this is sufficient in most cases).
So I tested the build on a few more machines (a completely clean W7 VM, a W7 dev machine, W8.1 dev machine) and they both produced the incorrect deploymentProvider line.
Still curious to the actual root cause of the issue so I don't want to mark it as answered.

Microsoft Team Foundation Server Express 2012 - How to clean up in tbl_TestResult (old test results)

I have installed a Microsoft Team Foundation Server Express 2012.
The table tbl_TestResult is using up 7000 MB of my Database space.
I tried to find information on how to clean up this Table but found no way to do so.
When I want to check in new files into TFS I get the Error TF30042: The database is full...
Over the Visul Studio I deleted all visible Tests but still the size of tbl_TestResult just decreased very little.
Can anyone explain to me how I can cleanup all test results in a proper way?
I am using the TFS client API to delete old test runs. Here's some sample code:
TfsTeamProjectCollection tpc = new TfsTeamProjectCollection(new Uri("http://tfs2012:8080/tfs/DefaultCollection/"));
TestManagementService testManagementService = tpc.GetService<TestManagementService>();
ITestManagementTeamProject teamProject = testManagementService.GetTeamProject("MyProject");
int totalRuns = teamProject.TestRuns.Count("SELECT * FROM TestRun");
// Limit by date, so the query doesn't take too long.
string query = "SELECT * FROM TestRun WHERE CreationDate < '2013/07/01'";
int numTotalToDelete = teamProject.TestRuns.Count(query);
if (numTotalToDelete == 0) { return; }
// Only delete 500 at a time, to give SQL Server time to breathe (don't ask).
var runsToDelete = teamProject.TestRuns.Query(query, false)
.Take(500);
teamProject.TestRuns.Delete(runsToDelete);
Query syntax comes from here: http://blogs.msdn.com/b/duat_le/archive/2010/02/25/wiql-for-test.aspx
We had the same issue for our on prem TFS. This is how we did it for TFS 2015 up to TFS 2017.
Install Microsoft Visual Studio Team Foundation Server 2015 Power Tools to get access to Test Attachment Cleaner(TAC), tcmpt.exe.
It installs to c:\Program Files (x86)\Microsoft Team Foundation Server 2015 Power Tools
Sample settings for TAC is located in the installation folder, at <INSTALL_DIR>\TestAttachmentCleaner_samples_settingsfile
Create your own settings file, for example, all-100mb.xml file:
<DeletionCriteria>
<TestRun>
<Created Before="2016-01-01" />
</TestRun>
<Attachment>
<Extensions>
<Include value="wmv" />
<Include value="xesc" />
<Include value="trmx" />
</Extensions>
<!-- size in MB -->
<SizeInMB GreaterThan="100" />
</Attachment>
<LinkedBugs>
<Exclude state="Active" />
</LinkedBugs>
</DeletionCriteria>
The above config file removes test attachments of inactive test runs which are larger than 100MB with extensions: wmv, xesc, trmx.
Perform a test attempt to find out the outcome with this command: tcmpt.exe AttachmentCleanup /settingsfile:"all-100mb.xml" /mode:preview /collection:"https://tfs.contoso.com/DefaultCollection" /teamproject:"TeamProjectName" /outputfile:all-100mb.log"
This produces a very detailed log file that includes key information you needed, like:
------------ Summary ------------
Number of attachments affected: 339
Total size of attachments: 39646,7 MB
Elapsed time was 247,94 seconds
When you are satisfied with the result, do this:
Turn off transaction logs for your TFS database, if you use MSSQL server.
If you don't, you may grow your transaction logs faster than you can remove the test attachments.
Run tcmpt.exe command above without the /mode:preview parameter to delete test attachments.
When done, run these 2 stored procedures from the TFS database:
EXEC dbo.prc_DeleteUnusedContent 1
EXEC dbo.prc_DeleteUnusedFiles 1, 0, 1000
The stored procedures removes emptied rows from the database.
If the stored procedures last less than 1 second each, wait some time and re-run them again.
Then you can choose to shrink your database.
Good luck with it.
if you delete unwanted builds then you will be presented with the option to delete test results associated with the build when you delete it.
You may find that the test attachments are consuming a lot of space as well, you can use the test attachment cleaner provided with TFS power tools to remove these.
TFPT can be found here

Azure: Unable to start cloud service running vbs startup task

For some specific purpose, I need to install some fonts on the instances. It comes as no surprise when I choose StartUp Task to accomplish that goal. I've configured the Service Definitions as below:
<Startup>
<Task commandLine="Fonts\InstallFonts.vbs" executionContext="elevated" taskType="simple" />
</Startup>
Nothing special here. Click and run, it failed. However, if I changed the commandLine into a cmd file including just nonsense, namely "echo test", the instance would run without ado. So there must be some issue with my scripting:
Const FONTS = &H14&
Set objShell = CreateObject("Shell.Application")
Set objFolder = objShell.Namespace(CreateObject("Scripting.FileSystemObject").GetAbsolutePathName("."))
Set fontFolder = objShell.Namespace(FONTS)
Set rxTTF = New RegExp
rxTTF.IgnoreCase = True
rxTTF.Pattern = "\.ttf$"
Set fso = CreateObject("Scripting.FileSystemObject")
FOR EACH FontFile IN objFolder.Items()
IF rxTTF.Test(FontFile.Path) THEN
IF NOT fso.FileExists(fontFolder.Self.Path+"\\"+FontFile.Name) THEN
FontFile.InvokeVerb("Install")
END IF
END IF
NEXT
The script should come with no error because I've tested it either locally or on Azure via RDP.
Weirdly, when I put it in the startup, the role just won't start. The instance just keeps recycling and at last says "I'm unhealthy". Even if I deprecate the vbs into just one line of code - the first line Const FONTS = &H14&, it just won't start. Even if I wrap the invocation of the vbs into a cmd file, namely to put something like "cscript /B file.vbs", it won't run either.
So I'm concluding that there must be some issue regarding the communication between the script and the Windows Azure monitor. I'm not sure but I think the monitor might take the running script as a failed task. Besides, I'm wondering if there is any timeout for the startup task, which should be the problem though, because the script can guarantee that no UI interaction block the process.
Any idea would be greatly appreciated.
I am sure you must have but just for the sake of confirmation, have you checked that the InstallFonts.vbs file is exported with the package? I mean is the "Copy To Output Directory" is set to "Copy Always/Copy if newer"?
This is pretty much possible that it is not able to locate your file.
You need to write a cmd file as a start up task. In your cmd file, you can call the vbs file using the command line tool cscript.
Azure start up can compile only command line tools.
Oh god, I finally solved the problem.
Although the compiler does quite a good job usually, it allows to use subfolder as a source of command, I mean something like "Subfolder\command.cmd", which will not work always. I've seen examples in which people put whatever we do in cmd in commandLine property, such as "copy fileA fileB" and it really works. But as for vbs, you need to be cautious. Until now I still don't know what's under the cover, but there should be some problem with the path. And the solution is definitely simple, instead of doing the subfolder work for tidiness, just leave the command file in the root folder like most people do:
<Startup>
<Task commandLine="InstallFonts.vbs" executionContext="elevated" taskType="simple" />
</Startup>
And thank you all the same, Kunal. :)

How do I install an ASP.NET MVC 3 application on IIS 6 using WIX?

Here's some considerations when installing onto IIS-6:
Needs to register ASP.NET 4 (likely using aspnet_regiis.exe)
Needs to allow for both ASP.NET v2 and v4
Needs to register aspnet_isapi.dll with support for wildcard mapping
And here's what I have so far:
<iis:WebDirProperties Id='WebDirProperties' Script='yes' Read='yes
Execute='no' WindowsAuthentication='yes' AnonymousAccess='no'
AuthenticationProviders='NTLM,Negotiate' />
<!-- SO has some good posts on selecting the website from a dropdown -->
<iis:WebSite Id='SelectedWebSite' Directory='WWWROOT' SiteId='[WEBSITE_ID]' Description='[WEBSITE_DESCRIPTION]'>
<iis:WebAddress Id='AllUnassigned' Port='80' IP='*'/>
</iis:WebSite>
<Component Id="ProjWebApp" Guid="{B4BE9223-7109-4943-AE4E-8F72FA350D02}"
Win64="$(var.IsWin64)" NeverOverwrite="yes" Transitive="yes">
<CreateFolder/>
<iis:WebAppPool Id="ProjAppPool" Name="[APPPOOLNAME]" Identity="networkService"
ManagedRuntimeVersion="v4.0" ManagedPipelineMode="integrated" />
<iis:WebVirtualDir Id="ProjVDir" DirProperties="WebDirProperties"
Alias="[WEBAPPNAME]" Directory="WEBFILESDIR" WebSite="SelectedWebSite">
<iis:WebApplication Id="ProjApp" Name="[WEBAPPNAME]" WebAppPool="ProjAppPool">
<iis:WebApplicationExtension
CheckPath="no"
Script="yes"
Executable="[ASPNETISAPIDLL]"
Verbs="GET,HEAD,POST"
/>
</iis:WebApplication>
</iis:WebVirtualDir>
</Component>
<!-- other apps may start using it once installed so it must be permanent -->
<Component Id="EnableASPNet4Extension" Permanent="yes" Guid="{C8CDAB96-5DDC-4B4C-AD7E-CD09B59F7813}">
<iis:WebServiceExtension Id="ASPNet4Extension" Group="ASP.NET v4.0.30319"
Allow="yes" File="[ASPNETISAPIDLL]" Description="ASP.NET v4.0.30319"
UIDeletable="no"
/>
</Component>
And I have a custom action to register ASP.NET with IIS:
<?if $(var.Platform) = x64 ?>
<CustomAction Id="SetProperty_AspNetRegIIS_InstallNet40Cmd"
Property="AspNetRegIIS_InstallNet40Cmd"
Value=""[NETFRAMEWORK40FULLINSTALLROOTDIR64]aspnet_regiis.exe" -ir"/>
<?else?>
<CustomAction Id="SetProperty_AspNetRegIIS_InstallNet40Cmd"
Property="AspNetRegIIS_InstallNet40Cmd"
Value=""[NETFRAMEWORK40FULLINSTALLROOTDIR]aspnet_regiis.exe" -ir"/>
<?endif?>
The Problem
This almost works. There are two problems at this point:
The IIS extension doesn't respect the managed runtime version on IIS-6, so the application doesn't have an ASP.NET version set.
If I use aspnet_regiis.exe -s APP_PATH to register it once it's created, it overwrites the wildcard mapping (and I don't know a commandline that I can run to restore it).
Given the above shortcomings, how can I use WIX to install an ASP.NET MVC 3 application onto IIS-6 with a proper wildcard mapping when it already has ASP.NET 2 installed?
It turns out this was a dumb error on my part. The above is sufficient to make ASP.NET v4 applications work when the pieces that I didn't include (custom actions and property definitions) are correct.
In my case, I had accidentally quoted the path to the aspnet_isapi.dll, so it wasn't actually being picked up correctly.
The IIS extension doesn't respect the managed runtime version on IIS-6, so the application doesn't have an ASP.NET version set.
That's partially true. Although it doesn't use the managed runtime version when setting the App Pool, IIS actually picks up the ASP.NET version as soon as something is correctly mapped to the aspnet_isapi.dll. As soon as I fixed the path, everything worked correctly.
If I use aspnet_regiis.exe -s APP_PATH to register it once it's created, it overwrites the wildcard mapping (and I don't know a commandline that I can run to restore it).
You can use adsutil.vbs in order to manage this if need be:
C:\Inetpub\AdminScripts>adsutil.vbs enum w3svc/998577302/root/AppName
KeyType : (STRING) "IIsWebVirtualDir"
...
ScriptMaps : (LIST) (1 Items)
"*,C:\WINDOWS\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll,1,GET,HEAD
,POST"
By using the set command in adsutil.vbs you can set the ScriptMaps property as needed.

Resources