Package standalone state tool and client plugin
This commit is contained in:
@@ -24,7 +24,6 @@ go/*.test
|
||||
go/coverage.out
|
||||
*.prof
|
||||
*.pprof
|
||||
go/internal/statebridge/tool/bd2-state.exe
|
||||
plugins/**/bin/
|
||||
plugins/**/obj/
|
||||
TestResults/
|
||||
|
||||
@@ -28,13 +28,13 @@ $env:PYTHONDONTWRITEBYTECODE = '1'
|
||||
python -m unittest discover -s .\tools\python\tests -p 'test_*.py' -v
|
||||
```
|
||||
|
||||
## 单文件发布
|
||||
## 发布包
|
||||
|
||||
```powershell
|
||||
.\build-release.ps1
|
||||
.\build-release.ps1 -GameDir "<客户端目录>"
|
||||
```
|
||||
|
||||
脚本构建并测试 Haskell CLI,将它嵌入 Go 服务,最终只生成 `.build\bin\bd2server.exe`。运行时 helper 位于临时目录并在进程退出时清理,不需要分发第二个可执行文件。Go 构建缓存只写入 `go\.cache`。运行服务仍需项目的本地配置和资源;“单文件”指两个程序的发布产物合并为一个 exe,不代表把外部资源打进程序。
|
||||
脚本生成 `.build\bd2server-windows-x64.zip`。解压后得到一个完整目录,其中包含 Go 服务端、独立 Haskell 状态工具、本地身份插件、运行种子和空存档目录。Haskell 不再嵌入 Go 可执行文件。Go 构建缓存只写入 `go\.cache`。
|
||||
|
||||
## 客户端插件项目
|
||||
|
||||
@@ -43,48 +43,39 @@ python -m unittest discover -s .\tools\python\tests -p 'test_*.py' -v
|
||||
|
||||
## 首次安装本地客户端
|
||||
|
||||
先设置客户端目录,修改客户端入口,并构建、安装 `BD2LocalIdentity.dll`:
|
||||
先手动安装 BepInEx:<https://github.com/BepInEx/BepInEx/releases>。然后修改客户端入口:
|
||||
|
||||
```powershell
|
||||
$clientDir = "<客户端目录>"
|
||||
|
||||
.\.build\bin\bd2server.exe patch-client `
|
||||
.\.build\package\bd2server\bd2server.exe patch-client `
|
||||
--game-dir $clientDir
|
||||
|
||||
dotnet build .\plugins\LocalIdentity\LocalIdentity.csproj `
|
||||
-c Release `
|
||||
-p:GameDir="$clientDir"
|
||||
|
||||
$pluginDir = Join-Path $clientDir 'BepInEx\plugins'
|
||||
New-Item -ItemType Directory -Force $pluginDir | Out-Null
|
||||
Copy-Item `
|
||||
.\plugins\LocalIdentity\bin\Release\netstandard2.1\BD2LocalIdentity.dll `
|
||||
(Join-Path $pluginDir 'BD2LocalIdentity.dll') `
|
||||
-Force
|
||||
```
|
||||
|
||||
客户端必须已经安装并加载 `BD2LocalIdentity.dll`,否则不能按本地服流程登录。不要把原版抓包插件 `BD2CaptureEnvironment.dll` 安装进这个客户端。
|
||||
启动服务端时会检查 BepInEx,并自动从发布目录安装或更新 `BD2LocalIdentity.dll`。如果未检测到 BepInEx,服务端只提示官方下载链接,不修改客户端,也不会继续启动。不要把原版抓包插件 `BD2CaptureEnvironment.dll` 安装进这个客户端。
|
||||
|
||||
## 启动顺序
|
||||
|
||||
顺序必须是:
|
||||
|
||||
1. 确认 `BD2LocalIdentity.dll` 已安装在本地客户端的 `BepInEx\plugins`。
|
||||
2. 启动服务器。
|
||||
1. 确认客户端已经安装 BepInEx。
|
||||
2. 启动服务器;服务器自动同步 `BD2LocalIdentity.dll`。
|
||||
3. 健康检查返回成功后,再启动游戏客户端。
|
||||
|
||||
启动服务器:
|
||||
|
||||
```powershell
|
||||
Push-Location .\go
|
||||
& ..\.build\bin\bd2server.exe serve `
|
||||
Push-Location .\.build\package\bd2server\go
|
||||
& ..\bd2server.exe serve `
|
||||
--game-dir "<客户端目录>" `
|
||||
--cdn "<ServerData目录>" `
|
||||
--game-data "<GameData目录>" `
|
||||
--game-data-version "<GameData版本>"
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
启动时先核验资源,Go 再调用内嵌的 Haskell CLI 检查和修复存档;通过后加载账号与玩法服务,最后监听指定地址(默认 `127.0.0.1:8080`)。本地身份插件目前固定连接该默认地址;要更换地址,需要同步修改插件。
|
||||
启动时先检查 BepInEx 并同步插件,再核验资源;Go 随后调用同目录的 Haskell CLI 检查和修复存档,通过后加载账号与玩法服务,最后监听指定地址(默认 `127.0.0.1:8080`)。本地身份插件目前固定连接该默认地址;要更换地址,需要同步修改插件。
|
||||
|
||||
健康检查:
|
||||
|
||||
@@ -99,6 +90,6 @@ Invoke-WebRequest http://127.0.0.1:8080/healthz
|
||||
构建服务后运行:
|
||||
|
||||
```powershell
|
||||
.\.build\bin\bd2server.exe patch-client --game-dir "<客户端目录>" --verify
|
||||
.\.build\bin\bd2server.exe patch-client --game-dir "<客户端目录>"
|
||||
.\.build\package\bd2server\bd2server.exe patch-client --game-dir "<客户端目录>" --verify
|
||||
.\.build\package\bd2server\bd2server.exe patch-client --game-dir "<客户端目录>"
|
||||
```
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
# BD2 本地服务器发布包
|
||||
|
||||
## 首次使用
|
||||
|
||||
1. 为游戏手动安装 BepInEx。官方下载:
|
||||
<https://github.com/BepInEx/BepInEx/releases>
|
||||
2. 修改本地客户端入口:
|
||||
|
||||
```powershell
|
||||
.\bd2server.exe patch-client --game-dir "<客户端目录>"
|
||||
```
|
||||
|
||||
3. 进入发布包的 `go` 目录并启动服务器:
|
||||
|
||||
```powershell
|
||||
Push-Location .\go
|
||||
& ..\bd2server.exe serve `
|
||||
--game-dir "<客户端目录>" `
|
||||
--cdn "<ServerData目录>" `
|
||||
--game-data "<GameData目录>" `
|
||||
--game-data-version "<GameData版本>"
|
||||
Pop-Location
|
||||
```
|
||||
|
||||
服务器会检查 BepInEx,并自动把包内 `plugins\BD2LocalIdentity.dll` 安装或更新到客户端。如果没有安装 BepInEx,服务器会给出上述下载地址、保持客户端不变并拒绝启动。
|
||||
|
||||
4. 确认健康检查成功:
|
||||
|
||||
```powershell
|
||||
Invoke-WebRequest http://127.0.0.1:8080/healthz
|
||||
```
|
||||
|
||||
5. 最后启动游戏客户端。请勿在服务器启动前运行客户端。
|
||||
|
||||
`bd2-state.exe` 是服务端启动期使用的 Haskell 状态工具,必须与 `bd2server.exe` 保持在同一目录。
|
||||
+60
-24
@@ -1,17 +1,43 @@
|
||||
[CmdletBinding()]
|
||||
param([switch]$SkipTests)
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string]$GameDir,
|
||||
|
||||
[switch]$SkipTests
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$root = $PSScriptRoot
|
||||
$goRoot = Join-Path $root 'go'
|
||||
$haskellRoot = Join-Path $root 'haskell'
|
||||
$outputDir = Join-Path $root '.build\bin'
|
||||
$embedDir = Join-Path $goRoot 'internal\statebridge\tool'
|
||||
$embedTool = Join-Path $embedDir 'bd2-state.exe'
|
||||
$localIdentityProject = Join-Path $root 'plugins\LocalIdentity\LocalIdentity.csproj'
|
||||
$buildRoot = Join-Path $root '.build'
|
||||
$packageParent = Join-Path $buildRoot 'package'
|
||||
$packageDir = Join-Path $packageParent 'bd2server'
|
||||
$packagePluginDir = Join-Path $packageDir 'plugins'
|
||||
$packageGoDir = Join-Path $packageDir 'go'
|
||||
$packageStateDir = Join-Path $packageDir 'data\state'
|
||||
$archive = Join-Path $buildRoot 'bd2server-windows-x64.zip'
|
||||
|
||||
$GameDir = [IO.Path]::GetFullPath($GameDir)
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $GameDir 'BrownDust II.exe') -PathType Leaf)) {
|
||||
throw "GameDir does not contain BrownDust II.exe: $GameDir"
|
||||
}
|
||||
|
||||
$env:GOCACHE = Join-Path $goRoot '.cache\go-build'
|
||||
New-Item -ItemType Directory -Force -Path $env:GOCACHE, $outputDir, $embedDir | Out-Null
|
||||
if (Test-Path -LiteralPath $embedTool) { Remove-Item -LiteralPath $embedTool -Force }
|
||||
New-Item -ItemType Directory -Force -Path $env:GOCACHE, $buildRoot | Out-Null
|
||||
|
||||
foreach ($target in @($packageParent, $archive)) {
|
||||
if (Test-Path -LiteralPath $target) {
|
||||
$resolved = [IO.Path]::GetFullPath($target)
|
||||
$expectedRoot = [IO.Path]::GetFullPath($buildRoot) + [IO.Path]::DirectorySeparatorChar
|
||||
if (-not $resolved.StartsWith($expectedRoot, [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to remove unexpected build path: $resolved"
|
||||
}
|
||||
Remove-Item -LiteralPath $resolved -Recurse -Force
|
||||
}
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $packageDir, $packagePluginDir, $packageGoDir, $packageStateDir | Out-Null
|
||||
|
||||
Push-Location $root
|
||||
try {
|
||||
@@ -33,25 +59,35 @@ try {
|
||||
}
|
||||
$haskellTool = (cabal list-bin exe:bd2-state).Trim()
|
||||
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $haskellTool)) {
|
||||
throw 'cannot locate the Cabal-built bd2-state executable'
|
||||
throw 'Cannot locate the Cabal-built bd2-state executable.'
|
||||
}
|
||||
Copy-Item -LiteralPath $haskellTool -Destination $embedTool -Force
|
||||
Copy-Item -LiteralPath $haskellTool -Destination (Join-Path $packageDir 'bd2-state.exe') -Force
|
||||
} finally { Pop-Location }
|
||||
|
||||
try {
|
||||
Push-Location $goRoot
|
||||
try {
|
||||
if (-not $SkipTests) {
|
||||
go test ./...
|
||||
if ($LASTEXITCODE -ne 0) { throw "go test failed with exit code $LASTEXITCODE" }
|
||||
go vet ./...
|
||||
if ($LASTEXITCODE -ne 0) { throw "go vet failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
$output = Join-Path $outputDir 'bd2server.exe'
|
||||
go build -tags embedded_state_tool -trimpath -ldflags '-s -w' -o $output .\cmd\bd2server
|
||||
if ($LASTEXITCODE -ne 0) { throw "go build failed with exit code $LASTEXITCODE" }
|
||||
Write-Host "Built single-file release: $output"
|
||||
} finally { Pop-Location }
|
||||
} finally {
|
||||
if (Test-Path -LiteralPath $embedTool) { Remove-Item -LiteralPath $embedTool -Force }
|
||||
dotnet build $localIdentityProject -c Release "-p:GameDir=$GameDir" --nologo
|
||||
if ($LASTEXITCODE -ne 0) { throw "LocalIdentity build failed with exit code $LASTEXITCODE" }
|
||||
$identityPlugin = Join-Path $root 'plugins\LocalIdentity\bin\Release\netstandard2.1\BD2LocalIdentity.dll'
|
||||
if (-not (Test-Path -LiteralPath $identityPlugin -PathType Leaf)) {
|
||||
throw "LocalIdentity build output is missing: $identityPlugin"
|
||||
}
|
||||
Copy-Item -LiteralPath $identityPlugin -Destination (Join-Path $packagePluginDir 'BD2LocalIdentity.dll') -Force
|
||||
|
||||
Push-Location $goRoot
|
||||
try {
|
||||
if (-not $SkipTests) {
|
||||
go test ./...
|
||||
if ($LASTEXITCODE -ne 0) { throw "go test failed with exit code $LASTEXITCODE" }
|
||||
go vet ./...
|
||||
if ($LASTEXITCODE -ne 0) { throw "go vet failed with exit code $LASTEXITCODE" }
|
||||
}
|
||||
go build -trimpath -ldflags '-s -w' -o (Join-Path $packageDir 'bd2server.exe') .\cmd\bd2server
|
||||
if ($LASTEXITCODE -ne 0) { throw "go build failed with exit code $LASTEXITCODE" }
|
||||
} finally { Pop-Location }
|
||||
|
||||
Copy-Item -LiteralPath (Join-Path $goRoot 'seed') -Destination (Join-Path $packageGoDir 'seed') -Recurse -Force
|
||||
Copy-Item -LiteralPath (Join-Path $root 'RELEASE.md') -Destination (Join-Path $packageDir 'README.md') -Force
|
||||
Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination (Join-Path $packageDir 'LICENSE') -Force
|
||||
|
||||
Compress-Archive -LiteralPath $packageDir -DestinationPath $archive -CompressionLevel Optimal
|
||||
Write-Host "Built release directory: $packageDir"
|
||||
Write-Host "Built release archive: $archive"
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"bd2server/internal/account"
|
||||
"bd2server/internal/battle"
|
||||
"bd2server/internal/bootstrap"
|
||||
"bd2server/internal/clientplugin"
|
||||
"bd2server/internal/deck"
|
||||
"bd2server/internal/feature"
|
||||
"bd2server/internal/gacha"
|
||||
@@ -71,12 +72,27 @@ func serve(args []string) error {
|
||||
deckSeed := fs.String("deck-seed", `seed\v2_34_13\decks.json`, "versioned starter deck")
|
||||
deckState := fs.String("deck-state", `..\data\state\deck.json`, "local deck and waypoint save")
|
||||
worldSeed := fs.String("world-seed", `seed\v2_34_13\world.json`, "versioned starter world")
|
||||
stateTool := fs.String("state-tool", "", "optional Haskell state tool override (release builds embed it)")
|
||||
stateTool := fs.String("state-tool", "", "optional Haskell state tool override (release package includes it)")
|
||||
gameDir := fs.String("game-dir", "", "Brown Dust II client directory (required)")
|
||||
identityPlugin := fs.String("identity-plugin", "", "optional BD2LocalIdentity.dll override for development")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *cdn == "" || *gameData == "" || *gameDataVersion == "" {
|
||||
return errors.New("serve requires --cdn, --game-data, and --game-data-version")
|
||||
if *cdn == "" || *gameData == "" || *gameDataVersion == "" || *gameDir == "" {
|
||||
return errors.New("serve requires --game-dir, --cdn, --game-data, and --game-data-version")
|
||||
}
|
||||
packagedPlugin, err := clientplugin.ResolvePackaged(*identityPlugin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pluginResult, err := clientplugin.Install(*gameDir, packagedPlugin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pluginResult.Changed {
|
||||
slog.Info("installed local identity plugin", "path", pluginResult.Destination)
|
||||
} else {
|
||||
slog.Info("local identity plugin is current", "path", pluginResult.Destination)
|
||||
}
|
||||
base := "http://" + *listen + "/game/"
|
||||
cfg := bootstrap.Config{
|
||||
@@ -337,7 +353,7 @@ func usage() {
|
||||
fmt.Fprintln(os.Stderr, `bd2server - BrownDust II 2.34.13 local development server
|
||||
|
||||
Usage:
|
||||
bd2server serve --cdn DIR --game-data DIR --game-data-version VERSION [options]
|
||||
bd2server serve --game-dir DIR --cdn DIR --game-data DIR --game-data-version VERSION [options]
|
||||
bd2server patch-client --game-dir DIR [options]
|
||||
bd2server state check [options]
|
||||
bd2server state migrate-v1-v2 [options]
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package clientplugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
FileName = "BD2LocalIdentity.dll"
|
||||
BepInExReleasesURL = "https://github.com/BepInEx/BepInEx/releases"
|
||||
)
|
||||
|
||||
type Result struct {
|
||||
Destination string
|
||||
Changed bool
|
||||
}
|
||||
|
||||
func ResolvePackaged(explicit string) (string, error) {
|
||||
if explicit != "" {
|
||||
return filepath.Clean(explicit), nil
|
||||
}
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("clientplugin: resolve server executable: %w", err)
|
||||
}
|
||||
return filepath.Join(filepath.Dir(executable), "plugins", FileName), nil
|
||||
}
|
||||
|
||||
// Install verifies that the user installed BepInEx, then atomically stages the
|
||||
// packaged local-identity plugin into its plugins directory. It never installs
|
||||
// or downloads BepInEx itself.
|
||||
func Install(gameDir, source string) (Result, error) {
|
||||
if gameDir == "" || source == "" {
|
||||
return Result{}, errors.New("clientplugin: game directory and plugin source are required")
|
||||
}
|
||||
gameDir = filepath.Clean(gameDir)
|
||||
source = filepath.Clean(source)
|
||||
gameExecutable := filepath.Join(gameDir, "BrownDust II.exe")
|
||||
if info, err := os.Stat(gameExecutable); err != nil || info.IsDir() {
|
||||
return Result{}, fmt.Errorf("clientplugin: game executable is unavailable at %q", gameExecutable)
|
||||
}
|
||||
bepInEx := filepath.Join(gameDir, "BepInEx", "core", "BepInEx.dll")
|
||||
if info, err := os.Stat(bepInEx); err != nil || info.IsDir() {
|
||||
return Result{}, fmt.Errorf("clientplugin: BepInEx is not installed; install it manually from %s, then restart the server; %s was not copied", BepInExReleasesURL, FileName)
|
||||
}
|
||||
sourceData, err := os.ReadFile(source)
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("clientplugin: read packaged %s: %w", FileName, err)
|
||||
}
|
||||
if len(sourceData) == 0 {
|
||||
return Result{}, fmt.Errorf("clientplugin: packaged %s is empty", FileName)
|
||||
}
|
||||
pluginDir := filepath.Join(gameDir, "BepInEx", "plugins")
|
||||
destination := filepath.Join(pluginDir, FileName)
|
||||
if installed, err := os.ReadFile(destination); err == nil {
|
||||
if bytes.Equal(hash(installed), hash(sourceData)) {
|
||||
return Result{Destination: destination}, nil
|
||||
}
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return Result{}, fmt.Errorf("clientplugin: inspect installed plugin: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(pluginDir, 0o755); err != nil {
|
||||
return Result{}, fmt.Errorf("clientplugin: create plugin directory: %w", err)
|
||||
}
|
||||
temporary, err := os.CreateTemp(pluginDir, ".BD2LocalIdentity-*.tmp")
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("clientplugin: create temporary plugin: %w", err)
|
||||
}
|
||||
temporaryPath := temporary.Name()
|
||||
defer os.Remove(temporaryPath)
|
||||
if _, err = io.Copy(temporary, bytes.NewReader(sourceData)); err == nil {
|
||||
err = temporary.Sync()
|
||||
}
|
||||
if closeErr := temporary.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
return Result{}, fmt.Errorf("clientplugin: stage plugin: %w", err)
|
||||
}
|
||||
if err := replaceFile(temporaryPath, destination); err != nil {
|
||||
return Result{}, fmt.Errorf("clientplugin: install plugin (close the game client first): %w", err)
|
||||
}
|
||||
return Result{Destination: destination, Changed: true}, nil
|
||||
}
|
||||
|
||||
func hash(data []byte) []byte {
|
||||
sum := sha256.Sum256(data)
|
||||
return sum[:]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package clientplugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInstallRequiresBepInExWithoutCopyingPlugin(t *testing.T) {
|
||||
gameDir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(gameDir, "BrownDust II.exe"), []byte("game"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := filepath.Join(t.TempDir(), FileName)
|
||||
if err := os.WriteFile(source, []byte("plugin"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := Install(gameDir, source)
|
||||
if err == nil || !strings.Contains(err.Error(), BepInExReleasesURL) {
|
||||
t.Fatalf("missing BepInEx error=%v", err)
|
||||
}
|
||||
if _, statErr := os.Stat(filepath.Join(gameDir, "BepInEx", "plugins", FileName)); !os.IsNotExist(statErr) {
|
||||
t.Fatalf("plugin was copied without BepInEx: %v", statErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallCopiesUpdatesAndSkipsIdenticalPlugin(t *testing.T) {
|
||||
gameDir := t.TempDir()
|
||||
for path, data := range map[string][]byte{
|
||||
filepath.Join(gameDir, "BrownDust II.exe"): []byte("game"),
|
||||
filepath.Join(gameDir, "BepInEx", "core", "BepInEx.dll"): []byte("bepinex"),
|
||||
} {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
source := filepath.Join(t.TempDir(), FileName)
|
||||
if err := os.WriteFile(source, []byte("v1"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := Install(gameDir, source)
|
||||
if err != nil || !first.Changed {
|
||||
t.Fatalf("first install=%+v err=%v", first, err)
|
||||
}
|
||||
second, err := Install(gameDir, source)
|
||||
if err != nil || second.Changed {
|
||||
t.Fatalf("idempotent install=%+v err=%v", second, err)
|
||||
}
|
||||
if err := os.WriteFile(source, []byte("v2"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
third, err := Install(gameDir, source)
|
||||
if err != nil || !third.Changed {
|
||||
t.Fatalf("update=%+v err=%v", third, err)
|
||||
}
|
||||
got, err := os.ReadFile(third.Destination)
|
||||
if err != nil || string(got) != "v2" {
|
||||
t.Fatalf("installed=%q err=%v", got, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package clientplugin
|
||||
|
||||
import "os"
|
||||
|
||||
func replaceFile(source, destination string) error {
|
||||
return os.Rename(source, destination)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//go:build windows
|
||||
|
||||
package clientplugin
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var moveFileEx = syscall.NewLazyDLL("kernel32.dll").NewProc("MoveFileExW")
|
||||
|
||||
func replaceFile(source, destination string) error {
|
||||
sourcePtr, err := syscall.UTF16PtrFromString(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destinationPtr, err := syscall.UTF16PtrFromString(destination)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result, _, callErr := moveFileEx.Call(
|
||||
uintptr(unsafe.Pointer(sourcePtr)),
|
||||
uintptr(unsafe.Pointer(destinationPtr)),
|
||||
0x1|0x8, // MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH
|
||||
)
|
||||
if result == 0 {
|
||||
if callErr != syscall.Errno(0) {
|
||||
return callErr
|
||||
}
|
||||
return os.ErrInvalid
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ResolveTool prefers an explicit development override. Release builds embed
|
||||
// the Haskell executable and materialize it into a private temporary directory.
|
||||
// ResolveTool prefers an explicit development override. Release packages place
|
||||
// bd2-state.exe beside bd2server.exe so both components remain inspectable.
|
||||
func ResolveTool(explicit string) (string, func(), error) {
|
||||
if explicit != "" {
|
||||
return filepath.Clean(explicit), func() {}, nil
|
||||
@@ -15,16 +15,11 @@ func ResolveTool(explicit string) (string, func(), error) {
|
||||
if env := os.Getenv("BD2_STATE_TOOL"); env != "" {
|
||||
return filepath.Clean(env), func() {}, nil
|
||||
}
|
||||
if path, cleanup, ok, err := embeddedStateTool(); err != nil {
|
||||
return "", func() {}, err
|
||||
} else if ok {
|
||||
return path, cleanup, nil
|
||||
}
|
||||
if executable, err := os.Executable(); err == nil {
|
||||
sibling := filepath.Join(filepath.Dir(executable), "bd2-state.exe")
|
||||
if info, statErr := os.Stat(sibling); statErr == nil && !info.IsDir() {
|
||||
return sibling, func() {}, nil
|
||||
}
|
||||
}
|
||||
return "", func() {}, errors.New("statebridge: no Haskell state tool; use --state-tool/--tool or build the embedded release")
|
||||
return "", func() {}, errors.New("statebridge: bd2-state.exe is missing beside bd2server.exe; use --state-tool/--tool for a development build")
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//go:build embedded_state_tool
|
||||
|
||||
package statebridge
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
//go:embed tool/bd2-state.exe
|
||||
var embeddedStateToolBytes []byte
|
||||
|
||||
func embeddedStateTool() (string, func(), bool, error) {
|
||||
sum := sha256.Sum256(embeddedStateToolBytes)
|
||||
dir, err := os.MkdirTemp("", "bd2-state-"+hex.EncodeToString(sum[:6])+"-")
|
||||
if err != nil {
|
||||
return "", func() {}, true, err
|
||||
}
|
||||
cleanup := func() { _ = os.RemoveAll(dir) }
|
||||
path := filepath.Join(dir, "bd2-state.exe")
|
||||
if err := os.WriteFile(path, embeddedStateToolBytes, 0o700); err != nil {
|
||||
cleanup()
|
||||
return "", func() {}, true, err
|
||||
}
|
||||
return path, cleanup, true, nil
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
//go:build !embedded_state_tool
|
||||
|
||||
package statebridge
|
||||
|
||||
func embeddedStateTool() (string, func(), bool, error) {
|
||||
return "", func() {}, false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user