diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..2dcfc9a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.insertSpaces": true +} \ No newline at end of file diff --git a/Extensions/Common/DeploymentSDK/Src/InvokeRemoteDeployment.ps1 b/Extensions/Common/DeploymentSDK/Src/InvokeRemoteDeployment.ps1 index c403135..07f9607 100644 --- a/Extensions/Common/DeploymentSDK/Src/InvokeRemoteDeployment.ps1 +++ b/Extensions/Common/DeploymentSDK/Src/InvokeRemoteDeployment.ps1 @@ -131,6 +131,7 @@ function Invoke-RemoteDeployment { Write-Host "Performing deployment in parallel on all the machines." [hashtable]$Jobs = @{} + $dtlsdkErrors = @() foreach($resource in $resourceList) { $winRmPort = [System.Convert]::ToInt32($resource.port) @@ -161,6 +162,7 @@ function Invoke-RemoteDeployment } Write-Verbose ($output|Format-List -Force|Out-String) Write-Host "Deployment failed on machine $machineName with following message : $errorMsg" + $dtlsdkErrors += $output.DeploymentSummary } else { @@ -173,7 +175,10 @@ function Invoke-RemoteDeployment if($operationStatus -ne "Passed") { - $errorMsg = 'Deployment on one or more machines failed.' + foreach ($error in $dtlsdkErrors) { + Write-Telemetry "DTLSDK_Error" $error + } + $errorMsg = [string]::Format("Deployment on one or more machines failed. {0}", $errorMsg) } } else @@ -190,12 +195,13 @@ function Invoke-RemoteDeployment if ($deploymentResponse.Status -ne "Passed") { $operationStatus = "Failed" + Write-Telemetry "DTLSDK_Error" $deploymentResponse.DeploymentSummary Write-Verbose ($deploymentResponse|Format-List -Force|Out-String) if($deploymentResponse.Error -ne $null) { $errorMsg = $deploymentResponse.Error.ToString() break - } + } } else { @@ -253,6 +259,7 @@ function Get-Credentials Write-Verbose "Creating credentials object for connecting to remote host" if([string]::IsNullOrWhiteSpace($userName) -or [string]::IsNullOrWhiteSpace($password)) { + Write-Telemetry "Input_Validation" "Invalid administrator credentials. UserName/Password null/empty" throw "Invalid administrator credentials." } @@ -314,17 +321,20 @@ function Get-MachineNameAndPort if($tokens.Count -gt 2) { + Write-Telemetry "Input_Validation" "Invalid user input, speficy machines in machine:port format." throw "Invalid user input, speficy machines in machine:port format." } [System.Int32]$port = $null if($tokens.Count -eq 2 -and ![System.Int32]::TryParse($tokens[1], [ref]$port)) { + Write-Telemetry "Input_Validation" "Invalid user input, port is not an integer." throw "Invalid user input, port is not an integer." } if([string]::IsNullOrWhiteSpace($tokens[0])) { + Write-Telemetry "Input_Validation" "Invalid user input, machine name can not be empty." throw "Invalid user input, machine name can not be empty." } diff --git a/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Definition.dll b/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Definition.dll index 49f7168..208db21 100644 Binary files a/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Definition.dll and b/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Definition.dll differ diff --git a/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Deployment.dll b/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Deployment.dll index 80eadfd..d01cc12 100644 Binary files a/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Deployment.dll and b/Extensions/Common/DeploymentSDK/Src/Microsoft.VisualStudio.Services.DevTestLabs.Deployment.dll differ diff --git a/Extensions/Common/DeploymentSDK/Src/Newtonsoft.Json.dll b/Extensions/Common/DeploymentSDK/Src/Newtonsoft.Json.dll new file mode 100644 index 0000000..b7ef414 Binary files /dev/null and b/Extensions/Common/DeploymentSDK/Src/Newtonsoft.Json.dll differ diff --git a/Extensions/Common/DeploymentSDK/Src/Utility.ps1 b/Extensions/Common/DeploymentSDK/Src/Utility.ps1 new file mode 100644 index 0000000..fefb245 --- /dev/null +++ b/Extensions/Common/DeploymentSDK/Src/Utility.ps1 @@ -0,0 +1,54 @@ +function convertTo-JsonFormat($InputObject) { + if (Get-Command ConvertTo-Json -ErrorAction SilentlyContinue) + { + $jsonOutput = ConvertTo-Json -InputObject $InputObject + } + else + { + try + { + add-type -assembly system.web.extensions + $scriptSerializer = new-object system.web.script.serialization.javascriptSerializer + $jsonOutput = $scriptSerializer.Serialize($InputObject) + } + catch + { + Write-Verbose $_.Exception + $errorMessage = "Unable to convert json string to object. Please install WMF 4.0 and try again." + if($_.Exception.Message) + { + $errorMessage = [string]::Format("{0} {1} {2}", $errorMessage, [Environment]::NewLine, $_.Exception.Message) + } + throw $errorMessage + } + } + return $jsonOutput +} + +function convertFrom-JsonFormat($InputObject) { + if (Get-Command ConvertTo-Json -ErrorAction SilentlyContinue) + { + $convertedObject = ConvertFrom-Json -InputObject $InputObject + } + else + { + try + { + add-type -assembly system.web.extensions + $scriptSerializer = new-object system.web.script.serialization.javascriptSerializer + $convertedObject = ,$scriptSerializer.DeserializeObject($InputObject) + } + catch + { + Write-Verbose $_.Exception + $errorMessage = "Unable to convert json string to object. Please install WMF 4.0 and try again." + if($_.Exception.Message) + { + $errorMessage = [string]::Format("{0} {1} {2}", $errorMessage, [Environment]::NewLine, $_.Exception.Message) + } + throw $errorMessage + } + + } + return $convertedObject +} diff --git a/Extensions/Common/DeploymentSDK/Src/VisualStudioRemoteDeployer.exe b/Extensions/Common/DeploymentSDK/Src/VisualStudioRemoteDeployer.exe index 07a6441..4a46e66 100644 Binary files a/Extensions/Common/DeploymentSDK/Src/VisualStudioRemoteDeployer.exe and b/Extensions/Common/DeploymentSDK/Src/VisualStudioRemoteDeployer.exe differ diff --git a/Extensions/Common/DeploymentSDK/Tests/InvokeRemoteDeployment.Tests.ps1 b/Extensions/Common/DeploymentSDK/Tests/InvokeRemoteDeployment.Tests.ps1 index 0e1e2ff..c90d4eb 100644 --- a/Extensions/Common/DeploymentSDK/Tests/InvokeRemoteDeployment.Tests.ps1 +++ b/Extensions/Common/DeploymentSDK/Tests/InvokeRemoteDeployment.Tests.ps1 @@ -13,6 +13,9 @@ if(-not (Test-Path -Path $invokeRemoteDeployment )) . "$invokeRemoteDeployment" +## Mocking the telemetry method +function Write-Telemetry {} + Describe "Tests for testing InitializationScript block" { Context "Invoke-PsOnRemote successfully returns" { . $InitializationScript @@ -134,7 +137,7 @@ Describe "Tests for testing Invoke-RemoteDeployment functionality" { It "Should process jobs in parallel and wait for their completion"{ Assert-VerifiableMocks - ($errMsg) | Should Be "Deployment on one or more machines failed." + ($errMsg) | Should Be "Deployment on one or more machines failed. " Assert-MockCalled Write-Host -Times 8 -Exactly } } diff --git a/Extensions/Common/DeploymentSDK/Tests/Utility.Tests.ps1 b/Extensions/Common/DeploymentSDK/Tests/Utility.Tests.ps1 new file mode 100644 index 0000000..4c8953e --- /dev/null +++ b/Extensions/Common/DeploymentSDK/Tests/Utility.Tests.ps1 @@ -0,0 +1,75 @@ +$currentScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path +$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".") +$VerbosePreference = 'Continue' + +$UtilityScript = "$currentScriptPath\..\Src\$sut" + +if(-not (Test-Path -Path $UtilityScript)) +{ + throw [System.IO.FileNotFoundException] "Unable to find Utility.ps1 at $UtilityScript" +} + +. "$UtilityScript" + + +Describe "Tests for ConvertTo-JsonFormat method" { + Context "Convert object to json string on PS where ConvertTo-Json cmdlet exists" { + $argumentAsObject = @{Name="test";Type="task"} + + Mock Get-Command -Verifiable { return $true} + + $jsonOutput = ConvertTo-JsonFormat -InputObject $argumentAsObject + + It "Should convert object to json string" { + $jsonOutput | Should Not be $null + $jsonOutput.GetType().Name | Should Be "String" + $jsonOutput.Contains("Name") | Should Be $true + $jsonOutput.Contains("Type") | Should Be $true + } + } + + Context "Convert object to json string on PS where ConvertTo-Json cmdlet doesnot exists" { + $argumentAsObject = @{Name="test";Type="task"} + + Mock Get-Command -Verifiable { return $false} + + $jsonOutput = ConvertTo-JsonFormat -InputObject $argumentAsObject + + It "Should convert object to json string" { + $jsonOutput | Should Not be $null + $jsonOutput.GetType().Name | Should Be "String" + $jsonOutput.Contains("Name") | Should Be $true + $jsonOutput.Contains("Type") | Should Be $true + } + } + + Context "Convert json string to object on PS where ConvertTo-Json cmdlet exists" { + $jsonString = "{`"Name`":`"test`",`"Type`":`"task`"}" + + Mock Get-Command -Verifiable { return $true} + + $argObject = ConvertFrom-JsonFormat -InputObject $jsonString + + It "Should convert json string into object" { + $argObject | Should Not be $null + $argObject.GetType().Name | Should Be "PSCustomObject" + $argObject.Name | Should Be "test" + $argObject.Type | Should Be "task" + } + } + + Context "Convert json string to object on PS where ConvertTo-Json cmdlet doesnot exists" { + $jsonString = "{`"Name`":`"test`",`"Type`":`"task`"}" + + Mock Get-Command -Verifiable { return $false} + + $argObject = ConvertFrom-JsonFormat -InputObject $jsonString + + It "Should convert json string into object" { + $argObject | Should Not be $null + $argObject.Name | Should Be "test" + $argObject.Type | Should Be "task" + } + } +} + diff --git a/Extensions/Common/Helper/Src/Utility.ps1 b/Extensions/Common/Helper/Src/Utility.ps1 index aab2a14..e2e766a 100644 --- a/Extensions/Common/Helper/Src/Utility.ps1 +++ b/Extensions/Common/Helper/Src/Utility.ps1 @@ -1,87 +1,88 @@ -Import-Module $env:CURRENT_TASK_ROOTDIR\DeploymentSDK\InvokeRemoteDeployment.ps1 - -Write-Verbose "Entering script Utility.ps1" - -function Validate-WaitTime() -{ - [CmdletBinding()] - Param - ( - [Parameter(mandatory=$true)] - [string[]]$runLockTimeoutString - ) - - $parsedRunLockTimeout = $null - if([int32]::TryParse($runLockTimeoutString , [ref]$parsedRunLockTimeout)) - { - if($parsedRunLockTimeout -gt 0) - { - return - } - } - - throw "Please provide a valid timeout input in seconds. It should be an integer greater than 0" -} - -function Remote-ServiceStartStop() -{ - [CmdletBinding()] - Param - ( - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $serviceNames, - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $machinesList, - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminUserName, - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminPassword, - [string][Parameter(Mandatory=$true)][ValidateSet("Disabled", "Manual", "Automatic")] $startupType, - [string][Parameter(Mandatory=$true)] $protocol, - [string][Parameter(Mandatory=$true)] $testCertificate, - [string][Parameter(Mandatory=$true)] $waitTimeoutInSeconds, - [string][Parameter(Mandatory=$true)] $internStringFileName, - [string][Parameter(Mandatory=$true)] $killIfTimedOut, - [bool]$runPowershellInParallel - ) - - Validate-WaitTime $waitTimeoutInSeconds - - $scriptArguments = "-serviceNames $serviceNames -startupType $startupType -waitTimeoutInSeconds $waitTimeoutInSeconds -killIfTimedOut $killIfTimedOut" - - Write-Host "ScriptArguments: $scriptArguments" - - Remote-RunScript -machinesList $machinesList -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate -internStringFileName $internStringFileName -scriptEntryPoint "StartStopServices" -scriptArguments $scriptArguments -runPowershellInParallel $runPowershellInParallel -} - -function Remote-RunScript() -{ - [CmdletBinding()] - Param - ( - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $machinesList, - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminUserName, - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminPassword, - [string][Parameter(Mandatory=$true)] $protocol, - [string][Parameter(Mandatory=$true)] $testCertificate, - [string][Parameter(Mandatory=$true)] $internStringFileName, - [string][Parameter(Mandatory=$true)] $scriptEntryPoint, - [string][Parameter(Mandatory=$true)] $scriptArguments, - [bool]$runPowershellInParallel - ) - - $internScriptPath = "$env:CURRENT_TASK_ROOTDIR\$internStringFileName" - - $scriptToRun = Get-Content $internScriptPath | Out-String - - $scriptToRun = [string]::Format("{0} {1} {2} {3} ", $scriptToRun, [Environment]::NewLine, $scriptEntryPoint, $scriptArguments) - - Write-Output "Invoking deployment" - - Write-Output "Script Body: $scriptToRun" - - $errorMessage = Invoke-RemoteDeployment -machinesList $machinesList -scriptToRun $scriptToRun -deployInParallel $runPowershellInParallel -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate - - if(-Not [string]::IsNullOrEmpty($errorMessage)) - { - $helpMessage = "Error returned from remote deployment." - Write-Error "$errorMessage`n$helpMessage" - return - } +Import-Module $env:CURRENT_TASK_ROOTDIR\DeploymentSDK\InvokeRemoteDeployment.ps1 + +Write-Verbose "Entering script Utility.ps1" + +function Validate-WaitTime() +{ + [CmdletBinding()] + Param + ( + [Parameter(mandatory=$true)] + [string[]]$runLockTimeoutString + ) + + $parsedRunLockTimeout = $null + if([int32]::TryParse($runLockTimeoutString , [ref]$parsedRunLockTimeout)) + { + if($parsedRunLockTimeout -gt 0) + { + return + } + } + + throw "Please provide a valid timeout input in seconds. It should be an integer greater than 0" +} + +function Remote-ServiceStartStop() +{ + [CmdletBinding()] + Param + ( + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $serviceNames, + [string][Parameter(Mandatory=$true)][AllowEmptyString()] $instanceName, + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $machinesList, + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminUserName, + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminPassword, + [string][Parameter(Mandatory=$true)][ValidateSet("Disabled", "Manual", "Automatic")] $startupType, + [string][Parameter(Mandatory=$true)] $protocol, + [string][Parameter(Mandatory=$true)] $testCertificate, + [string][Parameter(Mandatory=$true)] $waitTimeoutInSeconds, + [string][Parameter(Mandatory=$true)] $internStringFileName, + [string][Parameter(Mandatory=$true)] $killIfTimedOut, + [bool]$runPowershellInParallel + ) + + Validate-WaitTime $waitTimeoutInSeconds + + $scriptArguments = "-serviceNames $serviceNames -instanceName `"$instanceName`" -startupType $startupType -waitTimeoutInSeconds $waitTimeoutInSeconds -killIfTimedOut $killIfTimedOut" + + Write-Host "ScriptArguments: $scriptArguments" + + Remote-RunScript -machinesList $machinesList -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate -internStringFileName $internStringFileName -scriptEntryPoint "StartStopServices" -scriptArguments $scriptArguments -runPowershellInParallel $runPowershellInParallel +} + +function Remote-RunScript() +{ + [CmdletBinding()] + Param + ( + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $machinesList, + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminUserName, + [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $adminPassword, + [string][Parameter(Mandatory=$true)] $protocol, + [string][Parameter(Mandatory=$true)] $testCertificate, + [string][Parameter(Mandatory=$true)] $internStringFileName, + [string][Parameter(Mandatory=$true)] $scriptEntryPoint, + [string][Parameter(Mandatory=$true)] $scriptArguments, + [bool]$runPowershellInParallel + ) + + $internScriptPath = "$env:CURRENT_TASK_ROOTDIR\$internStringFileName" + + $scriptToRun = Get-Content $internScriptPath | Out-String + + $scriptToRun = [string]::Format("{0} {1} {2} {3} ", $scriptToRun, [Environment]::NewLine, $scriptEntryPoint, $scriptArguments) + + Write-Output "Invoking deployment" + + Write-Output "Script Body: $scriptToRun" + + $errorMessage = Invoke-RemoteDeployment -machinesList $machinesList -scriptToRun $scriptToRun -deployInParallel $runPowershellInParallel -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate + + if(-Not [string]::IsNullOrEmpty($errorMessage)) + { + $helpMessage = "Error returned from remote deployment." + Write-Error "$errorMessage`n$helpMessage" + return + } } \ No newline at end of file diff --git a/Extensions/Common/TelemetryHelper/Src/TelemetryHelper.ps1 b/Extensions/Common/TelemetryHelper/Src/TelemetryHelper.ps1 new file mode 100644 index 0000000..3520bf6 --- /dev/null +++ b/Extensions/Common/TelemetryHelper/Src/TelemetryHelper.ps1 @@ -0,0 +1,34 @@ +# Telemetry Codes +$telemetryCodes = +@{ + "Input_Validation" = "Input_Validation_Error"; + "Task_InternalError" = "Task_Internal_Error"; + "DTLSDK_Error" = "Dtl_Sdk_Error"; + } + + # Telemetry Write Method +function Write-Telemetry +{ + [CmdletBinding()] + param( + [Parameter(Mandatory=$True,Position=1)] + [string]$codeKey, + + [Parameter(Position=2)] + [string]$errorMsg + ) + + $erroCodeMsg = $telemetryCodes[$codeKey] + + ## If no error is passed mark it as not available + if([string]::IsNullOrEmpty($errorMsg)) + { + $errorMsg = "No error details available" + } + $erroCode = ('"{0}":{1}' -f $erroCodeMsg, $errorMsg) + ## Form errorcode as json string + $erroCode = '{' + $erroCode + '}' + + $telemetryString = "##vso[task.logissue type=error;code=" + $erroCode + ";]" + Write-Host $telemetryString +} \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRight.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRight.ps1 index 70f36a0..10cb70c 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRight.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRight.ps1 @@ -1,31 +1,40 @@ -[CmdletBinding()] -Param() - -Trace-VstsEnteringInvocation $MyInvocation - -Try -{ - [string]$userNames = Get-VstsInput -Name userNames -Require - [string]$environmentName = Get-VstsInput -Name environmentName -Require - [string]$adminUserName = Get-VstsInput -Name adminUserName -Require - [string]$adminPassword = Get-VstsInput -Name adminPassword -Require - [string]$protocol = Get-VstsInput -Name protocol -Require - [string]$testCertificate = Get-VstsInput -Name testCertificate -Require - [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool - - Write-Output "Granting LogonAsAService to $userNames. Version: {{tokens.BuildNumber}}" - - $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path - - . $env:CURRENT_TASK_ROOTDIR\Utility.ps1 - - $userNames = $userNames -replace '\s','' # no spaces allows in argument lists - - $scriptArguments = "-userNames $userNames" - - Remote-RunScript -machinesList $environmentName -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate -internStringFileName "GrantLogonAsAServiceRightIntern.ps1" -scriptEntryPoint "GrantLogonAsService" -scriptArguments $scriptArguments -runPowershellInParallel $runPowershellInParallel -} -finally -{ - Trace-VstsLeavingInvocation $MyInvocation +[CmdletBinding()] +Param() + +Trace-VstsEnteringInvocation $MyInvocation + +Try { + [string]$userNames = Get-VstsInput -Name userNames -Require + [bool]$targetIsDeploymentGroup = Get-VstsInput -Name deploymentGroup -Require -AsBool + + $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path + + if ($targetIsDeploymentGroup) + { + . $env:CURRENT_TASK_ROOTDIR\GrantLogonAsAServiceRightIntern.ps1 + + $userNamesArray = [string[]]($userNames.Split(@(",", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries).Trim()) + + GrantLogonAsServiceArray $userNamesArray; + } + else + { + . $env:CURRENT_TASK_ROOTDIR\TelemetryHelper\TelemetryHelper.ps1 + . $env:CURRENT_TASK_ROOTDIR\Utility.ps1 + + $scriptArguments = "-userNames " + '"' + $userNames + '"' + + [string]$environmentName = Get-VstsInput -Name environmentName -Require + [string]$adminUserName = Get-VstsInput -Name adminUserName -Require + [string]$adminPassword = Get-VstsInput -Name adminPassword -Require + [string]$protocol = Get-VstsInput -Name protocol -Require + [string]$testCertificate = Get-VstsInput -Name testCertificate -Require + [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool + + Remote-RunScript -machinesList $environmentName -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate -internStringFileName "GrantLogonAsAServiceRightIntern.ps1" -scriptEntryPoint "GrantLogonAsService" -scriptArguments $scriptArguments -runPowershellInParallel $runPowershellInParallel + } + +} +finally { + Trace-VstsLeavingInvocation $MyInvocation } \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRightIntern.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRightIntern.ps1 index 032051b..66423ce 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRightIntern.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/GrantLogonAsAServiceRightIntern.ps1 @@ -1,45 +1,45 @@ -function GrantLogonAsService( - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $userNames -) -{ - function New-TemporaryDirectory { - $parent = [System.IO.Path]::GetTempPath() - [string] $name = [System.Guid]::NewGuid() - New-Item -ItemType Directory -Path (Join-Path $parent $name) - } - - [string[]] $userNamesArray = ($userNames -split ',').Trim() - - foreach ($userName in $userNamesArray) - { - $tempDir = New-TemporaryDirectory - #Get list of currently used SIDs - secedit /export /cfg $tempDir\tempexport.inf - $curSIDs = Select-String $tempDir\tempexport.inf -Pattern "SeServiceLogonRight" - $Sids = $curSIDs.line - $sidstring = "" - - $objUser = New-Object System.Security.Principal.NTAccount($userName) - $strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier]) - if(!$Sids.Contains($strSID) -and !$sids.Contains($userName)) - { - $sidstring += ",*$strSID" - } - if($sidstring) - { - $newSids = $sids + $sidstring - Write-Verbose "New Sids: $newSids" - $tempinf = Get-Content $tempDir\tempexport.inf - $tempinf = $tempinf.Replace($Sids,$newSids) - Add-Content -Path $tempDir\tempimport.inf -Value $tempinf - secedit /import /db $tempDir\secedit.sdb /cfg "$tempDir\tempimport.inf" - secedit /configure /db $tempDir\secedit.sdb - } - else - { - Write-Verbose "No new sids" - } - - del "$tempDir" -Recurse -force -ErrorAction SilentlyContinue - } -} +function GrantLogonAsServiceArray( + [string[]][Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()] $userNames +) { + function New-TemporaryDirectory { + $parent = [System.IO.Path]::GetTempPath() + [string] $name = [System.Guid]::NewGuid() + New-Item -ItemType Directory -Path (Join-Path $parent $name) + } + foreach ($userName in $userNames) { + $tempDir = New-TemporaryDirectory + #Get list of currently used SIDs + secedit /export /cfg $tempDir\tempexport.inf + $curSIDs = Select-String $tempDir\tempexport.inf -Pattern "SeServiceLogonRight" + $Sids = $curSIDs.line + $sidstring = "" + + $objUser = New-Object System.Security.Principal.NTAccount($userName) + $strSID = $objUser.Translate([System.Security.Principal.SecurityIdentifier]) + if (!$Sids.Contains($strSID) -and !$sids.Contains($userName)) { + $sidstring += ",*$strSID" + } + if ($sidstring) { + $newSids = $sids + $sidstring + Write-Verbose "New Sids: $newSids" + $tempinf = Get-Content $tempDir\tempexport.inf + $tempinf = $tempinf.Replace($Sids, $newSids) + Add-Content -Path $tempDir\tempimport.inf -Value $tempinf + secedit /import /db $tempDir\secedit.sdb /cfg "$tempDir\tempimport.inf" + secedit /configure /db $tempDir\secedit.sdb + } + else { + Write-Verbose "No new sids" + } + + del "$tempDir" -Recurse -force -ErrorAction SilentlyContinue + } +} + +function GrantLogonAsService( + [string[]][Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()] $userNames +) { + [string[]] $userNamesArray = [string[]]($userNames.Split(@(",", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries).Trim()) + + return GrantLogonAsServiceArray $userNamesArray +} \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/task.json b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/task.json index 4a182f0..2be108e 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/task.json +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/GrantLogonAsAServiceRight/task.json @@ -1,90 +1,102 @@ -{ - "id": "762A1A9C-A8B0-4EC2-993F-42DBBB09FFBD", - "name": "GrantLogonAsAServiceRight", - "friendlyName": "Grant Logon As A Service Right", - "description": "Grant logon as a service.", - "helpMarkDown": "Version: {{tokens.BuildNumber}} [More Information](https://github.com/jabbera/my-vsts-tasks)", - "category": "Utility", - "visibility": [ - "Build", - "Release" - ], - "author": "Michael Barry", - "version": { - "Major": "{{tokens.Major}}", - "Minor": "{{tokens.Minor}}", - "Patch": "{{tokens.Patch}}" - }, - "minimumAgentVersion": "1.95.0", - "inputs": [ - { - "name": "UserNames", - "type": "multiLine", - "label": "User Names", - "defaultValue": "", - "required": true, - "helpMarkDown": "The usernames to grant logon as a service with the domain appended." - }, - { - "name": "EnvironmentName", - "type": "multiLine", - "label": "Machines", - "defaultValue": "", - "required": true, - "helpMarkDown": "Provide a comma separated list of machine IP addresses or FQDNs.
Eg: dbserver.fabrikam.com,192.168.12.34
Or provide output variable of other tasks. Eg: $(variableName)" - }, - { - "name": "AdminUserName", - "type": "string", - "label": "Admin Login", - "defaultValue": "", - "required": true, - "helpMarkDown": "Administrator login for the target machines." - }, - { - "name": "AdminPassword", - "type": "string", - "label": "Password", - "defaultValue": "", - "required": true, - "helpMarkDown": "Password for administrator login for the target machines.
It can accept variable defined in Build/Release definitions as '$(passwordVariable)'.
You may mark variable type as 'secret' to secure it. " - }, - { - "name": "protocol", - "type": "radio", - "label": "Protocol", - "required": true, - "defaultValue": "Http", - "options": { - "Http": "HTTP", - "Https": "HTTPS" - }, - "helpMarkDown": "Select the network protocol to use for the WinRM connection with the machine(s). The default is HTTPS." - }, - { - "name": "TestCertificate", - "type": "boolean", - "label": "Test Certificate", - "defaultValue": "true", - "visibleRule": "protocol = Https", - "required": false, - "helpMarkDown": "Select the option to skip validating the authenticity of the machine's certificate from a trusted certification authority. The parameter is required for the WinRM HTTPS protocol." - }, - { - "name": "RunPowershellInParallel", - "type": "boolean", - "label": "Run PowerShell in Parallel", - "defaultValue": "true", - "required": false, - "helpMarkDown": "Setting it to true will run the PowerShell scripts in parallel on the target machines." - } - ], - "instanceNameFormat": "Grant Logon As A Service: $(UserNames)", - "execution": { - "PowerShell3": { - "target": "$(currentDirectory)\\GrantLogonAsAServiceRight.ps1", - "argumentFormat": "", - "workingDirectory": "$(currentDirectory)" - } - } +{ + "id": "762A1A9C-A8B0-4EC2-993F-42DBBB09FFBD", + "name": "GrantLogonAsAServiceRight", + "friendlyName": "Grant Logon As A Service Right", + "description": "Grant logon as a service.", + "helpMarkDown": "Version: {{tokens.BuildNumber}} [More Information](https://github.com/jabbera/my-vsts-tasks)", + "category": "Utility", + "visibility": [ + "Build", + "Release" + ], + "author": "Michael Barry", + "version": { + "Major": "{{tokens.Major}}", + "Minor": "{{tokens.Minor}}", + "Patch": "{{tokens.Patch}}" + }, + "minimumAgentVersion": "1.95.0", + "inputs": [{ + "name": "UserNames", + "type": "multiLine", + "label": "User Names", + "defaultValue": "", + "required": true, + "helpMarkDown": "The usernames to grant logon as a service with the domain appended." + }, + { + "name": "deploymentGroup", + "type": "boolean", + "label": "Target is Deployment Group", + "required": true, + "defaultValue": "false", + "helpMarkDown": "Agents run directly on the server in deployment group. No WinRM Necessary." + }, + { + "name": "EnvironmentName", + "type": "multiLine", + "label": "Machines", + "defaultValue": "", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Provide a comma separated list of machine IP addresses or FQDNs.
Eg: dbserver.fabrikam.com,192.168.12.34
Or provide output variable of other tasks. Eg: $(variableName)" + }, + { + "name": "AdminUserName", + "type": "string", + "label": "Admin Login", + "defaultValue": "", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Administrator login for the target machines." + }, + { + "name": "AdminPassword", + "type": "string", + "label": "Password", + "defaultValue": "", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Password for administrator login for the target machines.
It can accept variable defined in Build/Release definitions as '$(passwordVariable)'.
You may mark variable type as 'secret' to secure it. " + }, + { + "name": "protocol", + "type": "radio", + "label": "Protocol", + "required": false, + "visibleRule": "deploymentGroup = false", + "defaultValue": "Http", + "options": { + "Http": "HTTP", + "Https": "HTTPS" + }, + "helpMarkDown": "Select the network protocol to use for the WinRM connection with the machine(s). The default is HTTPS." + }, + { + "name": "TestCertificate", + "type": "boolean", + "label": "Test Certificate", + "defaultValue": "true", + "visibleRule": "protocol = Https && deploymentGroup = false", + "required": false, + "helpMarkDown": "Select the option to skip validating the authenticity of the machine's certificate from a trusted certification authority. The parameter is required for the WinRM HTTPS protocol." + }, + { + "name": "RunPowershellInParallel", + "type": "boolean", + "label": "Run PowerShell in Parallel", + "defaultValue": "true", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Setting it to true will run the PowerShell scripts in parallel on the target machines." + } + ], + "instanceNameFormat": "Grant Logon As A Service: $(UserNames)", + "execution": { + "PowerShell3": { + "target": "$(currentDirectory)\\GrantLogonAsAServiceRight.ps1", + "argumentFormat": "", + "workingDirectory": "$(currentDirectory)" + } + } } \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/InstallTopshelfService.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/InstallTopshelfService.ps1 index b34521a..a365c17 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/InstallTopshelfService.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/InstallTopshelfService.ps1 @@ -3,110 +3,106 @@ Param() Trace-VstsEnteringInvocation $MyInvocation -Try -{ +Try { [string]$topshelfExePaths = Get-VstsInput -Name topshelfExePaths -Require - [string]$environmentName = Get-VstsInput -Name environmentName -Require - [string]$adminUserName = Get-VstsInput -Name adminUserName -Require - [string]$adminPassword = Get-VstsInput -Name adminPassword -Require - [string]$protocol = Get-VstsInput -Name protocol -Require - [string]$testCertificate = Get-VstsInput -Name testCertificate -Require - [string]$specialUser = Get-VstsInput -Name specialUser -Require - [string]$serviceUsername = Get-VstsInput -Name serviceUsername - [string]$servicePassword = Get-VstsInput -Name servicePassword + [string]$specialUser = Get-VstsInput -Name specialUser -Require [string]$instanceName = Get-VstsInput -Name instanceName - [string]$serviceName = Get-VstsInput -Name serviceName - [string]$displayName = Get-VstsInput -Name displayName - [string]$description = Get-VstsInput -Name description - [string]$startupType = Get-VstsInput -Name startupType -Default "default" - [string]$uninstallFirst = Get-VstsInput -Name uninstallFirst - [string]$killMmcTaskManager = Get-VstsInput -Name killMmcTaskManager - [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool - - $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path - - Import-Module $env:CURRENT_TASK_ROOTDIR\DeploymentSDK\InvokeRemoteDeployment.ps1 - - Write-Output "Installing TopShelf service: $topshelfExePaths with instanceName: $instanceName. Version: {{tokens.BuildNumber}}" - - $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path - - [string[]] $topshelfExePathsArray = ($topshelfExePaths -split ',').Trim() - - $servicePassword = $servicePassword.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&').Replace('''', '`''').Replace('(','`(').Replace(')','`)').Replace('@','`@').Replace('}','`}').Replace('{','`{') - - Write-Host ("##vso[task.setvariable variable=E34A69771F47424D9217F3A4D6BCDC94;issecret=true;]$servicePassword") # Make sure the password doesn't show up in the log. - - $instanceName = $instanceName.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&') - - $additionalSharedArguments = "" - if ($specialUser -eq "custom") - { - $additionalSharedArguments += " -username:$serviceUsername" - if (-Not [string]::IsNullOrWhiteSpace($servicePassword)) - { - $additionalSharedArguments += " -password:$servicePassword" - } - } - else - { - $additionalSharedArguments += " --$specialUser" - } - - if (-Not [string]::IsNullOrWhiteSpace($instanceName)) - { - $additionalSharedArguments += " -instance:$instanceName" - } - if (-Not [string]::IsNullOrWhiteSpace($serviceName)) - { - $additionalSharedArguments += " -servicename:$serviceName" - } - if (-Not [string]::IsNullOrWhiteSpace($displayName)) - { - $additionalSharedArguments += " -displayname ""$displayName""" - } - if (-Not [string]::IsNullOrWhiteSpace($description)) - { - $additionalSharedArguments += " -description ""$description""" - } - - $additonalInstallArguments = "" - if ($startupType -ne "default") - { - $additonalInstallArguments = "--$startupType" - } - - $cmd = "`$env:DT_DISABLEINITIALLOGGING='true'`n" - $cmd += "`$env:DT_LOGLEVELCON='NONE'`n" - - if ($killMmcTaskManager -eq "true") - { - $cmd += "Stop-Process -name mmc,taskmgr -Force -ErrorAction SilentlyContinue`n" - } - - foreach($topShelfExe in $topshelfExePathsArray) - { - if ($uninstallFirst -eq "true") - { - $cmd += "& ""$topShelfExe"" uninstall $additionalSharedArguments`n" - } - - $cmd += "& ""$topShelfExe"" install $additionalSharedArguments $additonalInstallArguments`n" - } - - Write-Output "CMD: $cmd" - - $errorMessage = Invoke-RemoteDeployment -machinesList $environmentName -scriptToRun $cmd -deployInParallel $runPowershellInParallel -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate - - if(-Not [string]::IsNullOrEmpty($errorMessage)) - { - $helpMessage = "For more info please google." - Write-Error "$errorMessage`n$helpMessage" - return - } + [string]$serviceName = Get-VstsInput -Name serviceName + [string]$displayName = Get-VstsInput -Name displayName + [string]$description = Get-VstsInput -Name description + [string]$startupType = Get-VstsInput -Name startupType -Default "default" + [string]$uninstallFirst = Get-VstsInput -Name uninstallFirst + [string]$killMmcTaskManager = Get-VstsInput -Name killMmcTaskManager + [bool]$targetIsDeploymentGroup = Get-VstsInput -Name deploymentGroup -Require -AsBool + + Write-Output "Installing TopShelf service: $topshelfExePaths with instanceName: $instanceName. Version: {{tokens.BuildNumber}}" + + $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path + + [string[]] $topshelfExePathsArray = ($topshelfExePaths -split ',').Trim() + + Write-Host ("##vso[task.setvariable variable=E34A69771F47424D9217F3A4D6BCDC94;issecret=true;]$servicePassword") # Make sure the password doesn't show up in the log. + + $instanceName = $instanceName.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&') + + $additionalSharedArguments = "" + if ($specialUser -eq "custom") { + [string]$serviceUsername = Get-VstsInput -Name serviceUsername -Require + [string]$servicePassword = Get-VstsInput -Name servicePassword + + $servicePassword = $servicePassword.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&').Replace('''', '`''').Replace('(', '`(').Replace(')', '`)').Replace('@', '`@').Replace('}', '`}').Replace('{', '`{') + + $additionalSharedArguments += " -username:$serviceUsername" + if (-Not [string]::IsNullOrWhiteSpace($servicePassword)) { + $additionalSharedArguments += " -password:""$servicePassword""" + } + } + else { + $additionalSharedArguments += " --$specialUser" + } + + if (-Not [string]::IsNullOrWhiteSpace($instanceName)) { + $additionalSharedArguments += " -instance:$instanceName" + } + if (-Not [string]::IsNullOrWhiteSpace($serviceName)) { + $additionalSharedArguments += " -servicename:$serviceName" + } + if (-Not [string]::IsNullOrWhiteSpace($displayName)) { + $additionalSharedArguments += " -displayname ""$displayName""" + } + if (-Not [string]::IsNullOrWhiteSpace($description)) { + $additionalSharedArguments += " -description ""$description""" + } + + $additonalInstallArguments = "" + if ($startupType -ne "default") { + $additonalInstallArguments = "--$startupType" + } + + $cmd = "`$env:DT_DISABLEINITIALLOGGING='true'`n" + $cmd += "`$env:DT_LOGLEVELCON='NONE'`n" + + if ($killMmcTaskManager -eq "true") { + $cmd += "Stop-Process -name mmc,taskmgr -Force -ErrorAction SilentlyContinue`n" + } + + foreach ($topShelfExe in $topshelfExePathsArray) { + if ($uninstallFirst -eq "true") { + $cmd += "& ""$topShelfExe"" uninstall $additionalSharedArguments`n" + } + + $cmd += "& ""$topShelfExe"" install $additionalSharedArguments $additonalInstallArguments`n" + } + + Write-Output "CMD: $cmd" + + if ($targetIsDeploymentGroup) + { + Invoke-Expression $cmd + } + else + { + . $env:CURRENT_TASK_ROOTDIR\TelemetryHelper\TelemetryHelper.ps1 + Import-Module $env:CURRENT_TASK_ROOTDIR\DeploymentSDK\InvokeRemoteDeployment.ps1 + + [string]$environmentName = Get-VstsInput -Name environmentName -Require + [string]$adminUserName = Get-VstsInput -Name adminUserName -Require + [string]$adminPassword = Get-VstsInput -Name adminPassword -Require + [string]$protocol = Get-VstsInput -Name protocol -Require + [string]$testCertificate = Get-VstsInput -Name testCertificate -Require + [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool + + + $errorMessage = Invoke-RemoteDeployment -machinesList $environmentName -scriptToRun $cmd -deployInParallel $runPowershellInParallel -adminUserName $adminUserName -adminPassword $adminPassword -protocol $protocol -testCertificate $testCertificate + + if (-Not [string]::IsNullOrEmpty($errorMessage)) { + $helpMessage = "For more info please google." + Write-Error "$errorMessage`n$helpMessage" + return + } + } } -finally -{ - Trace-VstsLeavingInvocation $MyInvocation +finally { + Trace-VstsLeavingInvocation $MyInvocation } \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/task.json b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/task.json index 044dc17..84783ba 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/task.json +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/InstallTopshelfService/task.json @@ -25,59 +25,6 @@ "required": true, "helpMarkDown": "Full path to topshelf executables. (Comma separated list)" }, - { - "name": "EnvironmentName", - "type": "multiLine", - "label": "Machines", - "defaultValue": "", - "required": true, - "helpMarkDown": "Provide a comma separated list of machine IP addresses or FQDNs.
Eg: dbserver.fabrikam.com,192.168.12.34
Or provide output variable of other tasks. Eg: $(variableName)" - }, - { - "name": "AdminUserName", - "type": "string", - "label": "Admin Login", - "defaultValue": "", - "required": true, - "helpMarkDown": "Administrator login for the target machines." - }, - { - "name": "AdminPassword", - "type": "string", - "label": "Password", - "defaultValue": "", - "required": true, - "helpMarkDown": "Password for administrator login for the target machines.
It can accept variable defined in Build/Release definitions as '$(passwordVariable)'.
You may mark variable type as 'secret' to secure it. " - }, - { - "name": "protocol", - "type": "radio", - "label": "Protocol", - "required": true, - "defaultValue": "Http", - "options": { - "Http": "HTTP", - "Https": "HTTPS" - }, - "helpMarkDown": "Select the network protocol to use for the WinRM connection with the machine(s). The default is HTTPS." - }, - { - "name": "TestCertificate", - "type": "boolean", - "label": "Test Certificate", - "defaultValue": "true", - "visibleRule": "protocol = Https", - "required": false, - "helpMarkDown": "Select the option to skip validating the authenticity of the machine's certificate from a trusted certification authority. The parameter is required for the WinRM HTTPS protocol." - }, - { - "name": "RunPowershellInParallel", - "type": "boolean", - "label": "Run PowerShell in Parallel", - "defaultValue": "true", - "required": false, - "helpMarkDown": "Setting it to true will run the PowerShell scripts in parallel on the target machines." - }, { "name": "specialUser", "type": "radio", @@ -173,6 +120,72 @@ "visibleRule": "UninstallFirst = true", "required": true, "helpMarkDown": "Check to make the task kill all open instances of mmc and taskmgr in an attempt to avoid the dreaded: The specified service has been marked for deletion. See: http://stackoverflow.com/questions/20561990/how-to-solve-the-specified-service-has-been-marked-for-deletion-error" + }, + { + "name": "deploymentGroup", + "type": "boolean", + "label": "Target is Deployment Group", + "required": true, + "defaultValue": "false", + "helpMarkDown": "Agents run directly on the server in deployment group. No WinRM Necessary." + }, + { + "name": "EnvironmentName", + "type": "multiLine", + "label": "Machines", + "defaultValue": "", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Provide a comma separated list of machine IP addresses or FQDNs.
Eg: dbserver.fabrikam.com,192.168.12.34
Or provide output variable of other tasks. Eg: $(variableName)" + }, + { + "name": "AdminUserName", + "type": "string", + "label": "Admin Login", + "defaultValue": "", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Administrator login for the target machines." + }, + { + "name": "AdminPassword", + "type": "string", + "label": "Password", + "defaultValue": "", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Password for administrator login for the target machines.
It can accept variable defined in Build/Release definitions as '$(passwordVariable)'.
You may mark variable type as 'secret' to secure it. " + }, + { + "name": "protocol", + "type": "radio", + "label": "Protocol", + "required": false, + "defaultValue": "Http", + "visibleRule": "deploymentGroup = false", + "options": { + "Http": "HTTP", + "Https": "HTTPS" + }, + "helpMarkDown": "Select the network protocol to use for the WinRM connection with the machine(s). The default is HTTPS." + }, + { + "name": "TestCertificate", + "type": "boolean", + "label": "Test Certificate", + "defaultValue": "true", + "visibleRule": "protocol = Https && deploymentGroup = false", + "required": false, + "helpMarkDown": "Select the option to skip validating the authenticity of the machine's certificate from a trusted certification authority. The parameter is required for the WinRM HTTPS protocol." + }, + { + "name": "RunPowershellInParallel", + "type": "boolean", + "label": "Run PowerShell in Parallel", + "defaultValue": "true", + "visibleRule": "deploymentGroup = false", + "required": false, + "helpMarkDown": "Setting it to true will run the PowerShell scripts in parallel on the target machines." } ], "instanceNameFormat": "Install a Topshelf Service: $(TopshelfExePaths)", diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsService.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsService.ps1 index 2669849..ed9ccb6 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsService.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsService.ps1 @@ -1,31 +1,44 @@ -[CmdletBinding()] -Param() - -Trace-VstsEnteringInvocation $MyInvocation - -Try -{ - [string]$serviceNames = Get-VstsInput -Name serviceNames -Require - [string]$environmentName = Get-VstsInput -Name environmentName -Require - [string]$adminUserName = Get-VstsInput -Name adminUserName -Require - [string]$adminPassword = Get-VstsInput -Name adminPassword -Require - [string]$startupType = Get-VstsInput -Name startupType -Require - [string]$protocol = Get-VstsInput -Name protocol -Require - [string]$testCertificate = Get-VstsInput -Name testCertificate -Require - [string]$waitTimeoutInSeconds = Get-VstsInput -Name waitTimeoutInSeconds -Require - [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool - - Write-Output "Starting Windows Services $serviceNames and setting startup type to: $startupType. Version: {{tokens.BuildNumber}}" - - $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path - - . $env:CURRENT_TASK_ROOTDIR\Utility.ps1 - - $serviceNames = '"' + $serviceNames.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&').Replace('''', '`''') + '"' - - Remote-ServiceStartStop -serviceNames $serviceNames -machinesList $environmentName -adminUserName $adminUserName -adminPassword $adminPassword -startupType $startupType -protocol $protocol -testCertificate $testCertificate -waitTimeoutInSeconds $waitTimeoutInSeconds -internStringFileName "StartWindowsServiceIntern.ps1" -killIfTimedOut "false" -runPowershellInParallel $runPowershellInParallel -} -finally -{ - Trace-VstsLeavingInvocation $MyInvocation +[CmdletBinding()] +Param() + +Trace-VstsEnteringInvocation $MyInvocation + +Try { + [string]$serviceNames = Get-VstsInput -Name serviceNames -Require + [string]$instanceName = Get-VstsInput -Name instanceName + [string]$waitTimeoutInSeconds = Get-VstsInput -Name waitTimeoutInSeconds -Require + [string]$startupType = Get-VstsInput -Name startupType -Require + [bool]$targetIsDeploymentGroup = Get-VstsInput -Name deploymentGroup -Require -AsBool + + Write-Output "Starting Windows Services $serviceNames and setting startup type to: $startupType. Version: {{tokens.BuildNumber}}" + + $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path + + if ($targetIsDeploymentGroup) + { + . $env:CURRENT_TASK_ROOTDIR\StartWindowsServiceIntern.ps1 + + $serviceNamesArray = [string[]]($serviceNames.Split(@(",", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries).Trim()) + + StartStopServicesArray $serviceNamesArray $instanceName $startupType $waitTimeoutInSeconds + } + else + { + $serviceNames = '"' + $serviceNames.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&').Replace('''', '`''') + '"' + + . $env:CURRENT_TASK_ROOTDIR\TelemetryHelper\TelemetryHelper.ps1 + . $env:CURRENT_TASK_ROOTDIR\Utility.ps1 + + [string]$environmentName = Get-VstsInput -Name environmentName -Require + [string]$adminUserName = Get-VstsInput -Name adminUserName -Require + [string]$adminPassword = Get-VstsInput -Name adminPassword -Require + [string]$protocol = Get-VstsInput -Name protocol -Require + [string]$testCertificate = Get-VstsInput -Name testCertificate -Require + [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool + + Remote-ServiceStartStop -serviceNames $serviceNames -machinesList $environmentName -instanceName $instanceName -adminUserName $adminUserName -adminPassword $adminPassword -startupType $startupType -protocol $protocol -testCertificate $testCertificate -waitTimeoutInSeconds $waitTimeoutInSeconds -internStringFileName "StartWindowsServiceIntern.ps1" -killIfTimedOut "false" -runPowershellInParallel $runPowershellInParallel + } +} +finally { + Trace-VstsLeavingInvocation $MyInvocation } \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsServiceIntern.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsServiceIntern.ps1 index 92909dd..848ada4 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsServiceIntern.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/StartWindowsServiceIntern.ps1 @@ -1,37 +1,47 @@ -function StartStopServices( - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $serviceNames, - [string][Parameter(Mandatory=$true)][ValidateSet("Manual", "Automatic")] $startupType, - [int][Parameter(Mandatory=$true)] $waitTimeoutInSeconds, - [string][Parameter(Mandatory=$true)] $killIfTimedOut -) -{ - [string[]] $servicesNamesArray = ($serviceNames -split ',' -replace '"').Trim() +function StartStopServicesArray( + [string[]][Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()] $services, + [string][Parameter(Mandatory=$true)][AllowEmptyString()] $instanceName, + [string][Parameter(Mandatory = $true)][ValidateSet("Manual", "Automatic")] $startupType, + [int][Parameter(Mandatory = $true)] $waitTimeoutInSeconds +) { + [bool] $atLeastOneServiceWasNotFound = $false + $presentServicesArray = $null + $services | ForEach-Object { + $serviceName = $_ + if (-not [System.string]::IsNullOrWhiteSpace($instanceName)) { + $serviceName += "$" + $instanceName + } + + $matchingServices = [PSCustomObject[]] (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) - [bool] $atLeastOneServiceWasNotFound = $false - $presentServicesArray = $null - $servicesNamesArray | ForEach-Object { - $serviceName = $_ - $matchingServices = [PSCustomObject[]] (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) + if ($matchingServices -eq $null) { + Write-Error "No services match the name: $serviceName" + $atLeastOneServiceWasNotFound = $true + } + else { + $presentServicesArray += $matchingServices + } + } - if ($matchingServices -eq $null) - { - Write-Error "No services match the name: $serviceName" - $atLeastOneServiceWasNotFound = $true - } - else - { - $presentServicesArray += $matchingServices - } - } + if ($atLeastOneServiceWasNotFound) { + return -1; + } - if ($atLeastOneServiceWasNotFound) - { - return -1; - } + Write-Verbose ("The following services were found: {0}" -f ($presentServicesArray -join ',')) - Write-Verbose ("The following services were found: {0}" -f ($presentServicesArray -join ',')) + $presentServicesArray | % { Set-Service -Name $_.Name -StartupType $startupType } + $presentServicesArray | Where-Object { $_.Status -ne "Running" } | % { $_.Start() } + $presentServicesArray | % { $_.WaitForStatus("Running", [TimeSpan]::FromSeconds($waitTimeoutInSeconds)) } +} + +function StartStopServices( + [string][Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()] $serviceNames, + [string][Parameter(Mandatory=$true)][AllowEmptyString()] $instanceName, + [string][Parameter(Mandatory = $true)][ValidateSet("Manual", "Automatic")] $startupType, + [int][Parameter(Mandatory = $true)] $waitTimeoutInSeconds, + [string][Parameter(Mandatory = $true)] $killIfTimedOut +) { + [string[]] $serviceNamesArray = [string[]]($serviceNames.Split(@(",", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries).Trim()) - $presentServicesArray | % { Set-Service -Name $_.Name -StartupType $startupType } - $presentServicesArray | Where-Object { $_.Status -ne "Running" } | % { $_.Start() } - $presentServicesArray | % { $_.WaitForStatus("Running", [TimeSpan]::FromSeconds($waitTimeoutInSeconds)) } + return StartStopServicesArray $serviceNamesArray $instanceName $startupType $waitTimeoutInSeconds } \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/task.json b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/task.json index 4c3152c..d42f874 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/task.json +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StartWindowsService/task.json @@ -16,8 +16,7 @@ "Patch": "{{tokens.Patch}}" }, "minimumAgentVersion": "1.95.0", - "inputs": [ - { + "inputs": [{ "name": "ServiceNames", "type": "multiLine", "label": "Service Names", @@ -25,12 +24,49 @@ "required": true, "helpMarkDown": "The name of the windows service to start. Comma separate to stop multiple. Supports wildcards (*)." }, + { + "name": "InstanceName", + "type": "string", + "label": "TopShelf Instance Name", + "defaultValue": "", + "required": false, + "helpMarkDown": "If you use topshelf, the instance name. This name will be appended to the end of each service in ServiceNames" + }, + { + "name": "StartupType", + "type": "radio", + "label": "Service Startup Type", + "required": true, + "defaultValue": "Automatic", + "options": { + "Manual": "Manual", + "Automatic": "Automatic" + }, + "helpMarkDown": "Select the startup type for the service." + }, + { + "name": "WaitTimeoutInSeconds", + "type": "string", + "label": "Wait timeout", + "defaultValue": "120", + "required": true, + "helpMarkDown": "Amount of time in seconds to wait for the service to start" + }, + { + "name": "deploymentGroup", + "type": "boolean", + "label": "Target is Deployment Group", + "required": true, + "defaultValue": "false", + "helpMarkDown": "Agents run directly on the server in deployment group. No WinRM Necessary." + }, { "name": "EnvironmentName", "type": "multiLine", "label": "Machines", "defaultValue": "", - "required": true, + "visibleRule": "deploymentGroup = false", + "required": false, "helpMarkDown": "Provide a comma separated list of machine IP addresses or FQDNs.
Eg: dbserver.fabrikam.com,192.168.12.34
Or provide output variable of other tasks. Eg: $(variableName)" }, { @@ -38,7 +74,8 @@ "type": "string", "label": "Admin Login", "defaultValue": "", - "required": true, + "visibleRule": "deploymentGroup = false", + "required": false, "helpMarkDown": "Administrator login for the target machines." }, { @@ -46,34 +83,16 @@ "type": "string", "label": "Password", "defaultValue": "", - "required": true, + "visibleRule": "deploymentGroup = false", + "required": false, "helpMarkDown": "Password for administrator login for the target machines.
It can accept variable defined in Build/Release definitions as '$(passwordVariable)'.
You may mark variable type as 'secret' to secure it. " }, - { - "name": "StartupType", - "type": "radio", - "label": "Service Startup Type", - "required": false, - "defaultValue": "Automatic", - "options": { - "Manual": "Manual", - "Automatic": "Automatic" - }, - "helpMarkDown": "Select the startup type for the service." - }, - { - "name": "WaitTimeoutInSeconds", - "type": "string", - "label": "Wait timeout", - "defaultValue": "120", - "required": true, - "helpMarkDown": "Amount of time in seconds to wait for the service to start" - }, - { + { "name": "protocol", "type": "radio", "label": "Protocol", - "required": true, + "required": false, + "visibleRule": "deploymentGroup = false", "defaultValue": "Http", "options": { "Http": "HTTP", @@ -86,7 +105,7 @@ "type": "boolean", "label": "Test Certificate", "defaultValue": "true", - "visibleRule": "protocol = Https", + "visibleRule": "protocol = Https && deploymentGroup = false", "required": false, "helpMarkDown": "Select the option to skip validating the authenticity of the machine's certificate from a trusted certification authority. The parameter is required for the WinRM HTTPS protocol." }, @@ -95,6 +114,7 @@ "type": "boolean", "label": "Run PowerShell in Parallel", "defaultValue": "true", + "visibleRule": "deploymentGroup = false", "required": false, "helpMarkDown": "Setting it to true will run the PowerShell scripts in parallel on the target machines." } diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsService.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsService.ps1 index 1e70874..c23d714 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsService.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsService.ps1 @@ -1,32 +1,45 @@ -[CmdletBinding()] -Param() - -Trace-VstsEnteringInvocation $MyInvocation - -Try -{ - [string]$serviceNames = Get-VstsInput -Name serviceNames -Require - [string]$environmentName = Get-VstsInput -Name environmentName -Require - [string]$adminUserName = Get-VstsInput -Name adminUserName -Require - [string]$adminPassword = Get-VstsInput -Name adminPassword -Require - [string]$startupType = Get-VstsInput -Name startupType -Require - [string]$protocol = Get-VstsInput -Name protocol -Require - [string]$testCertificate = Get-VstsInput -Name testCertificate -Require - [string]$waitTimeoutInSeconds = Get-VstsInput -Name waitTimeoutInSeconds -Require - [string]$killIfTimedOut = Get-VstsInput -Name killIfTimedOut -Require - [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool - - Write-Output "Stopping Windows Service: $serviceName and setting startup type to: $startupType. Kill: $killIfTimedOut Version: {{tokens.BuildNumber}}" - - $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path - - . $env:CURRENT_TASK_ROOTDIR\Utility.ps1 - - $serviceNames = '"' + $serviceNames.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&').Replace('''', '`''') + '"' - - Remote-ServiceStartStop -serviceNames $serviceNames -machinesList $environmentName -adminUserName $adminUserName -adminPassword $adminPassword -startupType $startupType -protocol $protocol -testCertificate $testCertificate -waitTimeoutInSeconds $waitTimeoutInSeconds -internStringFileName "StopWindowsServiceIntern.ps1" -killIfTimedOut $killIfTimedOut -runPowershellInParallel $runPowershellInParallel -} -finally -{ - Trace-VstsLeavingInvocation $MyInvocation +[CmdletBinding()] +Param() + +Trace-VstsEnteringInvocation $MyInvocation + +Try { + [string]$serviceNames = Get-VstsInput -Name serviceNames -Require + [string]$instanceName = Get-VstsInput -Name instanceName + [string]$startupType = Get-VstsInput -Name startupType -Require + [string]$waitTimeoutInSeconds = Get-VstsInput -Name waitTimeoutInSeconds -Require + [string]$killIfTimedOut = Get-VstsInput -Name killIfTimedOut -Require + [bool]$targetIsDeploymentGroup = Get-VstsInput -Name deploymentGroup -Require -AsBool + + Write-Output "Stopping Windows Service: $serviceName and setting startup type to: $startupType. Kill: $killIfTimedOut Version: {{tokens.BuildNumber}}" + + $env:CURRENT_TASK_ROOTDIR = Split-Path -Parent $MyInvocation.MyCommand.Path + + if ($targetIsDeploymentGroup) + { + . $env:CURRENT_TASK_ROOTDIR\StopWindowsServiceIntern.ps1 + + $serviceNamesArray = [string[]]($serviceNames.Split(@(",", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries).Trim()) + + StartStopServicesArray $serviceNamesArray $instanceName $startupType $waitTimeoutInSeconds $killIfTimedOut + } + else + { + $serviceNames = '"' + $serviceNames.Replace('`', '``').Replace('"', '`"').Replace('$', '`$').Replace('&', '`&').Replace('''', '`''') + '"' + + [string]$environmentName = Get-VstsInput -Name environmentName -Require + [string]$adminUserName = Get-VstsInput -Name adminUserName -Require + [string]$adminPassword = Get-VstsInput -Name adminPassword -Require + [string]$protocol = Get-VstsInput -Name protocol -Require + [string]$testCertificate = Get-VstsInput -Name testCertificate -Require + [bool]$runPowershellInParallel = Get-VstsInput -Name RunPowershellInParallel -Default $true -AsBool + + . $env:CURRENT_TASK_ROOTDIR\TelemetryHelper\TelemetryHelper.ps1 + . $env:CURRENT_TASK_ROOTDIR\Utility.ps1 + + Remote-ServiceStartStop -serviceNames $serviceNames -instanceName $instanceName -machinesList $environmentName -adminUserName $adminUserName -adminPassword $adminPassword -startupType $startupType -protocol $protocol -testCertificate $testCertificate -waitTimeoutInSeconds $waitTimeoutInSeconds -internStringFileName "StopWindowsServiceIntern.ps1" -killIfTimedOut $killIfTimedOut -runPowershellInParallel $runPowershellInParallel + } +} +finally { + Trace-VstsLeavingInvocation $MyInvocation } \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsServiceIntern.ps1 b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsServiceIntern.ps1 index 543a3ae..9d3e189 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsServiceIntern.ps1 +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/StopWindowsServiceIntern.ps1 @@ -1,61 +1,71 @@ -function StartStopServices( - [string][Parameter(Mandatory=$true)][ValidateNotNullOrEmpty()] $serviceNames, - [string][Parameter(Mandatory=$true)][ValidateSet("Disabled", "Manual", "Automatic")] $startupType, - [int][Parameter(Mandatory=$true)] $waitTimeoutInSeconds, - [string][Parameter(Mandatory=$true)] $killIfTimedOut -) -{ - [string[]] $servicesNamesArray = ($serviceNames -split ',' -replace '"').Trim() - - $presentServicesArray = $null - $servicesNamesArray | ForEach-Object { - $serviceName = $_ - $matchingServices = [PSCustomObject[]] (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) - - if ($matchingServices -eq $null) - { - Write-Verbose "No services match the name: $serviceName" - } - else - { - $presentServicesArray += $matchingServices - } - } - - if ($presentServicesArray.Length -eq 0) - { - Write-Verbose "No services matching the given names were found." - return - } - - Write-Verbose ("The following services were found: {0}" -f ($presentServicesArray -join ',')) - - try - { - $presentServicesArray | % { Set-Service -Name $_.Name -StartupType $startupType -ErrorAction SilentlyContinue } - $presentServicesArray | Where-Object { $_.Status -ne "Stopped" } | % { $_.Stop() } - $ErrorActionPreference = "SilentlyContinue" # I don't want the wait for status to throw in the case of a timeout - $presentServicesArray | % { $_.WaitForStatus("Stopped", [TimeSpan]::FromSeconds($waitTimeoutInSeconds)) } - $ErrorActionPreference = "Stop" - } - Catch - { - $ErrorActionPreference = "Stop" - if ($killIfTimedOut -eq "false") - { - $errorMessage = $_.Exception.Message - Write-Verbose $errorMessage - throw - } - - $nonStoppedServices = $presentServicesArray | Where-Object { $_.Status -ne "Stopped" } | % { $_.ServiceName } - - $nonStoppedServices | % { Write-Verbose "Killing service after not stopping within timeout: $_" } - - (get-wmiobject win32_Service | Where-Object { $nonStoppedServices -contains $_.Name }).ProcessID | % { Stop-Process -Force $_ } - } - Finally - { - $presentServicesArray | % { Set-Service -Name $_.Name -StartupType $startupType } - } +function StartStopServicesArray( + [string[]][Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()] $services, + [string][Parameter(Mandatory=$true)][AllowEmptyString()] $instanceName, + [string][Parameter(Mandatory = $true)][ValidateSet("Disabled", "Manual", "Automatic")] $startupType, + [int][Parameter(Mandatory = $true)] $waitTimeoutInSeconds, + [string][Parameter(Mandatory = $true)] $killIfTimedOut +) { + $presentServicesArray = $null + $services | ForEach-Object { + $serviceName = $_ + + if (-not [System.string]::IsNullOrWhiteSpace($instanceName)) { + $serviceName += "$" + $instanceName + } + + $matchingServices = [PSCustomObject[]] (Get-Service -Name $serviceName -ErrorAction SilentlyContinue) + + if ($matchingServices -eq $null) + { + Write-Verbose "No services match the name: $serviceName" + } + else + { + $presentServicesArray += $matchingServices + } + } + + if ($presentServicesArray.Length -eq 0) { + Write-Verbose "No services matching the given names were found." + return + } + + Write-Verbose ("The following services were found: {0}" -f ($presentServicesArray -join ',')) + + try { + $presentServicesArray | % { Set-Service -Name $_.Name -StartupType $startupType -ErrorAction SilentlyContinue } + $presentServicesArray | Where-Object { $_.Status -ne "Stopped" } | % { $_.Stop() } + $ErrorActionPreference = "SilentlyContinue" # I don't want the wait for status to throw in the case of a timeout + $presentServicesArray | % { $_.WaitForStatus("Stopped", [TimeSpan]::FromSeconds($waitTimeoutInSeconds)) } + $ErrorActionPreference = "Stop" + } + Catch { + $ErrorActionPreference = "Stop" + if ($killIfTimedOut -eq "false") { + $errorMessage = $_.Exception.Message + Write-Verbose $errorMessage + throw + } + + $nonStoppedServices = $presentServicesArray | Where-Object { $_.Status -ne "Stopped" } | % { $_.ServiceName } + + $nonStoppedServices | % { Write-Verbose "Killing service after not stopping within timeout: $_" } + + (get-wmiobject win32_Service | Where-Object { $nonStoppedServices -contains $_.Name }).ProcessID | % { Stop-Process -Force $_ } + } + Finally { + $presentServicesArray | % { Set-Service -Name $_.Name -StartupType $startupType } + } } + +function StartStopServices( + [string][Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()] $serviceNames, + [string][Parameter(Mandatory=$true)][AllowEmptyString()] $instanceName, + [string][Parameter(Mandatory = $true)][ValidateSet("Disabled", "Manual", "Automatic")] $startupType, + [int][Parameter(Mandatory = $true)] $waitTimeoutInSeconds, + [string][Parameter(Mandatory = $true)] $killIfTimedOut +) { + [string[]] $serviceNamesArray = [string[]]($serviceNames.Split(@(",", "`r", "`n"), [System.StringSplitOptions]::RemoveEmptyEntries).Trim()) + + return StartStopServicesArray $serviceNamesArray $instanceName $startupType $waitTimeoutInSeconds $killIfTimedOut +} \ No newline at end of file diff --git a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/task.json b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/task.json index cbb160c..d35b187 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/task.json +++ b/Extensions/WindowsServiceReleaseTasks/Src/Tasks/StopWindowsService/task.json @@ -16,8 +16,7 @@ "Patch": "{{tokens.Patch}}" }, "minimumAgentVersion": "1.95.0", - "inputs": [ - { + "inputs": [{ "name": "ServiceNames", "type": "multiLine", "label": "Service Names", @@ -25,12 +24,50 @@ "required": true, "helpMarkDown": "The name of the windows service to stop. Comma separate to stop multiple. Supports wildcards (*)." }, + { + "name": "InstanceName", + "type": "string", + "label": "TopShelf Instance Name", + "defaultValue": "", + "required": false, + "helpMarkDown": "If you use topshelf, the instance name. This name will be appended to the end of each service in ServiceNames" + }, + { + "name": "StartupType", + "type": "radio", + "label": "Service Startup Type", + "required": true, + "defaultValue": "Disabled", + "options": { + "Disabled": "Disabled", + "Manual": "Manual", + "Automatic": "Automatic" + }, + "helpMarkDown": "Select the startup type for the service." + } , + { + "name": "KillIfTimedOut", + "type": "boolean", + "label": "Kill service if stop fails", + "defaultValue": "true", + "required": true, + "helpMarkDown": "Select this option for force terminate the service if the stop fails." + }, + { + "name": "deploymentGroup", + "type": "boolean", + "label": "Target is Deployment Group", + "required": true, + "defaultValue": "false", + "helpMarkDown": "Agents run directly on the server in deployment group. No WinRM Necessary." + }, { "name": "EnvironmentName", "type": "multiLine", "label": "Machines", "defaultValue": "", - "required": true, + "visibleRule": "deploymentGroup = false", + "required": false, "helpMarkDown": "Provide a comma separated list of machine IP addresses or FQDNs.
Eg: dbserver.fabrikam.com,192.168.12.34
Or provide output variable of other tasks. Eg: $(variableName)" }, { @@ -38,7 +75,8 @@ "type": "string", "label": "Admin Login", "defaultValue": "", - "required": true, + "visibleRule": "deploymentGroup = false", + "required": false, "helpMarkDown": "Administrator login for the target machines." }, { @@ -46,23 +84,11 @@ "type": "string", "label": "Password", "defaultValue": "", - "required": true, + "visibleRule": "deploymentGroup = false", + "required": false, "helpMarkDown": "Password for administrator login for the target machines.
It can accept variable defined in Build/Release definitions as '$(passwordVariable)'.
You may mark variable type as 'secret' to secure it. " }, - { - "name": "StartupType", - "type": "radio", - "label": "Service Startup Type", - "required": false, - "defaultValue": "Disabled", - "options": { - "Disabled": "Disabled", - "Manual": "Manual", - "Automatic": "Automatic" - }, - "helpMarkDown": "Select the startup type for the service." - }, - { + { "name": "WaitTimeoutInSeconds", "type": "string", "label": "Wait timeout", @@ -70,11 +96,12 @@ "required": true, "helpMarkDown": "Amount of time in seconds to wait for the service to start" }, - { + { "name": "protocol", "type": "radio", "label": "Protocol", - "required": true, + "required": false, + "visibleRule": "deploymentGroup = false", "defaultValue": "Http", "options": { "Http": "HTTP", @@ -87,7 +114,7 @@ "type": "boolean", "label": "Test Certificate", "defaultValue": "true", - "visibleRule": "protocol = Https", + "visibleRule": "protocol = Https && deploymentGroup = false", "required": false, "helpMarkDown": "Select the option to skip validating the authenticity of the machine's certificate from a trusted certification authority. The parameter is required for the WinRM HTTPS protocol." }, @@ -97,15 +124,8 @@ "label": "Run PowerShell in Parallel", "defaultValue": "true", "required": false, + "visibleRule": "deploymentGroup = false", "helpMarkDown": "Setting it to true will run the PowerShell scripts in parallel on the target machines." - }, - { - "name": "KillIfTimedOut", - "type": "boolean", - "label": "Kill service if stop fails", - "defaultValue": "true", - "required": true, - "helpMarkDown": "Select this option for force terminate the service if the stop fails." } ], "instanceNameFormat": "Stop Windows Service: $(ServiceNames)", diff --git a/Extensions/WindowsServiceReleaseTasks/Src/overview.md b/Extensions/WindowsServiceReleaseTasks/Src/overview.md index 11fadc3..90966bc 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/overview.md +++ b/Extensions/WindowsServiceReleaseTasks/Src/overview.md @@ -1,33 +1,31 @@ -# Windows service release management tasks -This extension contains tasks to start and stop windows services as well as change the startup type. This uses powershell remoting. - -1. **Start Windows Service(s)** - - This task will start windows service(s) on a list of machines and change the startup type to Automatic or Manual. - -2. **Stop Windows Service(s)** - - This task will stop windows service(s) on a list of machines and change the startup type to Disabled, Automatic or Manual. - -3. **Install (TopShelf) Windows Service(s)** - - Can be used to call executables that use [TopShelf](http://topshelf-project.com/) and install them as a service. - -4. **Grant Logon As A Service Right** - - Topshelf handles this for you, but if you are winging it, you'll need this. - -Note: This release migrates to the VSTS sdk and includes some major enhancements\fixes. - -Primary Changes include: - - * Ability to kill all open mmc\taskmgr to attempt to avoid the: Service is pending deletion issue - * Proper escaping of passwords - * Support spaces and $ in service names - * Embed the DeploymentSdk and use that - * Support parallel running of tasks - * Chnage to the powershell 3 VSTS-SDK - - -Icons made by [Google](http://www.flaticon.com/authors/google) [www.flaticon.com](http://www.flaticon.com) [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/) -Icons made by [Freepik](http://www.freepik.com) [www.flaticon.com](http://www.flaticon.com) [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/) +# Windows service release management tasks + +### Now Supporting deployment groups +No more usernames and passwords for deployment accounts! + +This extension contains tasks to start and stop windows services as well as change the startup type. + +1. **Start Windows Service(s)** + + This task will start windows service(s) on a list of machines and change the startup type to Automatic or Manual. + +2. **Stop Windows Service(s)** + + This task will stop windows service(s) on a list of machines and change the startup type to Disabled, Automatic or Manual. + +3. **Install (TopShelf) Windows Service(s)** + + Can be used to call executables that use [TopShelf](http://topshelf-project.com/) and install them as a service. + +4. **Grant Logon As A Service Right** + + Topshelf handles this for you, but if you are winging it, you'll need this. + + +Version 7 changes: + + * Support deployment groups nativly. No more managing remote credentials in your releases! + * Support instance names in start\stop tasks + +Icons made by [Google](http://www.flaticon.com/authors/google) [www.flaticon.com](http://www.flaticon.com) [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/) +Icons made by [Freepik](http://www.freepik.com) [www.flaticon.com](http://www.flaticon.com) [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/) diff --git a/Extensions/WindowsServiceReleaseTasks/Src/vss-extension.json b/Extensions/WindowsServiceReleaseTasks/Src/vss-extension.json index 05fd10f..36ecb23 100644 --- a/Extensions/WindowsServiceReleaseTasks/Src/vss-extension.json +++ b/Extensions/WindowsServiceReleaseTasks/Src/vss-extension.json @@ -1,101 +1,95 @@ -{ - "manifestVersion": 1, - "id": "windows-service-release-tasks", - "name": "Windows Service Release Tasks", - "version": "{{tokens.BuildNumber}}", - "publisher": "jabbera", - "public": "true", - "targets": [ - { - "id": "Microsoft.VisualStudio.Services" - } - ], - "description": "Release management tasks for the starting, stopping, and install(TopShelf) of windows services.", - "categories": [ - "Build and release" - ], - "tags": [ "service", "release", "windows service" ], - "content": { - "license": { - "path": "license.txt" - }, - "details": { - "path": "overview.md" - } - }, - "screenshots": [ - { - "path": "screenshots/screenshot1.png" - } - ], - "links": { - "getstarted": { - "uri": "https://github.com/jabbera/my-vsts-tasks" - }, - "support": { - "uri": "https://github.com/jabbera/my-vsts-tasks/issues" - } - }, - "icons": { - "default": "extension-icon.png" - }, - "files": [ - { - "path": "Tasks/StartWindowsService" - }, - { - "path": "Tasks/StopWindowsService" - }, - { - "path": "Tasks/InstallTopshelfService" - }, - { - "path": "Tasks/GrantLogonAsAServiceRight" - } - ], - "contributions": [ - { - "id": "start-windows-service-task", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "Tasks/StartWindowsService" - } - }, - { - "id": "stop-windows-service-task", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "Tasks/StopWindowsService" - } - }, - { - "id": "install-topshelf-service-task", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "Tasks/InstallTopshelfService" - } - }, - { - "id": "grant-logon-as-a-service-right-task", - "type": "ms.vss-distributed-task.task", - "targets": [ - "ms.vss-distributed-task.tasks" - ], - "properties": { - "name": "Tasks/GrantLogonAsAServiceRight" - } - } - ], - "galleryFlags": [ - "Public" - ] +{ + "manifestVersion": 1, + "id": "windows-service-release-tasks", + "name": "Windows Service Release Tasks", + "version": "{{tokens.BuildNumber}}", + "publisher": "jabbera", + "public": "true", + "targets": [{ + "id": "Microsoft.VisualStudio.Services" + }], + "description": "Release management tasks for the starting, stopping, and install(TopShelf) of windows services.", + "categories": [ + "Build and release" + ], + "tags": ["service", "release", "windows service"], + "content": { + "license": { + "path": "license.txt" + }, + "details": { + "path": "overview.md" + } + }, + "screenshots": [{ + "path": "screenshots/screenshot1.png" + }], + "links": { + "getstarted": { + "uri": "https://github.com/jabbera/my-vsts-tasks" + }, + "support": { + "uri": "https://github.com/jabbera/my-vsts-tasks/issues" + } + }, + "icons": { + "default": "extension-icon.png" + }, + "files": [{ + "path": "Tasks/StartWindowsService" + }, + { + "path": "Tasks/StopWindowsService" + }, + { + "path": "Tasks/InstallTopshelfService" + }, + { + "path": "Tasks/GrantLogonAsAServiceRight" + } + ], + "contributions": [{ + "id": "start-windows-service-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Tasks/StartWindowsService" + } + }, + { + "id": "stop-windows-service-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Tasks/StopWindowsService" + } + }, + { + "id": "install-topshelf-service-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Tasks/InstallTopshelfService" + } + }, + { + "id": "grant-logon-as-a-service-right-task", + "type": "ms.vss-distributed-task.task", + "targets": [ + "ms.vss-distributed-task.tasks" + ], + "properties": { + "name": "Tasks/GrantLogonAsAServiceRight" + } + } + ], + "galleryFlags": [ + "Public" + ] } \ No newline at end of file diff --git a/common.json b/common.json index 08fd89e..e7cdd8d 100644 --- a/common.json +++ b/common.json @@ -1,54 +1,66 @@ -{ - "StartWindowsService": [ - { - "module": "Helper", - "dest": "" - }, - { - "module": "DeploymentSDK", - "dest": "DeploymentSDK" - }, - { - "module": "VstsTaskSdk", - "dest": "ps_modules" - } - ], - "StopWindowsService": [ - { - "module": "Helper", - "dest": "" - }, - { - "module": "DeploymentSDK", - "dest": "DeploymentSDK" - }, - { - "module": "VstsTaskSdk", - "dest": "ps_modules" - } - ], - "GrantLogonAsAServiceRight": [ - { - "module": "Helper", - "dest": "" - }, - { - "module": "DeploymentSDK", - "dest": "DeploymentSDK" - }, - { - "module": "VstsTaskSdk", - "dest": "ps_modules" - } - ], - "InstallTopshelfService": [ - { - "module": "DeploymentSDK", - "dest": "DeploymentSDK" - }, - { - "module": "VstsTaskSdk", - "dest": "ps_modules" - } - ] +{ + "StartWindowsService": [{ + "module": "Helper", + "dest": "" + }, + { + "module": "DeploymentSDK", + "dest": "DeploymentSDK" + }, + { + "module": "VstsTaskSdk", + "dest": "ps_modules" + }, + { + "module": "TelemetryHelper", + "dest": "TelemetryHelper" + } + ], + "StopWindowsService": [{ + "module": "Helper", + "dest": "" + }, + { + "module": "DeploymentSDK", + "dest": "DeploymentSDK" + }, + { + "module": "VstsTaskSdk", + "dest": "ps_modules" + }, + { + "module": "TelemetryHelper", + "dest": "TelemetryHelper" + } + ], + "GrantLogonAsAServiceRight": [{ + "module": "Helper", + "dest": "" + }, + { + "module": "DeploymentSDK", + "dest": "DeploymentSDK" + }, + { + "module": "VstsTaskSdk", + "dest": "ps_modules" + }, + { + "module": "TelemetryHelper", + "dest": "TelemetryHelper" + } + ], + "InstallTopshelfService": [{ + "module": "DeploymentSDK", + "dest": "DeploymentSDK" + }, + { + "module": "VstsTaskSdk", + "dest": "ps_modules" + }, + { + "module": "TelemetryHelper", + "dest": "TelemetryHelper" + } + ] } \ No newline at end of file diff --git a/config.bootstrap.json b/config.bootstrap.json index 9d16bfe..314b4f0 100644 --- a/config.bootstrap.json +++ b/config.bootstrap.json @@ -1,8 +1,8 @@ { - "tokens":{ - "Major": 6, - "Patch": 1 - }, - "prefix" : "{{", - "suffix" : "}}" + "tokens": { + "Major": 6, + "Patch": 1 + }, + "prefix": "{{", + "suffix": "}}" } \ No newline at end of file diff --git a/config.json b/config.json index 7eabe76..67f7c4a 100644 --- a/config.json +++ b/config.json @@ -1,10 +1,10 @@ -{ - "tokens":{ - "BuildNumber": "{{tokens.Major}}.{{tokens.Minor}}.{{tokens.Patch}}", - "Major": {{tokens.Major}}, - "Minor": {{tokens.Minor}}, - "Patch": {{tokens.Patch}} - }, - "prefix" : "{{", - "suffix" : "}}" +{ + "tokens":{ + "BuildNumber": "{{tokens.Major}}.{{tokens.Minor}}.{{tokens.Patch}}", + "Major": {{tokens.Major}}, + "Minor": {{tokens.Minor}}, + "Patch": {{tokens.Patch}} + }, + "prefix" : "{{", + "suffix" : "}}" } \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index 4abc1b9..4c95e03 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,106 +1,114 @@ -var del = require("del"); -var gulp = require("gulp"); -var gulpUtil = require('gulp-util'); -var path = require("path"); -var shell = require("shelljs"); -var spawn = require('child_process').spawn; -var fs = require('fs-extra'); -var pkgm = require('./package'); -var replace = require('gulp-token-replace'); -var debug = require('gulp-debug'); -var chmod = require('gulp-chmod'); - -var buildRoot = "_build"; -var packageRoot = "_package"; -var extnBuildRoot = "_build/Extensions/"; -var sourcePaths = "Extensions/**/*"; -var vstsTaskSdkPath = "node_modules/vsts-task-sdk/VstsTaskSdk/**/*"; -var commonSrc = "Extensions/Common"; - -gulp.task("clean", function() { - return del([buildRoot, packageRoot]); -}); - -gulp.task("compile", ["clean"], function(done) { - return gulp.src(sourcePaths, { base: "." }).pipe(chmod(666)).pipe(gulp.dest(buildRoot)); -}); - -gulp.task("copyPowershellModulesToCommon", ["compile"], function (done) { - var dest = path.join(extnBuildRoot, 'Common', 'VstsTaskSdk', 'Src'); - return gulp.src(vstsTaskSdkPath, { base: "node_modules/vsts-task-sdk" }).pipe(gulp.dest(dest)); -}); - -gulp.task("build", ["copyPowershellModulesToCommon"], function () { - //Foreach task under extensions copy common modules - fs.readdirSync(extnBuildRoot).filter(function (file) { - return fs.statSync(path.join(extnBuildRoot, file)).isDirectory() && file != "Common"; - }).forEach(copyCommonModules); -}); - -gulp.task("package", ["token-replace"], function() { - fs.readdirSync(extnBuildRoot).filter(function (file) { - return fs.statSync(path.join(extnBuildRoot, file)).isDirectory() && file != "Common"; - }).forEach(createVsixPackage); -}); - -gulp.task("token-replace", ["token-replace-bootstrap"], function(){ - var config = require("./" + buildRoot + "/config.json"); - return gulp.src([ - extnBuildRoot + "/**/*.json", - extnBuildRoot + "/**/*.ps1" - ]) - .pipe(replace({tokens:config})) - .pipe(gulp.dest(extnBuildRoot)); - -}); - -gulp.task("token-replace-bootstrap", ["build"], function(){ - var config = require("./config.bootstrap.json"); - config.tokens["Minor"] = getMinorVersion(); - return gulp.src("./config.json") - .pipe(replace({tokens:config})) - .pipe(gulp.dest(buildRoot)); -}); - -var copyCommonModules = function(extensionName) { - - var commonDeps = require('./common.json'); - var commonSrc = path.join(__dirname, extnBuildRoot, 'Common'); - - var currentExtnRoot = path.join(__dirname, extnBuildRoot, extensionName); - return gulp.src(path.join(currentExtnRoot, '**/task.json')) - .pipe(pkgm.copyCommonModules(currentExtnRoot, commonDeps, commonSrc)); -} - -var createVsixPackage = function(extensionName) { - var extnOutputPath = path.join(packageRoot, extensionName); - var extnManifestPath = path.join(extnBuildRoot, extensionName, "Src"); - del(extnOutputPath); - shell.mkdir("-p", extnOutputPath); - var packagingCmd = "node_modules\\.bin\\tfx extension create --manifeset-globs vss-extension.json --root " + extnManifestPath + " --output-path " + extnOutputPath; - executeCommand(packagingCmd, function() {}); -} - -var executeCommand = function(cmd, callback) { - shell.exec(cmd, {silent: true}, function(code, output) { - if(code != 0) { - console.error("command failed: " + cmd + "\nManually execute to debug"); - } - else { - callback(); - } - }); -} - -var getMinorVersion = function() -{ - var now = new Date(); - var start = new Date(now.getFullYear(), 0, 0); - var diff = now - start; - var oneDay = 1000 * 60 * 60 * 24; - var day = Math.floor(diff / oneDay); - var twoDigitYear = now.getFullYear().toString().substr(2,2); - return twoDigitYear.toString() + day.toString(); -} - +var del = require("del"); +var gulp = require("gulp"); +var gulpUtil = require('gulp-util'); +var path = require("path"); +var shell = require("shelljs"); +var spawn = require('child_process').spawn; +var fs = require('fs-extra'); +var pkgm = require('./package'); +var replace = require('gulp-token-replace'); +var debug = require('gulp-debug'); +var chmod = require('gulp-chmod'); + +var buildRoot = "_build"; +var packageRoot = "_package"; +var extnBuildRoot = "_build/Extensions/"; +var sourcePaths = "Extensions/**/*"; +var vstsTaskSdkPath = "node_modules/vsts-task-sdk/VstsTaskSdk/**/*"; +var commonSrc = "Extensions/Common"; + +gulp.task("clean", function () { + return del([buildRoot, packageRoot]); +}); + +gulp.task("compile", ["clean"], function (done) { + return gulp.src(sourcePaths, { + base: "." + }).pipe(chmod(666)).pipe(gulp.dest(buildRoot)); +}); + +gulp.task("copyPowershellModulesToCommon", ["compile"], function (done) { + var dest = path.join(extnBuildRoot, 'Common', 'VstsTaskSdk', 'Src'); + return gulp.src(vstsTaskSdkPath, { + base: "node_modules/vsts-task-sdk" + }).pipe(gulp.dest(dest)); +}); + +gulp.task("build", ["copyPowershellModulesToCommon"], function () { + //Foreach task under extensions copy common modules + fs.readdirSync(extnBuildRoot).filter(function (file) { + return fs.statSync(path.join(extnBuildRoot, file)).isDirectory() && file != "Common"; + }).forEach(copyCommonModules); +}); + +gulp.task("package", ["token-replace"], function () { + fs.readdirSync(extnBuildRoot).filter(function (file) { + return fs.statSync(path.join(extnBuildRoot, file)).isDirectory() && file != "Common"; + }).forEach(createVsixPackage); +}); + +gulp.task("token-replace", ["token-replace-bootstrap"], function () { + var config = require("./" + buildRoot + "/config.json"); + return gulp.src([ + extnBuildRoot + "/**/*.json", + extnBuildRoot + "/**/*.ps1" + ]) + .pipe(replace({ + tokens: config + })) + .pipe(gulp.dest(extnBuildRoot)); + +}); + +gulp.task("token-replace-bootstrap", ["build"], function () { + var config = require("./config.bootstrap.json"); + config.tokens["Minor"] = getMinorVersion(); + return gulp.src("./config.json") + .pipe(replace({ + tokens: config + })) + .pipe(gulp.dest(buildRoot)); +}); + +var copyCommonModules = function (extensionName) { + + var commonDeps = require('./common.json'); + var commonSrc = path.join(__dirname, extnBuildRoot, 'Common'); + + var currentExtnRoot = path.join(__dirname, extnBuildRoot, extensionName); + return gulp.src(path.join(currentExtnRoot, '**/task.json')) + .pipe(pkgm.copyCommonModules(currentExtnRoot, commonDeps, commonSrc)); +} + +var createVsixPackage = function (extensionName) { + var extnOutputPath = path.join(packageRoot, extensionName); + var extnManifestPath = path.join(extnBuildRoot, extensionName, "Src"); + del(extnOutputPath); + shell.mkdir("-p", extnOutputPath); + var packagingCmd = "node_modules\\.bin\\tfx extension create --manifeset-globs vss-extension.json --root " + extnManifestPath + " --output-path " + extnOutputPath; + executeCommand(packagingCmd, function () {}); +} + +var executeCommand = function (cmd, callback) { + shell.exec(cmd, { + silent: true + }, function (code, output) { + if (code != 0) { + console.error("command failed: " + cmd + "\nManually execute to debug"); + } else { + callback(); + } + }); +} + +var getMinorVersion = function () { + var now = new Date(); + var start = new Date(now.getFullYear(), 0, 0); + var diff = now - start; + var oneDay = 1000 * 60 * 60 * 24; + var day = Math.floor(diff / oneDay); + var twoDigitYear = now.getFullYear().toString().substr(2, 2); + return twoDigitYear.toString() + day.toString(); +} + gulp.task("default", ["build"]); \ No newline at end of file