TypeConverter (registration) throws NullRefException in PowerShell - powershell

I have a PowerShell class, which I want to automatically be convertable from string.
So I defined a TypeConverter like this:
class StringToAcmeStateConverter : System.Management.Automation.PSTypeConverter {
[bool] CanConvertFrom([object] $object, [Type] $destinationType) {
if($object -is [string]) {
return Test-Path ([string]$object);
}
return $false;
}
[bool] CanConvertTo([object] $object, [Type] $destinationType) {
return $false
}
[object] ConvertFrom([object] $sourceValue, [Type] $destinationType,
[IFormatProvider] $formatProvider, [bool] $ignoreCase)
{
if($null -eq $sourceValue) { return $null; }
if(-not $this.CanConvertFrom($sourceValue, $destinationType)) {
throw [System.InvalidCastException]::new();
}
$paths = [AcmeStatePaths]::new($sourceValue);
return [AcmeDiskPersistedState]::new($paths, $false, $true);
}
[object] ConvertTo([object] $sourceValue, [Type] $destinationType,
[IFormatProvider] $formatProvider, [bool] $ignoreCase)
{
throw [System.NotImplementedException]::new();
}
}
[System.ComponentModel.TypeConverter([StringToAcmeStateConverter])]
<# abstract #> class AcmeState {
AcmeState() {
if ($this.GetType() -eq [AcmeState]) {
throw [System.InvalidOperationException]::new("This is intended to be abstract - inherit To it.");
}
}
<# omitted #>
}
(Full code listing here: https://raw.githubusercontent.com/PKISharp/ACMESharpCore-PowerShell/deep-state/ACME-PS/internal/classes/AcmeState.ps1)
But PowerShell now throws a NullRefException from inside the pipeline.
How would I make PS use the Converter correctly.
Update
Since this question did not contain enough information for a full repro, I created a gist containing the current (failing) code of the module: gist.github.com/glatzert/ba32f291b9155e6d19c29fbe9594a7c5

TypeConversion with PowerShell classes has some non-obvious issues.
My first approach with the TypeConverter-Attribute fails with either an NullRefException or UnknownTypeException (this depends on the order of the classes in your *.ps1).
I dug into the Types.ps1xml and created the following xml:
<Types>
<Type>
<Name>AcmeState</Name>
<TypeConverter>
<TypeName>StringToAcmeStateConverter</TypeName>
</TypeConverter>
</Type>
</Types>
and added TypesToProcess to the .psd1 pointing to the aforementioned ps1xml.
This will fail, stating the Converter is not known, which probably means, that PowerShell will process that file either in another context as the module or prior to loading the module.
To fix that problem, I removed the TypesToProcess from the .psd1 and added Update-TypeData Types.ps1xml as last line to my module, thus it will be run automatically during module import - and voila! This works.
TLDR:
If you want to register a TypeConverter defined in a PowerShell class, you need to use Update-TypeData

Related

powershell access sibling method

I'm having trouble accessing sibling methods in powershell
$group.search.has_member(search) is the same once it gets the members, but
$group.retrieve.Members() gets the members differently for 365 groups vs local groups
So I'm trying to have .search.has_member(search) call .retrieve.Members() to minimize code duplication (IRL there's around 15 different versions some with unique retrieval methods, some with shared approaches). But I'm having trouble accessing the sibling's methods
My main goal is I'm trying to build an interface facade that will unify how you interact with Exchange mail objects. 365 mailboxes, local mailboxes, 365 groups, and local groups (among others) all can have Members, MemberOf, Email Addresses and Distinguished Name, but while DN is a property on all objects, Email addresses property is formatted differently on 365 vs local groups (which affects .MatchesSMTP($search)), and retrieving Members() is different for 365 groups vs local groups and should return null for mailboxes and mail contacts regardless of whether it's 365 or local, and retrieval of MemberOf is unique for each object type.
This leads to a significant level of complexity. Originally I was trying to break them out using inheritance based first on Type(mailbox vs group) then on Server(mailbox_365, mailbox_local, etc), but I ended up with too much duplicated code. Same issue when swapping the order (365_mailbox, 365_group, etc).
So now I'm trying to implement abstractions based on behaviors to better share code when possible, and select style/strategy separately for versions that need a more unique approach. But the .Searches are having trouble accessing the .Retrievals.
I guess I could pass the retrievals as a parameter to the searches constructor like I am for config, but that approach doesn't scale well if I start ending up with more behavior categories with interlaced dependencies. Also if I can get the sibling access working I can stop passing config as a parameter and just access them directly, which should clean up the code a bit more.
Below is a reduced example for reference (yes, despite it's complexity it's very reduced, the final version has around 26 classes last time I counted)
Class Mail_Factory
{
static [Mail_Interface] BuildExample1()
{
$mail_object = "Pretend 365 group"
return [Mail_Factory]::Build($mail_object)
}
static [Mail_Interface] BuildExample2()
{
$mail_object = "Pretend Local Group"
return [Mail_Factory]::Build($mail_object)
}
static [Mail_Interface] Build($mail_object)
{
[Mail_Interface] $interface = [Mail_Interface]::new()
$interface.config = [Mail_Config]::new($mail_object)
$interface.retrieve = [Mail_Retrieve]::new($interface.config)
$interface.search = [Mail_Search]::new($interface.config)
[Mail_Retrieve_Members_Style] $members_style = switch ($mail_object)
{
("Pretend 365 group") { [Mail_Retrieve_Members_365Group]::new($interface.config) }
("Pretend Local Group") { [Mail_Retrieve_Members_LocalGroup]::new($interface.config) }
}
# notice that we're setting a specific retreival strategy for "members" depending on object type
$interface.config.members_style = $members_style
return $interface
}
}
Class Mail_Interface
{
Mail_Interface(){}
[Mail_Config] $config
[Mail_Retrieve] $retrieve # This is a facade to unify the way we call different kinds of retreivals (directly from settings, derived from settings, shared based on type, or unique to a specific combination of settings)
[Mail_Search] $search # This is a facade for the same reasons as $retreive
[bool] has_member($search) {return $this.search.has_member($search)}
[object] members() {return #($this.retrieve.members())}
}
Class Mail_Config
{
Mail_Config($mail_object) {$this.mail_object = $mail_object}
[Mail_Retrieve_Members_Style] $members_style # set by factory
[object] $mail_object
}
Class Mail_Retrieve
{
Mail_Retrieve($config){$this.config = $config}
[Mail_Config] $config
[object] Members(){return $this.config.members_style.Members()}
}
Class Mail_Retrieve_Members_Style
{
Mail_Retrieve_Members_Style($config){$this.config = $config}
[Mail_Config] $config
[object] Members(){throw("Syle not yet selected")}
}
Class Mail_Retrieve_Members_365Group : Mail_Retrieve_Members_Style
{
Mail_Retrieve_Members_365Group($config) : base($config) {}
# inherited: [Mail_Config] $config
[object] Members(){return "member of 365 group"}
}
Class Mail_Retrieve_Members_LocalGroup : Mail_Retrieve_Members_Style
{
Mail_Retrieve_Members_LocalGroup($config) : base($config) {}
# inherited: [Mail_Config] $config
[object] Members(){return "member of local group"}
}
Class Mail_Search
{
Mail_Search($config){$this.config = $config}
[Mail_Config] $config
[bool] has_member($search)
{
$members = $this.parent.retrieve.members() # !!! Problem exists here !!!
if ($members -contains "$search") {return $true}
else {return $false}
}
}
function TestExample1()
{
# from
# $a.search.has_member()
# we're trying to access
# $a.retrieve.Members()
$a = [Mail_Factory]::BuildExample1()
$member_a = $a.members()[0]
if ($a.has_member($member_a))
{"Success!"}
else
{"Failed, member match incorrect"}
}
function TestExample2()
{
# from
# $b.search.has_member()
# we're trying to access
# $b.retrieve.Members()
$b = [Mail_Factory]::BuildExample2()
$member_b = $b.members()[0]
$b.has_member($member_b)
if ($b.has_member($member_b))
{"Success!"}
else
{"Failed, member match incorrect"}
}
$result_a = TestExample1
$result_b = TestExample2
$result_a
$result_b
This spits out the following error (I've marked the line with !!! Problem exists here !!!)
You cannot call a method on a null-valued expression.
At line:88 char:9
+ $members = $this.super.retrieve.members() # !!! Problem exist ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
I have full control over this project and currently I'm open to completely refactoring to a different approach if I'm going about this the wrong way. I'm comfortable using chained constructors especially if they improve readability.
And I've been exploring design patterns on G4G, but my experience with them is still neophytic
I ended up replacing the reference to Config with a reference to the parent (Interface). While this opens the possibility of accidental recursion, this appears to be the only way to do it (that I've yet found)
I also flattened the methods, removing .search and .retrieve as they turned out not to be needed for now.
Class Mail_Factory
{
static [Mail_Interface] BuildExample1()
{
$mail_object = "Pretend 365 group"
return [Mail_Factory]::Build($mail_object)
}
static [Mail_Interface] BuildExample2()
{
$mail_object = "Pretend Local Group"
return [Mail_Factory]::Build($mail_object)
}
static [Mail_Interface] Build($mail_object)
{
[Mail_Interface] $interface = [Mail_Interface]::new()
$interface.config = [Mail_Config]::new($mail_object)
#$interface.retrieve = [Mail_Retrieve]::new($interface)
#$interface.search = [Mail_Search]::new($interface)
[Mail_Members_Style] $members_style = switch ($mail_object)
{
("Pretend 365 group") { [Mail_Members_365Group]::new($interface) }
("Pretend Local Group") { [Mail_Members_LocalGroup]::new($interface) }
}
# notice that we're setting a specific retreival strategy for "members" depending on object type
$interface.members_style = $members_style
$interface.has_member_style = [Mail_HasMember_Default]::new($interface)
return $interface
}
}
Class Mail_Interface
{
Mail_Interface(){}
[Mail_Config] $config
# set by factory #
[Mail_HasMember_Style]$has_member_style
[Mail_Members_Style]$members_style
# set by factory #
[bool] HasMember($search) {return $this.has_member_style.HasMember($search)}
[object] members() {return #($this.members_style.Members())}
}
Class Mail_Config
{
Mail_Config($mail_object) {$this.mail_object = $mail_object}
# IRL we store a lot more in here
[object] $mail_object
}
Class Mail_Members_Style
{
Mail_Members_Style($interface){$this.interface = $interface}
[Mail_Interface] $interface
[object] Members(){throw("Syle not yet selected")}
}
Class Mail_Members_365Group : Mail_Members_Style
{
Mail_Members_365Group($interface) : base($interface) {}
# inherited: [Mail_Interface] $interface
[object] Members(){return "member of 365 group"}
}
Class Mail_Members_LocalGroup : Mail_Members_Style
{
Mail_Members_LocalGroup($interface) : base($interface) {}
# inherited: [Mail_Interface] $interface
[object] Members(){return "member of local group"}
}
Class Mail_HasMember_Style
{
Mail_HasMember_Style($interface){$this.interface = $interface}
[Mail_Interface] $interface
[bool] HasMember($search){throw("Syle not yet selected")}
}
Class Mail_HasMember_Default : Mail_HasMember_Style
{
Mail_HasMember_Default($interface) : base($interface) {}
# inherited: [Mail_Interface] $interface
[bool] HasMember($search)
{
$members = $this.interface.members()
if ($members -contains "$search") {return $true}
else {return $false}
}
}
function TestExample1()
{
# from
# $a.has_member_style.has_member()
# we're trying to access
# $a.members_style.Members()
$a = [Mail_Factory]::BuildExample1()
$member_a = $a.members()[0]
if ($a.HasMember($member_a))
{"Success!"}
else
{"Failed, member match incorrect"}
}
function TestExample2()
{
# from
# $b.has_member_style.hasmember()
# we're trying to access
# $b.members_style.Members()
$b = [Mail_Factory]::BuildExample2()
$member_b = $b.members()[0]
if ($b.HasMember($member_b))
{"Success!"}
else
{"Failed, member match incorrect"}
}
function TestExample3()
{
# Verifying HasMember is actually checking stuff
$b = [Mail_Factory]::BuildExample1()
if ($b.HasMember("Not A Member"))
{"Failed, HasMember is incorrectly returning true"}
else
{"Success!"}
}
$result_a = TestExample1
$result_b = TestExample2
$result_c = TestExample2
$result_a
$result_b
$result_c
And the test results
Success!
Success!
Success!

Accept certificate permanently during FtpWebRequest via PowerShell

Recently I encounter some problems making the connection to a FTP server but there will be some popup asking for the acceptance on the certificate.
I don't know how to overcome this via PowerShell during invoke method $ftpRequest.GetResponse(). I found some solution regarding overriding the callback method on certificate like this one [System.Net.ServicePointManager]::ServerCertificateValidationCallback
The solution is given on C# & I don't know how to port it to PowerShell yet.
My code is as below
function Create-FtpDirectory {
param(
[Parameter(Mandatory=$true)]
[string]
$sourceuri,
[Parameter(Mandatory=$true)]
[string]
$username,
[Parameter(Mandatory=$true)]
[string]
$password
)
if ($sourceUri -match '\\$|\\\w+$') { throw 'sourceuri should end with a file name' }
$ftprequest = [System.Net.FtpWebRequest]::Create($sourceuri);
Write-Information -MessageData "Create folder to store backup (Get-FolderName -Path $global:backupFolder)"
$ftprequest.Method = [System.Net.WebRequestMethods+Ftp]::MakeDirectory
$ftprequest.UseBinary = $true
$ftprequest.Credentials = New-Object System.Net.NetworkCredential($username,$password)
$ftprequest.EnableSsl = $true
$response = $ftprequest.GetResponse();
Write-Host "Folder created successfully, status $response.StatusDescription"
$response.Close();
}
[UPDATED] While searching for Invoke-RestRequest, I found this solution from Microsoft example
Caution: this is actually accept ANY Certificate
# Next, allow the use of self-signed SSL certificates.
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $True }
More information (thanks to #Nimral) : https://learn.microsoft.com/en-us/dotnet/api/system.net.servicepointmanager.servercertificatevalidationcallback?view=netcore-3.1
It's a bit hacky, but you can use raw C# in PowerShell via Add-Type. Here's an example class I've used to be able to toggle certificate validation in the current PowerShell session.
if (-not ([System.Management.Automation.PSTypeName]'CertValidation').Type)
{
Add-Type #"
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
public class CertValidation
{
static bool IgnoreValidation(object o, X509Certificate c, X509Chain ch, SslPolicyErrors e) {
return true;
}
public static void Ignore() {
ServicePointManager.ServerCertificateValidationCallback = IgnoreValidation;
}
public static void Restore() {
ServicePointManager.ServerCertificateValidationCallback = null;
}
}
"#
}
Then you can use it prior to calling your function like this.
[CertValidation]::Ignore()
And later, restore default cert validation like this.
[CertValidation]::Restore()
Keep in mind though that it's much safer to just fix your service's certificate so that validation actually succeeds. Ignoring certificate validation should be your last resort if you have no control over the environment.

Calling AppDomain.DoCallback from Powershell

This is based on the Stack Overflow question: How to load an assembly as reflection-only in a new AppDomain?
I am attempting to determine the runtime version of an assembly, but that assembly could be loaded multiple times as I traverse through nested folders. Loading the assembly directly using
[Reflection.Assembly]::ReflectionOnlyLoadFrom($assembly)
will therefore not work, as the assembly can only be loaded once in the app-domain.
Given the following function to load an assembly in a separate AppDomain:
function Load-AssemblyInNewAppDomain($assembly)
{
Write-Host $assembly.FullName
$domain = [AppDomain]::CreateDomain([Guid]::NewGuid())
$domain.DoCallback
({
$loaded = [Reflection.Assembly]::Load($assembly)
$runtime = $loaded.ImageRuntimeVersion
Write-Host $runtime
})
}
This outputs the contents of the delegate to the console, rather than executing it:
OverloadDefinitions
-------------------
void DoCallBack(System.CrossAppDomainDelegate callBackDelegate)
void _AppDomain.DoCallBack(System.CrossAppDomainDelegate theDelegate)
$loaded = [Reflection.Assembly]::Load($assembly)
$runtime = $loaded.ImageRuntimeVersion
Write-Host $runtime
Note that the results are the same, whether I use PowerShell 4 or 5
Any help/guidance appreciated
First thought: don't muck around with AppDomains at all and use a completely separate process. Those are (relatively) easily launched from PowerShell, at least. The drawback is that it's potentially much slower if you're doing this for lots of files.
$myAssemblyPath = "C:\..."
$getImageRuntimeVersion = {
[Reflection.Assembly]::ReflectionOnlyLoadFrom($input).ImageRuntimeVersion
}
$encodedCommand = [Convert]::ToBase64String(
[Text.Encoding]::Unicode.GetBytes($getImageRuntimeVersion)
)
$imageRuntimeVersion = $myAssemblyPath | powershell -EncodedCommand $encodedCommand
So, is there no way at all to do this with AppDomains in PowerShell? Well, there is, but it's not pretty. You can't use AppDomain.DoCallBack because, as you've discovered, PowerShell can't remote delegates that way (because, under the covers, it produces dynamic methods).
However, it's easy to host the PowerShell runtime, and all PowerShell objects know how to serialize (a requirement for cross-domain remoting), so invoking a PowerShell script in another AppDomain is fairly simple (but still ugly):
$scriptInvokerAssembly = [System.IO.Path]::GetTempFileName() + ".dll"
Add-Type -OutputAssembly $tempAssembly -TypeDefinition #"
using System;
using System.Reflection;
using System.Collections.Generic;
using System.Management.Automation;
public class ScriptInvoker : MarshalByRefObject {
public IEnumerable<PSObject> Invoke(ScriptBlock scriptBlock, PSObject[] parameters) {
using (var powerShell = PowerShell.Create()) {
powerShell.Commands.AddScript(scriptBlock.ToString());
if (parameters != null) {
powerShell.AddParameters(parameters);
}
return powerShell.Invoke();
}
}
}
"#
[Reflection.Assembly]::LoadFile($scriptInvokerAssembly) | Out-Null
Function Invoke-CommandInTemporaryAppDomain([ScriptBlock] $s, [object[]] $arguments) {
$setup = New-Object System.AppDomainSetup
$setup.ApplicationBase = Split-Path ([ScriptInvoker].Assembly.Location) -Parent
$domain = [AppDomain]::CreateDomain([Guid]::NewGuid(), $null, $setup)
$scriptInvoker = $domain.CreateInstanceAndUnwrap(
[ScriptInvoker].Assembly.FullName, [ScriptInvoker]
);
$scriptInvoker.Invoke($s, $arguments)
[AppDomain]::Unload($domain)
}
And now you can do
Invoke-CommandInTemporaryAppDomain {
[Reflection.Assembly]::ReflectionOnlyLoadFrom($args[0]).ImageRuntimeVersion
} $myAssemblyPath
Note that we have to generate a temporary assembly on disk and have AppDomain load it from there. This is ugly, but you can't have Add-Type produce an in-memory assembly, and even if you do end up with a byte[] getting that to load in another AppDomain is anything but trivial because you can't hook AppDomain.AssemblyResolve in PowerShell. If this command was packaged in a module, you'd compile the assembly containing the ScriptInvoker ahead of time, so I don't see working around this as a priority.
You can't run DoCallback via powershell alone. But DoCallBack does work with some inline C#. As Jeroen says it's ugly, but this works:
$assm = "C:\temp\so\bin\dynamic-assembly.dll"
Add-Type -TypeDefinition #"
using System.Reflection;
using System;
namespace Example
{
public class AppDomainUtil
{
public void LoadInAppDomain(AppDomain childDomain, string assemblyName)
{
childDomain.SetData("assemblyName", assemblyName);
childDomain.DoCallBack( new CrossAppDomainDelegate(LoadAssembly)) ;
}
public static void LoadAssembly()
{
string assemblyName = (string)AppDomain.CurrentDomain.GetData("assemblyName");
// console not available from another domain
string log = "c:\\temp\\hello.txt";
System.IO.File.WriteAllText(log, string.Format("Hello from {0}\r\n",AppDomain.CurrentDomain.FriendlyName));
System.IO.File.AppendAllText(log, string.Format("Assembly to load is {0}\r\n",assemblyName));
Assembly loaded = Assembly.Load(assemblyName);
System.IO.File.AppendAllText(log, string.Format("Assemblyloaded: {0}\r\n",loaded.FullName));
}
}
}
"# -OutputAssembly $assm -OutputType Library # must set output assembly otherwise assembly generated in-memory and it will break with Type errors.
Add-Type -Path $assm
function Load-AssemblyInNewAppDomain([string]$assembly) {
Write-Host "Parent domain: $([AppDomain]::CurrentDomain.FriendlyName)"
$util = New-Object Example.AppDomainUtil
$ads = New-Object System.AppDomainSetup
$cd = [AppDomain]::CurrentDomain
# set application base
$ads.ApplicationBase = [IO.path]::GetDirectoryName( $assm )
[System.AppDomain]$newDomain = [System.AppDomain]::CreateDomain([System.Guid]::NewGuid().ToString(), $null, $ads);
Write-Host "Created child domain: $($newDomain.FriendlyName)"
$util.LoadInAppDomain($newDomain, $assembly)
}
Testing it out:
PS C:\WINDOWS\system32> Load-AssemblyInNewAppDomain "".GetType().Assembly.FullName
Parent domain: PowerShell_ISE.exe
Created child domain: 61ab2dbb-8b33-4e7e-84db-5fabfded53aa
PS C:\WINDOWS\system32> cat C:\temp\hello.txt
Hello from 61ab2dbb-8b33-4e7e-84db-5fabfded53aa
Assembly to load is mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
Assemblyloaded: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

PowerShell - InvalidCastException when returning Boolean to explicitly declared variable [duplicate]

This question already has answers here:
Function return value in PowerShell
(10 answers)
Closed 5 years ago.
I've written a PowerShell script to perform some pre-installation setup for a series of patches I'm deploying to client computers across our estate and I'm hitting a bit of an odd issue that I can't wrap my head around.
The setup patch checks the 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe.config' file due to a "feature" of PowerShell 2.0 whereby the application uses .NET Framework 2.0.0 by default instead of 4.5.2, preventing certain functions from being executed. If the file doesn't exist or the evaluated values don't match a specification, I add the XML file and provide the necessary values.
The command I run is as follows:
$psConfigDir = "C:\Windows\System32\WindowsPowerShell\v1.0"
$psConfigFileName = "powershell.exe.config"
[boolean]$psExeXml = Set-PSRuntimeConfigs -FilePath ( [String]::Format("{0}\{1}", $psConfigDir, $psConfigFileName) ) -CLRVersions #("v4.0.30319", "v2.0.50727")
...and the Set-PSRuntimeConfigs method is found in a PowerShell Module I created with the code below:
Function Set-PSRuntimeConfigs {
[CmdletBinding()]
Param(
[String]$FilePath,
[System.Collections.ArrayList]$CLRVersions
)
Try {
$xmlWriter = New-Object System.Xml.XmlTextWriter($FilePath, $null)
$xmlWriter.Formatting = "Indented"
$xmlWriter.Indentation = 4
$xmlWriter.WriteStartDocument()
$xmlWriter.WriteStartElement("configuration")
$xmlWriter.WriteStartElement("startup")
$xmlWriter.WriteAttributeString("useLegacyV2RuntimeActivationPolicy", $true)
$CLRVersions | ForEach-Object {
$xmlWriter.WriteStartElement("supportedRuntime")
$xmlWriter.WriteAttributeString("version", $_)
$xmlWriter.WriteEndElement()
}
$xmlWriter.WriteEndElement()
$xmlWriter.WriteEndElement()
$xmlWriter.WriteEndDocument()
$xmlWriter.Close()
$xmlWriter.Dispose()
return $true
} Catch {
echo "ERROR: Exception occurred during XML write process!"
echo "ERROR: Exception message: $($_.Exception.Message)"
return $false
}
}
However, the function is returning an InvalidCastException when trying to assign the result of the function to the $psExeXml variable. Oddly, PowerShell returns with an error stating that [System.Object()] cannot be converted to type [Boolean] despite the fact that only $true or $false is returned from the function.
My first thought is that an exception was being thrown by the function due to a code issue but the function is written to report the error in the prompt and just return $false in that case... Regardless, I'm stuck and can't figure out where to proceed with this...
If the function produces any output then the result will be an array containing the strings that were output and then the final element will be your boolean.
So for this code:
echo "ERROR: Exception occurred during XML write process!"
echo "ERROR: Exception message: $($_.Exception.Message)"
return $false
the function returns an array of two strings and a boolean.

Get Tfs Shelveset file contents at the command prompt?

I'm interested in getting the contents of a shelveset at the command prompt. Now, you would think that a cmdlet such as Get-TfsShelveset, available in the TFS Power Tools, would do this. You might also think that "tf.exe shelvesets" would do this.
However, unless I've missed something, I'm appalled to report that neither of these is the case. Instead, each command requires you to give it a shelveset name, and then simply regurgitates a single line item for that shelveset, along with some metadata about the shelveset such as creationdate, displayname, etc. But as far as I can tell, no way to tell what's actually in the shelf.
This is especially heinous for Get-TfsShelveset, which has the ability to include an array of file descriptors along with the Shelveset object it returns. I even tried to get clever, thinking that I could harvest the file names from using -WhatIf with Restore-TfsShelveset, but sadly Restore-TfsShelveset doesn't implement -WhatIf.
Please, someone tell me I'm wrong about this!
tf status /shelveset:name
will list out the content of the named shelveset (you can also supplier an owner: see tf help status).
With the TFS PowerToy's PowerShell snapin:
Get-TfsPendingChange -Shelveset name
for the same information.
It is possible to construct a small command-line application that uses the TFS SDK, which returns the list of files contained in a given shelveset.
The sample below assumes knowledge of the Shelveset name & it's owner:
using System;
using System.IO;
using System.Collections.ObjectModel;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Common;
using Microsoft.TeamFoundation.Framework.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
namespace ShelvesetDetails
{
class Program
{
static void Main(string[] args)
{
Uri tfsUri = (args.Length < 1) ? new Uri("TFS_URI") : new Uri(args[0]);
TfsConfigurationServer configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(tfsUri);
ReadOnlyCollection<CatalogNode> collectionNodes = configurationServer.CatalogNode.QueryChildren(
new[] { CatalogResourceTypes.ProjectCollection },
false, CatalogQueryOptions.None);
CatalogNode collectionNode = collectionNodes[0];
Guid collectionId = new Guid(collectionNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection teamProjectCollection = configurationServer.GetTeamProjectCollection(collectionId);
var vcServer = teamProjectCollection.GetService<VersionControlServer>();
Shelveset[] shelves = vcServer.QueryShelvesets(
"SHELVESET_NAME", "SHELVESET_OWNER");
Shelveset shelveset = shelves[0];
PendingSet[] sets = vcServer.QueryShelvedChanges(shelveset);
foreach (PendingSet set in sets)
{
PendingChange[] changes = set.PendingChanges;
foreach (PendingChange change in changes)
{
Console.WriteLine(change.FileName);
}
}
}
}
}
Invoking this console app & catching the outcome during execution of the powershell should be possible.
Try:
tfpt review
/shelveset:shelvesetName;userName
You may also need to add on the server option so something like:
tfpt review /shelveset:Code Review;jim
/sever:company-source
I think this is what you are looking for.
This is what I ended up with, based on pentelif's code and the technique in the article at http://akutz.wordpress.com/2010/11/03/get-msi/ linked in my comment.
function Get-TfsShelvesetItems
{
[CmdletBinding()]
param
(
[string] $ShelvesetName = $(throw "-ShelvesetName must be specified."),
[string] $ShelvesetOwner = "$env:USERDOMAIN\$env:USERNAME",
[string] $ServerUri = $(throw "-ServerUri must be specified."),
[string] $Collection = $(throw "-Collection must be specified.")
)
$getShelvesetItemsClassDefinition = #'
public IEnumerable<PendingChange> GetShelvesetItems(string shelvesetName, string shelvesetOwner, string tfsUriString, string tfsCollectionName)
{
Uri tfsUri = new Uri(tfsUriString);
TfsConfigurationServer configurationServer = TfsConfigurationServerFactory.GetConfigurationServer(tfsUri);
ReadOnlyCollection<CatalogNode> collectionNodes = configurationServer.CatalogNode.QueryChildren( new[] { CatalogResourceTypes.ProjectCollection }, false, CatalogQueryOptions.None);
CatalogNode collectionNode = collectionNodes.Where(node => node.Resource.DisplayName == tfsCollectionName).SingleOrDefault();
Guid collectionId = new Guid(collectionNode.Resource.Properties["InstanceId"]);
TfsTeamProjectCollection teamProjectCollection = configurationServer.GetTeamProjectCollection(collectionId);
var vcServer = teamProjectCollection.GetService<VersionControlServer>();
var changes = new List<PendingChange>();
foreach (Shelveset shelveset in vcServer.QueryShelvesets(shelvesetName, shelvesetOwner))
{
foreach (PendingSet set in vcServer.QueryShelvedChanges(shelveset))
{
foreach ( PendingChange change in set.PendingChanges )
{
changes.Add(change);
}
}
}
return changes.Count == 0 ? null : changes;
}
'#;
$getShelvesetItemsType = Add-Type `
-MemberDefinition $getShelvesetItemsClassDefinition `
-Name "ShelvesetItemsAPI" `
-Namespace "PowerShellTfs" `
-Language CSharpVersion3 `
-UsingNamespace System.IO, `
System.Linq, `
System.Collections.ObjectModel, `
System.Collections.Generic, `
Microsoft.TeamFoundation.Client, `
Microsoft.TeamFoundation.Framework.Client, `
Microsoft.TeamFoundation.Framework.Common, `
Microsoft.TeamFoundation.VersionControl.Client `
-ReferencedAssemblies "C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll", `
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Common.dll", `
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.VersionControl.Client.dll" `
-PassThru;
# Initialize an instance of the class.
$getShelvesetItems = New-Object -TypeName "PowerShellTfs.ShelvesetItemsAPI";
# Emit the pending changes to the pipeline.
$getShelvesetItems.GetShelvesetItems($ShelvesetName, $ShelvesetOwner, $ServerUri, $Collection);
}
Spent a few days trying to do this as well, this always popped up on google so here is what I found to help future generations:
To get the contents of the shelveset (at least with Team Explorer Everywhere),
use the command: tf difference /shelveset:<Shelveset name>
That will print out the contents of the shelveset and give filenames in the form :
<Changetype>: <server file path>; C<base change number>
Shelved Change: <server file path again>;<shelveset name>
So if your file is contents/test.txt
in the shelveset shelve1 (with base revision 1), you will see :
edit: $/contents/file.txt;C1
Shelved Change: $/contents/file.txt;shelve1
After that, using the tf print command
(or view if not using TEE) on $/contents/file.txt;shelve1 should get you the contents :
tf print $/contents/file.txt;shelve1
Shows you what is in the file.txt in shelveset shelve1
If you want get shelveset changes from server by using tfs command
Using power shell:
Get-TfsPendingChange -Server http://example.com/org -Shelveset shelvsetName
Using vs commands:
c:\projects>tf shelvesets BuddyTest_23
more info about this please see here
https://learn.microsoft.com/en-us/azure/devops/repos/tfvc/shelvesets-command?view=azure-devops