mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-18 03:57:06 +08:00
734 lines
17 KiB
Go
734 lines
17 KiB
Go
package updater
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"cursor/internal/buildinfo"
|
|
"cursor/internal/logger"
|
|
"cursor/internal/netproxy"
|
|
|
|
"github.com/wailsapp/wails/v3/pkg/application"
|
|
)
|
|
|
|
const checkInterval = 20 * time.Minute
|
|
|
|
type State string
|
|
|
|
const (
|
|
StateIdle State = "idle"
|
|
StateChecking State = "checking"
|
|
StateDownloading State = "downloading"
|
|
StateReady State = "ready"
|
|
StateInstalling State = "installing"
|
|
StateError State = "error"
|
|
)
|
|
|
|
var errNoSupportedAsset = errors.New("no supported update asset for current platform")
|
|
|
|
type manifest struct {
|
|
Version string `json:"version"`
|
|
ReleaseDate string `json:"release_date"`
|
|
ReleaseNotes string `json:"release_notes"`
|
|
Platforms map[string]manifestPlatform `json:"platforms"`
|
|
Mandatory bool `json:"mandatory"`
|
|
}
|
|
|
|
type manifestPlatform struct {
|
|
URL string `json:"url"`
|
|
Size int64 `json:"size"`
|
|
Checksum string `json:"checksum"`
|
|
}
|
|
|
|
type UpdateInfo struct {
|
|
Version string
|
|
ReleaseDate string
|
|
ReleaseNotes string
|
|
Mandatory bool
|
|
PlatformKey string
|
|
Asset manifestPlatform
|
|
}
|
|
|
|
type Manager struct {
|
|
app *application.App
|
|
|
|
client *http.Client
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
|
|
mu sync.Mutex
|
|
state State
|
|
currentInfo *UpdateInfo
|
|
readyInfo *UpdateInfo
|
|
downloadedPath string
|
|
}
|
|
|
|
func NewManager(app *application.App) *Manager {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
return &Manager{
|
|
app: app,
|
|
client: netproxy.NewHTTPClient(5 * time.Minute),
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
state: StateIdle,
|
|
}
|
|
}
|
|
|
|
func (m *Manager) Start() {
|
|
m.emitState(StateIdle, nil, "", "", false, "")
|
|
go m.loop()
|
|
}
|
|
|
|
func (m *Manager) Shutdown() {
|
|
m.cancel()
|
|
}
|
|
|
|
func (m *Manager) CheckNow(manual bool) {
|
|
go m.checkNow(manual)
|
|
}
|
|
|
|
func (m *Manager) InstallReadyUpdate() error {
|
|
return m.installReadyUpdate()
|
|
}
|
|
|
|
func (m *Manager) loop() {
|
|
m.checkNow(false)
|
|
|
|
ticker := time.NewTicker(checkInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-m.ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
m.checkNow(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *Manager) checkNow(manual bool) {
|
|
if err := m.ctx.Err(); err != nil {
|
|
return
|
|
}
|
|
|
|
m.mu.Lock()
|
|
switch m.state {
|
|
case StateReady:
|
|
info := m.readyInfo
|
|
m.mu.Unlock()
|
|
m.emitReady(info, true)
|
|
return
|
|
case StateChecking, StateDownloading, StateInstalling:
|
|
state := m.state
|
|
info := m.currentInfo
|
|
m.mu.Unlock()
|
|
if manual {
|
|
m.emitState(state, info, "", fmt.Sprintf("当前正在%s,请稍后再试。", stateLabel(state)), true, "idle")
|
|
}
|
|
return
|
|
default:
|
|
m.state = StateChecking
|
|
m.currentInfo = nil
|
|
m.readyInfo = nil
|
|
m.downloadedPath = ""
|
|
m.mu.Unlock()
|
|
}
|
|
m.emitState(StateChecking, nil, "", "", false, "")
|
|
|
|
ctx, cancel := context.WithTimeout(m.ctx, 90*time.Second)
|
|
defer cancel()
|
|
|
|
info, err := m.fetchUpdateInfo(ctx)
|
|
if err != nil {
|
|
logger.Errorf("检查更新失败: %v", err)
|
|
m.setState(StateError, nil, "")
|
|
m.emitError(nil, err.Error(), manual)
|
|
return
|
|
}
|
|
if info == nil {
|
|
m.setState(StateIdle, nil, "")
|
|
m.emitState(StateIdle, nil, "", fmt.Sprintf("当前已是最新版本(v%s)。", buildinfo.CurrentVersion()), manual, "idle")
|
|
return
|
|
}
|
|
|
|
logger.Infof("发现新版本:current=%s latest=%s platform=%s", buildinfo.CurrentVersion(), info.Version, info.PlatformKey)
|
|
m.setState(StateDownloading, info, "")
|
|
m.emitState(StateDownloading, info, "", "", false, "")
|
|
|
|
archivePath, err := m.downloadUpdate(ctx, info)
|
|
if err != nil {
|
|
logger.Errorf("下载更新失败: %v", err)
|
|
m.setState(StateError, info, "")
|
|
m.emitError(info, err.Error(), manual)
|
|
return
|
|
}
|
|
|
|
m.setState(StateReady, info, archivePath)
|
|
m.emitState(StateReady, info, "", "", false, "")
|
|
m.emitReady(info, true)
|
|
}
|
|
|
|
func (m *Manager) fetchUpdateInfo(ctx context.Context) (*UpdateInfo, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, buildinfo.UpdateBaseURL+"update.json", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := m.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("update manifest request failed: %s", resp.Status)
|
|
}
|
|
|
|
var data manifest
|
|
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if compareVersions(data.Version, buildinfo.CurrentVersion()) <= 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
platformKey, err := currentPlatformKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
asset, ok := data.Platforms[platformKey]
|
|
if !ok {
|
|
return nil, errNoSupportedAsset
|
|
}
|
|
|
|
return &UpdateInfo{
|
|
Version: strings.TrimSpace(data.Version),
|
|
ReleaseDate: strings.TrimSpace(data.ReleaseDate),
|
|
ReleaseNotes: strings.TrimSpace(data.ReleaseNotes),
|
|
Mandatory: data.Mandatory,
|
|
PlatformKey: platformKey,
|
|
Asset: asset,
|
|
}, nil
|
|
}
|
|
|
|
func (m *Manager) downloadUpdate(ctx context.Context, info *UpdateInfo) (string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, info.Asset.URL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
resp, err := m.client.Do(req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return "", fmt.Errorf("download request failed: %s", resp.Status)
|
|
}
|
|
|
|
tempFile, err := os.CreateTemp("", "cursor-byok-update-*"+archiveSuffix(info.Asset.URL))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer func() {
|
|
_ = tempFile.Close()
|
|
}()
|
|
|
|
hasher := sha256.New()
|
|
total := info.Asset.Size
|
|
if resp.ContentLength > 0 {
|
|
total = resp.ContentLength
|
|
}
|
|
progress := newProgressWriter(func(downloaded int64) {
|
|
m.emitProgress(info, downloaded, total)
|
|
})
|
|
m.emitProgress(info, 0, total)
|
|
if _, err := io.Copy(io.MultiWriter(tempFile, hasher, progress), resp.Body); err != nil {
|
|
_ = os.Remove(tempFile.Name())
|
|
return "", err
|
|
}
|
|
m.emitProgress(info, total, total)
|
|
|
|
expectedChecksum := strings.TrimSpace(strings.TrimPrefix(info.Asset.Checksum, "sha256:"))
|
|
actualChecksum := hex.EncodeToString(hasher.Sum(nil))
|
|
if expectedChecksum != "" && !strings.EqualFold(expectedChecksum, actualChecksum) {
|
|
_ = os.Remove(tempFile.Name())
|
|
return "", fmt.Errorf("checksum mismatch: expected %s got %s", expectedChecksum, actualChecksum)
|
|
}
|
|
|
|
logger.Infof("更新包下载完成:version=%s path=%s", info.Version, tempFile.Name())
|
|
return tempFile.Name(), nil
|
|
}
|
|
|
|
func (m *Manager) installReadyUpdate() error {
|
|
m.mu.Lock()
|
|
if m.state != StateReady || m.readyInfo == nil || m.downloadedPath == "" {
|
|
m.mu.Unlock()
|
|
return errors.New("当前没有可安装的更新")
|
|
}
|
|
info := m.readyInfo
|
|
archivePath := m.downloadedPath
|
|
m.state = StateInstalling
|
|
m.mu.Unlock()
|
|
m.emitState(StateInstalling, info, "", "", false, "")
|
|
|
|
logger.Infof("开始安装更新:version=%s path=%s", info.Version, archivePath)
|
|
|
|
if err := m.spawnInstaller(archivePath); err != nil {
|
|
m.setState(StateReady, info, archivePath)
|
|
m.emitState(StateReady, info, "", "", false, "")
|
|
m.emitError(info, err.Error(), true)
|
|
return err
|
|
}
|
|
|
|
m.app.Quit()
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) spawnInstaller(archivePath string) error {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return m.spawnDarwinInstaller(archivePath)
|
|
case "linux":
|
|
return m.spawnLinuxInstaller(archivePath)
|
|
case "windows":
|
|
return m.spawnWindowsInstaller(archivePath)
|
|
default:
|
|
return fmt.Errorf("unsupported updater platform: %s", runtime.GOOS)
|
|
}
|
|
}
|
|
|
|
func (m *Manager) spawnDarwinInstaller(archivePath string) error {
|
|
executablePath, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
executablePath, err = filepath.Abs(executablePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appBundlePath, err := resolveMacBundlePath(executablePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
scriptPath, err := writeHelperScript("cursor-byok-update-*.sh", darwinInstallerScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command(
|
|
"/bin/sh",
|
|
scriptPath,
|
|
fmt.Sprintf("%d", os.Getpid()),
|
|
archivePath,
|
|
appBundlePath,
|
|
)
|
|
return cmd.Start()
|
|
}
|
|
|
|
func (m *Manager) spawnWindowsInstaller(archivePath string) error {
|
|
executablePath, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
executablePath, err = filepath.Abs(executablePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
scriptPath, err := writeHelperScript("cursor-byok-update-*.ps1", windowsInstallerScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command(
|
|
"powershell",
|
|
"-NoProfile",
|
|
"-ExecutionPolicy",
|
|
"Bypass",
|
|
"-WindowStyle",
|
|
"Hidden",
|
|
"-File",
|
|
scriptPath,
|
|
"-PidToWait",
|
|
fmt.Sprintf("%d", os.Getpid()),
|
|
"-ArchivePath",
|
|
archivePath,
|
|
"-TargetExecutable",
|
|
executablePath,
|
|
)
|
|
return cmd.Start()
|
|
}
|
|
|
|
func (m *Manager) spawnLinuxInstaller(archivePath string) error {
|
|
executablePath, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
executablePath, err = filepath.Abs(executablePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ensureWritableDirectory(filepath.Dir(executablePath)); err != nil {
|
|
return fmt.Errorf("当前 Linux 安装目录不可写,无法执行原地更新: %w", err)
|
|
}
|
|
|
|
scriptPath, err := writeHelperScript("cursor-byok-update-*.sh", linuxInstallerScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.Command(
|
|
"/bin/sh",
|
|
scriptPath,
|
|
fmt.Sprintf("%d", os.Getpid()),
|
|
archivePath,
|
|
executablePath,
|
|
)
|
|
return cmd.Start()
|
|
}
|
|
|
|
func (m *Manager) emitState(state State, info *UpdateInfo, errMsg, message string, prompt bool, promptKind string) {
|
|
if m.app == nil {
|
|
return
|
|
}
|
|
|
|
payload := StatePayload{
|
|
State: string(state),
|
|
Error: strings.TrimSpace(errMsg),
|
|
Message: strings.TrimSpace(message),
|
|
Prompt: prompt,
|
|
PromptKind: strings.TrimSpace(promptKind),
|
|
}
|
|
if info != nil {
|
|
payload.Version = info.Version
|
|
payload.ReleaseDate = info.ReleaseDate
|
|
payload.ReleaseNotes = info.ReleaseNotes
|
|
}
|
|
m.app.Event.Emit(EventState, payload)
|
|
}
|
|
|
|
func (m *Manager) emitProgress(info *UpdateInfo, downloaded, total int64) {
|
|
if m.app == nil {
|
|
return
|
|
}
|
|
|
|
if total < 0 {
|
|
total = 0
|
|
}
|
|
percentage := 0.0
|
|
if total > 0 {
|
|
percentage = (float64(downloaded) / float64(total)) * 100
|
|
if percentage > 100 {
|
|
percentage = 100
|
|
}
|
|
}
|
|
|
|
payload := ProgressPayload{
|
|
State: string(StateDownloading),
|
|
Downloaded: downloaded,
|
|
Total: total,
|
|
Percentage: percentage,
|
|
}
|
|
if info != nil {
|
|
payload.Version = info.Version
|
|
}
|
|
m.app.Event.Emit(EventProgress, payload)
|
|
}
|
|
|
|
func (m *Manager) emitReady(info *UpdateInfo, prompt bool) {
|
|
if m.app == nil || info == nil {
|
|
return
|
|
}
|
|
|
|
payload := ReadyPayload{
|
|
State: string(StateReady),
|
|
Version: info.Version,
|
|
ReleaseDate: info.ReleaseDate,
|
|
ReleaseNotes: info.ReleaseNotes,
|
|
Prompt: prompt,
|
|
PromptKind: "ready",
|
|
}
|
|
m.app.Event.Emit(EventReady, payload)
|
|
}
|
|
|
|
func (m *Manager) emitError(info *UpdateInfo, errMsg string, prompt bool) {
|
|
if m.app == nil {
|
|
return
|
|
}
|
|
|
|
payload := ErrorPayload{
|
|
State: string(StateError),
|
|
Error: strings.TrimSpace(errMsg),
|
|
Prompt: prompt,
|
|
PromptKind: "error",
|
|
}
|
|
if info != nil {
|
|
payload.Version = info.Version
|
|
}
|
|
m.app.Event.Emit(EventError, payload)
|
|
}
|
|
|
|
func (m *Manager) setState(state State, info *UpdateInfo, archivePath string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.state = state
|
|
m.currentInfo = info
|
|
m.readyInfo = info
|
|
m.downloadedPath = archivePath
|
|
}
|
|
|
|
func currentPlatformKey() (string, error) {
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
switch runtime.GOARCH {
|
|
case "arm64":
|
|
return "macos-arm64", nil
|
|
case "amd64":
|
|
return "macos-amd64", nil
|
|
}
|
|
case "windows":
|
|
if runtime.GOARCH == "amd64" {
|
|
return "windows-amd64", nil
|
|
}
|
|
case "linux":
|
|
if runtime.GOARCH == "amd64" {
|
|
return "linux-amd64", nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("unsupported platform for updater: %s/%s", runtime.GOOS, runtime.GOARCH)
|
|
}
|
|
|
|
func archiveSuffix(downloadURL string) string {
|
|
if strings.HasSuffix(downloadURL, ".tar.gz") {
|
|
return ".tar.gz"
|
|
}
|
|
return filepath.Ext(downloadURL)
|
|
}
|
|
|
|
func stateLabel(state State) string {
|
|
switch state {
|
|
case StateChecking:
|
|
return "检查更新"
|
|
case StateDownloading:
|
|
return "下载更新"
|
|
case StateInstalling:
|
|
return "安装更新"
|
|
case StateError:
|
|
return "更新失败"
|
|
default:
|
|
return string(state)
|
|
}
|
|
}
|
|
|
|
func resolveMacBundlePath(executablePath string) (string, error) {
|
|
contentsDir := filepath.Dir(filepath.Dir(executablePath))
|
|
if filepath.Base(contentsDir) != "Contents" {
|
|
return "", errors.New("当前 macOS 运行环境不是 .app 包,无法执行原地更新")
|
|
}
|
|
appBundlePath := filepath.Dir(contentsDir)
|
|
if filepath.Ext(appBundlePath) != ".app" {
|
|
return "", errors.New("无法定位 macOS 应用包路径")
|
|
}
|
|
return appBundlePath, nil
|
|
}
|
|
|
|
func writeHelperScript(pattern, content string) (string, error) {
|
|
file, err := os.CreateTemp("", pattern)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, err := file.WriteString(content); err != nil {
|
|
_ = file.Close()
|
|
_ = os.Remove(file.Name())
|
|
return "", err
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
_ = os.Remove(file.Name())
|
|
return "", err
|
|
}
|
|
if err := os.Chmod(file.Name(), 0o700); err != nil {
|
|
_ = os.Remove(file.Name())
|
|
return "", err
|
|
}
|
|
return file.Name(), nil
|
|
}
|
|
|
|
func ensureWritableDirectory(dir string) error {
|
|
dir = strings.TrimSpace(dir)
|
|
if dir == "" {
|
|
return errors.New("target directory is empty")
|
|
}
|
|
|
|
probe, err := os.CreateTemp(dir, ".cursor-byok-writecheck-*")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
name := probe.Name()
|
|
if err := probe.Close(); err != nil {
|
|
_ = os.Remove(name)
|
|
return err
|
|
}
|
|
return os.Remove(name)
|
|
}
|
|
|
|
type progressWriter struct {
|
|
onProgress func(downloaded int64)
|
|
downloaded int64
|
|
lastEmit time.Time
|
|
}
|
|
|
|
func newProgressWriter(onProgress func(downloaded int64)) *progressWriter {
|
|
return &progressWriter{
|
|
onProgress: onProgress,
|
|
lastEmit: time.Now(),
|
|
}
|
|
}
|
|
|
|
func (w *progressWriter) Write(p []byte) (int, error) {
|
|
n := len(p)
|
|
w.downloaded += int64(n)
|
|
if w.onProgress != nil && time.Since(w.lastEmit) >= 120*time.Millisecond {
|
|
w.lastEmit = time.Now()
|
|
w.onProgress(w.downloaded)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
const darwinInstallerScript = `#!/bin/sh
|
|
set -eu
|
|
|
|
PID_TO_WAIT="$1"
|
|
ARCHIVE_PATH="$2"
|
|
TARGET_APP="$3"
|
|
TMP_DIR="$(mktemp -d)"
|
|
|
|
cleanup() {
|
|
rm -rf "$TMP_DIR"
|
|
rm -f "$ARCHIVE_PATH"
|
|
rm -f "$0"
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
while kill -0 "$PID_TO_WAIT" 2>/dev/null; do
|
|
sleep 1
|
|
done
|
|
|
|
tar -xzf "$ARCHIVE_PATH" -C "$TMP_DIR"
|
|
|
|
EXTRACTED_APP="$(find "$TMP_DIR" -maxdepth 1 -type d -name '*.app' | head -n 1)"
|
|
if [ -z "$EXTRACTED_APP" ]; then
|
|
echo "No .app bundle found in archive" >&2
|
|
exit 1
|
|
fi
|
|
|
|
rm -rf "$TARGET_APP"
|
|
mv "$EXTRACTED_APP" "$TARGET_APP"
|
|
open "$TARGET_APP"
|
|
`
|
|
|
|
const windowsInstallerScript = `param(
|
|
[int]$PidToWait,
|
|
[string]$ArchivePath,
|
|
[string]$TargetExecutable
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
$TargetDir = Split-Path -Parent $TargetExecutable
|
|
$TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ("cursor-byok-update-" + [System.Guid]::NewGuid().ToString("N"))
|
|
|
|
New-Item -ItemType Directory -Path $TempDir | Out-Null
|
|
|
|
try {
|
|
while (Get-Process -Id $PidToWait -ErrorAction SilentlyContinue) {
|
|
Start-Sleep -Seconds 1
|
|
}
|
|
|
|
Expand-Archive -LiteralPath $ArchivePath -DestinationPath $TempDir -Force
|
|
$ExtractedExecutables = @(Get-ChildItem -LiteralPath $TempDir -File | Where-Object { $_.Extension -ieq ".exe" })
|
|
if ($ExtractedExecutables.Count -eq 0) {
|
|
throw "No executable found in Windows update archive"
|
|
}
|
|
|
|
$TargetExecutableName = [System.IO.Path]::GetFileName($TargetExecutable)
|
|
$PayloadExecutable = $ExtractedExecutables | Where-Object { $_.Name -ieq $TargetExecutableName } | Select-Object -First 1
|
|
if ($null -eq $PayloadExecutable) {
|
|
$PayloadExecutable = $ExtractedExecutables[0]
|
|
}
|
|
|
|
Get-ChildItem -LiteralPath $TempDir -Force | ForEach-Object {
|
|
if ($_.PSIsContainer) {
|
|
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $TargetDir $_.Name) -Recurse -Force
|
|
} elseif ($_.Extension -ine ".exe") {
|
|
Copy-Item -LiteralPath $_.FullName -Destination (Join-Path $TargetDir $_.Name) -Force
|
|
}
|
|
}
|
|
Copy-Item -LiteralPath $PayloadExecutable.FullName -Destination $TargetExecutable -Force
|
|
Start-Process -FilePath $TargetExecutable
|
|
} finally {
|
|
Remove-Item -LiteralPath $ArchivePath -Force -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $TempDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force -ErrorAction SilentlyContinue
|
|
}
|
|
`
|
|
|
|
const linuxInstallerScript = `#!/bin/sh
|
|
set -eu
|
|
|
|
PID_TO_WAIT="$1"
|
|
ARCHIVE_PATH="$2"
|
|
TARGET_EXECUTABLE="$3"
|
|
TARGET_DIR="$(dirname "$TARGET_EXECUTABLE")"
|
|
TMP_DIR="$(mktemp -d)"
|
|
REPLACEMENT_PATH="${TARGET_EXECUTABLE}.new"
|
|
|
|
cleanup() {
|
|
rm -rf "$TMP_DIR"
|
|
rm -f "$ARCHIVE_PATH"
|
|
rm -f "$REPLACEMENT_PATH"
|
|
rm -f "$0"
|
|
}
|
|
|
|
trap cleanup EXIT
|
|
|
|
while kill -0 "$PID_TO_WAIT" 2>/dev/null; do
|
|
sleep 1
|
|
done
|
|
|
|
tar -xzf "$ARCHIVE_PATH" -C "$TMP_DIR"
|
|
|
|
EXTRACTED_EXECUTABLE="$(find "$TMP_DIR" -maxdepth 1 -type f | head -n 1)"
|
|
if [ -z "$EXTRACTED_EXECUTABLE" ]; then
|
|
echo "No executable found in Linux update archive" >&2
|
|
exit 1
|
|
fi
|
|
|
|
cp "$EXTRACTED_EXECUTABLE" "$REPLACEMENT_PATH"
|
|
chmod +x "$REPLACEMENT_PATH"
|
|
mv -f "$REPLACEMENT_PATH" "$TARGET_EXECUTABLE"
|
|
chmod +x "$TARGET_EXECUTABLE"
|
|
cd "$TARGET_DIR"
|
|
nohup "$TARGET_EXECUTABLE" >/dev/null 2>&1 &
|
|
`
|