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: Support for new version detection feature. #688

Merged
merged 1 commit into from
Dec 10, 2024
Merged
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
2 changes: 1 addition & 1 deletion COMPILATION_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,5 +133,5 @@ CROSS_ARCH=arm64 make

## eBPF学习资料

* [eBPF PDF资料精选](https://github/gojue/ebpf-slide)
* [eBPF PDF资料精选](https://github.com/gojue/ebpf-slide)
* [CFC4N的博客](https://www.cnxct.com)
18 changes: 15 additions & 3 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ const (
)

var (
GitVersion = "v0.0.0_unknow"
// GitVersion default value, eg: linux_arm64:v0.8.10-20241116-fcddaeb:5.15.0-125-generic
GitVersion = "os_arch:v0.0.0-20221111-develop:default_kernel"
//ReleaseDate = "2022-03-16"
)

Expand Down Expand Up @@ -221,7 +222,7 @@ func runModule(modName string, modConfig config.IConfig) {
// listen http server
go func() {
logger.Info().Str("listen", globalConf.Listen).Send()
logger.Info().Msg("https server starting...You can update the configuration file via the HTTP interface.")
logger.Info().Msg("https server starting...You can upgrade the configuration file via the HTTP interface.")
var ec = http.NewHttpServer(globalConf.Listen, reRloadConfig, logger)
err = ec.Run()
if err != nil {
Expand All @@ -230,6 +231,18 @@ func runModule(modName string, modConfig config.IConfig) {
}
}()

ctx, cancelFun := context.WithCancel(context.TODO())

// upgrade check
go func() {
tags, url, e := upgradeCheck(ctx)
if e != nil {
logger.Debug().Msgf("upgrade check failed: %v", e)
return
}
logger.Warn().Msgf("A new version %s is available:%s", tags, url)
}()

// run module
{
// config check
Expand All @@ -246,7 +259,6 @@ func runModule(modName string, modConfig config.IConfig) {
// 初始化
logger.Warn().Msg("========== module starting. ==========")
mod := modFunc()
ctx, cancelFun := context.WithCancel(context.TODO())
err = mod.Init(ctx, &logger, modConfig, ecw)
if err != nil {
logger.Fatal().Err(err).Bool("isReload", isReload).Msg("module initialization failed")
Expand Down
78 changes: 78 additions & 0 deletions cli/cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package cmd

import (
"context"
"errors"
"fmt"
"github.com/gojue/ecapture/pkg/upgrade"
"golang.org/x/sys/unix"
"regexp"
"strings"
)

const urlReleases = "https://api.github.com/repos/gojue"
const urlReleasesCN = "https://image.cnxct.com"
const apiReleases string = "/ecapture/releases/latest"

var (
ErrOsArchNotFound = errors.New("new tag found, but no os/arch match")
ErrAheadOfLatestVersion = errors.New("local version is ahead of latest version")
)

func upgradeCheck(ctx context.Context) (string, string, error) {

// uname -a
var uname unix.Utsname
err := unix.Uname(&uname)
if err != nil {
return "", "", fmt.Errorf("Error getting uname: %v", err)
}
var useragent = fmt.Sprintf("eCapture Cli (%s %s %s)",
byteToString(uname.Sysname[:]), // 系统名称
byteToString(uname.Release[:]), // 版本号
byteToString(uname.Machine[:]), // 机器类型
)
var arch = "amd64"
if byteToString(uname.Machine[:]) == "aarch64" {
arch = "arm64"
}
rex := regexp.MustCompile(`([^:]+):v?(\d+\.\d+\.\d+)[^:]+:[^:]+`)
verMatch := rex.FindStringSubmatch(GitVersion)
if len(verMatch) <= 2 {
return "", "", fmt.Errorf("error matching version: %s, verMatch:%v", GitVersion, verMatch)
}
var os = "linux"
if strings.Contains(verMatch[1], "androidgki") {
os = "android"
}
githubResp, err := upgrade.GetLatestVersion(useragent, fmt.Sprintf("%s%s?ver=%s", urlReleasesCN, apiReleases, GitVersion), ctx)
if err != nil {
return "", "", fmt.Errorf("error getting latest version: %v", err)
}

comp, err := upgrade.CheckVersion(verMatch[2], githubResp.TagName)
if err != nil {
return "", "", fmt.Errorf("error checking version: %v", err)
}

if comp >= 0 {
return "", "", ErrAheadOfLatestVersion
}

// "name": "ecapture-v0.8.12-android-amd64.tar.gz",
var targetAsset = fmt.Sprintf("ecapture-%s-%s-%s.tar.gz", githubResp.TagName, os, arch)
for _, asset := range githubResp.Assets {
if asset.Name == targetAsset {
return githubResp.TagName, asset.BrowserDownloadURL, nil
}
}
return "", "", ErrOsArchNotFound
}

func byteToString(b []byte) string {
n := 0
for n < len(b) && b[n] != 0 {
n++
}
return string(b[:n])
}
84 changes: 84 additions & 0 deletions pkg/upgrade/github_response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2024 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package upgrade

import (
"time"
)

// from https://api.github.com/repos/gojue/ecapture/releases/latest
// https://developer.github.com/v3/repos/releases/

// GithubReleaseResp data related to a release.
type GithubReleaseResp struct {
URL string `json:"url"`
AssetsURL string `json:"assets_url"`
UploadURL string `json:"upload_url"`
HTMLURL string `json:"html_url"`
ID int64 `json:"id"`
Author Author `json:"author"`
NodeID string `json:"node_id"`
TagName string `json:"tag_name"`
TargetCommitish string `json:"target_commitish"`
Name string `json:"name"`
Draft bool `json:"draft"`
Prerelease bool `json:"prerelease"`
CreatedAt time.Time `json:"created_at"`
PublishedAt time.Time `json:"published_at"`
Assets []ReleaseAsset `json:"assets"`
TarballURL string `json:"tarball_url"`
ZipballURL string `json:"zipball_url"`
Body string `json:"body"`
MentionsCount int `json:"mentions_count"`
}

type Author struct {
Login string `json:"login"`
ID int64 `json:"id"`
NodeID string `json:"node_id"`
AvatarURL string `json:"avatar_url"`
GravatarID string `json:"gravatar_id"`
URL string `json:"url"`
HTMLURL string `json:"html_url"`
FollowersURL string `json:"followers_url"`
FollowingURL string `json:"following_url"`
GistsURL string `json:"gists_url"`
StarredURL string `json:"starred_url"`
SubscriptionsURL string `json:"subscriptions_url"`
OrganizationsURL string `json:"organizations_url"`
ReposURL string `json:"repos_url"`
EventsURL string `json:"events_url"`
ReceivedEventsURL string `json:"received_events_url"`
Type string `json:"type"`
UserViewType string `json:"user_view_type"`
SiteAdmin bool `json:"site_admin"`
}

// ReleaseAsset data related to a release asset.
type ReleaseAsset struct {
URL string `json:"url"`
ID int64 `json:"id"`
NodeID string `json:"node_id"`
Name string `json:"name"`
Label string `json:"label"`
Uploader Author `json:"uploader"`
ContentType string `json:"content_type"`
State string `json:"state"`
Size int64 `json:"size"`
DownloadCount int `json:"download_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
BrowserDownloadURL string `json:"browser_download_url"`
}
150 changes: 150 additions & 0 deletions pkg/upgrade/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// Copyright 2024 CFC4N <cfc4n.cs@gmail.com>. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package upgrade

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
)

/*

https://api.github.com/repos/gojue/ecapture/releases/latest
https://api.github.com/repos/gojue/ecapture/releases/tags/v0.1.0
https://github.com/gojue/ecapture/releases/download/v0.8.12/checksum-v0.8.12.txt

image.cnxct.com/ecapture/releases/latest
image.cnxct.com/ecapture/releases/tags/v0.1.0
image.cnxct.com/ecapture/download/v0.8.12/checksum-v0.8.12.txt
*/

// we use the GitHub REST V3 API as no login is required
// https://docs.github.com/zh/rest/using-the-rest-api

func GetLatestVersion(ua, url string, ctx context.Context) (GithubReleaseResp, error) {
var release GithubReleaseResp
err := makeGithubRequest(ctx, ua, url, &release)
if err != nil {
return release, err
}

return release, nil

}

func CheckVersion(localVer, remoteVer string) (int, error) {

localVer = strings.ReplaceAll(localVer, "v", "")
remoteVer = strings.ReplaceAll(remoteVer, "v", "")

v1, err := ParseVersion(localVer)
if err != nil {
return 0, err
}

v2, err := ParseVersion(remoteVer)
if err != nil {
return 0, err
}

comparison := CompareVersions(v1, v2)
return comparison, nil
}

func makeGithubRequest(ctx context.Context, ua, url string, output interface{}) error {
transport := &http.Transport{Proxy: http.ProxyFromEnvironment}

client := &http.Client{
Timeout: 3 * time.Second,
Transport: transport,
}

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)

req.Header.Add("Accept", "application/json") // gh api recommendation , send header with api version
req.Header.Set("User-Agent", ua) // eCapture Cli Linux 5.15.0-125-generic aarch64
response, err := client.Do(req)
if err != nil {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("API request failed: %w", err)
}

if response.StatusCode != http.StatusOK {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("API request failed, statusCOde: %s", response.Status)
}

defer response.Body.Close()

data, err := io.ReadAll(response.Body)
if err != nil {
//lint:ignore ST1005 Github is a proper capitalized noun
return fmt.Errorf("API read response failed: %w", err)
}

err = json.Unmarshal(data, output)
if err != nil {
return fmt.Errorf("unmarshalling Github API response failed: %w", err)
}

return nil
}

// Version 结构体表示一个版本号
type Version struct {
Major int
Minor int
Patch int
}

// ParseVersion 解析版本号字符串
func ParseVersion(versionStr string) (Version, error) {
parts := strings.Split(versionStr, ".")
if len(parts) != 3 {
return Version{}, fmt.Errorf("invalid version format")
}

major, err := strconv.Atoi(parts[0])
if err != nil {
return Version{}, err
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return Version{}, err
}
patch, err := strconv.Atoi(parts[2])
if err != nil {
return Version{}, err
}

return Version{Major: major, Minor: minor, Patch: patch}, nil
}

// CompareVersions 比较两个版本号
func CompareVersions(v1, v2 Version) int {
if v1.Major != v2.Major {
return v1.Major - v2.Major
}
if v1.Minor != v2.Minor {
return v1.Minor - v2.Minor
}
return v1.Patch - v2.Patch
}
Loading
Loading