Building a Barebones Task Runner
Several people have already build a bazillion frameworks or libraries in which you can define and run a set of tasks and their dependencies. PowerShell has psake and Invoke-Build. I was curious how these systems work, so I wrote a small PowerShell library which only showcases a trivial implementation.
Tasks and their dependencies form a directed acyclic graph (DAG). We only want to run each task once, even if several tasks depend on it. A naive task runner might operate like this:
- Identify all leaves (tasks at the end of the graph which have not been run)
- Run these tasks one after the other
- Mark these tasks as finished (which “removes” them from the graph)
Here’s a short build.ps1
script which defines a few tasks:
param(
[ValidateSet('Help', 'First', 'Second', 'Third')]
[string]$Target = 'Third'
)
$ErrorActionPreference = 'Stop'
Import-Module '.\task.psm1'
Register-Task -Name 'Help' {
Write-TaskOverview
}
$firstTask = Register-Task -Name 'First' -PassThru {
Write-Output 'First!'
}
$secondTask = Register-Task -Name 'Second' -DependsOn $firstTask -PassThru {
Write-Output 'Second!'
}
Register-Task -Name 'Third' -DependsOn $firstTask, $secondTask {
Write-Output 'Third!'
}
Get-Task $Target | Invoke-Task
Write-TaskReport
We can run any task by passing its name:
build.ps1 -Target Help
build.ps1 -Target Second
- …
The task.psm1
file looks like this:
$registeredTasks = @{}
$taskResults = [ordered]@{}
class TaskObject {
[string]$Name
[TaskObject[]]$DependsOn
[scriptblock]$Do
}
class TaskResult {
[string]$Name
[TimeSpan]$Duration
}
function Register-Task {
param(
[Parameter(Mandatory = $true)]
[string]$Name,
[scriptblock]$Do = { },
[TaskObject[]]$DependsOn = @(),
[switch]$PassThru
)
$task = [TaskObject]::new()
$task.Name = $Name
$task.DependsOn = $DependsOn
$task.Do = $Do
$registeredTasks[$Name] = $task
if ($PassThru) {
$task
}
}
function Get-Task {
param(
[Parameter(Mandatory = $true)]
[string]$Name
)
$registeredTasks[$Name]
}
function Invoke-SingleTask {
param(
[Parameter(Mandatory = $true)]
[TaskObject]$Task
)
Write-Output ''
Write-Output '================================================================================'
Write-Output "Task '$($Task.Name)'"
Write-Output '================================================================================'
$start = Get-Date
& $Task.Do
$end = Get-Date
$taskResult = [TaskResult]::new()
$taskResult.Name = $Task.Name
$taskResult.Duration = $end - $start
$taskResults[$runnableTask.Name] = $taskResult
}
function Invoke-Task {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[TaskObject]$Task
)
$taskResults.Clear()
Do {
$runnableTasks = Get-TaskLeave -Task $Task
foreach ($runnableTask in $runnableTasks) {
Invoke-SingleTask -Task $runnableTask
}
} While($runnableTasks.Length -ne 0)
}
function Write-TaskReport {
$totalDuration = [timespan]::Zero
$results = New-Object System.Collections.Generic.List[TaskResult]
foreach ($key in $taskResults.Keys) {
$taskResult = $taskResults[$key]
$totalDuration += $taskResult.Duration
$results.Add($taskResult)
}
$totalResult = [TaskResult]::new()
$totalResult.Name = 'Total'
$totalResult.Duration = $totalDuration
$results.Add($totalResult)
$results | Format-Table
}
function Write-TaskOverview {
$tasks = New-Object System.Collections.Generic.List[TaskObject]
foreach ($key in $registeredTasks.Keys) {
$tasks.Add($registeredTasks[$key])
}
$tasks | Format-Table -Property Name, DependsOn
}
function Get-TaskLeave {
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[TaskObject]$Task
)
$runnableTasks = New-Object System.Collections.Generic.List[TaskObject]
$hasRunnableDependencies = $false
foreach ($dependency in $Task.DependsOn) {
foreach ($leave in (Get-TaskLeave -Task $dependency)) {
if ((-not $taskResults.Contains($leave.Name)) -and (-not $runnableTasks.Contains($leave))) {
$hasRunnableDependencies = $true
$runnableTasks.Add($leave)
}
}
}
if ((-not $hasRunnableDependencies) -and (-not $taskResults.Contains($Task.Name)) -and (-not $runnableTasks.Contains($leave))) {
$runnableTasks.Add($Task)
}
$runnableTasks
}