From 0131ad5656eb629c4cad2b3b0c25e5cfb698dec6 Mon Sep 17 00:00:00 2001 From: CFC4N Date: Wed, 11 Dec 2024 00:02:25 +0800 Subject: [PATCH] feat: Support for new version detection feature. (#688) Signed-off-by: CFC4N --- COMPILATION_CN.md | 2 +- cli/cmd/root.go | 18 +++- cli/cmd/upgrade.go | 78 +++++++++++++++++ pkg/upgrade/github_response.go | 84 ++++++++++++++++++ pkg/upgrade/upgrade.go | 150 +++++++++++++++++++++++++++++++++ pkg/upgrade/upgrade_test.go | 103 ++++++++++++++++++++++ variables.mk | 4 +- 7 files changed, 433 insertions(+), 6 deletions(-) create mode 100644 cli/cmd/upgrade.go create mode 100644 pkg/upgrade/github_response.go create mode 100644 pkg/upgrade/upgrade.go create mode 100644 pkg/upgrade/upgrade_test.go diff --git a/COMPILATION_CN.md b/COMPILATION_CN.md index cbac89726..33b76900e 100644 --- a/COMPILATION_CN.md +++ b/COMPILATION_CN.md @@ -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) \ No newline at end of file diff --git a/cli/cmd/root.go b/cli/cmd/root.go index acd586c18..0ab6c42c6 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -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" ) @@ -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 { @@ -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 @@ -245,7 +258,6 @@ func runModule(modName string, modConfig config.IConfig) { reload: // 初始化 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") diff --git a/cli/cmd/upgrade.go b/cli/cmd/upgrade.go new file mode 100644 index 000000000..3e8331d59 --- /dev/null +++ b/cli/cmd/upgrade.go @@ -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]) +} diff --git a/pkg/upgrade/github_response.go b/pkg/upgrade/github_response.go new file mode 100644 index 000000000..8d2553f3b --- /dev/null +++ b/pkg/upgrade/github_response.go @@ -0,0 +1,84 @@ +// Copyright 2024 CFC4N . 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"` +} diff --git a/pkg/upgrade/upgrade.go b/pkg/upgrade/upgrade.go new file mode 100644 index 000000000..6fcac6663 --- /dev/null +++ b/pkg/upgrade/upgrade.go @@ -0,0 +1,150 @@ +// Copyright 2024 CFC4N . 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 +} diff --git a/pkg/upgrade/upgrade_test.go b/pkg/upgrade/upgrade_test.go new file mode 100644 index 000000000..b737c021b --- /dev/null +++ b/pkg/upgrade/upgrade_test.go @@ -0,0 +1,103 @@ +// Copyright 2024 CFC4N . 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" + "fmt" + "golang.org/x/sys/unix" + "regexp" + "strings" + "testing" +) + +const urlReleases = "https://api.github.com/repos/gojue" +const urlReleasesCN = "https://image.cnxct.com" +const apiReleases string = "/ecapture/releases/latest" + +func TestCheckLatest(t *testing.T) { + var uname unix.Utsname + + // 调用 uname 系统调用 + err := unix.Uname(&uname) + if err != nil { + t.Logf("Upgrader: Error getting uname: %v", err) + return + } + + var useragent = fmt.Sprintf("eCapture Cli %s %s %s", + byteToString(uname.Sysname[:]), // 系统名称 + byteToString(uname.Release[:]), // 版本号 + byteToString(uname.Machine[:]), // 机器类型 + ) + t.Logf("User-Agent:%d, %s", len(useragent), useragent) + //var ver = "linux_arm64:v0.8.8:5.15.0-125-generic" + var ver = "androidgki:v0.8.8:5.15.0-125-generic" + ver = "linux_arm64:v0.8.10-20241116-fcddaeb:5.15.0-125-generic" + var arch = "amd64" + if byteToString(uname.Machine[:]) == "aarch64" { + arch = "arm64" + } + + rex := regexp.MustCompile(`([^:]+):v?(\d+\.\d+\.\d+)[^:]+:[^:]+`) + + verMatch := rex.FindStringSubmatch(ver) + if len(verMatch) <= 2 { + t.Fatalf("Error matching version: %s", ver) + } + t.Logf("match Version: %v", verMatch) + var os = "linux" + if strings.Contains(verMatch[1], "androidgki") { + os = "android" + } + + githubResp, err := GetLatestVersion(useragent, fmt.Sprintf("%s%s?ver=%s", urlReleasesCN, apiReleases, ver), context.Background()) + if err != nil { + t.Fatalf("Error getting latest version: %v", err) + } + + t.Logf("Latest version: %v", githubResp.TagName) + comp, err := CheckVersion(verMatch[2], githubResp.TagName) + if err != nil { + t.Fatalf("Error checking version: %v", err) + } + t.Logf("Version comparison: %v", comp) + + if comp >= 0 { + t.Logf("Local version is ahead of latest version") + return + } + + t.Logf("Local version is behind latest version") + + // "name": "ecapture-v0.8.12-android-amd64.tar.gz", + var targetAsset = fmt.Sprintf("ecapture-%s-%s-%s.tar.gz", githubResp.TagName, os, arch) + t.Logf("Target asset: %s", targetAsset) + for _, asset := range githubResp.Assets { + if asset.Name == targetAsset { + t.Logf("Found target asset, downloadUrl:%s", asset.BrowserDownloadURL) + break + } + //t.Logf("Asset: %s", asset.Name) + } +} + +func byteToString(b []byte) string { + n := 0 + for n < len(b) && b[n] != 0 { + n++ + } + return string(b[:n]) +} diff --git a/variables.mk b/variables.mk index 287aa74ac..be271d376 100644 --- a/variables.mk +++ b/variables.mk @@ -83,7 +83,7 @@ TAG_COMMIT := $(shell git rev-list --abbrev-commit --tags --max-count=1) TAG := $(shell git describe --abbrev=0 --tags ${TAG_COMMIT} 2>/dev/null || true) COMMIT := $(shell git rev-parse --short HEAD) DATE := $(shell git log -1 --format=%cd --date=format:"%Y%m%d") -LAST_GIT_TAG := $(TAG:v%=%)-$(DATE)-$(COMMIT) +LAST_GIT_TAG := $(TAG)-$(DATE)-$(COMMIT) RPM_RELEASE := $(DATE).$(COMMIT) #VERSION_NUM ?= $(if $(SNAPSHOT_VERSION),$(SNAPSHOT_VERSION),$(LAST_GIT_TAG)) @@ -181,7 +181,7 @@ KERN_RELEASE ?= $(UNAME_R) KERN_BUILD_PATH ?= $(if $(KERN_HEADERS),$(KERN_HEADERS),/lib/modules/$(KERN_RELEASE)/build) KERN_SRC_PATH ?= $(if $(KERN_HEADERS),$(KERN_HEADERS),$(if $(wildcard /lib/modules/$(KERN_RELEASE)/source),/lib/modules/$(KERN_RELEASE)/source,$(KERN_BUILD_PATH))) -BPF_NOCORE_TAG = $(subst .,_,$(KERN_RELEASE)).$(subst .,_,$(VERSION_NUM)) +BPF_NOCORE_TAG = $(subst .,_,$(KERN_RELEASE)):$(subst .,_,$(VERSION_NUM)) # # BPF Source file