Including Windows PowerShell script as part of your build
and deployment process, brings you the flexibility of easily and effectively
customize your packaging and deployment process. With the proper combination of
environment configuration files (XML) and PowerShell scripts you can achieve
the impossible. This post will show you how to run Windows PowerShell scripts
remotely from a TFS build process.
Using CredSSP for
second-hop remoting
One common issue with PowerShell remoting is the “double
hop” problem. When the scripts are executed remotely on a Server A and then it
tries to connect from Server A to Server B, the second connection fails to send
the credentials to that server. As a result the second server fails to
authenticate the request and rejects the connection. To get around this issue
you need to use the CredSSP authentication mechanism in PowerShell.
Credential Security Service Provider (CredSSP) is a new
security service provider that is available through the Security Support
Provider Interface (SSPI) in Windows. CredSSP enables an application to
delegate the user’s credentials from the client (by using the client-side SSP)
to the target server (through the server-side SSP).
To setup the machines, for CredSSP you can follow the below
given steps:
On the local computer that needs to connect to a remote
server, execute the command Enable-WSManCredSSP
-Role client -DelegateComputer *. This will make the local computer role as client since so
that it can connect to remote computer which acts as server.
On the remote server run the command Enable-WSManCredSSP -Role server.
Remote execution
of the PowerShell script from TFS build
For executing PowerShell scripts from the C# code you need
to make use of the API’s in the System.Management.Automation.dll
assembly. You can use the PowerShell instance directly but a better way is to
create a Runspace
instance. Every PowerShell instance works in a Runspace. You can have multiple Runspaces to connect to
different remote hosts at the same time.
To get the remote execution working properly you need to
first construct an instance of WSManConnectionInfo
and pass that on to the RunspaceFactory.CreateRunspace(..)
method. You can use the following code snippet to construct the connection
object.
private PSCredential BuildCredentials(string username, string domain, string password)
{
PSCredential credential = null;
if (String.IsNullOrWhiteSpace(username))
{
return credential;
}
if (!String.IsNullOrWhiteSpace(domain))
{
username = domain + "\\" + username;
}
var securePassword = new SecureString();
if (!String.IsNullOrEmpty(password))
{
foreach (char c in password)
{
securePassword.AppendChar(c);
}
}
securePassword.MakeReadOnly();
credential = new PSCredential(username, securePassword);
return credential;
}
private WSManConnectionInfo ConstructConnectionInfo()
{
//The connection info values are created after looking into the ouput of the command in the target machine
/*
* dir WSMan:\localhost\Listener\Listener_*\*
* /
//Output
/*
* Name Value Type
* ---- ----- ----
* Address * System.String
* Transport HTTP System.String
* Port 5985 System.String
* URLPrefix wsman System.String
*
*/
const string SHELL_URI = http://schemas.microsoft.com/powershell/Microsoft.PowerShell;
var credentials = BuildCredentials("username", "domain", "password");
var connectionInfo = new WSManConnectionInfo(false, "remoteserver", 5985, "/wsman", SHELL_URI, credentials);
connectionInfo.AuthenticationMechanism = AuthenticationMechanism.Credssp;
return connectionInfo;
}
Next you can use this connection object to pass it to the
RunspaceFactory.CreateRunspace method and invoke the PowerShell script as given
below.
var scriptOutput = new ScriptOutput();
var tfsBuildHost = new TFSBuildHost(scriptOutput);
var connection = ConstructConnectionInfo();
using (var runspace = RunspaceFactory.CreateRunspace(tfsBuildHost, connection))
{
runspace.Open();
InvokePSScript(runspace, scriptOutput);
runspace.Close();
}
private void InvokePSScript(Runspace runspace, ScriptOutput scriptOutput)
{
using (var ps = PowerShell.Create())
{
ps.Runspace = runspace;
var commandToExecute = ConstructScriptExecutionCommand("path to script file", "parameter1", "parameter2");
ps.AddScript(commandToExecute);
try
{
var results = ps.Invoke();
foreach (var result in results)
{
if (result.BaseObject != null)
{
scriptOutput.WriteResults(result.BaseObject.ToString());
}
}
}
catch(Exception)
{
if (ps.InvocationStateInfo.State != PSInvocationState.Failed)
{
return;
}
scriptOutput.WriteError(ps.InvocationStateInfo.Reason.Message);
}
}
}
You can also notice the TFSBuildHost object in the code.
This class provides communications between the Windows PowerShell engine and
the user. The implementation details are as given below.
internal class TFSBuildHost : PSHost{
private ScriptOutput _output;
private CultureInfo originalCultureInfo =
System.Threading.Thread.CurrentThread.CurrentCulture;
private CultureInfo originalUICultureInfo =
System.Threading.Thread.CurrentThread.CurrentUICulture;
private Guid _hostId = Guid.NewGuid();
private TFSBuildUserInterface _tfsBuildInterface;
public TFSBuildHost(ScriptOutput output)
{
this._output = output;
_tfsBuildInterface = new TFSBuildUserInterface(_output);
}
public override System.Globalization.CultureInfo CurrentCulture
{
get { return this.originalCultureInfo; }
}
public override System.Globalization.CultureInfo CurrentUICulture
{
get { return this.originalUICultureInfo; }
}
public override Guid InstanceId
{
get { return this._hostId; }
}
public override string Name
{
get { return "TFSBuildPowerShellImplementation"; }
}
public override Version Version
{
get { return new Version(1, 0, 0, 0); }
}
public override PSHostUserInterface UI
{
get { return this._tfsBuildInterface; }
}
public override void NotifyBeginApplication()
{
_output.Started = true;
}
public override void NotifyEndApplication()
{
_output.Started = false;
_output.Stopped = true;
}
public override void SetShouldExit(int exitCode)
{
this._output.ShouldExit = true;
this._output.ExitCode = exitCode;
}
public override void EnterNestedPrompt()
{
throw new NotImplementedException(
"The method or operation is not implemented.");
}
public override void ExitNestedPrompt()
{
throw new NotImplementedException(
"The method or operation is not implemented.");
}
}
internal class TFSBuildUserInterface : PSHostUserInterface
{
private ScriptOutput _output;
private TFSBuildRawUserInterface _tfsBuildRawUi = new TFSBuildRawUserInterface();
public TFSBuildUserInterface(ScriptOutput output)
{
_output = output;
}
public override PSHostRawUserInterface RawUI
{
get { return this._tfsBuildRawUi; }
}
public override string ReadLine()
{
return Environment.NewLine;
}
public override void Write(string value)
{
_output.Write(value);
}
public override void Write(
ConsoleColor foregroundColor,
ConsoleColor backgroundColor,
string value)
{
//Ignore the colors for TFS build process.
_output.Write(value);
}
public override void WriteDebugLine(string message)
{
Debug.WriteLine(String.Format("DEBUG: {0}", message));
}
public override void WriteErrorLine(string value)
{
_output.WriteError(value);
}
public override void WriteLine()
{
_output.WriteLine();
}
public override void WriteLine(string value)
{
_output.WriteLine(value);
}
public override void WriteLine(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value)
{
//Ignore the colors for TFS build process.
_output.WriteLine(value);
}
}
internal class TFSBuildRawUserInterface : PSHostRawUserInterface
{
private ConsoleColor _backColor = ConsoleColor.Black;
private ConsoleColor _foreColor = ConsoleColor.White;
private Size _bufferSize = new Size(300, 900);
private Size _windowSize = new Size(100, 400);
private Coordinates _cursorPosition = new Coordinates { X = 0, Y = 0 };
private Coordinates _windowPosition = new Coordinates { X = 50, Y = 10 };
private int _cursorSize = 1;
private string _title = "TFS build process";
public override ConsoleColor BackgroundColor
{
get { return _backColor; }
set { _backColor = value; }
}
public override Size BufferSize
{
get { return _bufferSize; }
set { _bufferSize = value; }
}
public override Coordinates CursorPosition
{
get { return _cursorPosition; }
set { _cursorPosition = value; }
}
public override int CursorSize
{
get { return _cursorSize; }
set { _cursorSize = value; }
}
public override ConsoleColor ForegroundColor
{
get { return _foreColor; }
set { _foreColor = value; }
}
public override bool KeyAvailable
{
get { return false; }
}
public override Size MaxPhysicalWindowSize
{
get { return new Size(500, 2000); }
}
public override Size MaxWindowSize
{
get { return new Size(500, 2000); }
}
public override Coordinates WindowPosition
{
get { return _windowPosition; }
set { _windowPosition = value; }
}
public override Size WindowSize
{
get { return _windowSize; }
set { _windowSize = value; }
}
public override string WindowTitle
{
get { return _title; }
set { _title = value; }
}
}