app: skip macOS startup from Trash

This commit is contained in:
draplater 2026-10-09 11:02:15 +08:00
parent c2b7368d41
commit a44228ce3b
3 changed files with 167 additions and 0 deletions

View file

@ -181,6 +181,8 @@ func darwinStartHiddenTasks() {
}
func init() {
exitIfRunningFromTrash()
// Temporary code to mimic Squirrel ShipIt behavior
if len(os.Args) > 2 {
if os.Args[1] == "___launch___" {

View file

@ -0,0 +1,48 @@
package main
/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#include <Foundation/Foundation.h>
#include <stdlib.h>
static bool isInTrash(const char *path) {
@autoreleasepool {
NSURL *url = [[NSURL fileURLWithPath:@(path)] URLByResolvingSymlinksInPath];
NSURLRelationship relationship;
return [[NSFileManager defaultManager] getRelationship:&relationship
ofDirectory:NSTrashDirectory
inDomain:0
toItemAtURL:url
error:nil]
&& relationship == NSURLRelationshipContains;
}
}
*/
import "C"
import (
"log/slog"
"os"
"unsafe"
)
func isInTrash(path string) bool {
if path == "" {
return false
}
p := C.CString(path)
defer C.free(unsafe.Pointer(p))
return bool(C.isInTrash(p))
}
func exitIfRunningFromTrash() {
// Use the executable's actual location, not updater.BundlePath, which may
// resolve to an installed copy. This also covers the background helper and
// the legacy ShipIt entry point, before either can launch another process.
executable, err := os.Executable()
if err == nil && isInTrash(executable) {
slog.Info("not starting Ollama from Trash", "path", executable)
os.Exit(0)
}
}

View file

@ -0,0 +1,117 @@
package main
import (
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func testTrashDirectory(t *testing.T) string {
t.Helper()
home, err := os.UserHomeDir()
if err != nil {
t.Fatal(err)
}
dir, err := os.MkdirTemp(filepath.Join(home, ".Trash"), "ollama-startup-test-")
if err != nil {
t.Skipf("cannot create a temporary directory in Trash: %v", err)
}
t.Cleanup(func() { os.RemoveAll(dir) })
return dir
}
func TestIsInTrash(t *testing.T) {
trash := testTrashDirectory(t)
normal := t.TempDir()
for _, tc := range []struct {
name string
path string
want bool
}{
{"trashed bundle", filepath.Join(trash, "Ollama.app"), true},
{"nested backup", filepath.Join(trash, "uninstalled", "Ollama.app"), true},
{"ordinary bundle", filepath.Join(normal, "Ollama.app"), false},
{"unrelated Trash name", filepath.Join(normal, "Trash", "Ollama.app"), false},
{"unrelated dot Trash name", filepath.Join(normal, ".Trash", "Ollama.app"), false},
} {
t.Run(tc.name, func(t *testing.T) {
if err := os.MkdirAll(tc.path, 0o700); err != nil {
t.Fatal(err)
}
if got := isInTrash(tc.path); got != tc.want {
t.Fatalf("isInTrash(%q) = %v, want %v", tc.path, got, tc.want)
}
})
}
link := filepath.Join(normal, "linked-app")
if err := os.Symlink(filepath.Join(trash, "Ollama.app"), link); err != nil {
t.Fatal(err)
}
if !isInTrash(link) {
t.Fatal("symlink to a trashed bundle was not recognized")
}
if isInTrash("") {
t.Fatal("empty path is in Trash")
}
}
func TestTrashStartupHelper(t *testing.T) {
if os.Getenv("OLLAMA_TEST_TRASH_STARTUP") != "1" {
return
}
exitIfRunningFromTrash()
os.Exit(42) // Startup was allowed to continue.
}
func TestTrashedExecutableDoesNotStart(t *testing.T) {
trash := testTrashDirectory(t)
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
for _, path := range []string{
"Ollama.app/Contents/MacOS/Ollama",
"backup/Ollama.app/Contents/Frameworks/Squirrel.framework/Versions/A/Squirrel",
} {
t.Run(path, func(t *testing.T) {
destination := filepath.Join(trash, path)
if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil {
t.Fatal(err)
}
src, err := os.Open(executable)
if err != nil {
t.Fatal(err)
}
defer src.Close()
dst, err := os.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o700)
if err != nil {
t.Fatal(err)
}
_, copyErr := io.Copy(dst, src)
closeErr := dst.Close()
if copyErr != nil || closeErr != nil {
t.Fatalf("copy test executable: %v, %v", copyErr, closeErr)
}
run := func(path string) ([]byte, error) {
cmd := exec.Command(path, "-test.run=^TestTrashStartupHelper$")
cmd.Env = append(os.Environ(), "OLLAMA_TEST_TRASH_STARTUP=1")
return cmd.CombinedOutput()
}
output, err := run(destination)
if err != nil || !strings.Contains(string(output), "not starting Ollama from Trash") {
t.Fatalf("trashed executable did not exit cleanly: %v\n%s", err, output)
}
restored := filepath.Join(t.TempDir(), "Ollama")
if err := os.Rename(destination, restored); err != nil {
t.Fatal(err)
}
output, err = run(restored)
if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 42 {
t.Fatalf("restored executable was not allowed to continue: %v\n%s", err, output)
}
})
}
}