update: a magpie off the Mac that can't write to the folder it runs from (a magpie.exe kept at C:\) says so, naming the folder, beside "a new version is out" and on the Update pill, instead of opening the release page with no reason (#1277, baymax0306)
Before, a binary in a folder magpie may not write to, on a system that
can't ask for the administrator's password (Windows; Linux without
pkexec), was left with no exe to replace and shown only "{v} is out";
Download opened GitHub. The reporter found the cause themselves: the exe
was at C:\. updater.placeExe now marks it stuck "not-writable" with the
folder, and updateStuck says to move it to a folder the user can write
to; zh, zh-TW, ja and de strings added.
Verified:
- TestUpdateNotWritableSaysWhy (internal/gui) fails without the fix:
"a binary in a folder magpie can't write to: {State:available ...
Stuck: StuckDir: ...}"; passes with it.
- update-stuck.test.cjs, Chromium and WebKit, en/zh/ja/de at 1000px and
440px: 18/18 pass; without the app.js change it fails (the reason text
never appears, waitForFunction timeout).
- gui-ja, gui-de, gui-zh-tw, i18n-keys, update-*.test.cjs, cli-path:
136/136 pass in both engines.
- go vet ./internal/gui, go build, GOOS=linux/windows go vet -tags nogui
./internal/gui, GOOS=windows go build; sandbox HOME go test -tags nogui
./... all ok.
- On the Windows box, update.Writable (a windows/amd64 build, over ssh)
is false for a folder whose ACL denies the user write. Over ssh the
token is High integrity, so C:\ itself reads writable there; a
standard user's C:\ and the GUI on Windows weren't tried.
This commit is contained in:
1 parent
0b7a740ee6
commit
f7cf98ee3a
5 files changed
+200
-9
No files matched your search
@@ -3165,8 +3165,10 @@ async function backAsNew(was) {
|
||||
}
|
||||
}
|
||||
|
||||
// updateStuck says why this magpie can't replace itself where it is.
|
||||
// updateStuck says why this magpie can't replace itself where it is: off
|
||||
// the Mac, a folder it may not write to, such as C:\ (#1277).
|
||||
function updateStuck(u) {
|
||||
if (u.stuck === "not-writable") return t("magpie can't write to the folder it runs from ({dir}), so it can't update itself; move it to a folder you can write to and open it from there.", { dir: u.stuckDir || "" });
|
||||
return u.stuck === "translocated"
|
||||
? t("macOS is running magpie from a temporary copy, so it can't update itself; move magpie to Applications and open it from there.")
|
||||
: t("magpie is running from its disk image, so it can't update itself; drag it to Applications and open it from there.");
|
||||
|
||||
@@ -2470,6 +2470,7 @@ const I18N = {
|
||||
"Click to open the download page.": "点击打开下载页面。",
|
||||
"macOS is running magpie from a temporary copy, so it can't update itself; move magpie to Applications and open it from there.": "macOS 正从临时副本运行 magpie,无法自动更新;请将 magpie 移到「应用程序」文件夹后从那里打开。",
|
||||
"magpie is running from its disk image, so it can't update itself; drag it to Applications and open it from there.": "magpie 正从磁盘映像运行,无法自动更新;请将它拖到「应用程序」文件夹后从那里打开。",
|
||||
"magpie can't write to the folder it runs from ({dir}), so it can't update itself; move it to a folder you can write to and open it from there.": "magpie 无法写入它所在的文件夹({dir}),所以无法自动更新;请把它移到你有写入权限的文件夹,再从那里打开。",
|
||||
"Config folder": "配置目录",
|
||||
"Command line": "命令行",
|
||||
"Checking your shells…": "正在检查你的 shell…",
|
||||
@@ -6211,6 +6212,7 @@ const I18N = {
|
||||
"Click to open the download page.": "點選開啟下載頁面。",
|
||||
"macOS is running magpie from a temporary copy, so it can't update itself; move magpie to Applications and open it from there.": "macOS 正從臨時副本執行 magpie,無法自動更新;請將 magpie 移到「應用程式」資料夾後從那裡開啟。",
|
||||
"magpie is running from its disk image, so it can't update itself; drag it to Applications and open it from there.": "magpie 正從磁碟映像執行,無法自動更新;請將它拖到「應用程式」資料夾後從那裡開啟。",
|
||||
"magpie can't write to the folder it runs from ({dir}), so it can't update itself; move it to a folder you can write to and open it from there.": "magpie 無法寫入它所在的資料夾({dir}),所以無法自動更新;請把它移到你有寫入權限的資料夾,再從那裡開啟。",
|
||||
"Config folder": "設定目錄",
|
||||
"Command line": "命令列",
|
||||
"Checking your shells…": "正在檢查你的 shell…",
|
||||
@@ -9923,6 +9925,7 @@ const I18N = {
|
||||
"Click to open the download page.": "クリックするとダウンロードページを開きます。",
|
||||
"macOS is running magpie from a temporary copy, so it can't update itself; move magpie to Applications and open it from there.": "macOS が magpie を一時的なコピーから実行しているため、自動アップデートできません。magpie を「アプリケーション」フォルダに移動し、そこから開いてください。",
|
||||
"magpie is running from its disk image, so it can't update itself; drag it to Applications and open it from there.": "magpie がディスクイメージから実行されているため、自動アップデートできません。「アプリケーション」フォルダにドラッグし、そこから開いてください。",
|
||||
"magpie can't write to the folder it runs from ({dir}), so it can't update itself; move it to a folder you can write to and open it from there.": "magpie は実行中のフォルダ({dir})に書き込めないため、自動アップデートできません。書き込み可能なフォルダに移動し、そこから開いてください。",
|
||||
"Config folder": "設定フォルダ",
|
||||
"Command line": "コマンドライン",
|
||||
"Checking your shells…": "シェルを確認しています…",
|
||||
@@ -13654,6 +13657,7 @@ const I18N = {
|
||||
"Click to open the download page.": "Klicken, um die Download-Seite zu öffnen.",
|
||||
"macOS is running magpie from a temporary copy, so it can't update itself; move magpie to Applications and open it from there.": "macOS führt magpie aus einer temporären Kopie aus, daher kann es sich nicht selbst aktualisieren; verschieben Sie magpie in „Programme“ und öffnen Sie es von dort.",
|
||||
"magpie is running from its disk image, so it can't update itself; drag it to Applications and open it from there.": "magpie läuft aus seinem Disk-Image und kann sich daher nicht selbst aktualisieren; ziehen Sie es in „Programme“ und öffnen Sie es von dort.",
|
||||
"magpie can't write to the folder it runs from ({dir}), so it can't update itself; move it to a folder you can write to and open it from there.": "magpie kann nicht in den Ordner schreiben, aus dem es läuft ({dir}), und kann sich daher nicht selbst aktualisieren; verschieben Sie es in einen Ordner mit Schreibrechten und öffnen Sie es von dort.",
|
||||
"Config folder": "Konfigurationsordner",
|
||||
"Command line": "Befehlszeile",
|
||||
"Checking your shells…": "Shells werden geprüft…",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// Run with Node's test runner and Playwright on the module path; see README.md.
|
||||
// Settings → Version and the Update pill, for a magpie that can't replace
|
||||
// itself where it runs: a magpie.exe kept at C:\ (#1277) said only that a new
|
||||
// version was out, and its Download opened the release page with nothing
|
||||
// saying why. Both now say the folder magpie may not write to, in English,
|
||||
// Chinese, Japanese and German, at a narrow window too. No backend, the API
|
||||
// is faked here.
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs/promises");
|
||||
const path = require("node:path");
|
||||
const { test } = require("node:test");
|
||||
const { chromium, webkit } = require("playwright");
|
||||
|
||||
const assets = path.resolve(__dirname, "../assets");
|
||||
const DIR = "C:\\";
|
||||
|
||||
const words = {
|
||||
en: { version: "Version", why: "magpie can't write to the folder it runs from (C:\\), so it can't update itself; move it to a folder you can write to and open it from there." },
|
||||
zh: { version: "版本", why: "magpie 无法写入它所在的文件夹(C:\\),所以无法自动更新;请把它移到你有写入权限的文件夹,再从那里打开。" },
|
||||
ja: { why: "magpie は実行中のフォルダ(C:\\)に書き込めないため、自動アップデートできません。書き込み可能なフォルダに移動し、そこから開いてください。" },
|
||||
de: { why: "magpie kann nicht in den Ordner schreiben, aus dem es läuft (C:\\), und kann sich daher nicht selbst aktualisieren; verschieben Sie es in einen Ordner mit Schreibrechten und öffnen Sie es von dort." },
|
||||
};
|
||||
|
||||
const update = { state: "available", current: "0.1.400", latest: "0.1.401", url: "https://github.com/yetone/magpie-releases/releases/tag/v0.1.401", stuck: "not-writable", stuckDir: DIR };
|
||||
|
||||
function server(lang, ctl) {
|
||||
const settings = {
|
||||
theme: "light", lang, tray: "panel", quotaLeft: false, currency: "usd",
|
||||
version: "0.1.400", dir: "~/.config/magpie", gateway: "http://127.0.0.1:3999",
|
||||
proxyNow: "none", proxySource: "none", login: false, redactWords: [],
|
||||
visionModels: [], imageGenModels: [], workbuddyCheckins: [], lanURLs: [],
|
||||
fx: { rate: 7.2, at: new Date().toISOString(), stale: false },
|
||||
};
|
||||
return async (route) => {
|
||||
const req = route.request(), url = new URL(req.url());
|
||||
const json = (data) => route.fulfill({ json: data });
|
||||
if (url.pathname === "/boot.js") return route.fulfill({ contentType: "text/javascript", body: `window.bootPrefs = {lang:"${lang}",theme:"light",web:false};` });
|
||||
if (url.pathname === "/wails/runtime.js") return route.fulfill({ contentType: "text/javascript", body: "export const Window = {};" });
|
||||
if (url.pathname === "/api/state") return json({ agents: [], profiles: [], settings: { lang, theme: "light" }, fx: settings.fx });
|
||||
if (url.pathname === "/api/settings") return json(settings);
|
||||
if (url.pathname === "/api/update" || url.pathname === "/api/update/check") return json({ ...update });
|
||||
if (url.pathname === "/api/update/install") { ctl.installs++; return json({ ...update }); }
|
||||
if (url.pathname === "/api/usage/quotas") return json([]);
|
||||
if (url.pathname === "/api/groups") return json({ groups: [], models: [] });
|
||||
if (url.pathname.startsWith("/api/")) return json({});
|
||||
const file = path.join(assets, url.pathname === "/" ? "index.html" : url.pathname);
|
||||
const contentType = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png" }[path.extname(file)];
|
||||
try { await route.fulfill({ body: await fs.readFile(file), contentType }); } catch { await route.fulfill({ status: 404, body: "" }); }
|
||||
};
|
||||
}
|
||||
|
||||
for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium", "webkit"])) {
|
||||
test(engine + ": a magpie that can't write to its folder says why it can't update", async (t) => {
|
||||
const browser = await (engine === "webkit" ? webkit.launch() : chromium.launch({ channel: "chromium" }));
|
||||
t.after(() => browser.close());
|
||||
for (const lang of ["en", "zh", "ja", "de"]) {
|
||||
for (const width of [1000, 440]) {
|
||||
await t.test(`${lang} at ${width}px`, async () => {
|
||||
const ctl = { installs: 0 };
|
||||
const errors = [];
|
||||
const page = await (await browser.newContext({ viewport: { width, height: 700 }, reducedMotion: "reduce" })).newPage();
|
||||
page.setDefaultTimeout(5000);
|
||||
page.on("pageerror", (e) => errors.push(e.message));
|
||||
await page.route("**/*", server(lang, ctl));
|
||||
await page.goto("http://magpie.test/?view=settings&tab=about");
|
||||
const want = words[lang].why;
|
||||
// the Version row: the version out, then why it can't be put in here
|
||||
// (the row is drawn again as the answer is read, so it is found
|
||||
// and read in one go)
|
||||
const sub = await (await page.waitForFunction((w) => [...document.querySelectorAll("#about .row.pref .sub")].find((s) => s.textContent.includes(w))?.textContent, want)).jsonValue();
|
||||
assert.ok(sub.includes("0.1.401"), sub);
|
||||
// the pill's tooltip says it too, before the click opens the page
|
||||
await page.waitForFunction(() => !document.querySelector("#update")?.hidden);
|
||||
const title = await page.locator("#update").getAttribute("title");
|
||||
assert.ok(title.startsWith(want), title);
|
||||
// nothing is cut off or pushes the page sideways at a narrow width
|
||||
const over = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
assert.ok(over <= 0, `the page scrolls sideways by ${over}px`);
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
+39
-8
@@ -31,8 +31,9 @@ type updater struct {
|
||||
latest *update.Release
|
||||
err string
|
||||
bundle string // the .app to replace, "" when not in one or stuck
|
||||
stuck string // why the .app can't be replaced where it is (update.Stuck)
|
||||
stuck string // why the .app or binary can't be replaced where it is (update.Stuck, exeStuck)
|
||||
exe string // off the Mac: the binary to replace, "" when not writable
|
||||
exeDir string // off the Mac: the binary's folder, named when it is stuck there
|
||||
self os.FileInfo // exe as this process started from it
|
||||
retry bool // error: the download failed, and may be tried again
|
||||
mirror string // error: the mirror the failed download came through (#893)
|
||||
@@ -71,7 +72,9 @@ type updateJSON struct {
|
||||
Notes string `json:"notes,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Stuck string `json:"stuck,omitempty"` // available: why it can't update itself
|
||||
Retry bool `json:"retry,omitempty"` // error: a click downloads it again
|
||||
// available, stuck "not-writable": the folder magpie may not write to
|
||||
StuckDir string `json:"stuckDir,omitempty"`
|
||||
Retry bool `json:"retry,omitempty"` // error: a click downloads it again
|
||||
// error: the download failed through this mirror (Settings' UpdateMirror),
|
||||
// and GitHub itself may do (#893)
|
||||
Mirror string `json:"mirror,omitempty"`
|
||||
@@ -104,12 +107,8 @@ func (u *updater) start() {
|
||||
u.bundle = b
|
||||
}
|
||||
} else if runtime.GOOS != "darwin" {
|
||||
if exe, err := update.Executable(); err == nil && (update.Writable(filepath.Dir(exe)) || update.CanElevate()) {
|
||||
u.exe = exe
|
||||
u.self, _ = os.Stat(exe)
|
||||
update.RemoveOld(exe) // what the last updates on Windows moved aside
|
||||
update.RemoveStaleNew(exe) // what a magpie left running downloaded again
|
||||
u.blocked = update.ReadBlocked(Version)
|
||||
if exe, err := update.Executable(); err == nil {
|
||||
u.placeExe(exe)
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
@@ -126,6 +125,35 @@ func (u *updater) start() {
|
||||
}()
|
||||
}
|
||||
|
||||
// placeExe takes exe, off the Mac, as the binary an update replaces, or
|
||||
// says why it can't be (stuck).
|
||||
func (u *updater) placeExe(exe string) {
|
||||
u.exeDir = filepath.Dir(exe)
|
||||
if u.stuck = exeStuck(u.exeDir); u.stuck != "" {
|
||||
return
|
||||
}
|
||||
u.exe = exe
|
||||
u.self, _ = os.Stat(exe)
|
||||
update.RemoveOld(exe) // what the last updates on Windows moved aside
|
||||
update.RemoveStaleNew(exe) // what a magpie left running downloaded again
|
||||
u.blocked = update.ReadBlocked(Version)
|
||||
}
|
||||
|
||||
// exeStuck says why the binary in dir can't be replaced off the Mac, or ""
|
||||
// when it can: "not-writable" when magpie may not write to dir and this
|
||||
// system can't ask for the administrator's password. On Windows a magpie.exe
|
||||
// kept at C:\ updated only by opening the release page, with nothing saying
|
||||
// why (#1277).
|
||||
func exeStuck(dir string) string {
|
||||
if update.Writable(dir) || canElevate() {
|
||||
return ""
|
||||
}
|
||||
return "not-writable"
|
||||
}
|
||||
|
||||
// canElevate is update.CanElevate; tests stand in for it.
|
||||
var canElevate = update.CanElevate
|
||||
|
||||
// check asks the feed and, when it can, stages the new version.
|
||||
func (u *updater) check() {
|
||||
if u.begin() {
|
||||
@@ -352,6 +380,9 @@ func (u *updater) jsonIn(lang string) updateJSON {
|
||||
}
|
||||
if u.state == "available" {
|
||||
j.Stuck = u.stuck
|
||||
if u.stuck == "not-writable" {
|
||||
j.StuckDir = u.exeDir
|
||||
}
|
||||
}
|
||||
if u.state == "downloading" {
|
||||
j.Done, j.Total = u.done, u.total
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/yetone/magpie/internal/update"
|
||||
)
|
||||
|
||||
// #1277: a magpie.exe kept at C:\ may not write there, so it can't replace
|
||||
// itself. It still says a new version is out, and says why it opens the
|
||||
// release page instead of updating, naming the folder.
|
||||
func TestUpdateNotWritableSaysWhy(t *testing.T) {
|
||||
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
|
||||
t.Skip("needs a folder this user may not write to")
|
||||
}
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(update.Release{Version: "0.1.11", Assets: map[string]update.Asset{
|
||||
update.BinaryAsset(): {URL: "http://" + r.Host + "/asset"},
|
||||
}})
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("MAGPIE_UPDATE_FEED", srv.URL)
|
||||
old := Version
|
||||
Version = "0.1.10"
|
||||
defer func() { Version = old }()
|
||||
was := canElevate
|
||||
defer func() { canElevate = was }()
|
||||
|
||||
dir := t.TempDir()
|
||||
exe := filepath.Join(dir, "magpie.exe")
|
||||
if err := os.WriteFile(exe, []byte("magpie"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(dir, 0o555); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(dir, 0o755)
|
||||
|
||||
canElevate = func() bool { return false }
|
||||
u := &updater{}
|
||||
u.placeExe(exe)
|
||||
u.check()
|
||||
j := u.json()
|
||||
if j.State != "available" || j.Stuck != "not-writable" || j.StuckDir != dir {
|
||||
t.Fatalf("a binary in a folder magpie can't write to: %+v", j)
|
||||
}
|
||||
|
||||
// where the system can ask for the password, it updates as before
|
||||
canElevate = func() bool { return true }
|
||||
u = &updater{}
|
||||
u.placeExe(exe)
|
||||
if u.stuck != "" || u.exe != exe {
|
||||
t.Fatalf("stuck %q, exe %q", u.stuck, u.exe)
|
||||
}
|
||||
// and a folder it may write to is never stuck
|
||||
canElevate = func() bool { return false }
|
||||
os.Chmod(dir, 0o755)
|
||||
u = &updater{}
|
||||
u.placeExe(exe)
|
||||
if u.stuck != "" || u.exe != exe {
|
||||
t.Fatalf("a writable folder: stuck %q, exe %q", u.stuck, u.exe)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user