From 19fce382f81b4d23a935e7d3321e0768d4c178fc Mon Sep 17 00:00:00 2001 From: dawn-lc Date: Tue, 17 Jun 2025 16:23:37 +0800 Subject: [PATCH 1/2] Update install-as-a-windows-service.md --- guide/network/install-as-a-windows-service.md | 1078 +++++++++++++---- 1 file changed, 850 insertions(+), 228 deletions(-) diff --git a/guide/network/install-as-a-windows-service.md b/guide/network/install-as-a-windows-service.md index 8e8f48a..8beedc4 100644 --- a/guide/network/install-as-a-windows-service.md +++ b/guide/network/install-as-a-windows-service.md @@ -39,369 +39,1001 @@ 2. 创建脚本工具 -在当前目录下创建`install.cmd`文件并写入以下内容: +在当前目录下创建`Install.cmd`文件并写入以下内容: ```PowerShell -@echo off -@chcp 65001 > nul -cd /d "%~dp0" -title 正在启动脚本... -where /q powershell -if %ERRORLEVEL% NEQ 0 echo PowerShell is not installed. && pause > nul && exit -powershell -command "if ($PSVersionTable.PSVersion.Major -lt 3) {throw 'Requires PowerShell 3.0 or higher.'}; $content = Get-Content -Path '%0' -Raw -Encoding UTF8; $mainIndex = $content.LastIndexOf('#powershell#'); if ($mainIndex -le 0) {throw 'PowerShell script not found.'}; $content = $content.Substring($mainIndex + '#powershell#'.Length); $content = [ScriptBlock]::Create($content); Invoke-Command -ScriptBlock $content -ArgumentList (('%*' -split ' ') + @((Get-Location).Path));" -exit -#powershell# +::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)] + [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]) } - return $Host.UI.PromptForChoice($Title, $Message, $choices, $DefaultIndex) -eq 0 + + try { + return $Host.UI.PromptForChoice($Title, $Message, $choices, $DefaultIndex) -eq 0 + } + catch { + Write-Error "显示选择提示时出错: $_" + return $false + } } + function Show-MultipleChoicePrompt { + [CmdletBinding()] param( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] [string]$Message, - [Parameter(Mandatory = $true)] + [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 的数量必须相同。" } - $choices = for ($i = 0; $i -lt $Options.Count; $i++) { - [System.Management.Automation.Host.ChoiceDescription]::new("&$i.$($Options[$i])", $Helps[$i]) + + $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 + } + } } - return $Host.UI.PromptForChoice($Title, $Message, $choices, $DefaultIndex) } + function Get-InputWithNoNullOrWhiteSpace { + [CmdletBinding()] param( + [Parameter(Mandatory = $true, Position = 0)] + [ValidateNotNullOrEmpty()] [string]$Prompt ) + while ($true) { - $response = Read-Host "请输入${Prompt}(必填)" - if ([string]::IsNullOrWhiteSpace($response)) { - Write-Host "${Prompt}不能为空!" -ForegroundColor Red + try { + $response = Read-Host "请输入${Prompt}(必填)" + if ([string]::IsNullOrWhiteSpace($response)) { + Write-Host "${Prompt}不能为空!" -ForegroundColor Red + continue + } + if ($response -match '^(?!").*(? nul -cd /d "%~dp0" -title 正在启动脚本... -where /q powershell -if %ERRORLEVEL% NEQ 0 echo PowerShell is not installed. && pause > nul && exit -powershell -command "if ($PSVersionTable.PSVersion.Major -lt 3) {throw 'Requires PowerShell 3.0 or higher.'}; $content = Get-Content -Path '%0' -Raw -Encoding UTF8; $mainIndex = $content.LastIndexOf('#powershell#'); if ($mainIndex -le 0) {throw 'PowerShell script not found.'}; $content = $content.Substring($mainIndex + '#powershell#'.Length); $content = [ScriptBlock]::Create($content); Invoke-Command -ScriptBlock $content -ArgumentList (('%*' -split ' ') + @((Get-Location).Path));" -exit -#powershell# -function Pause { - param ( - [string]$Text = "按任意键继续..." +::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 Remove-ServiceName { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$FilePath, -# 初始化路径 -Set-Location -Path $args[-1] -$ScriptRoot = (Get-Location).Path + [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, -# 修改标题 -$host.ui.rawui.WindowTitle = "卸载EasyTier服务" + [Parameter(Mandatory = $true)] + [string]$ServiceName + ) -# 检查管理员权限 -if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { - Write-Host "请使用管理员权限运行!" -ForegroundColor Red - Pause -Text "按任意键退出..." - exit 1 + 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 - Pause -Text "按任意键退出..." + 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) +} -$SERVICE_NAME = "EasyTierService" # 服务卸载部分 try { $nssm = Join-Path $ScriptRoot "nssm.exe" - # 停止服务 - Write-Host "正在停止服务 $SERVICE_NAME ..." - & $nssm stop $SERVICE_NAME + foreach ($service in $services) { + # 停止服务 + Write-Host "正在停止服务 $service ..." + & $nssm stop $service - # 删除服务(自动确认) - Write-Host "正在移除服务 $SERVICE_NAME ..." - & $nssm remove $SERVICE_NAME confirm + # 删除服务(自动确认) + Write-Host "正在移除服务 $service ..." + & $nssm remove $service confirm - Write-Host "`n服务 $SERVICE_NAME 已卸载" -ForegroundColor Green + 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 - Pause -Text "按任意键退出..." + Show-Pause -Text "按任意键退出..." exit 1 } -Pause -Text "按任意键退出..." +Show-Pause -Text "按任意键退出..." exit ``` @@ -409,28 +1041,18 @@ exit ## 三、安装服务 -1. **以管理员身份**运行`install.cmd` -2. 按照提示输入配置信息: - - 网络名称 (必填) - - 网络密钥 (必填) - - 中继节点 (默认: tcp://public.easytier.cn:11010) - - TUN设备名称 (默认: EasyTierNET) -3. 选择可选功能: - - 低延迟优先模式 - - 多线程模式 - - KCP代理 - - 系统代理转发 +1. 运行`Install.cmd` +2. 按照提示输入配置信息。 4. 安装完成后会自动启动服务。 ## 四、卸载服务 -1. **以管理员身份**运行`uninstall.cmd` -2. 脚本会自动停止并删除服务 +1. 运行`Uninstall.cmd` +2. 脚本会自动停止并删除服务。 ## 五、注意事项 -1. 安装/卸载必须使用管理员权限 -2. 安装后不要移动程序文件位置 +1. 安装后不要移动程序文件位置 ## 六、常见问题 From 9e552a9a9882e01d7df462524eae9c5438e42bef Mon Sep 17 00:00:00 2001 From: dawn-lc <30336566+dawn-lc@users.noreply.github.com> Date: Fri, 20 Jun 2025 10:04:26 +0800 Subject: [PATCH 2/2] Update install-as-a-windows-service.md --- guide/network/install-as-a-windows-service.md | 1021 +---------------- 1 file changed, 16 insertions(+), 1005 deletions(-) diff --git a/guide/network/install-as-a-windows-service.md b/guide/network/install-as-a-windows-service.md index 8beedc4..a9d7bd8 100644 --- a/guide/network/install-as-a-windows-service.md +++ b/guide/network/install-as-a-windows-service.md @@ -10,11 +10,11 @@ **下载 EasyTier 应用**: -下载最新版本的`Windows`操作系统的`命令行程序`压缩包。 +下载最新版本的 `Windows` 操作系统的 `命令行程序` 压缩包。 下载完成后,将该压缩包解压到本地目录,比如`D:\EasyTier`。 -当前目录下应包含以下文件: +当前目录下应至少包含以下文件: - `easytier-core.exe` (核心程序) - `easytier-cli.exe` (命令行工具) - `Packet.dll` (运行库) @@ -28,6 +28,15 @@ 下载完成后,找到对应您设备架构的版本(如:`win64`),将其中的`nssm.exe`解压到`EasyTier`所在的本地目录。 + +**下载 安装/卸载 脚本**: + +在当前目录下启动PowerShell并执行以下命令: + +`iwr "https://github.com/EasyTier/EasyTier/raw/refs/heads/main/script/install.cmd" -OutFile "install.cmd"` + +`iwr "https://github.com/EasyTier/EasyTier/raw/refs/heads/main/script/uninstall.cmd" -OutFile "uninstall.cmd"` + ## 二、准备工作 1. 确保当前目录下包含以下文件: @@ -36,1018 +45,20 @@ - `nssm.exe` (服务管理工具) - `Packet.dll` (运行库) - `wintun.dll` (运行库) + - `install.cmd` (安装脚本) + - `uninstall.cmd` (卸载脚本) -2. 创建脚本工具 - -在当前目录下创建`Install.cmd`文件并写入以下内容: - -```PowerShell -::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 -``` - -3. 将整个文件夹放在固定位置。 +2. 将整个文件夹放在固定位置。 ## 三、安装服务 -1. 运行`Install.cmd` +1. 运行`install.cmd` 2. 按照提示输入配置信息。 4. 安装完成后会自动启动服务。 ## 四、卸载服务 -1. 运行`Uninstall.cmd` +1. 运行`uninstall.cmd` 2. 脚本会自动停止并删除服务。 ## 五、注意事项