plugins: the GitHub list waits up to 3s for a repository's https icon fetched the first time (yetone: 非官方插件显示不出来 logo)
RepoIcons starts every fetch, then waits for those not in yet, so the list's first answer has them; one slower than the wait shows the next time. The fetch takes fetchPluginIcon before its goroutine starts. Ran: go test ./internal/provider ./internal/gui; the new test -race -count=10, and red with a 1ns wait. A sandbox build (fresh HOME, real GitHub): the first /api/plugins/github answer has all 8 icons the 13 tagged repositories give, screenshot in light and dark.
This commit is contained in:
1 parent
3e5bc3d8fe
commit
52f6b3a3ab
3 files changed
+121
-10
No files matched your search
@@ -281,8 +281,12 @@ func pluginRoutes(mux *http.ServeMux, w Windows) {
|
||||
// each one's own picture as the page can show it, kept here as an
|
||||
// installed plugin's is (a data URI isn't sent on to the page)
|
||||
repos := append([]plugin.Tagged(nil), plugin.TaggedRepos(r.Context())...)
|
||||
said := make([]string, len(repos))
|
||||
for i := range repos {
|
||||
repos[i].Icon = provider.RepoIcon(repos[i].Icon)
|
||||
said[i] = repos[i].Icon
|
||||
}
|
||||
for i, ic := range provider.RepoIcons(said, 3*time.Second) {
|
||||
repos[i].Icon = ic
|
||||
}
|
||||
writeJSON(rw, map[string]any{"repos": repos, "topic": plugin.Topic})
|
||||
})
|
||||
|
||||
@@ -32,6 +32,8 @@ var plugIcons struct {
|
||||
from string // the file they were read from
|
||||
kept map[string]string
|
||||
tried map[string]time.Time
|
||||
// the URLs being fetched now, each closed once its fetch is over
|
||||
busy map[string]chan struct{}
|
||||
}
|
||||
|
||||
// fetchPluginIcon fetches an https picture as an import link's is: a
|
||||
@@ -98,24 +100,72 @@ func pluginOwnIcon(said string) string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if plugIcons.busy == nil {
|
||||
plugIcons.busy = map[string]chan struct{}{}
|
||||
}
|
||||
done := make(chan struct{})
|
||||
plugIcons.busy[k] = done
|
||||
fetch := fetchPluginIcon
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
ic, err := fetchPluginIcon(ctx, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ic, err := fetch(ctx, u)
|
||||
plugIcons.Lock()
|
||||
keepPlugIcon(k, ic)
|
||||
if err == nil {
|
||||
keepPlugIcon(k, ic)
|
||||
}
|
||||
delete(plugIcons.busy, k)
|
||||
plugIcons.Unlock()
|
||||
close(done)
|
||||
}()
|
||||
return ""
|
||||
}
|
||||
|
||||
// RepoIcon is the Icon value of the picture a plugin not installed yet
|
||||
// gives in its package.json (a repository tagged magpie-plugin), checked
|
||||
// and kept as an installed plugin's is: "" while there is none to show.
|
||||
func RepoIcon(said string) string { return pluginOwnIcon(said) }
|
||||
// RepoIcons are the Icon values of the pictures plugins not installed yet
|
||||
// give in their package.json (repositories tagged magpie-plugin), checked
|
||||
// and kept as an installed plugin's are: "" where there is none to show.
|
||||
// An https picture not fetched before is waited for up to wait, so the
|
||||
// list's first answer has it (yetone: 非官方插件显示不出来 logo); one
|
||||
// slower than that shows the next time the list is asked.
|
||||
func RepoIcons(said []string, wait time.Duration) []string {
|
||||
out := make([]string, len(said))
|
||||
var busy []chan struct{}
|
||||
for i, s := range said {
|
||||
out[i] = pluginOwnIcon(s)
|
||||
if out[i] == "" && s != "" {
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
plugIcons.Lock()
|
||||
if ch := plugIcons.busy[hex.EncodeToString(sum[:16])]; ch != nil {
|
||||
busy = append(busy, ch)
|
||||
}
|
||||
plugIcons.Unlock()
|
||||
}
|
||||
}
|
||||
if len(busy) == 0 {
|
||||
return out
|
||||
}
|
||||
t := time.NewTimer(wait)
|
||||
defer t.Stop()
|
||||
for _, ch := range busy {
|
||||
select {
|
||||
case <-ch:
|
||||
case <-t.C:
|
||||
return refill(out, said)
|
||||
}
|
||||
}
|
||||
return refill(out, said)
|
||||
}
|
||||
|
||||
// refill asks again for the pictures not in yet, which those fetched since
|
||||
// now have
|
||||
func refill(out, said []string) []string {
|
||||
for i, s := range said {
|
||||
if out[i] == "" && s != "" {
|
||||
out[i] = pluginOwnIcon(s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// keptPluginIcons are the names of the stored pictures plugins gave, which
|
||||
// pruneIcons leaves.
|
||||
|
||||
@@ -94,3 +94,60 @@ func TestPluginOwnIcon(t *testing.T) {
|
||||
t.Errorf("fetched again: %d", fetched.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// yetone: 非官方插件显示不出来 logo. A repository's https picture not fetched
|
||||
// before is waited for, so the GitHub list's first answer has it; one slower
|
||||
// than the wait shows the next time, and the wait ends when it does.
|
||||
func TestRepoIconsWaitForFirstFetch(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
||||
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
||||
|
||||
png := []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR")
|
||||
slow := make(chan struct{})
|
||||
// the fetch left waiting ends with the test
|
||||
t.Cleanup(func() {
|
||||
close(slow)
|
||||
for i := 0; i < 200; i++ {
|
||||
plugIcons.Lock()
|
||||
n := len(plugIcons.busy)
|
||||
plugIcons.Unlock()
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Error("the slow fetch didn't end")
|
||||
})
|
||||
was := fetchPluginIcon
|
||||
t.Cleanup(func() { fetchPluginIcon = was })
|
||||
fetchPluginIcon = func(ctx context.Context, u string) (string, error) {
|
||||
if strings.Contains(u, "slow") {
|
||||
select {
|
||||
case <-slow:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return "", ctx.Err()
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
return StoreIcon(png)
|
||||
}
|
||||
|
||||
data := "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
said := []string{"https://gaccode.example/favicon.png", "", data}
|
||||
got := RepoIcons(said, 5*time.Second)
|
||||
if !strings.HasPrefix(got[0], "file:") || got[1] != "" || !strings.HasPrefix(got[2], "file:") {
|
||||
t.Fatalf("first answer = %q", got)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
got = RepoIcons([]string{"https://slow.example/icon.png"}, 300*time.Millisecond)
|
||||
if got[0] != "" {
|
||||
t.Fatalf("a fetch never finished = %q", got[0])
|
||||
}
|
||||
if d := time.Since(start); d < 250*time.Millisecond || d > 3*time.Second {
|
||||
t.Fatalf("waited %v for a 300ms wait", d)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user