From 16f6fb0c59847577ada891e2f39a066787ccc640 Mon Sep 17 00:00:00 2001 From: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Date: Sat, 21 Jun 2025 15:57:55 +0800 Subject: [PATCH] add Windows Service install script --- script/install.cmd | 796 +++++++++++++++++++++++++++++++++++++++++++ script/uninstall.cmd | 192 +++++++++++ 2 files changed, 988 insertions(+) create mode 100644 script/install.cmd create mode 100644 script/uninstall.cmd diff --git a/script/install.cmd b/script/install.cmd new file mode 100644 index 0000000..7396b1c --- /dev/null +++ b/script/install.cmd @@ -0,0 +1,796 @@ +::BATCH_START +@ECHO off +SETLOCAL EnableDelayedExpansion +TITLE Initializing Script... +CD /d %~dp0 +SET ScriptPath=\^"%~f0\^" +SET ScriptRoot=%~dp0 +SET ScriptRoot=\^"!ScriptRoot:~0,-1!\^" +SET Args=%* +IF DEFINED Args (SET Args=!Args:"=\"!) + $null +} + +function Show-YesNoPrompt { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$Message, + [string]$Title = "", + [ValidateRange(0, 1)] + [int]$DefaultIndex = 0, + [string[]]$Labels = @("&Yes", "&No"), + [string[]]$Helps = @("是", "否") + ) + + if ($Labels.Count -ne $Helps.Count) { + throw "Labels 和 Helps 的数量必须相同。" + } + + $choices = for ($i = 0; $i -lt $Labels.Count; $i++) { + [System.Management.Automation.Host.ChoiceDescription]::new($Labels[$i], $Helps[$i]) + } + + try { + return $Host.UI.PromptForChoice($Title, $Message, $choices, $DefaultIndex) -eq 0 + } + catch { + Write-Error "显示选择提示时出错: $_" + return $false + } +} + +function Show-MultipleChoicePrompt { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$Message, + [Parameter(Mandatory = $true, Position = 1)] + [ValidateNotNullOrEmpty()] + [string[]]$Options, + [string[]]$Helps = @(), + [string]$Title = "", + [int]$DefaultIndex = 0 + ) + + if ($Helps.Count -eq 0) { + $Helps = @("") + for ($i = 1; $i -lt $Options.Count; $i++) { + $Helps += "" + } + } + + if ($Options.Count -ne $Helps.Count) { + throw "Options 和 Helps 的数量必须相同。" + } + + if ($DefaultIndex -ge $Options.Count) { + $DefaultIndex = $Options.Count - 1 + } + $currentSelection = $DefaultIndex + + function Show-Menu { + param( + [int]$highlightIndex, + [string]$title, + [string]$message, + [string[]]$options, + [string[]]$helps, + [int]$prevIndex = -1 + ) + + try { + # 首次显示时绘制完整菜单 + if ($prevIndex -eq -1) { + Clear-Host + if (-not [string]::IsNullOrEmpty($title)) { + Write-Host "$title`n" -ForegroundColor Blue + } + Write-Host "$message" -ForegroundColor Yellow + + # 保存初始光标位置 + $script:menuTop = [Console]::CursorTop + + # 首次绘制所有选项 + for ($i = 0; $i -lt $options.Count; $i++) { + $prefix = if ($i -eq $highlightIndex) { "[>]" } else { "[ ]" } + $color = if ($i -eq $highlightIndex) { "Green" } else { "Gray" } + Write-Host "$prefix $($options[$i])" -ForegroundColor $color -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($helps[$i])) { " - $($helps[$i])" } else { "" }) -ForegroundColor DarkGray + } + } + + # 只更新变化的选项 + if ($prevIndex -ne -1) { + $safePrevPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $prevIndex) + [Console]::SetCursorPosition(0, $safePrevPos) + Write-Host "[ ] $($options[$prevIndex])" -ForegroundColor Gray -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($helps[$prevIndex])) { " - $($helps[$prevIndex])" } else { "" }) -ForegroundColor DarkGray + } + + $safeHighlightPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $highlightIndex) + [Console]::SetCursorPosition(0, $safeHighlightPos) + Write-Host "[>] $($options[$highlightIndex])" -ForegroundColor Green -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($helps[$highlightIndex])) { " - $($helps[$highlightIndex])" } else { "" }) -ForegroundColor DarkGray + + # 首次显示时绘制操作提示 + if ($prevIndex -eq -1) { + $safePos = [Math]::Min([Console]::WindowHeight - 2, $menuTop + $options.Count) + [Console]::SetCursorPosition(0, $safePos) + Write-Host "操作: 使用 ↑ / ↓ 移动 | Enter - 确认" + } + } + finally { + # 将光标移动到操作提示下方等待位置 + $waitPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $options.Count + 1) + [Console]::SetCursorPosition(0, $waitPos) + } + } + + $prevSelection = -1 + while ($true) { + Show-Menu -highlightIndex $currentSelection -title $Title -message $Message -options $Options -helps $Helps -prevIndex $prevSelection + $prevSelection = $currentSelection + + $key = [System.Console]::ReadKey($true) + switch ($key.Key) { + { $_ -eq [ConsoleKey]::UpArrow } { + $currentSelection = [Math]::Max(0, $currentSelection - 1) + } + { $_ -eq [ConsoleKey]::DownArrow } { + $currentSelection = [Math]::Min($Options.Count - 1, $currentSelection + 1) + } + { $_ -eq [ConsoleKey]::Enter } { + Clear-Host + return $currentSelection + } + } + } +} + +function Show-MultiSelectPrompt { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$Message, + [Parameter(Mandatory = $true, Position = 1)] + [ValidateNotNullOrEmpty()] + [string[]]$Options, + [string[]]$Helps = @(), + [string]$Title = "", + [int[]]$DefaultSelections = @() + ) + + if ($Helps.Count -eq 0) { + $Helps = @("") + for ($i = 1; $i -lt $Options.Count; $i++) { + $Helps += "" + } + } + + if ($Options.Count -ne $Helps.Count) { + throw "Options 和 Helps 的数量必须相同。" + } + + $selectedIndices = [System.Collections.Generic.List[int]]::new($DefaultSelections) + $currentSelection = 0 + + function Show-Menu { + param( + [int]$highlightIndex, + [System.Collections.Generic.List[int]]$selectedItems, + [int]$prevIndex = -1, + [int]$prevHighlight = -1 + ) + + try { + # 首次显示时绘制完整菜单 + if ($prevIndex -eq -1) { + Clear-Host + if (-not [string]::IsNullOrEmpty($Title)) { + Write-Host "$Title`n" -ForegroundColor Blue + } + Write-Host "$Message" -ForegroundColor Yellow + + # 保存初始光标位置 + $script:menuTop = [Console]::CursorTop + + # 首次绘制所有选项 + for ($i = 0; $i -lt $Options.Count; $i++) { + $isSelected = $selectedItems -contains $i + $prefix = if ($isSelected) { "[#]" } else { "[ ]" } + $color = if ($i -eq $highlightIndex) { "Green" } elseif ($isSelected) { "Cyan" } else { "Gray" } + Write-Host "$prefix $($Options[$i])" -ForegroundColor $color -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($Helps[$i])) { " - $($Helps[$i])" } else { "" }) -ForegroundColor DarkGray + } + } + + # 只更新变化的选项 + if ($prevIndex -ne -1) { + $safePrevPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $prevIndex) + [Console]::SetCursorPosition(0, $safePrevPos) + $isPrevSelected = $selectedItems -contains $prevIndex + $prefix = if ($isPrevSelected) { "[#]" } else { "[ ]" } + Write-Host "$prefix $($Options[$prevIndex])" -ForegroundColor $(if ($isPrevSelected) { "Cyan" } else { "Gray" }) -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($Helps[$prevIndex])) { " - $($Helps[$prevIndex])" } else { "" }) -ForegroundColor DarkGray + } + + if ($prevHighlight -ne -1 -and $prevHighlight -ne $highlightIndex) { + $safePrevHighlightPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $prevHighlight) + [Console]::SetCursorPosition(0, $safePrevHighlightPos) + $isPrevHighlightSelected = $selectedItems -contains $prevHighlight + $prefix = if ($isPrevHighlightSelected) { "[#]" } else { "[ ]" } + Write-Host "$prefix $($Options[$prevHighlight])" -ForegroundColor $(if ($isPrevHighlightSelected) { "Cyan" } else { "Gray" }) -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($Helps[$prevHighlight])) { " - $($Helps[$prevHighlight])" } else { "" }) -ForegroundColor DarkGray + } + + $safeHighlightPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $highlightIndex) + [Console]::SetCursorPosition(0, $safeHighlightPos) + $isSelected = $selectedItems -contains $highlightIndex + $prefix = if ($isSelected) { "[#]" } else { "[ ]" } + Write-Host "$prefix $($Options[$highlightIndex])" -ForegroundColor "Green" -NoNewline + Write-Host $(if (-not [string]::IsNullOrEmpty($Helps[$highlightIndex])) { " - $($Helps[$highlightIndex])" } else { "" }) -ForegroundColor DarkGray + + # 首次显示时绘制操作提示 + if ($prevIndex -eq -1) { + $safePos = [Math]::Min([Console]::WindowHeight - 2, $menuTop + $Options.Count) + [Console]::SetCursorPosition(0, $safePos) + Write-Host "操作: 使用 ↑ / ↓ 移动 | Space - 选中/取消 | Enter - 确认" + } + } + finally { + # 将光标移动到操作提示下方等待位置 + $waitPos = [Math]::Min([Console]::WindowHeight - 1, $menuTop + $Options.Count + 1) + [Console]::SetCursorPosition(0, $waitPos) + } + } + + $prevSelection = -1 + $prevHighlight = -1 + while ($true) { + Show-Menu -highlightIndex $currentSelection -selectedItems $selectedIndices -prevIndex $prevSelection -prevHighlight $prevHighlight + $prevHighlight = $currentSelection + + $key = [System.Console]::ReadKey($true) + switch ($key.Key) { + { $_ -eq [ConsoleKey]::UpArrow } { + $prevSelection = $currentSelection + $currentSelection = [Math]::Max(0, $currentSelection - 1) + } + { $_ -eq [ConsoleKey]::DownArrow } { + $prevSelection = $currentSelection + $currentSelection = [Math]::Min($Options.Count - 1, $currentSelection + 1) + } + { $_ -eq [ConsoleKey]::Spacebar } { + $prevSelection = $currentSelection + if ($selectedIndices.Contains($currentSelection)) { + $selectedIndices.Remove($currentSelection) + } + else { + $selectedIndices.Add($currentSelection) + } + } + { $_ -eq [ConsoleKey]::Enter } { + Clear-Host + return $selectedIndices + } + } + } +} + +function Get-InputWithNoNullOrWhiteSpace { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$Prompt + ) + + while ($true) { + try { + $response = Read-Host "请输入${Prompt}(必填)" + if ([string]::IsNullOrWhiteSpace($response)) { + Write-Host "${Prompt}不能为空!" -ForegroundColor Red + continue + } + if ($response -match '^(?!").*(? $null +} +function Show-YesNoPrompt { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] + [string]$Message, + [string]$Title = "", + [ValidateRange(0, 1)] + [int]$DefaultIndex = 0, + [string[]]$Labels = @("&Yes", "&No"), + [string[]]$Helps = @("是", "否") + ) + + if ($Labels.Count -ne $Helps.Count) { + throw "Labels 和 Helps 的数量必须相同。" + } + + $choices = for ($i = 0; $i -lt $Labels.Count; $i++) { + [System.Management.Automation.Host.ChoiceDescription]::new($Labels[$i], $Helps[$i]) + } + + try { + return $Host.UI.PromptForChoice($Title, $Message, $choices, $DefaultIndex) -eq 0 + } + catch { + Write-Error "显示选择提示时出错: $_" + return $false + } +} +function Remove-ServiceName { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string]$ServiceName + ) + if (Test-ServiceNameExists -FilePath $FilePath -ServiceName $ServiceName) { + $uniqueLines = Get-Content -Path $FilePath | Where-Object { $_ -ne $ServiceName } | Sort-Object -Unique + Set-Content -Path $FilePath -Value ($uniqueLines -join [Environment]::NewLine) -Encoding UTF8 -Force + } +} +function Test-ServiceNameExists { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(Mandatory = $true)] + [string]$ServiceName + ) + + if (-Not (Test-Path $FilePath)) { + Set-Content -Path $FilePath -Value "" -Encoding UTF8 -Force + return $false + } + $uniqueLines = Get-Content -Path $FilePath | Sort-Object -Unique + return $uniqueLines -contains $ServiceName +} + +$host.ui.rawui.WindowTitle = "卸载EasyTier服务" +Clear-Host +$ScriptRoot = (Get-Location).Path +$ServicesPath = Join-Path $ScriptRoot "services" + +# 必要文件检查 +$RequiredFiles = @("nssm.exe") +foreach ($file in $RequiredFiles) { + if (-not (Test-Path (Join-Path $ScriptRoot $file))) { + Write-Host "缺少必要文件: $file" -ForegroundColor Red + Show-Pause -Text "按任意键退出..." + exit 1 + } +} +if (-not (Test-ServiceNameExists -FilePath $ServicesPath -ServiceName $ServiceName)) { + Write-Host "服务未安装" -ForegroundColor Red + if (Show-YesNoPrompt -Message "是否强制卸载?" -DefaultIndex 1) { + $Force = $true + $Action = "all" + } + else { + Show-Pause -Text "按任意键退出..." + exit 1 + } +} + +# 参数处理 +if ($Action -eq "all") { + if (-not $Force) { + if (-not (Show-YesNoPrompt -Message "确定要完全卸载所有服务吗?" -DefaultIndex 1)) { + Write-Host "已取消卸载操作" -ForegroundColor Yellow + Show-Pause -Text "按任意键退出..." + exit 0 + } + } + Write-Host "`n正在卸载所有服务..." -ForegroundColor Cyan + + # 读取所有服务名 + $services = Get-Content $ServicesPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + if (-not $services) { + $services = @($ServiceName) + } +} +else { + $services = @($ServiceName) +} + +# 服务卸载部分 +try { + $nssm = Join-Path $ScriptRoot "nssm.exe" + + foreach ($service in $services) { + # 停止服务 + Write-Host "正在停止服务 $service ..." + & $nssm stop $service + + # 删除服务(自动确认) + Write-Host "正在移除服务 $service ..." + & $nssm remove $service confirm + + Remove-ServiceName -FilePath $ServicesPath -ServiceName $service + Write-Host "服务 $service 已卸载" -ForegroundColor Green + } + + # 如果是完全卸载,删除服务记录文件 + if ($Action -eq "all") { + Remove-Item $ServicesPath -Force + Write-Host "`n已删除服务列表文件" -ForegroundColor Green + } +} +catch { + Write-Host "`n卸载过程中发生错误: $_" -ForegroundColor Red + Show-Pause -Text "按任意键退出..." + exit 1 +} + +Show-Pause -Text "按任意键退出..." +exit