TaskRunner.psm1

<##################################################################################################
 
    Usage Example
    =============
 
    Import-Module <path to RunTask.psm1>
    RunTask -coreScriptFilePath "C:\Script\InstallProduct.ps1" -arguments "arg1 arg2"
     
 
    Help / Documentation
    ====================
    - To view a cmdlet's help description: Get-help "cmdlet-name" -Detailed
    - To view a cmdlet's usage example: Get-help "cmdlet-name" -Examples
 
 
    Pre-Requisites
    ==============
    - This module must run with Administrator privileges.
 
 
    Known Issues
    ============
    -
##################################################################################################>


$shortTaskLocation = "${env:Systemdrive}\_TaskScripts_"
$taskNameFile = "$shortTaskLocation\taskName.txt"

function RunTask {

    <#
        .SYNOPSIS
        Creates a new scheduled task and waits for the task to complete, while dumping live logs continuously
 
        .DESCRIPTION
        Creates a new scheduled task and waits for the task to complete, while dumping live logs continuously
 
        .EXAMPLE
        RunTask -coreScriptFilePath "C:\Script\InstallProduct.ps1" -arguments "arg1 arg2"
        RunTask -coreScriptFilePath "C:\Script\InstallProduct.ps1" -arguments "arg1 arg2" -waitForCompletion $true -cleanup $true
        RunTask -coreScriptFilePath "C:\Script\InstallProduct.ps1" -arguments "arg1 arg2" -waitForCompletion $true -timeoutInMins 30 -cleanup $true
 
        .INPUTS
        None.
    #>

    Param (

        [Parameter(Mandatory=$true)]
        [string] $coreScriptFilePath,

        [string] $arguments,

        [bool] $waitForCompletion = $true,

        [string] $timeoutInMins = 80,

        [bool] $cleanup = $false
    )

    PROCESS
    {

        $ErrorActionPreference = 'Stop'

        try
        {
            Write-Host "ScriptName : $coreScriptFilePath"
            Write-Host "Arguments : $arguments"
            Write-Host "Wait for Completion : $waitForCompletion"
            Write-Host "Timeout : $timeoutInMins"

            # Create a shorter task directory
            New-Item $shortTaskLocation -Type Directory -Force | Out-Null
            Write-Host "Directory to be used for task : $shortTaskLocation"

            # Get Signal script path
            $getSignalScriptFilePath = [System.IO.Path]::Combine($PSScriptRoot, "StartProcess_GetSignal.ps1")

            # Copy the required scripts to task directory
            Copy-Item $getSignalScriptFilePath $shortTaskLocation -Force
            Copy-Item $coreScriptFilePath $shortTaskLocation -Force
            Write-Host "Copied scripts to a shorter task directory : $shortTaskLocation"

            # New file paths
            $newCoreScriptFilePath = [System.IO.Path]::Combine($shortTaskLocation, [System.IO.Path]::GetFileName($coreScriptFilePath))
            $newGetSignalScriptFilePath = [System.IO.Path]::Combine($shortTaskLocation, [System.IO.Path]::GetFileName($getSignalScriptFilePath))

            # Task Name
            $newId = (Get-Random -minimum 1 -maximum 101).ToString()
            $taskName = [System.IO.Path]::GetFileNameWithoutExtension($coreScriptFilePath) + "." + $newId

            # Task signal file and output file
            $taskSignalFilePath = [System.IO.Path]::Combine($shortTaskLocation, $taskName + ".txt")
            $taskLogFilePath = [System.IO.Path]::Combine($shortTaskLocation, $taskName + "output.txt")

            # Task invoker script : Dump core script invocation code in a wrapper script, so as to avoid 261 char limit in schtasks.exe
            $taskInvokerFilePath = [System.IO.Path]::Combine($shortTaskLocation, "TaskInvokerScript.ps1")

            $fileContent = "& $newCoreScriptFilePath " + $arguments
            $fileContent | Out-File $taskInvokerFilePath
            Write-Host "Writing main script invocation code in a wrapper script : $newCoreScriptFilePath"
            Write-Host "Contents written : $fileContent"
    
            $command = 'powershell powershell -ExecutionPolicy Bypass -NoProfile -File ' + $newGetSignalScriptFilePath + ' ' + $taskInvokerFilePath + ' ' + $taskSignalFilePath + ' >' + $taskLogFilePath
            Write-Host "Scheduling task with command : $command"
            Invoke-Command -ErrorVariable err -OutVariable out -ScriptBlock { schtasks.exe /create /TN:$taskName /TR:$args /F /RL:HIGHEST /SC:MONTHLY /RU:SYSTEM; schtasks.exe /run /TN:$taskName ; Sleep 10 ; schtasks.exe /change /disable /TN:$taskName } -ArgumentList $command
            $exitCode = $LASTEXITCODE
            Write-Host "Schtasks output : $out"
            Write-Host "Schtasks error : $err"

            if ($exitCode -ne 0)
            {
                Write-Host "Non zero exit code received while authoring schedule tasks : $exitCode"
                return $exitCode
            }

            Get-ScheduledTask -TaskName $taskName | Out-Null
            Write-Host "Scheduled task exists"

            # Log the taskname so that next wait function may read it
            $taskName | Out-File $taskNameFile
            Write-Host "Logged the taskname : $taskName to file : $taskNameFile"
    
            if ($waitForCompletion -eq $true)
            {
                Write-Host "Need to wait for task to complete"
                $exitCode = WaitForTaskCompletion $taskName $timeoutInMins $cleanup
                Write-Host "Exit code returned by wait for task script : $exitCode"
            }

        }
        catch
        {
            if ($_ -ne $null) { Write-Host "Exception : $_.Message" }
            return -1
        }

        Write-Host "Returning from RunTask with exit code : $exitCode"
        return $exitCode
    }

}



function WaitForTaskCompletion {

    <#
        .SYNOPSIS
        Waits for a scheduled task to complete which was previously created by RunTask module, and keeps dumping live logs from the task
 
        .DESCRIPTION
        Waits for a scheduled task to complete which was previously created by RunTask module, and keeps dumping live logs from the task
 
        .EXAMPLE
        WaitForTaskCompletion -taskName $taskName timeoutInMins 60 -cleanup $true
 
        .INPUTS
        None.
    #>

    Param (

        [string] $taskName,

        [string] $timeoutInMins = 80,

        [bool] $cleanup = $false

    )

    PROCESS
    {
        try
        {
            $ErrorActionPreference = 'Stop'

            if ($taskName -eq $null -or $taskName -eq "") { Write-Host "Reading Task name from : $taskNameFile"; $taskName = Get-Content $taskNameFile }
            Write-Host "Task name : $taskName"

            $taskOutputFilePath = "$shortTaskLocation\$taskName" + "output.txt"
            $taskSignalFilePath = "$shortTaskLocation\$taskName" + ".txt"

            Write-Host "Task Output file : $taskOutputFilePath"
            Write-Host "Task Signal file : $taskSignalFilePath"

            $exitCode = WaitForSignalFile_Private -signalFilePath $taskSignalFilePath -taskOutputFile $taskOutputFilePath -timeOutInMins $timeoutInMins

            if ($cleanup -eq $true)
            {
                # Delete task directory
                Remove-Item $shortTaskLocation -Recurse -Force

                # Delete the task
                Invoke-Command -ScriptBlock { schtasks.exe /delete /TN:$taskName /F }
            }

            return $exitCode
        }
        catch
        {
            if ($_ -ne $null) { Write-Host "Exception : $_.Message" }
            return -1
        }        
    }
}


Function LogTail_Private()
{
    Param (
        [string] $FilePath,
        [string] $LogIndex
    )

        if ((Test-Path -Path $FilePath) -eq $false)
        {
            return
        }
        
        $Total = (Get-Content $FilePath).Length
        if ($LogIndex -ne $Total)
        {
            Get-Content $FilePath -Tail ($Total - $LogIndex) | Write-Host
            $LogIndex = $total
        }
        $LogIndex

}

Function WaitForSignalFile_Private() {
    Param (
        [string] $signalFilePath,
        [string] $taskOutputFilePath,
        [string] $timeOutInMins
    )

    # Now wait for the exit signal file
    $exitCode = -10
    $startTime = Get-Date
    $pollingTimeInSeconds = 3

    # Default code, means timeout
    $timeout = $false

    $logFileReadIndex = 0
    while ($timeout -eq $false)
    {
        if (Test-Path $signalFilePath)
        {
            $exitCode = Get-Content $signalFilePath -TotalCount 1
            $logFileReadIndex = LogTail_Private -FilePath $taskOutputFilePath -LogIndex $logFileReadIndex
            Write-Host "Task signal received ! task exited with code = $exitCode"
            break
        }

        # Read live logs from the task
        $logFileReadIndex = LogTail_Private -FilePath $taskOutputFilePath -LogIndex $logFileReadIndex

        Start-Sleep -s $pollingTimeInSeconds

        $now = Get-Date
        $timeout = (New-TimeSpan $startTime $now).TotalMinutes -gt $timeOutInMins
    }

    return $exitCode
}