4

I created the following script to reset the password of the local admin account on all machines in a specific host file. This script functions properly and provides a useful output, but it is slow as it only does one machine at a time.

# function to convert a secure string to a standard string
function ConvertTo-String {
    param(
        [System.Security.SecureString] $secureString
    )

    $marshal = [System.Runtime.InteropServices.Marshal]

    try {
        $intPtr = $marshal::SecureStringToBSTR($secureString)
        $string = $marshal::PtrToStringAuto($intPtr)
    }
    finally {
        if($intPtr) {
            $marshal::ZeroFreeBSTR($intPtr)
        }
    }
    $string
}

$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"

# prompt for password and confirm
do {
    $ss1 = Read-Host "Enter new password" -AsSecureString
    $ss2 = Read-Host "Enter again to confirm" -AsSecureString

# compare strings - proceed if same - prompt again if different
    $ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
    Write-Host "Passwords match"

    if(-not $ok) {
        Write-Host "Passwords do not match"
    }
}
until($ok)

# set password variable to string value
$adminPassword = ConvertTo-String $ss1

# setup job to reset password on each client
foreach($client in $clients) {
    $status = "OFFLINE"
    $isOnline = "OFFLINE"

    if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
        $isOnline = "ONLINE"
    }

    # change the password
    try {
        $localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
        $localAdminAccount.SetPassword($adminPassword)
        $localAdminAccount.SetInfo()
        Write-Verbose "Password change completed successfully"
    }
    catch {
        $status = "FAILED"
        Write-Verbose "Failed to change password"
    }

    # create psobject with system info
    $obj = New-Object -TypeName PSObject -Property @{
        ComputerName = $client
        Online = $isOnline
        ChangeStatus = $status
    }

    $obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append

    if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
        $stream.writeline("$client -t $status")
    }
}

$adminPassword = " "

Write-Verbose "Complete"

Invoke-Item C:\test.txt

To make the script run faster, I set it up to use background jobs so it could run on multiple clients at once. However, now I get no output in my text files. The new script is below. Lines 43 and 79-89 are the changes. What changes do I need to make to this script provide the output it does in the first version? I've tried a number of other ways to get the output than what I currently have on line 89.

# function to convert a secure string to a standard string
function ConvertTo-String {
    param(
        [System.Security.SecureString] $secureString
    )

    $marshal = [System.Runtime.InteropServices.Marshal]

    try {
        $intPtr = $marshal::SecureStringToBSTR($secureString)
        $string = $marshal::PtrToStringAuto($intPtr)
    }
    finally {
        if($intPtr) {
            $marshal::ZeroFreeBSTR($intPtr)
        }
    }
    $string
}

$clients = Get-Content -Path C:\scripts\utilities\hostnames_online.txt
$adminUser = "Administrator"

# prompt for password and confirm
do {
    $ss1 = Read-Host "Enter new password" -AsSecureString
    $ss2 = Read-Host "Enter again to confirm" -AsSecureString

# compare strings - proceed if same - prompt again if different
    $ok = (ConvertTo-String $ss1) -ceq (ConvertTo-String $ss2)
    Write-Host "Passwords match"

    if(-not $ok) {
        Write-Host "Passwords do not match"
    }
}
until($ok)

# set password variable to string value
$adminPassword = ConvertTo-String $ss1

# setup job to reset password on each client
#-----------------------------------------------------------------------------------------
$scriptBlock = {
#-----------------------------------------------------------------------------------------
    foreach($client in $clients) {
        $status = "OFFLINE"
        $isOnline = "OFFLINE"

        if((Test-Connection -ComputerName $client -Quiet -Count 1 -Delay 1) -eq $true) {
            $isOnline = "ONLINE"
        }

        # change the password
        try {
            $localAdminAccount = [adsi]"WinNT://$client/$adminuser,user"
            $localAdminAccount.SetPassword($adminPassword)
            $localAdminAccount.SetInfo()
            Write-Verbose "Password change completed successfully"
        }
        catch {
            $status = "FAILED"
            Write-Verbose "Failed to change password"
        }

        # create psobject with system info
        $obj = New-Object -TypeName PSObject -Property @{
            ComputerName = $client
            Online = $isOnline
            ChangeStatus = $status
        }

        $obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append

        if($status -eq "FAILED" -or $isOnline -eq "OFFLINE") {
            $stream.writeline("$client -t $status")
        }
    }
}
#-----------------------------------------------------------------------------------------
Get-Job | Remove-Job -Force

Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset

Get-Job

While(Get-Job -State "Running") {
    Start-Sleep -m 10
}

Receive-Job -name AdminPWReset | Out-File C:\test2.txt
#-----------------------------------------------------------------------------------------
$adminPassword = " "

Write-Verbose "Complete"

Invoke-Item C:\test.txt
Invoke-Item C:\test2.txt
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
cmorris14
  • 97
  • 1
  • 2
  • 7
  • There are no line numbers, could you add comments to go the code to show which lines you are talking about? – Eris Jan 08 '16 at 19:25
  • Where is `$stream` defined? If it's not defined, you can't write to it. – Eris Jan 08 '16 at 19:29
  • @Eris I added lines like this ------------------ in the affected areas. Also, you're right about the stream. I'll fix that, but the info from the first script that I really care about is - $obj | Select computerName, Online, changeStatus | Out-File -FilePath C:\test.txt -Append – cmorris14 Jan 08 '16 at 19:42

1 Answers1

3

You don't receive output, because you moved your entire loop into the scriptblock:

$scriptBlock = {
    foreach ($client in $clients) {
        ...
    }
}

but initialized your client list ($clients) outside the scriptblock, so that the variable $clients inside the scriptblock is a different (empty) variable since it's in a different scope. Because of that your job is iterating over an empty list, and is thus not producing any output.

To be able to use the client list inside the scriptblock you'd have to use the using: scope modifier:

$scriptBlock = {
    foreach ($client in $using:clients) {
        ...
    }
}

or pass the client list into the scriptblock as an argument:

$scriptBlock = {
    foreach ($client in $args[0]) {
        ...
    }
}

Start-Job -ScriptBlock $scriptBlock -ArgumentList $clients

As I can see from your code you're trying to do the latter:

Start-Job $scriptBlock -ArgumentList $_ -Name AdminPWReset

However, that doesn't work for two reasons:

  • In the context where you run Start-Job there is no current object, so $_ is empty and nothing is passed into the scriptblock.
  • The scriptblock neither has a Param() block, nor does it use the automatic variable $args, so even if you passed an argument into the scriptblock it would never be used.

With that said, you don't want to pass $clients in the first place, because even if it worked, it wouldn't speed up anything as the entire client list would still be processed sequentially. What you actually want to do is to process the list in parallel. For that you must put just the tests into the scriptblock and then start one job for each client in a loop:

$scriptBlock = {
    Param($client)

    $status = "OFFLINE"
    $isOnline = "OFFLINE"

    if (Test-Connection -Computer $client -Quiet -Count 1 -Delay 1) {
        $isOnline = "ONLINE"
    }
    ...
}

foreach ($client in $clients) {
    Start-Job -Name AdminPWReset -ScriptBlock $scriptBlock -ArgumentList $client
}

while (Get-Job -State "Running") {
    Start-Sleep -Milliseconds 100
}

Receive-Job -Name AdminPWReset | Out-File C:\test2.txt
Remove-Job -Name AdminPWReset

Note that if you have a great number of target hosts you may want to use a job queue, so that you don't have hundreds of jobs running in parallel.

Community
  • 1
  • 1
Ansgar Wiechers
  • 193,178
  • 25
  • 254
  • 328
  • I tried this and now the jobs work, but the actual action of resetting the password fails and I can't seem to figure out why. – cmorris14 Jan 11 '16 at 14:59
  • 2
    @cmorris14 Probably because you're using other variables inside the scriptblock that are defined outside the scriptblock (`$adminuser` for instance). There are other things about the scriptblock you should optimize, too (for instance the connection test should be outside to prevent attempts to start jobs on computers that aren't available in the first place. – Ansgar Wiechers Jan 11 '16 at 16:14