Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,15 @@ sendspin-cli vol 40
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/uninstall_linux.sh | bash
```

It removes the service and the installed files, and keeps your config and the player's
remembered state unless you pass `--purge`.
On Windows:

```powershell
Invoke-WebRequest https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/uninstall_windows.ps1 -OutFile uninstall_windows.ps1
powershell -ExecutionPolicy Bypass -File .\uninstall_windows.ps1
```

Each removes the service and the installed files, and keeps your config and the player's
remembered state unless you pass `--purge` (`-Purge` on Windows).

## Need more help?

Expand Down
59 changes: 38 additions & 21 deletions docs/wiki/Getting-Started-on-Windows.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ It prints what it is about to do, as you, and waits for you to say yes:
==> This is what it will do, as you, with no administrator rights

install C:\Users\you\AppData\Local\Programs\sendspin-cli\ (sendspin-cli.exe and its README, LICENSE and config example)
config C:\Users\you\AppData\Roaming\sendspin-cli\config (from the example; every line in it is commented out)
config C:\Users\you\AppData\Roaming\sendspin-cli\config (from the example, setting the name you choose and the output you choose)
PATH add C:\Users\you\AppData\Local\Programs\sendspin-cli to your user PATH, so 'sendspin-cli status' works anywhere
task 'sendspin-cli' in Task Scheduler: start the player, without a window, when you
log on, logging to C:\Users\you\AppData\Local\sendspin-cli\sendspin-cli.log; it checks every minute
Expand All @@ -30,9 +30,27 @@ It prints what it is about to do, as you, and waits for you to say yes:
Go ahead? [y/N]
```

`-Yes` skips the prompt, and is required when there is no console to ask on. `-Version v0.5.0`
installs a specific release instead of the newest. `-Firewall` adds the firewall rule without
asking, though Windows still asks for administrator approval.
Then it asks two things, the name a server shows for the player and what it plays through:

```
==> What it plays through

0) Whatever Windows' default output is -- change it in Settings > System > Sound
1) Speakers (USB DAC) (the default right now)
2) Odyssey G95NC (NVIDIA High Definition Audio)

Choose 0-2 [0]:
```

`0` follows Windows' default as you change it; a number pins that device, written to the
config as `output = portaudio:directsound:<name>`. Neither is asked again once the config
sets it, so running the script again to upgrade asks nothing.

`-Name kitchen` and `-Output "portaudio:directsound:Speakers (USB DAC)"` answer them up
front. `-Yes` skips every prompt, and is required when there is no console to ask on; with it,
only those two flags set anything. `-Version v0.5.0` installs a specific release instead of
the newest. `-Firewall` adds the firewall rule without asking, though Windows still asks for
administrator approval.

### What it actually does

Expand All @@ -45,10 +63,11 @@ asking, though Windows still asks for administrator approval.
already running from there is stopped cleanly first, so running the script again upgrades
it.
5. **Copies the annotated example config** to `AppData\Roaming\sendspin-cli\config` if you
have none. Every line in it is commented out, so it chooses nothing: the player plays
through your **default output device**, and follows it when you change it.
6. **Adds the install folder to your user PATH**, so `sendspin-cli status` works in any new
terminal.
have none, then **asks for a name and an output** unless the config already sets an
output, and writes your answers into it.
6. **Adds the install folder to your user PATH**, so `sendspin-cli status` works in any
terminal opened afterwards. One already open keeps the old PATH, and VS Code's terminals
do until VS Code restarts, so the script prints the full path to use straight away.
7. **Registers a scheduled task** that runs `sendspin-cli -z` when you log on, with no window.
The task also checks every minute: while the player runs, that check finds its pidfile
held and does nothing, and if the player has stopped, it starts it again.
Expand Down Expand Up @@ -142,22 +161,20 @@ rotated, so it only grows; it is safe to delete while the player is stopped.
## Uninstalling

```powershell
Disable-ScheduledTask sendspin-cli
taskkill /PID (Get-Content $env:LOCALAPPDATA\sendspin-cli\sendspin-cli.pid)
Unregister-ScheduledTask sendspin-cli -Confirm:$false
$dir = "$env:LOCALAPPDATA\Programs\sendspin-cli"
[Environment]::SetEnvironmentVariable('Path', (([Environment]::GetEnvironmentVariable('Path', 'User') -split ';' | Where-Object { $_ -and $_ -ne $dir }) -join ';'), 'User')
Remove-Item $dir -Recurse
Invoke-WebRequest https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/uninstall_windows.ps1 -OutFile uninstall_windows.ps1
powershell -ExecutionPolicy Bypass -File .\uninstall_windows.ps1
```

Your config, state and log are left where they are, so reinstalling keeps the player's
identity and pairings. Remove `%APPDATA%\sendspin-cli` and `%LOCALAPPDATA%\sendspin-cli` too
to forget it entirely; a server then sees a new, unpaired player. The firewall rule, if you
added it, goes from an administrator PowerShell:
It shows what it will remove and waits for you to say yes: the scheduled task, the running
player (stopped cleanly), the install folder, its PATH entry, and the firewall rule if you
added it, which brings up a UAC prompt. It also tidies the control-socket files players leave
in `%LOCALAPPDATA%`.

```powershell
Remove-NetFirewallRule -DisplayName sendspin-cli
```
Your config and the player's remembered state (`%APPDATA%\sendspin-cli` and
`%LOCALAPPDATA%\sendspin-cli`) are kept by default, so reinstalling keeps the player's
identity and pairings; it asks whether to remove them too. `-Purge` removes them without
asking, after which a server sees a new, unpaired player. `-Yes` asks nothing, keeping them
unless `-Purge`.

## Without the script

Expand Down
186 changes: 175 additions & 11 deletions scripts/get_started_windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,21 @@
# limitations under the License.
#
# Installs a released sendspin-cli for this Windows user and starts it at every logon.
# Verifies SHA256SUMS, installs under LocalAppData, writes a config and registers a scheduled
# task. Nothing needs administrator rights except the optional firewall rule, asked for apart.
# Verifies SHA256SUMS, installs under LocalAppData, asks for a name and an output device, writes
# a config and registers a scheduled task. Nothing needs administrator rights except the
# optional firewall rule, asked for apart. uninstall_windows.ps1 removes it all again.
#
# Usage: get_started_windows.ps1 [-Version <tag>] [-Yes] [-Firewall]
# Usage: get_started_windows.ps1 [-Version <tag>] [-Name <name>] [-Output <device>] [-Yes]
# [-Firewall]
#
# -Version <tag> install this release instead of the latest, e.g. -Version v0.5.0
# -Yes do not ask before installing. Required when there is no console to ask on
# -Firewall also add the firewall rule without asking (still needs a UAC prompt)
# -Version <tag> install this release instead of the latest, e.g. -Version v0.5.0
# -Name <name> the name a server shows for this player, instead of being asked.
# Ignored when the config already sets one
# -Output <device> the output device, e.g. "portaudio:directsound:Speakers (USB DAC)",
# instead of being asked. Ignored when the config already sets one
# -Yes do not ask anything, taking the name and output only from the flags
# above. Required when there is no console to ask on
# -Firewall also add the firewall rule without asking (still needs a UAC prompt)
#
# Environment:
#
Expand All @@ -32,6 +39,8 @@
[CmdletBinding()]
param(
[string]$Version = '',
[string]$Name = '',
[string]$Output = '',
[switch]$Yes,
[switch]$Firewall,
[switch]$Help
Expand Down Expand Up @@ -79,6 +88,68 @@ function Confirm-Action([string]$Question) {
return $answer -match '^(y|yes)$'
}

# Does the config set this key, uncommented.
function Test-ConfigSets([string]$Key) {
if (-not (Test-Path -LiteralPath $Config)) {
return $false
}
$pattern = '^\s*' + [regex]::Escape($Key) + '\s*='
return [bool]([IO.File]::ReadAllLines($Config) | Where-Object { $_ -match $pattern })
}

# Replaces the example's commented line for the key, else appends. Without a BOM: the player
# would read one as part of the first line and refuse the file.
function Set-ConfigKey([string]$Key, [string]$Value) {
$lines = [Collections.Generic.List[string]]::new([IO.File]::ReadAllLines($Config))
$at = -1
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i].StartsWith("#$Key = ")) {
$at = $i
break
}
}
if ($at -ge 0) {
$lines[$at] = "$Key = $Value"
} else {
$lines.Add("$Key = $Value")
}
[IO.File]::WriteAllLines($Config, $lines, (New-Object Text.UTF8Encoding $false))
}

# One entry per output device, from -l's DirectSound rows: full names, and Windows resamples.
function Get-OutputChoices {
# -l prints UTF-8, which PowerShell would read as the console's code page, turning a name
# with an umlaut in it into one no device has.
$encoding = [Console]::OutputEncoding
[Console]::OutputEncoding = New-Object Text.UTF8Encoding $false
try {
$listing = @(Invoke-Native { & $Binary -l })
} finally {
[Console]::OutputEncoding = $encoding
}
$default = ''
$choices = @()
$inOutputs = $false
foreach ($line in $listing) {
if ($line -match '^PortAudio output devices') {
$inOutputs = $true
} elseif ($inOutputs -and $line -notmatch '^\s') {
break
} elseif ($inOutputs -and $line -match '^\s+\d+\s+(.+?)\s+mme\s+\d+ ch.*\(system default\)') {
# MME carries the default, cut to 31 characters.
$default = $Matches[1]
} elseif ($inOutputs -and $line -match '^\s+\d+\s+(.+?)\s+directsound\s+\d+ ch') {
# Primary Sound Driver is DirectSound's own follow-the-default, which choice 0 is.
if ($Matches[1] -ne 'Primary Sound Driver') {
$choices += $Matches[1]
}
}
}
return @($choices | ForEach-Object {
[pscustomobject]@{ Name = $_; IsDefault = ($default -and $_.StartsWith($default)) }
})
}

if ($Help) {
# The header's usage block, from '# Usage:' to the end of the comment.
$inUsage = $false
Expand Down Expand Up @@ -227,11 +298,48 @@ try {
$HaveRule = $false
}

# From the flag, asked, or neither; a key the config already sets is never asked or replaced.
$NameFrom = 'none'
$OutputFrom = 'none'
if (Test-ConfigSets 'output') {
if ($Output) {
Say " note: $Config already sets an output, so -Output is ignored"
}
} elseif ($Output) {
$OutputFrom = 'flag'
} elseif (-not $Yes) {
$OutputFrom = 'ask'
}
if (Test-ConfigSets 'name') {
if ($Name) {
Say " note: $Config already sets a name, so -Name is ignored"
}
} elseif ($Name) {
$NameFrom = 'flag'
} elseif ($OutputFrom -eq 'ask') {
# A config with an output is set up: upgrading it asks nothing.
$NameFrom = 'ask'
}
if ("$Name$Output" -match '[\r\n]') {
Fail '-Name and -Output are one line each'
}

$Choosing = @()
if ($NameFrom -eq 'flag') { $Choosing += "name = $Name" }
if ($NameFrom -eq 'ask') { $Choosing += 'the name you choose' }
if ($OutputFrom -eq 'flag') { $Choosing += "output = $Output" }
if ($OutputFrom -eq 'ask') { $Choosing += 'the output you choose' }
$Setting = if ($Choosing) { ", setting $($Choosing -join ' and ')" } else { '' }

Step 'This is what it will do, as you, with no administrator rights'
Say ''
Say " install $InstallDir\ (sendspin-cli.exe and its README, LICENSE and config example)"
if ($SeedConfig) {
if ($SeedConfig -and $Choosing) {
Say " config $Config (from the example$Setting)"
} elseif ($SeedConfig) {
Say " config $Config (from the example; every line in it is commented out)"
} elseif ($Choosing) {
Say " config $Config (exists$Setting; nothing else in it changes)"
} else {
Say " config $Config (exists; left as it is)"
}
Expand Down Expand Up @@ -290,6 +398,57 @@ try {
Say " $Config (from the example; everything in it is commented out)"
}

# Name and output

if ($NameFrom -eq 'ask') {
Step 'Naming the player'
Say ''
Say ' This is what a Sendspin server shows it as.'
Say ''
$hostName = [Net.Dns]::GetHostName()
$answer = (Read-Host " Name [$hostName]").Trim()
$Name = if ($answer) { $answer } else { $hostName }
}
if ($OutputFrom -eq 'ask') {
Step 'What it plays through'
Say ''
$choices = @(Get-OutputChoices)
Say " 0) Whatever Windows' default output is -- change it in Settings > System > Sound"
for ($i = 0; $i -lt $choices.Count; $i++) {
Say (" {0}) {1}{2}" -f ($i + 1), $choices[$i].Name,
$(if ($choices[$i].IsDefault) { ' (the default right now)' } else { '' }))
}
Say ''
while ($true) {
$answer = (Read-Host " Choose 0-$($choices.Count) [0]").Trim()
if (-not $answer) {
$answer = '0'
}
# TryParse, so 00 is 0 and a number past Int32 is off the list rather than an error.
$number = 0
if ($answer -match '^\d+$' -and [int]::TryParse($answer, [ref]$number) -and
$number -le $choices.Count) {
break
}
Say " '$answer' is not on the list"
}
$Output = if ($number -eq 0) { 'portaudio' } else {
"portaudio:directsound:$($choices[$number - 1].Name)"
}
}
if ($NameFrom -ne 'none' -or $OutputFrom -ne 'none') {
Step "Writing $Config"
Say ''
if ($NameFrom -ne 'none') {
Set-ConfigKey 'name' $Name
Say " name = $Name"
}
if ($OutputFrom -ne 'none') {
Set-ConfigKey 'output' $Output
Say " output = $Output"
}
}

if ($AddToPath) {
$newPath = if ($UserPath) { "$UserPath;$InstallDir" } else { $InstallDir }
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
Expand Down Expand Up @@ -382,24 +541,29 @@ try {

Step 'Next'
Say ''
Say ' Ask it what it is doing (in a new terminal, so PATH is up to date):'
Say ' Ask it what it is doing:'
Say ''
Say ' sendspin-cli status'
Say " & '$Binary' status"
Say ''
Say " Plain 'sendspin-cli status' works in terminals opened from now on. One already open,"
Say " this one included, still has the old PATH; so do VS Code's until VS Code restarts."
Say ''
Say ' Pair it from your Sendspin server: when the server asks for a code, it is in the log.'
Say ''
Say " Get-Content '$LogFile' -Wait -Tail 20"
Say ''
Say " Change what it plays through, or anything else, in $Config --"
Say " 'sendspin-cli -l' lists the devices -- then stop it, and the task starts it again"
Say ' within a minute with the new settings:'
Say " & '$Binary' -l lists the devices -- then stop it, and the task starts it"
Say ' again within a minute with the new settings:'
Say ''
Say " taskkill /PID (Get-Content '$PidFile')"
Say ''
Say ' To keep it stopped, disable the task first:'
Say ''
Say " Disable-ScheduledTask $TaskName; taskkill /PID (Get-Content '$PidFile')"
Say ''
Say " To remove all of this again: https://github.com/$Repo#uninstall"
Say ''
} finally {
Remove-Item -LiteralPath $WorkDir -Recurse -Force -ErrorAction SilentlyContinue
}
Loading
Loading