Skip to content
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
175 changes: 143 additions & 32 deletions cmd/deskconn/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"os/signal"
"path/filepath"
"runtime"
"runtime/debug"
"strconv"
"strings"
"syscall"
Expand All @@ -25,6 +26,7 @@ import (
"github.com/godbus/dbus/v5"
"github.com/olekukonko/tablewriter"
log "github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
"golang.org/x/term"
"gopkg.in/yaml.v3"

Expand Down Expand Up @@ -97,7 +99,8 @@ func main() {
log.Fatal(err)
}

versionString := fmt.Sprintf("deskconn %s", version)
currentVersion := appVersion()
versionString := fmt.Sprintf("deskconn %s", currentVersion)
app := kingpin.New("deskconn", "Deskconn control CLI")

attachCmd := app.Command("attach", "Attach a device")
Expand Down Expand Up @@ -1533,7 +1536,7 @@ func main() {
fmt.Println("screenshot disabled")

case selfUpdateCmd.FullCommand():
if err := updateApp(cfgDirectory); err != nil {
if err := updateApp(cfgDirectory, currentVersion); err != nil {
fmt.Fprintln(os.Stderr, err)
return
}
Expand All @@ -1554,48 +1557,27 @@ type appUpdateResponse struct {
LatestVersion string `json:"latest_version"`
}

func updateApp(cfgDirectory string) error {
fmt.Printf("Checking for updates for %s on %s-%s...\n", version, runtime.GOOS, runtime.GOARCH)
func updateApp(cfgDirectory, currentVersion string) error {
fmt.Printf("Checking for updates for %s on %s-%s...\n", currentVersion, runtime.GOOS, runtime.GOARCH)

cloudSession, err := deskconn.ConnectCloudRealm(cfgDirectory)
updateResp, err := latestAppUpdate(cfgDirectory, currentVersion)
if err != nil {
if strings.Contains(err.Error(), deskconn.ErrAuthenticationFailed) {
_ = deskconn.RemoveCredentialsFiles(cfgDirectory)
return fmt.Errorf("invalid credentials, please login again")
}
return err
}

callResp := cloudSession.Call(deskconn.ProcedureAppUpdateCheck).Args("deskconn", version, runtime.GOOS,
runtime.GOARCH).Do()
if callResp.Err != nil {
return callResp.Err
}

if len(callResp.Args()) == 0 {
fmt.Printf("You're already on version %s of deskconn (the latest version).\n", version)
return nil
}

var updateResp appUpdateResponse
jsonData, err := json.Marshal(callResp.Args()[0])
if err != nil {
return fmt.Errorf("failed to marshal update response: %w", err)
}

if err := json.Unmarshal(jsonData, &updateResp); err != nil {
return fmt.Errorf("failed to parse update response: %w", err)
}

if updateResp.DownloadURL == "" {
return fmt.Errorf("update response missing download_url")
fmt.Printf("You're already on version %s of deskconn (the latest version).\n", currentVersion)
return nil
}

fmt.Printf("Found update at %s.\n", updateResp.DownloadURL)

if err := downloadAndInstallUpdate(updateResp.DownloadURL); err != nil {
return err
}
if err := writeInstalledAppVersion(cfgDirectory, updateResp.LatestVersion); err != nil {
return err
}

fmt.Printf("Restarting deskconnd service...\n")
cmd := exec.Command("systemctl", "--user", "restart", "deskconnd")
Expand All @@ -1605,10 +1587,139 @@ func updateApp(cfgDirectory string) error {
return fmt.Errorf("failed to restart deskconnd: %w", err)
}

fmt.Printf("Updated deskconn from version %s to %s.\n", version, updateResp.LatestVersion)
fmt.Printf("Updated deskconn from version %s to %s.\n", currentVersion, updateResp.LatestVersion)
return nil
}

func latestAppUpdate(cfgDirectory, currentVersion string) (appUpdateResponse, error) {
const releaseURL = "https://api.github.com/repos/xconnio/deskconn/releases/latest"

client := &http.Client{
Timeout: 30 * time.Second,
}
req, err := http.NewRequest(http.MethodGet, releaseURL, nil)
if err != nil {
return appUpdateResponse{}, err
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "deskconn-self-update")

resp, err := client.Do(req)
if err != nil {
return appUpdateResponse{}, fmt.Errorf("failed to check latest release: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return appUpdateResponse{}, fmt.Errorf("failed to check latest release: unexpected status %s", resp.Status)
}

var release struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
}
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
return appUpdateResponse{}, fmt.Errorf("failed to parse latest release: %w", err)
}

if release.TagName == "" {
return appUpdateResponse{}, fmt.Errorf("latest release response missing tag_name")
}
installedVersion, err := readInstalledAppVersion(cfgDirectory)
if err != nil {
return appUpdateResponse{}, err
}
if currentReleaseIsAtLeast(currentVersion, release.TagName) || installedVersion == release.TagName {
return appUpdateResponse{LatestVersion: release.TagName}, nil
}

assetName := releaseAssetName(release.TagName)
for _, asset := range release.Assets {
if asset.Name == assetName {
if asset.BrowserDownloadURL == "" {
return appUpdateResponse{}, fmt.Errorf("latest release asset %s missing browser_download_url", assetName)
}
return appUpdateResponse{
DownloadURL: asset.BrowserDownloadURL,
LatestVersion: release.TagName,
}, nil
}
}

return appUpdateResponse{}, fmt.Errorf("latest release %s missing asset %s", release.TagName, assetName)
}

func releaseAssetName(releaseVersion string) string {
return fmt.Sprintf("deskconn_%s_%s_%s.tar.gz", strings.TrimPrefix(releaseVersion, "v"), runtime.GOOS, runtime.GOARCH)
}

func appVersion() string {
if version != "v0.1.0-alpha" {
return version
}

info, ok := debug.ReadBuildInfo()
if !ok {
return version
}

for _, setting := range info.Settings {
if setting.Key == "vcs.revision" && len(setting.Value) >= 7 {
return version + "." + setting.Value[:7]
}
}
return version
}

func readInstalledAppVersion(cfgDirectory string) (string, error) {
data, err := os.ReadFile(filepath.Join(cfgDirectory, "self-update-version"))
if errors.Is(err, os.ErrNotExist) {
return "", nil
}
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}

func writeInstalledAppVersion(cfgDirectory, version string) error {
return os.WriteFile(filepath.Join(cfgDirectory, "self-update-version"), []byte(version+"\n"), 0600)
}

func currentReleaseIsAtLeast(current, latest string) bool {
current = strings.TrimPrefix(current, "v")
latest = strings.TrimPrefix(latest, "v")
if current == latest {
return true
}
if hasCommitSuffix(current) || hasCommitSuffix(latest) {
return false
}

currentSemver := "v" + current
latestSemver := "v" + latest
if semver.IsValid(currentSemver) && semver.IsValid(latestSemver) {
return semver.Compare(currentSemver, latestSemver) >= 0
}
return false
}

func hasCommitSuffix(v string) bool {
idx := strings.LastIndex(v, ".")
if idx == -1 || len(v)-idx-1 != 7 {
return false
}
for _, r := range v[idx+1:] {
if (r < '0' || r > '9') && (r < 'a' || r > 'f') {
return false
}
}
return true
}

const pathInstallerBlock = "\n# Added by deskconn installer\nexport PATH=\"$HOME/.local/bin:$PATH\"\n"

// confirmPrompt prints prompt, reads a line from stdin, and reports whether the response counts
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ require (
github.com/xconnio/xconn-webrtc-go v0.0.0-20260713105202-e23ffce34677
go.etcd.io/bbolt v1.4.3
golang.org/x/crypto v0.51.0
golang.org/x/mod v0.35.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
gopkg.in/yaml.v3 v3.0.1
Expand Down Expand Up @@ -78,7 +79,6 @@ require (
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/time v0.14.0 // indirect
Expand Down
Loading