Skip to content
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
6 changes: 3 additions & 3 deletions cmd/pro/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import (
proflags "github.com/skevetter/devpod/cmd/pro/flags"
"github.com/skevetter/devpod/pkg/config"
"github.com/skevetter/devpod/pkg/machineid"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/devpod/pkg/platform"
"github.com/skevetter/devpod/pkg/platform/client"
"github.com/skevetter/devpod/pkg/util"
"github.com/skevetter/log"
"github.com/skevetter/log/hash"
"github.com/skevetter/log/scanner"
"github.com/skevetter/log/survey"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -1272,7 +1272,7 @@ func (cmd *StartCmd) login(url string) error {
// check if we are already logged in
if cmd.isLoggedIn(url) {
// still open the UI
err := open.Run(url)
err := devpodopen.Run(url)
if err != nil {
return fmt.Errorf("couldn't open the login page in a browser: %w", err)
}
Expand Down Expand Up @@ -1357,7 +1357,7 @@ func (cmd *StartCmd) loginUI(url string) error {
)
loginURL := fmt.Sprintf("%s/login#%s", url, queryString)

err := open.Run(loginURL)
err := devpodopen.Run(loginURL)
if err != nil {
return fmt.Errorf("couldn't open the login page in a browser: %w", err)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/client/clientimplementation/daemonclient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
clientpkg "github.com/skevetter/devpod/pkg/client"
"github.com/skevetter/devpod/pkg/config"
daemon "github.com/skevetter/devpod/pkg/daemon/platform"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/devpod/pkg/options"
"github.com/skevetter/devpod/pkg/options/resolver"
"github.com/skevetter/devpod/pkg/platform"
Expand All @@ -24,7 +25,6 @@ import (
sshServer "github.com/skevetter/devpod/pkg/ssh/server"
"github.com/skevetter/devpod/pkg/ts"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
"golang.org/x/crypto/ssh"
"tailscale.com/client/local"
"tailscale.com/tailcfg"
Expand Down Expand Up @@ -147,7 +147,7 @@ func (c *client) CheckWorkspaceReachable(ctx context.Context) error {
c.workspace.Source.String(),
c.workspace.IDE.Name,
)
openErr := open.Run(deeplink)
openErr := devpodopen.Run(deeplink)
if openErr != nil {
return getWorkspaceErr // inform user about daemon state
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/ide/jetbrains/generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ import (
"github.com/skevetter/devpod/pkg/extract"
devpodhttp "github.com/skevetter/devpod/pkg/http"
"github.com/skevetter/devpod/pkg/ide"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/devpod/pkg/util"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
)

const (
Expand Down Expand Up @@ -89,7 +89,7 @@ type GenericJetBrainsServer struct {

func (o *GenericJetBrainsServer) OpenGateway(workspaceFolder, workspaceID string) error {
o.log.Infof("Starting %s through JetBrains Gateway...", o.options.DisplayName)
err := open.Run(
err := devpodopen.Run(
`jetbrains-gateway://connect#idePath=` + url.QueryEscape(
o.getDirectory(path.Join("/", "home", o.userName)),
) + `&projectPath=` + url.QueryEscape(
Expand Down
3 changes: 1 addition & 2 deletions pkg/ide/opener/opener.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/skevetter/devpod/pkg/port"
"github.com/skevetter/devpod/pkg/tunnel"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
)

// Params holds the parameters needed to open an IDE.
Expand Down Expand Up @@ -435,5 +434,5 @@ func startFleet(ctx context.Context, client client2.BaseWorkspaceClient, logger
)
logger.Infof("Starting Fleet at %s ...", url)

return open.Run(url)
return open2.Run(url)
}
4 changes: 2 additions & 2 deletions pkg/ide/vscode/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (

"github.com/skevetter/devpod/pkg/command"
pkgconfig "github.com/skevetter/devpod/pkg/config"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
)

const containersExtension = "ms-vscode-remote.remote-containers"
Expand Down Expand Up @@ -124,7 +124,7 @@ func openViaBrowser(params OpenParams) error {
openURL := u.String()

params.Log.Debugf("opening URL %s", openURL)
err := open.Run(openURL)
err := devpodopen.Run(openURL)
if err != nil {
params.Log.Errorf(
"flavor %s is not installed on host device: %v",
Expand Down
4 changes: 2 additions & 2 deletions pkg/ide/zed/zed.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
"fmt"

"github.com/skevetter/devpod/pkg/config"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
)

// Open first finds the zed binary for the local platform and then opens the zed editor with the given workspace folder.
Expand All @@ -26,7 +26,7 @@ func Open(

sshHost := workspaceID + config.SSHHostSuffix + workspaceFolder
openURL := fmt.Sprintf("zed://ssh/%s", sshHost)
err := open.Run(openURL)
err := devpodopen.Run(openURL)
if err != nil {
log.Debugf("Starting Zed caused error: %v", err)
log.Errorf("Seems like you don't have Zed installed on your computer locally")
Expand Down
5 changes: 2 additions & 3 deletions pkg/ide/zed/zed_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ package zed
import (
"context"
"fmt"
"os/exec"

"github.com/skevetter/devpod/pkg/config"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/log"
)

Expand All @@ -26,10 +26,9 @@ func Open(

sshHost := workspaceID + config.SSHHostSuffix + workspaceFolder
openURL := fmt.Sprintf("zed://ssh/%s", sshHost)
out, err := exec.Command("xdg-open", openURL).CombinedOutput()
err := devpodopen.Run(openURL)
if err != nil {
log.Debugf("Starting Zed caused error: %v", err)
log.Debugf("xdg-open %s output: %s", err, openURL, string(out))
log.Errorf("Seems like you don't have Zed installed on your computer locally")
return err
}
Expand Down
92 changes: 92 additions & 0 deletions pkg/open/appimage_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package open

import (
"os"
"os/exec"
"strings"
)

// isAppImage reports whether the current process is running inside an AppImage.
func isAppImage() bool {
return os.Getenv("APPIMAGE") != ""
}

// openURLSanitized opens a URL using xdg-open with a sanitized environment
// to work around AppImage library conflicts.
//
// When running inside an AppImage, the AppRun script prepends the image's
// bundled library paths to LD_LIBRARY_PATH. Child processes like xdg-open
// and gio then load these bundled libraries instead of the system ones,
// causing symbol lookup errors (e.g. "undefined symbol: g_unix_mount_entry_get_options").
//
// This is a well-known AppImage limitation:
// - https://github.com/AppImage/AppImageKit/issues/396
// - https://github.com/AppImage/AppImageKit/issues/616
//
// Tauri's AppImage builds also bundle their own xdg-open wrapper
// (APPIMAGE_BUNDLE_XDG_OPEN=1), which compounds the issue:
// - https://github.com/tauri-apps/tauri/issues/10617
// - https://github.com/tauri-apps/plugins-workspace/pull/2103
//
// The fix is to strip AppImage-injected environment variables before spawning
// xdg-open, and to use the system xdg-open by absolute path to bypass any
// bundled wrapper.
func openURLSanitized(url string) error {
// Use the system xdg-open to avoid Tauri's bundled wrapper.
xdgOpen := "/usr/bin/xdg-open"
if _, err := os.Stat(xdgOpen); err != nil {
xdgOpen = "xdg-open"
}

//nolint:gosec // xdgOpen is either "/usr/bin/xdg-open" or "xdg-open"
cmd := exec.Command(xdgOpen, url)
cmd.Env = sanitizedEnv()
return cmd.Run()
}

// sanitizedEnv returns a copy of the current environment with AppImage-injected
// variables removed or restored to their pre-AppImage values.
func sanitizedEnv() []string {
// Variables injected by AppImage's AppRun that cause library conflicts
// when inherited by system binaries.
strip := map[string]bool{
"APPDIR": true,
"APPIMAGE": true,
"ARGV0": true,
"OWD": true,
"APPIMAGE_BUNDLE_XDG_OPEN": true,
}

var env []string
for _, kv := range os.Environ() {
key, _, _ := strings.Cut(kv, "=")
if strip[key] {
continue
}
env = append(env, kv)
}

// Restore LD_LIBRARY_PATH to its pre-AppImage value if saved,
// otherwise remove it entirely so system binaries use system libs.
env = removeEnvKey(env, "LD_LIBRARY_PATH")
if orig := os.Getenv("ORIG_LD_LIBRARY_PATH"); orig != "" {
env = append(env, "LD_LIBRARY_PATH="+orig)
}

// LD_PRELOAD may be set to an exec interception library (exec.so);
// remove it so system binaries aren't affected.
env = removeEnvKey(env, "LD_PRELOAD")

return env
}

func removeEnvKey(env []string, key string) []string {
prefix := key + "="
result := env[:0:0]
for _, kv := range env {
if !strings.HasPrefix(kv, prefix) {
result = append(result, kv)
}
}
return result
}
13 changes: 13 additions & 0 deletions pkg/open/appimage_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//go:build !linux

package open

import "errors"

func isAppImage() bool {
return false
}

func openURLSanitized(_ string) error {
return errors.New("openURLSanitized is only available on Linux")
}
16 changes: 14 additions & 2 deletions pkg/open/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,24 @@ import (
"github.com/skratchdot/open-golang/open"
)

// Run opens the given URL in the default application.
// When running inside a Linux AppImage, it sanitizes the environment
// to avoid library conflicts before spawning xdg-open.
func Run(url string) error {
if isAppImage() {
return openURLSanitized(url)
}
return open.Run(url)
}

// Open opens the given url in the default application, retrying every second until the context is done.
func Open(ctx context.Context, url string, log log.Logger) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(time.Second):
err := tryOpen(ctx, url, open.Start, log)
err := tryOpen(ctx, url, Run, log)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if err == nil {
return nil
}
Expand Down Expand Up @@ -71,7 +81,9 @@ func tryOpen(ctx context.Context, url string, fn func(string) error, log log.Log
return nil
case <-time.After(time.Second):
}
_ = fn(url)
if err := fn(url); err != nil {
return fmt.Errorf("open url: %w", err)
}
log.WithFields(logrus.Fields{
"url": url,
}).Done("opened url")
Expand Down
4 changes: 2 additions & 2 deletions pkg/platform/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import (
storagev1 "github.com/loft-sh/api/v4/pkg/apis/storage/v1"
"github.com/loft-sh/api/v4/pkg/auth"
pkgconfig "github.com/skevetter/devpod/pkg/config"
devpodopen "github.com/skevetter/devpod/pkg/open"
"github.com/skevetter/devpod/pkg/platform/kube"
"github.com/skevetter/devpod/pkg/platform/project"
"github.com/skevetter/devpod/pkg/util"
"github.com/skevetter/devpod/pkg/version"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
Expand Down Expand Up @@ -314,7 +314,7 @@ func (c *client) Login(host string, insecure bool, log log.Logger) error {
}

server := startServer(fmt.Sprintf(RedirectPath, host), keyChannel, log)
err = open.Run(fmt.Sprintf(LoginPath, host))
err = devpodopen.Run(fmt.Sprintf(LoginPath, host))
if err != nil {
return fmt.Errorf(
"couldn't open the login page in a browser: %w. Please use the --access-key flag for the login command. "+
Expand Down