Skip to content
Closed
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
73 changes: 70 additions & 3 deletions desktop/src/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,15 @@ func pythonExe(root string) string {

func ensurePortable(status func(string)) error {
root := portableDir()
if launchReady(root) {
status("正在使用已有运行环境…")
if launchReady(root) && !portableNeedsReplace(root) {
status("正在加载运行环境…")
return nil
}
status("首次启动,正在解压内置运行环境…")
if launchReady(root) {
status("正在升级运行环境…")
} else {
status("正在准备运行环境…")
}
if err := extractPortable(root); err != nil {
return err
}
Expand Down Expand Up @@ -69,6 +73,69 @@ func extractPortable(root string) error {
return unzipGreen(zipPath, root)
}

func portableNeedsReplace(root string) bool {
bundled := bundledVERSION()
if len(bundled) == 0 {
return false
}
local, err := os.ReadFile(filepath.Join(root, "VERSION.txt"))
if err != nil {
return true
}
return !bytes.Equal(bytes.TrimSpace(local), bytes.TrimSpace(bundled))
}

func bundledVERSION() []byte {
if os.Getenv("OCTOP_DESKTOP_PORTABLE_ZIP") != "" {
path, err := bundledPortableZip()
if err != nil {
return nil
}
return zipPathVERSION(path)
}
if len(embeddedPortable) > 0 {
r, err := zip.NewReader(bytes.NewReader(embeddedPortable), int64(len(embeddedPortable)))
if err != nil {
return nil
}
return zipFilesVERSION(r.File)
}
path, err := bundledPortableZip()
if err != nil {
return nil
}
return zipPathVERSION(path)
}

func zipPathVERSION(zipPath string) []byte {
r, err := zip.OpenReader(zipPath)
if err != nil {
return nil
}
defer r.Close()
return zipFilesVERSION(r.File)
}

func zipFilesVERSION(files []*zip.File) []byte {
for _, f := range files {
parts := strings.SplitN(f.Name, "/", 2)
if len(parts) < 2 || parts[1] != "VERSION.txt" {
continue
}
rc, err := f.Open()
if err != nil {
return nil
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil
}
return data
}
return nil
}

func bundledPortableZip() (string, error) {
name := fmt.Sprintf("Octop-%s.zip", greenPlat())
if override := os.Getenv("OCTOP_DESKTOP_PORTABLE_ZIP"); override != "" {
Expand Down
97 changes: 93 additions & 4 deletions desktop/src/download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestEnsurePortableUsesEmbeddedPackage(t *testing.T) {
t.Setenv("OCTOP_DESKTOP_PORTABLE_ZIP", "")

zipPath := filepath.Join(t.TempDir(), "embedded.zip")
writeTestGreenZip(t, zipPath)
writeTestGreenZip(t, zipPath, "0.9.0")
data, err := os.ReadFile(zipPath)
if err != nil {
t.Fatal(err)
Expand All @@ -38,7 +38,7 @@ func TestEnsurePortableUsesBundledPackage(t *testing.T) {

zipPath := filepath.Join(t.TempDir(), "Octop-"+greenPlat()+".zip")
t.Setenv("OCTOP_DESKTOP_PORTABLE_ZIP", zipPath)
writeTestGreenZip(t, zipPath)
writeTestGreenZip(t, zipPath, "0.9.0")

var statuses []string
err := ensurePortable(func(status string) {
Expand All @@ -58,6 +58,69 @@ func TestEnsurePortableUsesBundledPackage(t *testing.T) {
}
}

func TestEnsurePortableReplacesDifferentVERSION(t *testing.T) {
home := t.TempDir()
t.Setenv("OCTOP_HOME", home)
root := portableDir()
seedLaunchReady(t, root, "0.9.0")
marker := filepath.Join(root, "keep-me.txt")
if err := os.WriteFile(marker, []byte("stale"), 0o644); err != nil {
t.Fatal(err)
}

zipPath := filepath.Join(t.TempDir(), "Octop-"+greenPlat()+".zip")
t.Setenv("OCTOP_DESKTOP_PORTABLE_ZIP", zipPath)
writeTestGreenZip(t, zipPath, "0.9.1")

var statuses []string
if err := ensurePortable(func(status string) {
statuses = append(statuses, status)
}); err != nil {
t.Fatal(err)
}
if len(statuses) == 0 || statuses[0] != "检测到新版本,正在更新运行环境…" {
t.Fatalf("unexpected statuses: %v", statuses)
}
if _, err := os.Stat(marker); err == nil {
t.Fatal("older portable dir should be removed before extract")
}
got, err := os.ReadFile(filepath.Join(root, "VERSION.txt"))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(got), "octop_version=0.9.1") {
t.Fatalf("VERSION.txt = %q, want 0.9.1", got)
}
}

func TestEnsurePortableKeepsMatchingVERSION(t *testing.T) {
home := t.TempDir()
t.Setenv("OCTOP_HOME", home)
root := portableDir()
seedLaunchReady(t, root, "0.9.1")
marker := filepath.Join(root, "keep-me.txt")
if err := os.WriteFile(marker, []byte("keep"), 0o644); err != nil {
t.Fatal(err)
}

zipPath := filepath.Join(t.TempDir(), "Octop-"+greenPlat()+".zip")
t.Setenv("OCTOP_DESKTOP_PORTABLE_ZIP", zipPath)
writeTestGreenZip(t, zipPath, "0.9.1")

var statuses []string
if err := ensurePortable(func(status string) {
statuses = append(statuses, status)
}); err != nil {
t.Fatal(err)
}
if len(statuses) == 0 || statuses[0] != "正在使用已有运行环境…" {
t.Fatalf("unexpected statuses: %v", statuses)
}
if _, err := os.Stat(marker); err != nil {
t.Fatal("matching portable dir should be kept")
}
}

func TestBundledPortableZipRequiresMatchingPackage(t *testing.T) {
t.Setenv("OCTOP_DESKTOP_PORTABLE_ZIP", filepath.Join(t.TempDir(), "missing.zip"))
if _, err := bundledPortableZip(); err == nil {
Expand All @@ -83,14 +146,37 @@ func TestLaunchReadyRejectsFlattenedPythonSymlink(t *testing.T) {
}
}

func writeTestGreenZip(t *testing.T, path string) {
func seedLaunchReady(t *testing.T, root, version string) {
t.Helper()
if err := os.MkdirAll(filepath.Join(root, "runtime", "bin"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "launch.py"), []byte("test"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "VERSION.txt"), []byte("octop_version="+version+"\n"), 0o644); err != nil {
t.Fatal(err)
}
py := filepath.Join(root, "runtime", "bin", "python3")
if runtime.GOOS == "windows" {
if err := os.MkdirAll(filepath.Join(root, "runtime"), 0o755); err != nil {
t.Fatal(err)
}
py = filepath.Join(root, "runtime", "python.exe")
}
if err := os.WriteFile(py, make([]byte, 2048), 0o755); err != nil {
t.Fatal(err)
}
}

func writeTestGreenZip(t *testing.T, path, version string) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
w := zip.NewWriter(f)
files := []string{"Octop-test/launch.py"}
files := []string{"Octop-test/launch.py", "Octop-test/VERSION.txt"}
if runtime.GOOS == "windows" {
files = append(files, "Octop-test/runtime/python.exe")
} else {
Expand All @@ -105,6 +191,9 @@ func writeTestGreenZip(t *testing.T, path string) {
if strings.HasSuffix(name, "/python3") {
header.SetMode(os.ModeSymlink | 0o755)
content = []byte("python3.12")
} else if strings.HasSuffix(name, "/VERSION.txt") {
header.SetMode(0o644)
content = []byte("octop_version=" + version + "\n")
} else {
header.SetMode(0o755)
if strings.HasSuffix(name, "/python3.12") || strings.HasSuffix(name, "/python.exe") {
Expand Down
32 changes: 30 additions & 2 deletions desktop/src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type App struct {
trayClickMu sync.Mutex
lastTrayClick time.Time
trayClickTimer *time.Timer

runtimeReady chan struct{}
runtimeReadyOnce sync.Once
}

func (a *App) ServiceName() string { return "desktop" }
Expand Down Expand Up @@ -185,10 +188,31 @@ func (a *App) boot() {
a.showDashboard(base)
}

func (a *App) signalRuntimeReady() {
a.runtimeReadyOnce.Do(func() {
if a.runtimeReady != nil {
close(a.runtimeReady)
}
})
}

func (a *App) waitRuntimeReady(timeout time.Duration) {
if a.runtimeReady == nil {
return
}
select {
case <-a.runtimeReady:
case <-time.After(timeout):
}
}

func (a *App) showDashboard(base string) {
if a.window == nil {
return
}
// ExecJS is queued until wails:runtime:ready. Dashboard never sends that
// handshake; wait for the shell page so overlay injects work after SetURL.
a.waitRuntimeReady(8 * time.Second)
a.window.SetURL(base)
a.scheduleDragOverlay()
s := a.store.get()
Expand Down Expand Up @@ -311,8 +335,9 @@ func (a *App) requestQuit() {
func main() {
store := &settingsStore{cur: loadSettings()}
api := &App{
store: store,
sleep: &sleepGuard{},
store: store,
sleep: &sleepGuard{},
runtimeReady: make(chan struct{}),
}

app := application.New(application.Options{
Expand Down Expand Up @@ -350,6 +375,9 @@ func main() {
BackgroundColour: application.NewRGB(247, 248, 250),
})
api.window = win
win.OnWindowEvent(events.Common.WindowRuntimeReady, func(_ *application.WindowEvent) {
api.signalRuntimeReady()
})
app.Event.On("desktop:toggle-maximise", func(_ *application.CustomEvent) {
win.ToggleMaximise()
})
Expand Down
Loading