From b32de4b92c0bef8233ae15f87be4d2bbc2b7c43c Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Mon, 29 Dec 2025 21:05:06 -0500 Subject: [PATCH 1/4] fix: update xauth on reboot/change Signed-off-by: Brian Ketelsen --- TODO.md | 13 ++++ cmd/enter.go | 5 ++ internal/incus/client.go | 80 ++++++++++++++++++++ internal/incus/client_test.go | 138 ++++++++++++++++++++++++++++++++++ 4 files changed, 236 insertions(+) create mode 100644 TODO.md create mode 100644 internal/incus/client_test.go diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..c401cb8 --- /dev/null +++ b/TODO.md @@ -0,0 +1,13 @@ +# Ideas, Features, Roadmap + +## Known Bugs + +- the xauthority device changes when x or wayland is restarted. `igloo enter` should validate the existence of the xauthority device on each enter, replacing it if necessary + +## Ideas + +- On a host with no VS Code install, we'll be running code inside each igloo container. It'd be nice to share settings between them. Can't really symlink the ~/.vscode dir from the host since it won't exist. Explore having a Shared State sort of thing where directories like that live in ~/.config/igloo/shared_state and are linked in (by default? configurable?). Implement: `[shared_state]` section in config file that stores listed directories in ~/.config/igloo/shared_state and symlinks listed directories into the container, allowing all igloo instances to share these directories. This feature could be used for one-off script storage too. + +- copy the igloo binary into the container, and add one or more commands intended to run inside the container. Perhaps showing, editing `shared_state` contents? definitely `igloo status` should work inside the container and show that it knows it's inside. + +- modify PS1 in the container to add `[igloo]` prefix, change color of prompt, something visual as an indicator? Or... simply add a tip in the readme showing how to do this. diff --git a/cmd/enter.go b/cmd/enter.go index cfccf13..db410ab 100644 --- a/cmd/enter.go +++ b/cmd/enter.go @@ -117,6 +117,11 @@ func runEnter() error { } } + // Update Xauthority mount if necessary (file path can change on Wayland) + if err := client.UpdateXauthority(cfg.Container.Name); err != nil { + fmt.Println(styles.Warning(fmt.Sprintf("Could not update Xauthority: %v", err))) + } + // Get user info username := os.Getenv("USER") cwd, err := os.Getwd() diff --git a/internal/incus/client.go b/internal/incus/client.go index 45076b6..df29ebf 100644 --- a/internal/incus/client.go +++ b/internal/incus/client.go @@ -154,6 +154,86 @@ func (c *Client) AddGPUDevice(name string) error { return cmd.Run() } +// RemoveDevice removes a device from an instance +func (c *Client) RemoveDevice(name, deviceName string) error { + cmd := exec.Command("incus", "config", "device", "remove", name, deviceName) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + +// DeviceExists checks if a device exists on an instance +func (c *Client) DeviceExists(name, deviceName string) (bool, error) { + cmd := exec.Command("incus", "config", "device", "show", name) + output, err := cmd.Output() + if err != nil { + return false, fmt.Errorf("failed to list devices: %w", err) + } + + // Simple check if device name appears in output + return strings.Contains(string(output), deviceName+":"), nil +} + +// GetDeviceSource gets the source path of a disk device +func (c *Client) GetDeviceSource(name, deviceName string) (string, error) { + cmd := exec.Command("incus", "config", "device", "get", name, deviceName, "source") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get device source: %w", err) + } + return strings.TrimSpace(string(output)), nil +} + +// UpdateXauthority updates the xauthority device mount if the source file has changed +// This is necessary because XWayland can create new Xauthority files when restarted +func (c *Client) UpdateXauthority(name string) error { + // Get current Xauthority file from environment + xauthFile := os.Getenv("XAUTHORITY") + if xauthFile == "" { + xauthFile = os.Getenv("HOME") + "/.Xauthority" + } + + // Check if the file exists + if _, err := os.Stat(xauthFile); os.IsNotExist(err) { + // No xauthority file available, nothing to update + return nil + } + + // Check if xauthority device exists + deviceExists, err := c.DeviceExists(name, "xauthority") + if err != nil { + return fmt.Errorf("failed to check xauthority device: %w", err) + } + + if !deviceExists { + // Device doesn't exist, add it + username := os.Getenv("USER") + xauthPath := fmt.Sprintf("/home/%s/.Xauthority", username) + return c.AddDiskDevice(name, "xauthority", xauthFile, xauthPath) + } + + // Device exists, check if source has changed + currentSource, err := c.GetDeviceSource(name, "xauthority") + if err != nil { + return fmt.Errorf("failed to get current xauthority source: %w", err) + } + + if currentSource != xauthFile { + // Source has changed, remove and re-add the device + if err := c.RemoveDevice(name, "xauthority"); err != nil { + return fmt.Errorf("failed to remove old xauthority device: %w", err) + } + + username := os.Getenv("USER") + xauthPath := fmt.Sprintf("/home/%s/.Xauthority", username) + if err := c.AddDiskDevice(name, "xauthority", xauthFile, xauthPath); err != nil { + return fmt.Errorf("failed to add new xauthority device: %w", err) + } + } + + return nil +} + // SetConfig sets a configuration option on an instance func (c *Client) SetConfig(name, key, value string) error { cmd := exec.Command("incus", "config", "set", name, key+"="+value) diff --git a/internal/incus/client_test.go b/internal/incus/client_test.go new file mode 100644 index 0000000..1019d87 --- /dev/null +++ b/internal/incus/client_test.go @@ -0,0 +1,138 @@ +package incus + +import ( + "os" + "testing" +) + +// TestUpdateXauthority_NoFile tests UpdateXauthority when no xauthority file exists +func TestUpdateXauthority_NoFile(t *testing.T) { + // Save current env vars + oldXauth := os.Getenv("XAUTHORITY") + oldHome := os.Getenv("HOME") + defer func() { + if oldXauth != "" { + _ = os.Setenv("XAUTHORITY", oldXauth) + } else { + _ = os.Unsetenv("XAUTHORITY") + } + _ = os.Setenv("HOME", oldHome) + }() + + // Set env to point to non-existent file + tmpDir := t.TempDir() + _ = os.Setenv("HOME", tmpDir) + _ = os.Unsetenv("XAUTHORITY") + + client := NewClient() + + // This should not error when file doesn't exist - it just returns nil + err := client.UpdateXauthority("test-container") + if err != nil { + t.Errorf("UpdateXauthority() should not error when file doesn't exist, got: %v", err) + } +} + +// TestUpdateXauthority_WithFile tests UpdateXauthority when xauthority file exists +// Note: This test can't fully test the incus commands without a running incus instance, +// but it verifies the file detection logic works +func TestUpdateXauthority_WithFile(t *testing.T) { + // Save current env vars + oldXauth := os.Getenv("XAUTHORITY") + oldHome := os.Getenv("HOME") + oldUser := os.Getenv("USER") + defer func() { + if oldXauth != "" { + _ = os.Setenv("XAUTHORITY", oldXauth) + } else { + _ = os.Unsetenv("XAUTHORITY") + } + _ = os.Setenv("HOME", oldHome) + _ = os.Setenv("USER", oldUser) + }() + + // Create a temporary xauthority file + tmpDir := t.TempDir() + xauthFile := tmpDir + "/.Xauthority" + if err := os.WriteFile(xauthFile, []byte("fake xauth data"), 0600); err != nil { + t.Fatalf("Failed to create test xauthority file: %v", err) + } + + _ = os.Setenv("HOME", tmpDir) + _ = os.Setenv("USER", "testuser") + _ = os.Unsetenv("XAUTHORITY") + + client := NewClient() + + // This will attempt to call incus commands which will fail without incus installed + // but we can verify it at least tries to process the file + err := client.UpdateXauthority("test-container") + + // We expect an error because incus isn't available in test environment, + // but we're verifying the logic path is taken when the file exists + // The function should have attempted to check device existence + if err == nil { + // If no error, then either incus is available (unlikely in CI) + // or the logic correctly handled the case + t.Log("UpdateXauthority succeeded (incus may be available)") + } else { + // Expected case: incus command failed + if !containsAny(err.Error(), []string{"incus", "device", "executable file not found", "command not found"}) { + t.Errorf("Expected incus-related error, got: %v", err) + } + } +} + +// TestUpdateXauthority_CustomPath tests UpdateXauthority with XAUTHORITY env var set +func TestUpdateXauthority_CustomPath(t *testing.T) { + // Save current env vars + oldXauth := os.Getenv("XAUTHORITY") + oldUser := os.Getenv("USER") + defer func() { + if oldXauth != "" { + _ = os.Setenv("XAUTHORITY", oldXauth) + } else { + _ = os.Unsetenv("XAUTHORITY") + } + _ = os.Setenv("USER", oldUser) + }() + + // Create a temporary xauthority file with custom path + tmpDir := t.TempDir() + xauthFile := tmpDir + "/.mutter-Xwaylandauth.ABC123" + if err := os.WriteFile(xauthFile, []byte("fake xauth data"), 0600); err != nil { + t.Fatalf("Failed to create test xauthority file: %v", err) + } + + _ = os.Setenv("XAUTHORITY", xauthFile) + _ = os.Setenv("USER", "testuser") + + client := NewClient() + + // This will attempt to call incus commands which will fail without incus installed + err := client.UpdateXauthority("test-container") + + // We expect an error because incus isn't available in test environment + if err == nil { + t.Log("UpdateXauthority succeeded (incus may be available)") + } else { + // Expected case: incus command failed + if !containsAny(err.Error(), []string{"incus", "device", "executable file not found", "command not found"}) { + t.Errorf("Expected incus-related error, got: %v", err) + } + } +} + +// Helper function to check if error contains any of the expected strings +func containsAny(s string, substrs []string) bool { + for _, substr := range substrs { + if len(s) >= len(substr) { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + } + } + return false +} From d662da912d7493d13a66b36768ce521925b3982f Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Mon, 29 Dec 2025 21:05:50 -0500 Subject: [PATCH 2/4] fix: update xauth on reboot/change Signed-off-by: Brian Ketelsen --- internal/incus/client_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/incus/client_test.go b/internal/incus/client_test.go index 1019d87..037dda4 100644 --- a/internal/incus/client_test.go +++ b/internal/incus/client_test.go @@ -67,7 +67,7 @@ func TestUpdateXauthority_WithFile(t *testing.T) { // This will attempt to call incus commands which will fail without incus installed // but we can verify it at least tries to process the file err := client.UpdateXauthority("test-container") - + // We expect an error because incus isn't available in test environment, // but we're verifying the logic path is taken when the file exists // The function should have attempted to check device existence @@ -111,7 +111,7 @@ func TestUpdateXauthority_CustomPath(t *testing.T) { // This will attempt to call incus commands which will fail without incus installed err := client.UpdateXauthority("test-container") - + // We expect an error because incus isn't available in test environment if err == nil { t.Log("UpdateXauthority succeeded (incus may be available)") From f5a4e4d19ed8fba11531a65209ad675b781b23ff Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Mon, 29 Dec 2025 21:15:31 -0500 Subject: [PATCH 3/4] fix: copilot suggestion Signed-off-by: Brian Ketelsen --- internal/incus/client_test.go | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/internal/incus/client_test.go b/internal/incus/client_test.go index 037dda4..c11d830 100644 --- a/internal/incus/client_test.go +++ b/internal/incus/client_test.go @@ -2,6 +2,7 @@ package incus import ( "os" + "strings" "testing" ) @@ -77,7 +78,9 @@ func TestUpdateXauthority_WithFile(t *testing.T) { t.Log("UpdateXauthority succeeded (incus may be available)") } else { // Expected case: incus command failed - if !containsAny(err.Error(), []string{"incus", "device", "executable file not found", "command not found"}) { + errMsg := err.Error() + if !strings.Contains(errMsg, "incus") && !strings.Contains(errMsg, "device") && + !strings.Contains(errMsg, "executable file not found") && !strings.Contains(errMsg, "command not found") { t.Errorf("Expected incus-related error, got: %v", err) } } @@ -117,22 +120,10 @@ func TestUpdateXauthority_CustomPath(t *testing.T) { t.Log("UpdateXauthority succeeded (incus may be available)") } else { // Expected case: incus command failed - if !containsAny(err.Error(), []string{"incus", "device", "executable file not found", "command not found"}) { + errMsg := err.Error() + if !strings.Contains(errMsg, "incus") && !strings.Contains(errMsg, "device") && + !strings.Contains(errMsg, "executable file not found") && !strings.Contains(errMsg, "command not found") { t.Errorf("Expected incus-related error, got: %v", err) } } } - -// Helper function to check if error contains any of the expected strings -func containsAny(s string, substrs []string) bool { - for _, substr := range substrs { - if len(s) >= len(substr) { - for i := 0; i <= len(s)-len(substr); i++ { - if s[i:i+len(substr)] == substr { - return true - } - } - } - } - return false -} From d5f62762cf76439ab5f93ef0c058a8c1a6bd7002 Mon Sep 17 00:00:00 2001 From: Brian Ketelsen Date: Mon, 29 Dec 2025 21:19:06 -0500 Subject: [PATCH 4/4] fix: copilot suggestion Signed-off-by: Brian Ketelsen --- internal/incus/client.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/incus/client.go b/internal/incus/client.go index df29ebf..7af866d 100644 --- a/internal/incus/client.go +++ b/internal/incus/client.go @@ -205,10 +205,11 @@ func (c *Client) UpdateXauthority(name string) error { return fmt.Errorf("failed to check xauthority device: %w", err) } + username := os.Getenv("USER") + xauthPath := fmt.Sprintf("/home/%s/.Xauthority", username) + if !deviceExists { // Device doesn't exist, add it - username := os.Getenv("USER") - xauthPath := fmt.Sprintf("/home/%s/.Xauthority", username) return c.AddDiskDevice(name, "xauthority", xauthFile, xauthPath) } @@ -224,8 +225,6 @@ func (c *Client) UpdateXauthority(name string) error { return fmt.Errorf("failed to remove old xauthority device: %w", err) } - username := os.Getenv("USER") - xauthPath := fmt.Sprintf("/home/%s/.Xauthority", username) if err := c.AddDiskDevice(name, "xauthority", xauthFile, xauthPath); err != nil { return fmt.Errorf("failed to add new xauthority device: %w", err) }