Add release auto-update launcher
All checks were successful
build-windows-exe / build (push) Successful in 1m47s
All checks were successful
build-windows-exe / build (push) Successful in 1m47s
This commit is contained in:
@@ -33,8 +33,11 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cd VRWT_Tool/VRC_OSC
|
cd VRWT_Tool/VRC_OSC
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
GO111MODULE=on GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o dist/vrc_osc.exe ./cmd/vrc_osc
|
VERSION="${GITHUB_REF_NAME}"
|
||||||
GO111MODULE=on GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o dist/vrwt_tool.exe ./cmd/vrwt_tool
|
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
GO111MODULE=on GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X vrc_osc_go/internal/buildinfo.Version=${VERSION} -X vrc_osc_go/internal/buildinfo.BuildTime=${BUILD_TIME}" -o dist/vrc_osc.exe ./cmd/vrc_osc
|
||||||
|
GO111MODULE=on GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X vrc_osc_go/internal/buildinfo.Version=${VERSION} -X vrc_osc_go/internal/buildinfo.BuildTime=${BUILD_TIME}" -o dist/vrc_osc_launcher.exe ./cmd/vrc_osc_launcher
|
||||||
|
GO111MODULE=on GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X vrc_osc_go/internal/buildinfo.Version=${VERSION} -X vrc_osc_go/internal/buildinfo.BuildTime=${BUILD_TIME}" -o dist/vrwt_tool.exe ./cmd/vrwt_tool
|
||||||
|
|
||||||
- name: Package executables
|
- name: Package executables
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -43,7 +46,7 @@ jobs:
|
|||||||
cp config/config.example.toml dist/config.example.toml
|
cp config/config.example.toml dist/config.example.toml
|
||||||
cp config/secrets.example.toml dist/secrets.example.toml
|
cp config/secrets.example.toml dist/secrets.example.toml
|
||||||
cp config/guests.example.txt dist/guests.example.txt
|
cp config/guests.example.txt dist/guests.example.txt
|
||||||
(cd dist && zip -r vrc_osc-windows.zip vrc_osc.exe vrwt_tool.exe config.example.toml secrets.example.toml guests.example.txt)
|
(cd dist && zip -r vrc_osc-windows.zip vrc_osc.exe vrc_osc_launcher.exe vrwt_tool.exe config.example.toml secrets.example.toml guests.example.txt)
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
env:
|
env:
|
||||||
|
|||||||
40
VRWT_Tool/VRC_OSC/cmd/vrc_osc_launcher/main.go
Normal file
40
VRWT_Tool/VRC_OSC/cmd/vrc_osc_launcher/main.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"vrc_osc_go/internal/buildinfo"
|
||||||
|
"vrc_osc_go/internal/common"
|
||||||
|
"vrc_osc_go/internal/update"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var baseURL string
|
||||||
|
var ownerRepo string
|
||||||
|
var assetName string
|
||||||
|
flag.StringVar(&baseURL, "base-url", "https://git.vrcworldtour.com", "Gitea base URL")
|
||||||
|
flag.StringVar(&ownerRepo, "repo", "every_holiday/VRCWT-OSC", "owner/repo")
|
||||||
|
flag.StringVar(&assetName, "asset", "vrc_osc-windows.zip", "release asset name")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
client := &update.Client{BaseURL: baseURL}
|
||||||
|
mgr := &update.Manager{
|
||||||
|
Client: client,
|
||||||
|
OwnerRepo: ownerRepo,
|
||||||
|
AssetName: assetName,
|
||||||
|
CurrentLabel: buildinfo.Version,
|
||||||
|
}
|
||||||
|
if _, err := mgr.CheckAndUpdate(); err != nil {
|
||||||
|
log.Printf("auto update skipped: %v", err)
|
||||||
|
}
|
||||||
|
exe := filepath.Join(common.RootDir(), "vrc_osc.exe")
|
||||||
|
if _, err := os.Stat(exe); err != nil {
|
||||||
|
log.Fatalf("missing exe: %v", err)
|
||||||
|
}
|
||||||
|
if err := update.Launch(exe, os.Args[1:]); err != nil {
|
||||||
|
log.Fatalf("launch failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
VRWT_Tool/VRC_OSC/internal/buildinfo/buildinfo.go
Normal file
6
VRWT_Tool/VRC_OSC/internal/buildinfo/buildinfo.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package buildinfo
|
||||||
|
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
BuildTime = "unknown"
|
||||||
|
)
|
||||||
106
VRWT_Tool/VRC_OSC/internal/update/manager.go
Normal file
106
VRWT_Tool/VRC_OSC/internal/update/manager.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"vrc_osc_go/internal/buildinfo"
|
||||||
|
"vrc_osc_go/internal/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
Client *Client
|
||||||
|
OwnerRepo string
|
||||||
|
AssetName string
|
||||||
|
CurrentLabel string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) CheckAndUpdate() (bool, error) {
|
||||||
|
if m.Client == nil || m.OwnerRepo == "" || m.AssetName == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
latest, err := m.Client.LatestRelease(m.OwnerRepo)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if latest.TagName == "" || latest.TagName == m.CurrentLabel {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
assetURL := ""
|
||||||
|
for _, a := range latest.Assets {
|
||||||
|
if a.Name == m.AssetName {
|
||||||
|
assetURL = a.URL
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if assetURL == "" {
|
||||||
|
return false, fmt.Errorf("asset %q not found in release %s", m.AssetName, latest.TagName)
|
||||||
|
}
|
||||||
|
base := common.RootDir()
|
||||||
|
tmpDir := filepath.Join(base, "update", latest.TagName)
|
||||||
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
zipPath := filepath.Join(tmpDir, m.AssetName)
|
||||||
|
log.Printf("update available: %s -> %s", m.CurrentLabel, latest.TagName)
|
||||||
|
if err := m.Client.DownloadAsset(assetURL, zipPath); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := ExtractZip(zipPath, tmpDir); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
exePath := filepath.Join(base, CurrentExeName())
|
||||||
|
newExe := filepath.Join(tmpDir, CurrentExeName())
|
||||||
|
if _, err := os.Stat(newExe); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
backup := exePath + ".bak"
|
||||||
|
_ = os.Remove(backup)
|
||||||
|
if err := os.Rename(exePath, backup); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if err := CopyFile(newExe, exePath); err != nil {
|
||||||
|
_ = os.Rename(backup, exePath)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
_ = os.Remove(backup)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyFile(src, dst string) error {
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
_, err = io.Copy(out, in)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Launch(exe string, args []string) error {
|
||||||
|
cmd := exec.Command(exe, args...)
|
||||||
|
cmd.Dir = filepath.Dir(exe)
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Version() string { return buildinfo.Version }
|
||||||
|
|
||||||
|
func CurrentExeName() string {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "vrc_osc.exe"
|
||||||
|
}
|
||||||
|
return "vrc_osc"
|
||||||
|
}
|
||||||
125
VRWT_Tool/VRC_OSC/internal/update/release.go
Normal file
125
VRWT_Tool/VRC_OSC/internal/update/release.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReleaseInfo struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
Assets []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"browser_download_url"`
|
||||||
|
} `json:"assets"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
BaseURL string
|
||||||
|
HTTP *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) LatestRelease(ownerRepo string) (*ReleaseInfo, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, c.apiURL(ownerRepo, "/releases/latest"), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var info ReleaseInfo
|
||||||
|
if err := c.doJSON(req, &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DownloadAsset(url, dst string) error {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := c.httpClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("download failed: %s", resp.Status)
|
||||||
|
}
|
||||||
|
f, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = io.Copy(f, resp.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractZip(zipPath, dstDir string) error {
|
||||||
|
zr, err := zip.OpenReader(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer zr.Close()
|
||||||
|
root := filepath.Clean(dstDir) + string(os.PathSeparator)
|
||||||
|
for _, f := range zr.File {
|
||||||
|
target := filepath.Clean(filepath.Join(dstDir, f.Name))
|
||||||
|
if target != filepath.Clean(dstDir) && !strings.HasPrefix(target, root) {
|
||||||
|
return fmt.Errorf("zip path escapes destination: %s", f.Name)
|
||||||
|
}
|
||||||
|
if f.FileInfo().IsDir() {
|
||||||
|
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
out, err := os.Create(target)
|
||||||
|
if err != nil {
|
||||||
|
rc.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, copyErr := io.Copy(out, rc)
|
||||||
|
out.Close()
|
||||||
|
rc.Close()
|
||||||
|
if copyErr != nil {
|
||||||
|
return copyErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) apiURL(ownerRepo, suffix string) string {
|
||||||
|
base := strings.TrimRight(c.BaseURL, "/")
|
||||||
|
return base + "/api/v1/repos/" + strings.Trim(ownerRepo, "/") + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) doJSON(req *http.Request, out any) error {
|
||||||
|
resp, err := c.httpClient().Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||||
|
return fmt.Errorf("request failed: %s: %s", resp.Status, strings.TrimSpace(string(body)))
|
||||||
|
}
|
||||||
|
return json.NewDecoder(resp.Body).Decode(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) httpClient() *http.Client {
|
||||||
|
if c.HTTP != nil {
|
||||||
|
return c.HTTP
|
||||||
|
}
|
||||||
|
return &http.Client{Timeout: 30 * time.Second}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user