Deploy Folding@home using PowerShell App Deployment Toolkit
The Folding@home client for Windows works well for per user installs on small numbers of machines, but it leaves something to be desired when used for mass deployments. After working recently to deploy the client at my university, I took the lessons learned in that effort and created a new deployment package using the PowerShell App Deployment Toolkit. If you are interested in running Folding@home in your environment, you can download the package from GitHub:
https://github.com/ladewig/psadt-foldingathome-client
The README.md should contain everything you need to get started, but if you’re interested in hearing more, keep reading.
The Toolkit
If you haven’t used the PowerShell App Deployment Toolkit (PSADT) before, it’s a framework you can use to build deployment packages for your applications. It was designed to work with Configuration Manager (ConfigMgr), but ConfigMgr isn’t a requirement. You can use it with other management tools or even on its own.
What’s great about the toolkit is it provides the plumbing needed to perform and manage application deployments, leaving you free to concentrate on what’s unique about installing, repairing, and uninstalling your application.
Assumptions
The package incorporates my assumptions about how the clients should run by default.
- The client should run silently in the background leaving the system available for someone to use.
- If the system has a GPU supported by the client, the client will run as a scheduled task which starts at boot.
- If the system does not have a supported GPU, the client will run as a service set to automatically start.
- Whether running as a service or as a scheduled task, it will run as LocalService for improved security. It has minimal privileges on the system and presents anonymous credentials on the network.
- The client data directory will be
%ProgramData%\FAHClient
, and LocalService will be granted Modify access to the folder and its contents.
Given those assumptions, the script does the following when installing the package.
- Create folder
%ProgramData%\FAHClient
- Grant NT AUTHORITY\LocalService modify access to
%ProgramData%\FAHClient
- Install the client with the silent option
fah-installer_7.6.13_x86.exe /S
- Set the registry REG_SZ value
DataDirectory
atHKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\FAHClient
to the expanded value of%ProgramData%\FAHClient
- Create folder
%ProgramData%\Microsoft\Start Menu\FAHClient
- Add firewall rule allowing access to program FAHClient.exe for Private and Domain profiles
- Create shortcuts for the FAH programs listed below under %ProgramData%\Microsoft\Start Menu\FAHClient with working directory set to %ProgramData%\FAHClient
- About Folding@home
- Data Directory
- FAHControl
- FAHViewer
- Web Control
- Start the client
How to Use
You can get the complete package from GitHub, and step-by-step instructions are in README.md. Try it and let me know what you think.
https://github.com/ladewig/psadt-foldingathome-client
Peek Under the Covers
I thought I’d mention how the script is handling some parts of the installation. Continuing is not needed to use the package, but I think it’s interesting. The PowerShell code shown will be primarily from Deploy-Application.ps1. I’ll note if a section is from another file.
Creating config.xml
The first installer I worked on for the university doesn’t do much with config.xml. It drops a customized config.xml file with our custom settings and a placeholder for the user value, then replaces the placeholder with the username that is generated.
For this package, I wanted to make it more flexible and easier to specify options, so the package creates the config.xml file using the client’s built-in command capability. User options are specified in a plain text file (defaults.txt) using the format property=value
with one property per line. The sample file in the package looks like this:
team=ReplaceWithYourTeamNumber
passkey=ReplaceWithYourPasskey
cause=COVID-19
password=ReplaceWithYourRemotePassword
allow=127.0.0.1,X.X.X.X/24
user=ReplaceWithYourUser
Additional properties can be specified as desired. We load the file into a hash table, then iterate over those values to create a string we can use to configure the client. We also check to see if ‘user’ has been set.
# Read custom settings for config.xml from text file into hash table
$FahDefaults = Get-Content -Raw -Path "$dirSupportFiles\$FahDefaultsFile" | ConvertFrom-StringData
# Build arguments for setting options from hash table
$FahOptions = $null
$IsUserSet = $false
$FahDefaults.GetEnumerator() | ForEach-Object {
$FahOptions += "$($_.Name)=$($_.Value) "
If ($_.Name -eq "User" -and $IsUserSet -eq $false) {
$IsUserSet = $true
}
}
If ‘user’ is not defined in options, we use a custom function to generate a username and add it to the string. The Get-FahUsername
function in the package looks for a department code in the machine name and compares it to a list of departments to generate a name. That’s how we handled it at the University, but if you want to do something else, you can rewrite that function to meet your needs.
# If username was not specified in defaults.txt, get username for system using custom function
If ($IsUserSet -eq $false) {
$FahUsername = Get-FahUsername
$FahOptions += "user=$FahUsername"
}
With the sample data shown above, the $FAHOptions
string looks like this:
team=ReplaceWithYourTeamNumber passkey=ReplaceWithYourPasskey cause=COVID-19 password=ReplacewithYourRemotePassword allow=127.0.0.1,x.x.x.x/24 user=ReplaceWithYourUser
We now have what we need to create the config.xml.
- Start the client in paused state. It needs to be available to process commands, but we don’t want it processing work units. Changing the directory ensures the config.xml file is created in the correct folder.
FAHClient.exe --chdir "C:\ProgramData\FAHClient" --pause-on-start=true
- When the client starts, it sees that there is no config.xml file in the data directory folder, and it configures the client to use a default configuration.
- Send command to the paused client to set the specified options.
FAHClient.exe --send-command options team=ReplaceWithYourTeamNumber passkey=ReplaceWithYourPasskey cause=COVID-19 password=ReplacewithYourRemotePassword allow=127.0.0.1,x.x.x.x/24 user=ReplaceWithYourUser
- Send command to the paused client to save the configuration (config.xml).
FAHClient.exe --send-command save
- Send command to the paused client to shut down.
FAHClient.exe --send-command shutdown
We now have a properly formatted config.xml file without ever having had to mess with XML using PowerShell (which is always more difficult than it should be).
Check for GPU
We need to know if a supported GPU is installed in the machine to decide whether to use a service or a scheduled task. Originally, I thought about rolling my own check against hardware IDs using the client’s GPUs.txt file (a combined blacklist/whitelist of GPUs), but I quickly realized this wasn’t needed.
When we create the config.xml file, the client starts with a default configuration. The default configuration contains a CPU slot, and if a supported GPU is present, a GPU slot. All we need to do is read the config.xml file and see if contains a GPU slot.
# Read config.xml and Check to see if GPU was detected by presence of a GPU slot
[xml]$FahConfig = Get-Content -Path "$FahDataDirectory\config.xml"
If ($FahConfig.config.slot.type -eq 'GPU') {
# A supported GPU was detected, run FAHClient using a scheduled task because a service doesn't have access to the GPU
...
} else {
# No supported GPU detected, run FAHClient as a service
...
}
No GPU? Configure a service
If a supported GPU isn’t found, we run the client as a service. The client sets itself up as a service:FAHClient.exe --install-service
The service is set to run as Local System by default. That will work, but the client doesn’t need that level of access. Instead we want to run as LocalService because it has limited rights on the system and only anonymous access on the network.
The first step is to give LocalService modify permission to the data directory folder.
$FahUserAccount = 'NT AUTHORITY\LocalService'
$FahUserPassword = '' # Password needs to be an empty string for Local Service when we modify the service later
$Rights = 'Modify'
$Inheritance = 'Containerinherit, ObjectInherit'
$Propagation = 'None'
$RuleType = 'Allow'
$Acl = Get-Acl $FahDataDirectory
$Perm = $FahUserAccount, $Rights, $Inheritance, $Propagation, $RuleType
$Rule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $Perm
$Acl.SetAccessRule($Rule)
$Acl | Set-Acl -Path $FahDataDirectory
Then we change the service account.
$Service = Get-CimInstance Win32_Service -Filter "Name='FAHClient'"
$null = Invoke-CimMethod -InputObject $Service -MethodName Change -Arguments @{StartName=$FahUserAccount;StartPassword=$FahUserPassword}
Found a GPU? Configure a scheduled task
If the system does have a supported GPU, we run the client as a scheduled task. This gets a bit more complicated, and we use a custom function to set that up.
First create the task action (what the task does), task trigger (when the task runs), and the task settings (how the task runs). We start with a trigger that starts the client at midnight every day.
# Create task aion
$TaskAction = New-ScheduledTaskAction -Execute $PathExe -WorkingDirectory $PathWorking
# Create task trigger to run daily at midnight
$TaskTrigger = New-ScheduledTaskTrigger -Daily -At 00:00
# Create default task settings set.
$TaskSettings = New-ScheduledTaskSettingsSet
Next, we create the task principal (the account the task runs as). We want to run the task as LocalService (same as we did for the service in the no-GPU scenario), but the package supports using different accounts. Because of that we need ot check to see if the specified account is a service account or not. For a service account we want to specify that the logontype is ‘ServiceAccount’.
If ($User -match '^(NT AUTHORITY\\(LocalService|NetworkService)|LocalSystem)') {
$TaskPrincipal = New-ScheduledTaskPrincipal -UserId $User -LogonType ServiceAccount
$IsServiceAccount= $true
} Else {
$TaskPrincipal = New-ScheduledTaskPrincipal -UserId $User -LogonType Password
$IsServiceAccount= $false
}
With all those set, we create the scheduled task.
# Create the task and register it
$Task = New-ScheduledTask -Action $TaskAction -Principal $TaskPrincipal -Trigger $TaskTrigger -Settings $TaskSettings
If ($IsServiceAccount) {
$null = Register-ScheduledTask -TaskName $Name -InputObject $Task
} else {
$null = Register-ScheduledTask -TaskName $Name -InputObject $Task -Password $Password
}
We have a scheduled task! But we need to make some modifications that couldn’t be set with the commands used. Here comes the XML part of this exercise.
Start by exporting the task to XML and get the namespace for the task. We’ll need this later.
# Export task to XML so that we can add a second trigger
[xml]$TaskXml = Export-ScheduledTask -TaskName $Name
# Get the namespace to use for creating elements
$TaskXmlNs = $TaskXml.Task.NamespaceURI
The task was set to start every day at midnight, but we don’t want the client to sit idle all day if something happens to it, so we want the task to run every hour. Don’t worry, if the task is still running, it won’t try to start another instance.
We build the trigger element by element, starting with the Repetition element, then adding the Interval and Duration elements. Interval and Duration are specified by setting the InnerText property as ISO 8601 duration values. The values we use are a P1D (one day) and PT1H (one hour). Note that we need to specify the namespace ($TaskXmlNs) when we create the elements; that’s why we retrieve that earlier.
# Add hourly recurrence for the daily trigger to ensure that client restarts if it stops due to error or other reason
# Create Repetition element
$RepetitionXml = $TaskXml.CreateElement("Repetition", $TaskXmlNs)
# Create Interval element, set value to one hour, and append to Repetition
$IntervalXml = $TaskXml.CreateElement("Interval", $TaskXmlNs)
$IntervalXml.InnerText = "PT1H"
$null = $RepetitionXml.AppendChild($IntervalXml)
# Create duration element, set value to one day, and append to Repetition
$DurationXml = $TaskXml.CreateElement("Duration", $TaskXmlNs)
$DurationXml.InnerText = "P1D"
$null = $RepetitionXml.AppendChild($DurationXml)
# Add Repetition element to CalendarTrigger
$null = $TaskXml.Task.Triggers.CalendarTrigger.AppendChild($RepetitionXml)
We also want to start the client at boot time because we don’t want it sitting idle waiting for the first hourly trigger. Create the BootTrigger element, append it to Triggers, and we’re done.
# We want the task to start on boot, so create a BootTrigger element$BootTriggerXml = $TaskXml.CreateElement("BootTrigger", $TaskXmlNs)
$null = $TaskXML.Task.Triggers.AppendChild($BootTriggerXml)
The last step is to replace the original task with our changes.
# Remove the existing task, then register a new task using the updated definition
$null = Unregister-ScheduledTask -TaskName $Name -Confirm:$false
$null = Register-ScheduledTask -TaskName $Name -Xml $TaskXml.OuterXml
And we’re done! It would be so much easier if we could simply specify all our triggers upfront, but this works well enough once you figure out all the little nuances of XML. This part of the package was certainly the most time-consuming part.
Get the Package and Start Folding
That’s all I have. If you’re still here reading this, thanks. Now get the package and start deploying Folding@home in your organization.
omg, i worked for 16 hours straight on a problem with windows 2016 server subinterfaces which your post on https://new.ladewig.com/say_the_magic_word_for_vlan_tagging/ FINALLY NAILED IT. There will be come clean up I have to do with powershell, but I set the ValueName VlanID ValueData from 1 to 0 with regedit, as your post stated. I am so thankful. The clean up I have to do is by using the powershell netlbfo modules and create a NIC teams, add the members, one for vlan 2 and one for vlan 172, and I guess the powershell cmdlet set-netadapteradvancedproperty (not sure after 16 hours, but it should be -Registry…something or other 0 (ZERO!!!) 🙂 ….as far as your previous post, computers at work should be downloading and saving instructional videos so you can watch them at 1.5 to 2.0 times speed when you get a chance (i use simplescreenrecorder). THANK YOU THANK YOU.
I’m glad that article helped!