Scripting and Embedded Scripting for AppV 5.0 (Dynamic Deployment and User Configuration Scripting)
written by Josh Davis, SDET 2
One of my early surprises at Microsoft was when I was working on a test machine doing some typical investigations. In Microsoft, it is fairly common to have a test machine provisioning solution that is custom tailored to your team’s needs and configures things in such a way that you automatically get good logging, all the diagnostic switches are flipped on, the post-mortem debugger is enabled and whatever other diagnostic knobs are otherwise enabled. So I was digging through the machine registry and noticed a registry path that seemed a little odd. The path in question was something like HKLM\SOFTWARE\Wow6432Node\Software\Microsoft\TestNode. Nothing out of the ordinary on the surface there, right? The problem was that I was on an x86 (32bit) machine. Without going into a rabbit hole of 64 bit vs. 32 bit OS Windows architecture, Windows added the Wow6432Node to handle the nuances between 32 and 64 bit registry code. In other words, there is no reason to have a Wow6432Node on a 32 bit OS. In my case, the TestNode was created by the provisioning structure and the script author decided it was easier to just blast potentially unnecessary registry data to the native node and the WOW node rather than doing the “correct” detection and only laying down what was necessary. Rather than trying to determine the architecture of the OS, the architecture of the process and crafting a way to walk around the potential registry redirection done by the OS, the author went for the “big hammer” approach.
What’s my point? Well, we all have to make really hard decisions in our jobs and determining which registry to write usually isn’t one of them. So even though “we’re Microsoft” and this process wouldn’t fly for a shipping product, when we’re talking about spinning up test environments, it’s more important that we spend the time on testing the product rather than creating the perfect test environment (which could be a whole blog in its own right).
In effect, the tools (editing the registry in a particular way) weren’t available to us in the fashion that we wanted and so we made a little bit of a mess in the process. You can’t blame us right? We made the impact small, but there was a cost tradeoff. Many scripts are written with that exact goal in mind. I need to execute some “one off” task but I want to keep the scope as small as possible. In the context of AppV 5.0, we’ve built a scripting feature that hopefully enables you do to just that.
Targeted Scripting
AppV 5.0 introduces a new management process for packages called Dynamic Configuration. Dynamic Configuration allows you to specify a policy for a package at either the machine level or at the user level. While there is much more in Dynamic Configuration than just scripts, we will only be talking about scripts in this post. So with the dynamic configuration documents, you can control whether you want to run a script at a machine level (global—typically used in an RDS scenario), or at a user level (1 user per machine or when you want different scripts run depending on the user) we provide you that flexibility. Also, we allow you to run scripts at various execution times of the package lifecycle. Below is a table that helps describe the various script events and when and how they can execute.
Script Execution Time |
Can be specified in Deployment Configuration |
Can be specified in User Configuration |
Can run in the Virtual Environment of the package |
Can be run in the context of a specific application |
Runs only once per package event* |
Runs in user context: (Deployment Config, User Config) |
AddPackage |
X |
X |
(SYSTEM, N/A) |
|||
PublishPackage |
X |
X |
(SYSTEM, User) |
|||
UnpublishPackage |
X |
X |
(SYSTEM, User) |
|||
RemovePackage |
X |
X |
(SYSTEM, N/A) |
|||
StartProcess |
X |
X |
X |
(N/A, User) |
||
ExitProcess |
X |
X |
(N/A, User) |
|||
StartVirtualEnvironment |
X |
X |
(N/A, User) |
|||
TerminateVirtualEnvironment |
X |
(N/A, User) |
*A script only runs once in certain contexts because it often doesn’t make sense to rerun it. E.g. if at AddPackage you add a script that installs a driver, but you modify the package and update the package path (via Set-AppVClientPackage, for example) you wouldn’t want to re-install the driver again so we don’t run the script. There is a potential issue (especially during script development) when you do want to rerun a script or update the deployment config (e.g. update the script argument list). In order to accomplish this, you will need to clean the package state so the event retriggers afresh (e.g. If you want to update the AddPackage script, you need to remove the package then add it again).
What this means is that you can write a script and it will be run at any of these events specified. For example, suppose you have a package that has a driver that needs to be extracted and deployed in order for the package to function correctly. Since AppV does not virtualize drivers, the only way to do this would be to install the driver natively. Rather than push this out to all users or try to write your script to call into AD to determine who needs it, etc., you can simply add this script to a Dynamic Configuration file (either Deployment or User) as a “PublishPackage” script (or “AddPackage” in the deployment configuration file) so that it’s available to the user by the time the package is. Here’s an example:
<PublishPackage>
<Path>powershell.exe </Path>
<Arguments>-file installDriver.ps1</Arguments>
</PublishPackage>
Enabling Scripting
Now that you’ve decided you want to actually use a script in your environment, you’ll first need to enable it for the machines you are targeting. This can be done by setting the client configuration setting “EnablePackageScripts”. This can be done at install time, through group policy or through powershell. Here is how you would do it in powershell:
PS > Set-AppvClientConfiguration -EnablePackageScripts $true
Microsoft believes in a secure system and frankly, if you don’t want any scripts executed in your environment, we want to enable you do that. And since we believe that a lot of scripting scenarios are reduced by our investments we’ve made in the 5.0 release, we opted for defaulting that scripts be disabled so that you the admin can make a conscious decision that scripting is still a necessary part of AppV in your environment.
User Context Execution
It should be fairly obvious (if one stops to think about it) that machine scripts will execute in a machine context (specifically “NT Authority\System” context) and that user scripts will execute in the user context. Obvious if one stops to think about it. However, all the implications of that may not be so obvious. Here are some examples of some “gotchas” that might show up in your scripts if you’re not careful and how you can resolve them. Pay attention to user context when considering your scripts.
1. Powershell scripts won’t execute
As a powershell script author, you’re no doubt familiar with the Get- and corresponding Set-ScriptExecutionPolicy cmdlets for enabling the execution of scripts. These settings are probably configured via a global group policy for the corporate domain and are “secure by default” settings that typically show up when you’re authoring scripts on test machines. In other words, the setting is probably something you “set and forget” and then remember only when you’re trying to spin up a new machine, write a new script, etc. Whatever the case, take the below script example. While you might have taken care to remove the security policy for your user scope by calling something like “Set-ScriptExecutionPolicy –ExecutionPolicy RemoteSigned”, you might not have noticed that the default “Scope” for this cmdlet is the current user scope. Meaning, it won’t affect the SYSTEM context at all. In the example below, you can see that we pass the “-ExecutionPolicy ByPass” value in the arguments to that script. You can also change the execution policy for a broader scope as well. Note, you should make sure your scripts adhere to your corporate security policies; this is just an example. As another aside, you can pass additional arguments to the script as in the below example (arg1, arg2).
<AddPackage>
<Path>powershell.exe </Path>
<Arguments>-ExecutionPolicy ByPass -File installDriver.ps1 arg1 arg2</Arguments>
</AddPackage >
2. Scripts are unable to access certain resources
You write a script that parses a file for some settings and then configures the user’s machine appropriately. This can go wrong because the user might not have access to a particular resource (SYSTEM32) or maybe the machine SYSTEM account doesn’t have access to your network user share. Pay attention to the kind of script event that you are executing and consider whether there are permissions issues that might arise in your production environment that wouldn’t in your test environment.
Writing a Script
Referencing script collateral
The first element you need to know about in the script object is the “Path” element. The path element represents the executable (yes, the binary you specify must be a .exe executable) that will be executed when the script is run. The path can be to a local or UNC path or can be a file embedded in the package (this is why sometimes call the feature “Embedded Scripting”). To access local system files or UNC files, you must use a full path. To access files inside of the package you can specify a relative path or a tokenized path. The working directory of the script is the root of the package (e.g. C:\ProgramData\App-V\A2D337CF-BAFD-4A51-A385-466B9E6053A7\B83CE87D-558A-4C20-BF23-B663BF19D922) so you can specify a script file with a relative path by doing something like the below:
<PublishPackage>
<Path>powershell.exe </Path>
<Arguments> .\Scripts\InstallDriver.ps1 </Arguments>
<Wait RollbackOnError="true" Timeout="120"/>
</PublishPackage>
Or, since the Script node and the Package root are siblings, you can use a relative path from the [{AppVPackageRoot}] token:
<PublishPackage>
<Path>powershell.exe </Path>
<Arguments >[{AppVPackageRoot}]\..\Scripts\InstallDriver.ps1 </Arguments>
<Wait RollbackOnError="true" Timeout="120"/>
</PublishPackage>
Introducing “blocking” logic
The “Wait” element can be used to introduce synchronization logic if the script is critical (i.e. is necessary in order for the package to work correctly). For example, in our driver scenario above, the application may not function correctly if the driver is not properly installed. “RollbackOnError” should be set to “true” in this case since the package is nonfunctional without it. Since you are introducing additional logic that may lock the publishing process, we allow a timeout in seconds to indicate how long the event should wait for the process to exit. In this case, we set it to two minutes for the installer to run. If it exits sooner, the event (a publish in this case) will complete. If it does not exit in time, we will fail.
You can imagine a scenario where the script is non-critical. E.g. you want to do some cleanup of some user data files if they are present, but it’s not critical to the package. In this case, you can set RollbackOnError to false or remove the <Wait> element altogether. Keep in mind, if you remove the <Wait> element, it will be executed asynchronously, meaning that we won’t wait at all for it to complete successfully or even to complete at all. So even if you don’t care whether it succeeds or not, you might want to keep the <Wait> element present anyway, then again you might not. An example of where you might want the <Wait> with <RollbackOnError>False</RollbackOnError> is where the event runs an installer on AddPackage, you don’t care whether it completes successfully, but you do care that it completes because subsequent steps or scripts will also attempt to do an install and if an install is in progress, they will fail. Specifying <Wait> allows you to be sure that the process exited.
The Timeout default is ‘0’ and does not need to be specified. A timeout of ‘0’ means “wait indefinitely” for the process to exit.
Runtime Flags
In addition to the blocking logic, there are flags that can be specified for Runtime Script events. Runtime script events are what I call the virtual application time scripting events like Process start or VirtualEnvironment shutdown. This can get a little tricky so let me spell of the flags out in detail.
RunInVirtualEnvironment – a Boolean (value can be ‘True’ or ‘False’) attribute which if specified, will dictate whether or not the script will run in the VE. If the attribute is not specified, by default the script will not run in the VE (the ‘VE’ represent the isolated package virtual environment including runtime (process and service objects) and state (filesystem and registry). Running a script in the VE will allow the script to run “in the bubble” of the virtual package. Keep in mind, a virtual environment is package specific (or package group if there is a connection group) and not app specific. A change to one app in the package will affect all apps in the package. Script events that can specify this attribute are ‘StartProcess’ and ‘StartVirtualEnvironment’
ApplicationId – Nonnullable. This is the token which represents the app associated with a process. In other words, when you click an app, say ‘Microsoft Word 2013’ it corresponds to a shortcut (on the desktop for example), this shortcut points to a file on disk called WinWord.exe and that object is represented with in the manifest and the default dynamic config files generated by the Sequencer as an ApplicationId. We use the application Id to track a number of things about the application and to make the application virtualization experience appear more native. The point for scripting though is that we use these tokens to uniquely represent an app so that when the end-user clicks a desktop shortcut, we know what app and what script to run. Only ‘StartProcess’ and ‘ExitProcess’ can specify this attribute and it is required for those scripts.
Embedding a script in the package
We previously referenced the “Scripts” directory under the package root. What is the significance of this directory? Well, firstly, it gives the sequencing engineer a common place to add scripts and script collateral to the package. This is so the package can be completely self-contained without referencing shares or local system resources (other than Powershell or something similar to run the script). Of course, if you want to put scripts on a share, that is fine, but if you don’t, AppV 5.0 enables this scenario for the first time. In the Sequencer “Package Files” tab, you will find the “Scripts” directory entry pre-created for you. By default it is empty, but if you want to embed a script file in the package, you can add a file here (see picture below)
Of course, this is not limited to scripts and executables but you can also add script collateral like .reg or .xml files, or any other collateral you might need. The special thing about collateral in this directory is that it is automatically streamed as part of the publishing of the package. I repeat, it is automatically streamed as part of the publishing of the package. So if you need the files to be there on user publish, they will be as part of the “Publishing Feature Block”. But if you are super concerned about keeping your publishings lightweight then you won’t want to do this because it could considerably slow down your publishing times depending on your users’ bandwidth and the size of the script payload.
How do I modify the Dynamic Configuration XML?
The easiest way to do this is to take the dynamic config files as generated by the Sequencer and modify them from there. User scripts can be specified in the ‘Dynamic User Configuration’ xml files (<PackageName>_UserConfig.xml, by default), while machine scripts can be specified in the ‘Dynamic Deployment Configuration’ xml files (<PackageName>_DeploymentConfig.xml, by default). As noted by the above table, some script events only make sense in a machine (‘deployment config’ essentially means ‘machine config’) like “AddPackage” since a user cannot add a package and—even if a user could add a package—there wouldn’t be a user to associate the ‘AddPackage’ event with since the package has not yet been published to any users yet.
So user scripts go with user configuration and machine scripts go with machine—er deployment—configuration.
Gotchas
There are some things that make sense when you are really thinking about it, but chances are (if you’re like me) you’re not always really thinking. Below are some bullet point examples:
1) Some scripts won’t rerun if you’re repeating the same event. Take for example a package publish. In this case, we have an event that could conceivably be done many times (for example on every login) but the script event is intended generally going to be for a user specific one-time configuration (running an installer in a user context for example) which cannot be done at time. The problem with this is that if we didn’t prevent the script from running again and again, then this installer would get run repeatedly and—if this installer wasn’t written to handle repeated installs—could cause some serious trouble. So we play it safe and think of add/publish/unpublish/remove scripts as one-time scripts. That is, one time per unique package ID and package version ID. If the package is upgrade (new Version ID) then we will re-run that script because for all we know it is a dramatically different package. The trick here is that if you publish a package and then decide you want to configure a runtime script _after_ the package has already been published then the script will not run. You will need to either upgrade the package or else unpublish and republish the package for the new script to take effect.
2) Powershell may require additional arguments in order to invoke the script. An example of this might be setting the execution policy explicitly for the script. Imagine that you have set the Powershell execution policy globally for your environment to be ‘Restricted’ which is the Powershell default. This would mean that no scripts can be run on the machine at all—clearly a problem if you want to run a powershell script to configure a package. Not to fear, Powershell.exe exposes overrides to combat this very problem. You can pass in “-ExecutionPolicy Bypass -File <path to script>” and override the execution policy assuming that you are running in an admin context. This would allow you to run scripts for a machine event (or a user event if the user is an admin) without changing the global execution policy for that machine or user. It will only persist across that single script session.
3) User scripts cannot be placed in the UserConfiguration section of the deployment policy. User scripts run in a user context and will only be invoked for user Publish and Unpublish as well as the corresponding runtime events (Start/Stop Process/VirtualEnvironment). If you do this, the package add will fail as this will invalidate the deployment config schema.
That’s it for this post. If I get enough demand, maybe I’ll post a bunch of different real-world examples of scripts in a future post.
Thanks for reading!