Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PowerShell ps1 script to manage bluetooth status stopped working #16731

Closed
5 tasks done
superbonaci opened this issue Jan 11, 2022 · 21 comments
Closed
5 tasks done

PowerShell ps1 script to manage bluetooth status stopped working #16731

superbonaci opened this issue Jan 11, 2022 · 21 comments
Labels
Issue-Question ideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a Needs-Triage The issue is new and needs to be triaged by a work group. Resolution-Duplicate The issue is a duplicate.

Comments

@superbonaci
Copy link

superbonaci commented Jan 11, 2022

Prerequisites

Steps to reproduce

Run this script in PowerShell 7.2.1:
https://superuser.com/a/1293303/298707

[CmdletBinding()] Param (
    [Parameter(Mandatory=$true)][ValidateSet('Off', 'On')][string]$BluetoothStatus
)
If ((Get-Service bthserv).Status -eq 'Stopped') { Start-Service bthserv }
Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]
Function Await($WinRtTask, $ResultType) {
    $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
    $netTask = $asTask.Invoke($null, @($WinRtTask))
    $netTask.Wait(-1) | Out-Null
    $netTask.Result
}
[Windows.Devices.Radios.Radio,Windows.System.Devices,ContentType=WindowsRuntime] | Out-Null
[Windows.Devices.Radios.RadioAccessStatus,Windows.System.Devices,ContentType=WindowsRuntime] | Out-Null
Await ([Windows.Devices.Radios.Radio]::RequestAccessAsync()) ([Windows.Devices.Radios.RadioAccessStatus]) | Out-Null
$radios = Await ([Windows.Devices.Radios.Radio]::GetRadiosAsync()) ([System.Collections.Generic.IReadOnlyList[Windows.Devices.Radios.Radio]])
$bluetooth = $radios | ? { $_.Kind -eq 'Bluetooth' }
[Windows.Devices.Radios.RadioState,Windows.System.Devices,ContentType=WindowsRuntime] | Out-Null
Await ($bluetooth.SetStateAsync($BluetoothStatus)) ([Windows.Devices.Radios.RadioAccessStatus]) | Out-Null

With either option .\bluetooth.ps1 -BluetoothStatus On or .\bluetooth.ps1 -BluetoothStatus Off.

Expected behavior

Run like in older PowerShell versions.

Actual behavior

Does not work.

Error details

Get-Error

Exception             :
    Type        : System.Management.Automation.RuntimeException
    ErrorRecord :
        Exception             :
            Type    : System.Management.Automation.ParentContainsErrorRecordException
            Message : You cannot call a method on a null-valued expression.
            HResult : -2146233087
        CategoryInfo          : InvalidOperation: (:) [], ParentContainsErrorRecordException
        FullyQualifiedErrorId : InvokeMethodOnNull
        InvocationInfo        :
            ScriptLineNumber : 19
            OffsetInLine     : 1
            HistoryId        : -1
            ScriptName       : C:\bin\bluetooth.ps1
            Line             : Await ($bluetooth.SetStateAsync($BluetoothStatus))
([Windows.Devices.Radios.RadioAccessStatus]) | Out-Null

            PositionMessage  : At C:\bin\bluetooth.ps1:19 char:1
                               + Await ($bluetooth.SetStateAsync($BluetoothStatus))
([Windows.Devices. …
                               +
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            PSScriptRoot     : C:\bin
            PSCommandPath    : C:\bin\bluetooth.ps1
            CommandOrigin    : Internal
        ScriptStackTrace      : at <ScriptBlock>, C:\bin\bluetooth.ps1: line 19
                                at <ScriptBlock>, <No file>: line 1
    TargetSite  : System.Object CallSite.Target(System.Runtime.CompilerServices.Closure,
System.Runtime.CompilerServices.CallSite, System.Object, System.Object)
    Message     : You cannot call a method on a null-valued expression.
    Data        : System.Collections.ListDictionaryInternal
    Source      : Anonymously Hosted DynamicMethods Assembly
    HResult     : -2146233087
    StackTrace  :
   at CallSite.Target(Closure , CallSite , Object , Object )
   at System.Dynamic.UpdateDelegates.UpdateAndExecute2[T0,T1,TRet](CallSite site, T0 arg0, T1 arg1)
   at System.Management.Automation.Interpreter.DynamicInstruction`3.Run(InterpretedFrame frame)
   at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame
frame)
CategoryInfo          : InvalidOperation: (:) [], RuntimeException
FullyQualifiedErrorId : InvokeMethodOnNull
InvocationInfo        :
    ScriptLineNumber : 19
    OffsetInLine     : 1
    HistoryId        : -1
    ScriptName       : C:\bin\bluetooth.ps1
    Line             : Await ($bluetooth.SetStateAsync($BluetoothStatus))
([Windows.Devices.Radios.RadioAccessStatus]) | Out-Null

    PositionMessage  : At C:\bin\bluetooth.ps1:19 char:1
                       + Await ($bluetooth.SetStateAsync($BluetoothStatus)) ([Windows.Devices. …
                       + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    PSScriptRoot     : C:\bin
    PSCommandPath    : C:\bin\bluetooth.ps1
    CommandOrigin    : Internal
ScriptStackTrace      : at <ScriptBlock>, C:\bin\bluetooth.ps1: line 19
                        at <ScriptBlock>, <No file>: line 1

Environment data

$PSVersionTable

Name                           Value
----                           -----
PSVersion                      7.2.1
PSEdition                      Core
GitCommitId                    7.2.1
OS                             Microsoft Windows 10.0.22000
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Visuals

No response

@superbonaci superbonaci added the Needs-Triage The issue is new and needs to be triaged by a work group. label Jan 11, 2022
@jhoneill
Copy link

This

>  Add-Type -AssemblyName System.Runtime.WindowsRuntime
> $asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]

MethodInvocationException: Exception calling "GetParameters" with "0" argument(s): "Operation is not supported on this 
platform. (0x80131539)"

suggests you've cut and pasted something tied to .NET Framework 4 into the .NET 6. version of PowerShell.

It simplifies to

> $asTaskMethods = [System.WindowsRuntimeSystemExtensions].GetMethods()  | where-object name -eq 'AsTask'                                                                                                         > $asTaskMethods[0].GetParameters()
MethodInvocationException: Exception calling "GetParameters" with "0" argument(s): "Operation is not supported on this platform. (0x80131539)"

And the equivalent in C# would give the same error if compiled with the current .NET.

@superbonaci
Copy link
Author

Fine, but how to I set inside the PowerShell script to use .NET version 4.x instead? Or just autodetect?

@superbonaci
Copy link
Author

superbonaci commented Jan 11, 2022

It simplifies to

Lines

Add-Type -AssemblyName System.Runtime.WindowsRuntime
$asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]

and

$asTaskMethods = [System.WindowsRuntimeSystemExtensions].GetMethods()  | where-object name -eq 'AsTask'

are not equivalent:

.\Set-bluetooth.ps1

cmdlet Set-bluetooth.ps1 at command pipeline position 1
Supply values for the following parameters:
BluetoothStatus: On
You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:7 char:5
+     $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:8 char:5
+     $netTask = $asTask.Invoke($null, @($WinRtTask))
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:9 char:5
+     $netTask.Wait(-1) | Out-Null
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:7 char:5
+     $asTask = $asTaskGeneric.MakeGenericMethod($ResultType)
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:8 char:5
+     $netTask = $asTask.Invoke($null, @($WinRtTask))
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:9 char:5
+     $netTask.Wait(-1) | Out-Null
+     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

You cannot call a method on a null-valued expression.
At C:\bin\Set-bluetooth.ps1:18 char:1
+ Await ($bluetooth.SetStateAsync($BluetoothStatus)) ([Windows.Devices. ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull

Please make tests before reporting invalid bugs.

@iSazonov
Copy link
Collaborator

It is know issue. WinRT support was removed from .Net 6.0 and as result from PowerShell 7.2 too.

@iSazonov iSazonov added Issue-Question ideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a Resolution-Answered The question is answered. labels Jan 11, 2022
@superbonaci
Copy link
Author

How can I tell PowerShell to use .NET 4.x from inside the script instead of "default" 6.x?

@iSazonov
Copy link
Collaborator

Only way is to run Windows PowerShell.

PS: there is separate project to support WinRT in .Net but MSFT PowerShell team want to keep PowerShell as small as possible so we could expect WinRT support will be added as an optional component/module in future.

@superbonaci
Copy link
Author

Can I run Windows PowerShell from inside the ps1 script?
So when I call .\Set-bluetooth.ps1 it autodetects if must run Windows Powershell or Powershell Core.

@jhoneill
Copy link

Please make tests before reporting invalid bugs.
@superbonaci you might want to heed your own advice

Here is your error

$asTask = $asTaskGeneric.MakeGenericMethod($ResultType)

You cannot call a method on a null-valued expression.

What method is being called, and what expression is null-valued ? It's $asTaskGeneric.
So where is $asTaskGeneric set ?

ps >   Add-Type -AssemblyName System.Runtime.WindowsRuntime
ps >    $asTaskGeneric = ([System.WindowsRuntimeSystemExtensions].GetMethods() | ? { $_.Name -eq 'AsTask' -and $_.GetParameters().Count -eq 1 -and $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' })[0]

If I paste those into PowerShell 7.2.1 I get multiple instances of

MethodInvocationException: Exception calling "GetParameters" with "0" argument(s): "Operation is not supported on this 
platform. (0x80131539)"

OK.
So lets see where the second line breaks,

ps >   $one = [System.WindowsRuntimeSystemExtensions].GetMethods()  

OK

ps >  $two =  $one | ? { $_.Name -eq 'AsTask'}

OK

ps > $three = $two | ? {$_.GetParameters().Count -eq 1}

MethodInvocationException: Exception calling "GetParameters" with "0" argument(s): "Operation is not supported on this platform. (0x80131539)"

How can I tell PowerShell to use .NET 4.x from inside the script instead of "default" 6.x?

.NET 6 is hard wired into PowerShell 7.2 , just as Dot net framework 4 is hardwired into Windows PowerShell 5.1

I can't find where I used it but somewhere I have some code like this

if ($PSVersionTable.PSVersion -ge "6.0.0") {
    powershell.exe $MyInvocation.Line  
    return
}

Which will run the same ps1 with the same parameters in the other version of PowerShell.

@SeeminglyScience
Copy link
Collaborator

You may be able to do this:

$session = New-PSSession -UseWindowsPowerShell
Invoke-Command -Session $session {
    code here
}

@superbonaci
Copy link
Author

@SeeminglyScience what happens if I am running that script inside PowerShell 7.x and Windows PowerShell is removed in a future Windows 11 update? Would that script fail?

@kilasuit
Copy link
Collaborator

what happens if I am running that script inside PowerShell 7.x and Windows PowerShell is removed in a future Windows 11 update? Would that script fail?

@superbonaci - that is almost never going to happen as it is part of the OS and whilst may become an optional feature, will be supported for at least another 10 years due to the Windows support lifecycle

@superbonaci
Copy link
Author

You may be able to do this:

$session = New-PSSession -UseWindowsPowerShell
Invoke-Command -Session $session {
    code here
}

Your code works now if the script is run inside PowerShell Core 7.x, however is not backwards compatible with Windows PowerShell. It must be backward compatible:

$PSVersionTable

Name                           Value
----                           -----
PSVersion                      5.1.22000.282
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.22000.282
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
 .\bluetooth.ps1
New-PSSession : A parameter cannot be found that matches parameter name 'UseWindowsPowerShell'.
At C:\bin\bluetooth.ps1:1 char:26
+ $session = New-PSSession -UseWindowsPowerShell
+                          ~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [New-PSSession], ParameterBindingException
    + FullyQualifiedErrorId : NamedParameterNotFound,Microsoft.PowerShell.Commands.NewPSSessionCommand

Invoke-Command : Cannot validate argument on parameter 'Session'. The argument is null or empty. Provide an argument that is not null or empty, and then try the command again.
At C:\bin\bluetooth.ps1:2 char:25
+ Invoke-Command -Session $session {
+                         ~~~~~~~~
    + CategoryInfo          : InvalidData: (:) [Invoke-Command], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationError,Microsoft.PowerShell.Commands.InvokeCommandCommand

@jhoneill
Copy link

They haven't removed cmd.exe or Wscript yet, so it's a bit early to be worrying about Windows PowerShell being removed.

@SeeminglyScience that's good if you know the script will run in 7.2 but it won't run natively on 5. That's why I dropped back
to saying "run the same command line in PowerShell.exe" and if the script is in the middle of a pipeline that won't work.

@superbonaci see the end of my previous comment for how to do that.

@SeeminglyScience
Copy link
Collaborator

Your code works now if the script is run inside PowerShell Core 7.x, however is not backwards compatible with Windows PowerShell. It must be backward compatible:

Yeah for sure, I'm just offering a work around. You'll need to apply it selectively with version checks if you want it to run no matter what executable it's invoked in.

The main issue of WinRT support being dropped is discussed in more detail in #13138.

@superbonaci
Copy link
Author

superbonaci commented Jan 11, 2022

if ($PSVersionTable.PSVersion -ge "6.0.0") {
    powershell.exe $MyInvocation.Line  
    return
}

Which will run the same ps1 with the same parameters in the other version of PowerShell.

How can I convert all existing code to a function?

if ($PSVersionTable.PSVersion -ge "6.0.0") {
    powershell.exe callcode()
    return
}

callcode(){
    original code
}

so callcode() just runs the existing code pasted in 1st post?

@237dmitry
Copy link

How can I convert all existing code to a function?

Just insert this checking after param block.

@superbonaci
Copy link
Author

How can I convert all existing code to a function?

Just insert this checking after param block.

How would the code look like?

@237dmitry
Copy link

How would the code look like?

[CmdletBinding()] Param (
    [Parameter(Mandatory=$true)][ValidateSet('Off', 'On')][string]$BluetoothStatus
)

if ($PSEdition -eq 'Core') {
    powershell.exe $MyInvocation.Line
    return
}
...

@superbonaci
Copy link
Author

superbonaci commented Jan 11, 2022

How would the code look like?

[CmdletBinding()] Param (
    [Parameter(Mandatory=$true)][ValidateSet('Off', 'On')][string]$BluetoothStatus
)

if ($PSEdition -eq 'Core') {
    powershell.exe $MyInvocation.Line
    return
}
...

That code asks twice for parameter if no arguments are given, which does not happen on original script. If argument is given then only asks one time.

@iSazonov
Copy link
Collaborator

Close as dup #13138.

@iSazonov iSazonov added Resolution-Duplicate The issue is a duplicate. and removed Resolution-Answered The question is answered. labels Jan 12, 2022
@ghost
Copy link

ghost commented Jan 13, 2022

This issue has been marked as duplicate and has not had any activity for 1 day. It has been closed for housekeeping purposes.

@ghost ghost closed this as completed Jan 13, 2022
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Issue-Question ideally support can be provided via other mechanisms, but sometimes folks do open an issue to get a Needs-Triage The issue is new and needs to be triaged by a work group. Resolution-Duplicate The issue is a duplicate.
Projects
None yet
Development

No branches or pull requests

6 participants