Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto updater #933

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"

go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/host/host.exe github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host.exe github.com/GopeedLab/gopeed/cmd/host
cd ui/flutter
$TAG = "v$env:VERSION"
flutter build windows
Expand Down Expand Up @@ -200,7 +200,7 @@ jobs:
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
lipo -create -output host amd64-host arm64-host
rm -rf amd64-lib arm64-lib amd64-host arm64-host
mv host ../../assets/host/host
mv host ../../assets/exec/host

cd $PROJECT_DIR/ui/flutter
flutter build macos
Expand Down Expand Up @@ -249,7 +249,7 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/host/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
cd ui/flutter
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
flutter_distributor package --platform linux --targets appimage,deb
Expand Down Expand Up @@ -280,7 +280,7 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/host/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
cd ui/flutter

sudo snap install snapcraft --classic
Expand Down
46 changes: 46 additions & 0 deletions cmd/updater/extract_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//go:build darwin

package main

import (
"fmt"
"os/exec"
"path/filepath"
"strings"
)

func extract(packagePath, destDir string) error {
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse", "-quiet").Output()
if err != nil {
return err
}

mountPoint := ""
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "/Volumes/") {
fields := strings.Fields(line)
if len(fields) > 2 {
mountPoint = fields[len(fields)-1]
break
}
}
}

if mountPoint == "" {
return fmt.Errorf("failed to get mount point")
}

matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
if err != nil {
return err
}
if len(matches) == 0 {
return fmt.Errorf("no .app found in dmg")
}

if err := exec.Command("cp", "-Rf", matches[0], destDir).Run(); err != nil {
return err
}

return exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
}
40 changes: 40 additions & 0 deletions cmd/updater/extract_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//go:build linux

package main

import (
"fmt"
"os/exec"
)

func extract(packagePath, destDir string) error {
terminals := []string{
"gnome-terminal", // GNOME
"konsole", // KDE
"xfce4-terminal", // XFCE
"xterm", // X11
}

// Command with auto-close after completion
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)

for _, term := range terminals {
if _, err := exec.LookPath(term); err == nil {
var cmd *exec.Cmd
switch term {
case "gnome-terminal", "xfce4-terminal":
cmd = exec.Command(term, "--", "bash", "-c", command)
case "konsole":
cmd = exec.Command(term, "-e", "bash", "-c", command)
case "xterm":
cmd = exec.Command(term, "-e", command)
}

if err := cmd.Start(); err == nil {
return nil
}
}
}

return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
}
50 changes: 50 additions & 0 deletions cmd/updater/extract_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//go:build windows

package main

import (
"archive/zip"
"io"
"os"
"path/filepath"
)

func extract(packagePath, destDir string) error {
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()

for _, file := range reader.File {
path := filepath.Join(destDir, file.Name)

if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}

if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}

dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}

srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}

_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
}
return nil
}
94 changes: 94 additions & 0 deletions cmd/updater/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"fmt"
"log"
"os"
"path/filepath"
"strconv"
"syscall"
"time"

"github.com/pkg/browser"
"github.com/pkg/errors"
)

func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: updater <pid> <asset> [log]")
os.Exit(1)
}

if len(os.Args) > 3 {
logFile, err := os.OpenFile(os.Args[3], os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err == nil {
defer logFile.Close()
log.SetOutput(logFile)
}
}

pid, err := strconv.Atoi(os.Args[1])
if err != nil {
log.Printf("Invalid PID: %v\n", err)
os.Exit(1)
}

packagePath := os.Args[2]
if err := update(pid, packagePath); err != nil {
log.Printf("Update failed: %v\n", err)
os.Exit(1)
}

// Restart the application
browser.OpenURL("gopeed:///")

os.Exit(0)
}

func update(pid int, packagePath string) error {
if err := killProcess(pid); err != nil {
return errors.Wrap(err, "failed to kill process")
}

if err := waitForProcessExit(pid); err != nil {
return errors.Wrap(err, "failed to wait for process exit")
}

appDir := filepath.Dir(os.Args[0])

if err := extract(packagePath, appDir); err != nil {
return errors.Wrap(err, "failed to extract package")
}

return nil
}

func waitForProcessExit(pid int) error {
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
process, err := os.FindProcess(pid)
if err != nil {
// On some systems, error is returned if process doesn't exist
return nil
}

// Send null signal to test if process exists
err = process.Signal(syscall.Signal(0))
if err != nil {
// If error occurs, the process no longer exists
return nil
}

time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("process %d still running after timeout", pid)
}

func killProcess(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
return err
}

return process.Kill()
}
6 changes: 4 additions & 2 deletions ui/flutter/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,7 @@ windows/flutter/generated_plugins.cmake
database.hive
database.lock

assets/host/host
assets/host/host.exe
assets/exec/host
assets/exec/host.exe
assets/exec/updater
assets/exec/updater.exe
File renamed without changes.
11 changes: 11 additions & 0 deletions ui/flutter/lib/app/modules/app/controllers/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import '../../../../main.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/updater.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
import '../../create/dto/create_router_params.dart';
Expand Down Expand Up @@ -109,6 +110,9 @@ class AppController extends GetxController with WindowListener, TrayListener {

_initLaunchAtStartup().onError((error, stackTrace) =>
logger.w("initLaunchAtStartup error", error, stackTrace));

_initCheckUpdate().onError((error, stackTrace) =>
logger.w("initCheckUpdate error", error, stackTrace));
}

@override
Expand Down Expand Up @@ -524,6 +528,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
autoStartup.value = await launchAtStartup.isEnabled();
}

Future<void> _initCheckUpdate() async {
final versionInfo = await checkUpdate();
if (versionInfo != null) {
await showUpdateDialog(Get.context!, versionInfo);
}
}

Future<void> saveConfig() async {
Database.instance.saveStartConfig(StartConfigEntity(
network: startConfig.value.network,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import 'dart:convert';

import 'package:get/get.dart';
import 'package:gopeed/api/api.dart';

import '../../../../util/updater.dart';

class SettingController extends GetxController {
final tapStatues = <String, bool>{}.obs;
final latestVersion = "".obs;
final latestVersion = Rxn<VersionInfo>();

@override
void onInit() {
Expand All @@ -26,23 +25,6 @@ class SettingController extends GetxController {

// fetch latest version
void fetchLatestVersion() async {
String? releaseDataStr;
try {
releaseDataStr = (await proxyRequest(
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
.data;
} catch (e) {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
}
if (releaseDataStr == null) {
return;
}
final releaseData = jsonDecode(releaseDataStr);
final tagName = releaseData["tag_name"];
if (tagName == null) {
return;
}
latestVersion.value = releaseData["tag_name"].substring(1);
latestVersion.value = await checkUpdate();
}
}
Loading
Loading