Démarrage du script .ps1 à partir de PowerShell avec paramètres et informations d'identification et obtention d'une sortie à l'aide d'une variable


10

Hello Stack Community :)

J'ai un objectif simple. Je voudrais démarrer un script PowerShell à partir d'un autre script Powershell, mais il y a 3 conditions:

  1. Je dois transmettre des informations d'identification (l'exécution se connecte à une base de données qui a un utilisateur spécifique)
  2. Il doit prendre certains paramètres
  3. Je voudrais passer la sortie dans une variable

Il y a une question similaire Lien . Mais la réponse est d'utiliser des fichiers comme moyen de communication entre 2 scripts PS. Je voudrais juste éviter les conflits d'accès. @Update: Le script principal va démarrer quelques autres scripts. donc la solution avec les fichiers peut être délicate, si l'exécution sera effectuée à partir de plusieurs utilisateurs en même temps.

Script1.ps1 est le script qui doit avoir une chaîne en sortie. (Juste pour être clair, c'est un script fictif, le vrai a 150 lignes, donc je voulais juste faire un exemple)

param(  
[String]$DeviceName
)
#Some code that needs special credentials
$a = "Device is: " + $DeviceName
$a

ExecuteScripts.ps1 devrait invoquer celui-là avec ces 3 conditions mentionnées ci-dessus

J'ai essayé plusieurs solutions. Celui-ci par exemple:

$arguments = "C:\..\script1.ps1" + " -ClientName" + $DeviceName
$output = Start-Process powershell -ArgumentList $arguments -Credential $credentials
$output 

Je ne reçois aucune sortie de cela et je ne peux pas simplement appeler le script avec

&C:\..\script1.ps1 -ClientName PCPC

Parce que je ne peux pas lui passer de -Credentialparamètre ..

Merci d'avance!


S'il s'agit simplement de conflits d'accès: la création de noms de fichiers uniques pour chaque appel résoudrait votre problème, non?
mklement0

1
@ mklement0 si c'est le seul moyen, j'empilerais avec cette solution. Générer simplement des noms de fichiers aléatoires, vérifier si un tel fichier existe ... J'exécuterai 6 à 10 scripts à partir de mon code Java et il faudrait 6 à 10 fichiers chaque fois que j'utilise ou que quelqu'un d'autre utilise mon application. Donc, c'est aussi une question de performance
Dmytro

Réponses:


2

Remarque:

  • La solution suivante fonctionne avec tout programme externe et capture invariablement la sortie sous forme de texte .

  • Pour appeler une autre instance PowerShell et capturer sa sortie en tant qu'objets riches (avec des limitations), consultez la solution variante dans la section inférieure ou considérez la réponse utile de Mathias R. Jessen , qui utilise le SDK PowerShell .

Voici une preuve de concept basée sur l'utilisation directe des types System.Diagnostics.Processet System.Diagnostics.ProcessStartInfo.NET pour capturer la sortie du processus en mémoire (comme indiqué dans votre question, Start-Processn'est pas une option, car il prend uniquement en charge la capture de la sortie dans des fichiers , comme indiqué dans cette réponse ) :

Remarque:

  • En raison de l'exécution en tant qu'utilisateur différent, cela est pris en charge sous Windows uniquement (à partir de .NET Core 3.1), mais dans les deux éditions PowerShell.

  • En raison de la nécessité d'exécuter en tant qu'utilisateur différent et de capturer la sortie, .WindowStylene peut pas être utilisé pour exécuter la commande cachée (car l'utilisation de .WindowStylerequire .UseShellExecuteest $true, ce qui est incompatible avec ces exigences); cependant, étant donné que toutes les sorties sont capturées , le réglage .CreateNoNewWindowsur $trueaboutit effectivement à une exécution cachée.

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # For demo purposes, use a simple `cmd.exe` command that echoes the username. 
  # See the bottom section for a call to `powershell.exe`.
  FileName = 'cmd.exe'
  Arguments = '/c echo %USERNAME%'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # (user@doamin.com), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output.
# By reading to the *end*, this implicitly waits for (near) termination
# of the process.
# Do NOT use $ps.WaitForExit() first, as that can result in a deadlock.
$stdout = $ps.StandardOutput.ReadToEnd()

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

"Running ``cmd /c echo %USERNAME%`` as user $($cred.UserName) yielded:"
$stdout

Ce qui précède donne quelque chose comme ce qui suit, montrant que le processus s'est correctement déroulé avec l'identité d'utilisateur donnée:

Running `cmd /c echo %USERNAME%` as user jdoe yielded:
jdoe

Puisque vous appelez une autre instance PowerShell , vous souhaiterez peut- être profiter de la capacité de la CLI PowerShell à représenter la sortie au format CLIXML, ce qui permet de désérialiser la sortie en objets riches , bien qu'avec une fidélité de type limitée , comme expliqué dans cette réponse associée .

# Get the target user's name and password.
$cred = Get-Credential

# Create a ProcessStartInfo instance
# with the relevant properties.
$psi = [System.Diagnostics.ProcessStartInfo] @{
  # Invoke the PowerShell CLI with a simple sample command
  # that calls `Get-Date` to output the current date as a [datetime] instance.
  FileName = 'powershell.exe'
  # `-of xml` asks that the output be returned as CLIXML,
  # a serialization format that allows deserialization into
  # rich objects.
  Arguments = '-of xml -noprofile -c Get-Date'
  # Set this to a directory that the target user
  # is permitted to access.
  WorkingDirectory = 'C:\'                                                                   #'
  # Ask that output be captured in the
  # .StandardOutput / .StandardError properties of
  # the Process object created later.
  UseShellExecute = $false # must be $false
  RedirectStandardOutput = $true
  RedirectStandardError = $true
  # Uncomment this line if you want the process to run effectively hidden.
  #   CreateNoNewWindow = $true
  # Specify the user identity.
  # Note: If you specify a UPN in .UserName
  # (user@doamin.com), set .Domain to $null
  Domain = $env:USERDOMAIN
  UserName = $cred.UserName
  Password = $cred.Password
}

# Create (launch) the process...
$ps = [System.Diagnostics.Process]::Start($psi)

# Read the captured standard output, in CLIXML format,
# stripping the `#` comment line at the top (`#< CLIXML`)
# which the deserializer doesn't know how to handle.
$stdoutCliXml = $ps.StandardOutput.ReadToEnd() -replace '^#.*\r?\n'

# Uncomment the following lines to report the process' exit code.
#   $ps.WaitForExit()
#   "Process exit code: $($ps.ExitCode)"

# Use PowerShell's deserialization API to 
# "rehydrate" the objects.
$stdoutObjects = [Management.Automation.PSSerializer]::Deserialize($stdoutCliXml)

"Running ``Get-Date`` as user $($cred.UserName) yielded:"
$stdoutObjects
"`nas data type:"
$stdoutObjects.GetType().FullName

Ce qui précède génère quelque chose comme ce qui suit, montrant que l' [datetime]instance ( System.DateTime) sortie par a Get-Dateété désérialisée en tant que telle:

Running `Get-Date` as user jdoe yielded:

Friday, March 27, 2020 6:26:49 PM

as data type:
System.DateTime

5

Start-Processserait mon dernier choix pour invoquer PowerShell à partir de PowerShell - en particulier parce que toutes les E / S deviennent des chaînes et non des objets (désérialisés).

Deux alternatives:

1. Si l'utilisateur est un administrateur local et que PSRemoting est configuré

Si une session à distance contre la machine locale (malheureusement limitée aux administrateurs locaux) est une option, j'irais certainement avec Invoke-Command:

$strings = Invoke-Command -FilePath C:\...\script1.ps1 -ComputerName localhost -Credential $credential

$strings contiendra les résultats.


2. Si l'utilisateur n'est pas un administrateur sur le système cible

Vous pouvez écrire votre propre "local uniquement Invoke-Command" en faisant tourner un espace d'exécution hors processus en:

  1. Création d'un PowerShellProcessInstance, sous une connexion différente
  2. Création d'un runspace dans ledit processus
  3. Exécutez votre code dans ledit espace d'exécution hors processus

J'ai mis en place une telle fonction ci-dessous, voir les commentaires en ligne pour une procédure pas à pas:

function Invoke-RunAs
{
    [CmdletBinding()]
    param(
        [Alias('PSPath')]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        ${FilePath},

        [Parameter(Mandatory = $true)]
        [pscredential]
        [System.Management.Automation.CredentialAttribute()]
        ${Credential},

        [Alias('Args')]
        [Parameter(ValueFromRemainingArguments = $true)]
        [System.Object[]]
        ${ArgumentList},

        [Parameter(Position = 1)]
        [System.Collections.IDictionary]
        $NamedArguments
    )

    begin
    {
        # First we set up a separate managed powershell process
        Write-Verbose "Creating PowerShellProcessInstance and runspace"
        $ProcessInstance = [System.Management.Automation.Runspaces.PowerShellProcessInstance]::new($PSVersionTable.PSVersion, $Credential, $null, $false)

        # And then we create a new runspace in said process
        $Runspace = [runspacefactory]::CreateOutOfProcessRunspace($null, $ProcessInstance)
        $Runspace.Open()
        Write-Verbose "Runspace state is $($Runspace.RunspaceStateInfo)"
    }

    process
    {
        foreach($path in $FilePath){
            Write-Verbose "In process block, Path:'$path'"
            try{
                # Add script file to the code we'll be running
                $powershell = [powershell]::Create([initialsessionstate]::CreateDefault2()).AddCommand((Resolve-Path $path).ProviderPath, $true)

                # Add named param args, if any
                if($PSBoundParameters.ContainsKey('NamedArguments')){
                    Write-Verbose "Adding named arguments to script"
                    $powershell = $powershell.AddParameters($NamedArguments)
                }

                # Add argument list values if present
                if($PSBoundParameters.ContainsKey('ArgumentList')){
                    Write-Verbose "Adding unnamed arguments to script"
                    foreach($arg in $ArgumentList){
                        $powershell = $powershell.AddArgument($arg)
                    }
                }

                # Attach to out-of-process runspace
                $powershell.Runspace = $Runspace

                # Invoke, let output bubble up to caller
                $powershell.Invoke()

                if($powershell.HadErrors){
                    foreach($e in $powershell.Streams.Error){
                        Write-Error $e
                    }
                }
            }
            finally{
                # clean up
                if($powershell -is [IDisposable]){
                    $powershell.Dispose()
                }
            }
        }
    }

    end
    {
        foreach($target in $ProcessInstance,$Runspace){
            # clean up
            if($target -is [IDisposable]){
                $target.Dispose()
            }
        }
    }
}

Ensuite, utilisez comme ceci:

$output = Invoke-RunAs -FilePath C:\path\to\script1.ps1 -Credential $targetUser -NamedArguments @{ClientDevice = "ClientName"}

0

rcv.ps1

param(
    $username,
    $password
)

"The user is:  $username"
"My super secret password is:  $password"

exécution à partir d'un autre script:

.\rcv.ps1 'user' 'supersecretpassword'

production:

The user is:  user
My super secret password is:  supersecretpassword

1
Je dois transmettre des informations d'identification à cette exectuion ...
Dmytro

mis à jour les parties pertinentes.
thepip3r

Pour clarifier: l'intention n'est pas simplement de transmettre les informations d'identification, mais de s'exécuter en tant qu'utilisateur identifié par les informations d'identification.
mklement0

1
@ mklement0, merci pour la clarification car cela n'était pas du tout clair pour moi par les différentes itérations de la question posée.
thepip3r

0

Ce que vous pouvez faire, procédez comme suit pour passer un paramètre à un script ps1.

Le premier script peut être un origin.ps1 où l'on écrit:

& C:\scripts\dest.ps1 Pa$$w0rd parameter_a parameter_n

Le script de destination dest.ps1 peut avoir le code suivant pour capturer les variables

$var0 = $args[0]
$var1 = $args[1]
$var2 = $args[2]
Write-Host "my args",$var0,",",$var1,",",$var2

Et le résultat sera

my args Pa$$w0rd, parameter_a, parameter_n

1
L'objectif principal est de combiner toutes les conditions en une seule exécution. Je dois passer des paramètres et passer des informations d'identification!
Dmytro

Que voulez-vous dire par "combiner toutes les conditions en une seule exécution". Je ne pense pas que vous puissiez ajouter un paramètre avec le symbole "-" comme vous l'avez fait .. Je pense que vous devez reformater les chaînes du script de destination
Andy McRae

Je dois exécuter un fichier PS1 avec des paramètres et passer un -Credential $credentialsparamètre à cette exécution et en obtenir la sortie dans une variable. La ps1. L'exécution du script im lance une chaîne de mots simples à la fin. Regardez simplement comment je l'ai fait, Start-processmais cette fonction ne génère pas de sortie
Dmytro

Je pense que powershell ne vous permet pas de passer un paramètre comme celui-ci $ arguments = "C: \ .. \ script1.ps1" + "-ClientName" + $ DeviceName. Vous devriez probablement penser à supprimer le "-"
Andy McRae

1
cela étant dit. Start-Process exécute le script avec des paramètres et des informations d'identification, mais il n'enregistre pas cette sortie dans une variable. Si je tente d'accéder à une $outputvariable, c'est NULL. L'autre idée qui vient de @ mklement0 est d'enregistrer la sortie dans un fichier. Mais dans mon cas, cela entraînera une énorme quantité de fichiers en un seul endroit. Tous créés à partir d'utilisateurs différents avec des scripts différents
Dmytro
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.