From a851e0f600e4979e2e87cbc27806ed505a739e4f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 9 Jun 2024 09:33:50 -0700 Subject: [PATCH 1/9] skeleton of IPC mechanism to allow playback control with cmdline flags --- backend/ipc/api.go | 19 ++++++++++ backend/ipc/conn_other.go | 14 +++++++ backend/ipc/conn_windows.go | 19 ++++++++++ backend/ipc/server.go | 73 +++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 6 files changed, 128 insertions(+) create mode 100644 backend/ipc/api.go create mode 100644 backend/ipc/conn_other.go create mode 100644 backend/ipc/conn_windows.go create mode 100644 backend/ipc/server.go diff --git a/backend/ipc/api.go b/backend/ipc/api.go new file mode 100644 index 0000000..32681f7 --- /dev/null +++ b/backend/ipc/api.go @@ -0,0 +1,19 @@ +package ipc + +const ( + PlayPath = "/transport/play" + PlayPausePath = "/transport/playpause" + PausePath = "/transport/pause" + StopPath = "/transport/stop" + PreviousPath = "/transport/previous" + NextPath = "/transport/next" + TimePosPath = "/transport/timepos" + + PlayTrackPath = "/queue/playtrack" + + VolumePath = "/volume" +) + +type Response struct { + Error string `json:"error"` +} diff --git a/backend/ipc/conn_other.go b/backend/ipc/conn_other.go new file mode 100644 index 0000000..62fdc9e --- /dev/null +++ b/backend/ipc/conn_other.go @@ -0,0 +1,14 @@ +//go:build !windows + +package ipc + +import "net" + +func Dial() (net.Conn, error) { + // TODO - use XDG runtime dir, also handle portable mode + return net.Dial("unix", "/tmp/supersonic.sock") +} + +func Listen() (net.Listener, error) { + return net.Listen("unix", "/tmp/supersonic.sock") +} diff --git a/backend/ipc/conn_windows.go b/backend/ipc/conn_windows.go new file mode 100644 index 0000000..59c0fad --- /dev/null +++ b/backend/ipc/conn_windows.go @@ -0,0 +1,19 @@ +//go:build windows + +package ipc + +import ( + "net" + "time" + + "github.com/Microsoft/go-winio" +) + +func Dial() (net.Conn, error) { + timeout := 300 * time.Millisecond + return winio.DialPipe("supersonic", &timeout) +} + +func Listen() (net.Listener, error) { + return winio.ListenPipe("supersonic", nil) +} diff --git a/backend/ipc/server.go b/backend/ipc/server.go new file mode 100644 index 0000000..00e4ddd --- /dev/null +++ b/backend/ipc/server.go @@ -0,0 +1,73 @@ +package ipc + +import ( + "encoding/json" + "net/http" +) + +type Handler interface { + PlayPause() error + Stop() error + Pause() error + Continue() error + SeekBackOrPrevious() error + SeekNext() error + SeekSeconds(float64) error + Volume() int + SetVolume(int) error +} + +type serverImpl struct { + handler Handler +} + +func NewServer(handler Handler) *http.Server { + s := serverImpl{handler: handler} + return &http.Server{ + Handler: s.createHandler(), + } +} + +func (s *serverImpl) createHandler() http.Handler { + m := http.NewServeMux() + m.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("The given path is not valid")) + }) + m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.handler.Continue)) + m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.handler.Pause)) + m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.handler.PlayPause)) + m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.handler.Stop)) + m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.handler.SeekBackOrPrevious)) + m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.handler.SeekNext)) + return m +} + +func (s *serverImpl) makeSimpleEndpointHandler(f func() error) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + if err := f(); err == nil { + s.writeOK(w) + } else { + s.writeErr(w, err) + } + } +} + +func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) { + var r Response + b, err := json.Marshal(&r) + if err != nil { + return 0, err + } + return w.Write(b) +} + +func (s *serverImpl) writeErr(w http.ResponseWriter, err error) (int, error) { + r := Response{Error: err.Error()} + b, err := json.Marshal(&r) + if err != nil { + return 0, err + } + w.WriteHeader(http.StatusInternalServerError) + return w.Write(b) +} diff --git a/go.mod b/go.mod index 72b5bb7..40e90e4 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( fyne.io/fyne/v2 v2.5.0 github.com/20after4/configdir v0.1.1 + github.com/Microsoft/go-winio v0.6.2 github.com/cenkalti/dominantcolor v1.0.2 github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 github.com/dweymouth/fyne-lyrics v0.0.0-20240528234907-15eee7ce5e64 diff --git a/go.sum b/go.sum index 6a858b2..335b6a5 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= From d6cac62704a766c21ab722cfa0d837a54f6b4640 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Sun, 9 Jun 2024 11:10:10 -0700 Subject: [PATCH 2/9] beginning of client --- backend/ipc/api.go | 34 +++++++++++++----- backend/ipc/client.go | 83 +++++++++++++++++++++++++++++++++++++++++++ backend/ipc/server.go | 36 ++++++++++++++++--- 3 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 backend/ipc/client.go diff --git a/backend/ipc/api.go b/backend/ipc/api.go index 32681f7..d75e5ae 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -1,19 +1,37 @@ package ipc const ( - PlayPath = "/transport/play" + // GET + PingPath = "/ping" + // POST + PlayPath = "/transport/play" + // POST PlayPausePath = "/transport/playpause" - PausePath = "/transport/pause" - StopPath = "/transport/stop" - PreviousPath = "/transport/previous" - NextPath = "/transport/next" - TimePosPath = "/transport/timepos" - + // POST + PausePath = "/transport/pause" + // POST + StopPath = "/transport/stop" + // POST + PreviousPath = "/transport/previous" + // POST + NextPath = "/transport/next" + // POST(TimePos) + TimePosPath = "/transport/timepos" + // POST to seek PlayTrackPath = "/queue/playtrack" - + // GET -> Volume + // POST(Volume) VolumePath = "/volume" ) +type TimePos struct { + Seconds float64 `json:"seconds"` +} + +type Volume struct { + Volume int `json:"volume"` +} + type Response struct { Error string `json:"error"` } diff --git a/backend/ipc/client.go b/backend/ipc/client.go new file mode 100644 index 0000000..134ed4f --- /dev/null +++ b/backend/ipc/client.go @@ -0,0 +1,83 @@ +package ipc + +import ( + "context" + "encoding/json" + "errors" + "net" + "net/http" +) + +var ErrPingFail = errors.New("ping failed") + +type Client struct { + httpC http.Client +} + +// Connect attempts to connect to the IPC socket as client. +func Connect() (*Client, error) { + conn, err := Dial() + if err != nil { + return nil, err + } + client := &Client{httpC: http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return conn, nil + }, + }, + }} + if err := client.Ping(); err != nil { + return nil, err + } + return client, nil +} + +func (c *Client) Ping() error { + if c.makeSimpleRequest(http.MethodGet, PingPath) != nil { + return ErrPingFail + } + return nil +} + +func (c *Client) Play() error { + return c.makeSimpleRequest(http.MethodPost, PlayPath) +} + +func (c *Client) Pause() error { + return c.makeSimpleRequest(http.MethodPost, PausePath) +} + +func (c *Client) PlayPause() error { + return c.makeSimpleRequest(http.MethodPost, PlayPausePath) +} + +func (c *Client) SeekNext() error { + return c.makeSimpleRequest(http.MethodPost, NextPath) +} + +func (c *Client) SeekBackOrPrevious() error { + return c.makeSimpleRequest(http.MethodPost, NextPath) +} + +func (c *Client) makeSimpleRequest(method string, path string) error { + var resp *http.Response + var err error + switch method { + case http.MethodGet: + resp, err = c.httpC.Get(path) + case http.MethodPost: + resp, err = c.httpC.Post(path, "application/json", nil) + } + + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + var r Response + json.NewDecoder(resp.Body).Decode(&r) + return errors.New(r.Error) + } + return nil +} diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 00e4ddd..40a38cd 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -34,22 +34,48 @@ func (s *serverImpl) createHandler() http.Handler { w.WriteHeader(http.StatusNotFound) w.Write([]byte("The given path is not valid")) }) + m.HandleFunc(PingPath, s.makeSimpleEndpointHandler(func() error { return nil })) m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.handler.Continue)) m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.handler.Pause)) m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.handler.PlayPause)) m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.handler.Stop)) m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.handler.SeekBackOrPrevious)) m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.handler.SeekNext)) + m.HandleFunc(TimePosPath, func(w http.ResponseWriter, r *http.Request) { + var t TimePos + if err := json.NewDecoder(r.Response.Body).Decode(&t); err != nil { + s.writeErr(w, err) + return + } + s.writeSimpleResponse(w, s.handler.SeekSeconds(t.Seconds)) + }) + m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + msg, _ := json.Marshal(Volume{Volume: s.handler.Volume()}) + w.Write(msg) + return + } + var v Volume + if err := json.NewDecoder(r.Response.Body).Decode(&v); err != nil { + s.writeErr(w, err) + return + } + s.writeSimpleResponse(w, s.handler.SetVolume(v.Volume)) + }) return m } func (s *serverImpl) makeSimpleEndpointHandler(f func() error) func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - if err := f(); err == nil { - s.writeOK(w) - } else { - s.writeErr(w, err) - } + s.writeSimpleResponse(w, f()) + } +} + +func (s *serverImpl) writeSimpleResponse(w http.ResponseWriter, err error) { + if err == nil { + s.writeOK(w) + } else { + s.writeErr(w, err) } } From 969c30f35a3a418e9f5499ad6dec21be9d61545a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 10 Jun 2024 07:59:06 -0700 Subject: [PATCH 3/9] add window actions to ipc --- backend/ipc/api.go | 6 ++++++ backend/ipc/server.go | 42 ++++++++++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/backend/ipc/api.go b/backend/ipc/api.go index d75e5ae..be82574 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -3,6 +3,7 @@ package ipc const ( // GET PingPath = "/ping" + // POST PlayPath = "/transport/play" // POST @@ -22,6 +23,11 @@ const ( // GET -> Volume // POST(Volume) VolumePath = "/volume" + + // POST + ShowPath = "/window/show" + // POST + QuitPath = "/window/quit" ) type TimePos struct { diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 40a38cd..3242e9d 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -5,7 +5,7 @@ import ( "net/http" ) -type Handler interface { +type PlaybackHandler interface { PlayPause() error Stop() error Pause() error @@ -17,12 +17,18 @@ type Handler interface { SetVolume(int) error } -type serverImpl struct { - handler Handler +type WindowHandler interface { + Show() + Quit() } -func NewServer(handler Handler) *http.Server { - s := serverImpl{handler: handler} +type serverImpl struct { + pbHandler PlaybackHandler + wdHandler WindowHandler +} + +func NewServer(pbHandler PlaybackHandler, wdHandler WindowHandler) *http.Server { + s := serverImpl{pbHandler: pbHandler, wdHandler: wdHandler} return &http.Server{ Handler: s.createHandler(), } @@ -35,23 +41,31 @@ func (s *serverImpl) createHandler() http.Handler { w.Write([]byte("The given path is not valid")) }) m.HandleFunc(PingPath, s.makeSimpleEndpointHandler(func() error { return nil })) - m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.handler.Continue)) - m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.handler.Pause)) - m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.handler.PlayPause)) - m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.handler.Stop)) - m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.handler.SeekBackOrPrevious)) - m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.handler.SeekNext)) + m.HandleFunc(ShowPath, s.makeSimpleEndpointHandler(func() error { + s.wdHandler.Show() + return nil + })) + m.HandleFunc(QuitPath, s.makeSimpleEndpointHandler(func() error { + go s.wdHandler.Quit() + return nil + })) + m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.pbHandler.Continue)) + m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause)) + m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause)) + m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop)) + m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious)) + m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext)) m.HandleFunc(TimePosPath, func(w http.ResponseWriter, r *http.Request) { var t TimePos if err := json.NewDecoder(r.Response.Body).Decode(&t); err != nil { s.writeErr(w, err) return } - s.writeSimpleResponse(w, s.handler.SeekSeconds(t.Seconds)) + s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(t.Seconds)) }) m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { - msg, _ := json.Marshal(Volume{Volume: s.handler.Volume()}) + msg, _ := json.Marshal(Volume{Volume: s.pbHandler.Volume()}) w.Write(msg) return } @@ -60,7 +74,7 @@ func (s *serverImpl) createHandler() http.Handler { s.writeErr(w, err) return } - s.writeSimpleResponse(w, s.handler.SetVolume(v.Volume)) + s.writeSimpleResponse(w, s.pbHandler.SetVolume(v.Volume)) }) return m } From e0974fdb032a8284638957d9e020b14c80a0c18f Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 10 Jun 2024 08:28:41 -0700 Subject: [PATCH 4/9] more WIP --- backend/app.go | 59 +++++++++++++++++++++++++++---------------- backend/ipc/client.go | 16 ++++++++++-- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/backend/app.go b/backend/app.go index 440e579..cd915ab 100644 --- a/backend/app.go +++ b/backend/app.go @@ -12,6 +12,7 @@ import ( "slices" "time" + "github.com/dweymouth/supersonic/backend/ipc" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player/mpv" @@ -83,22 +84,31 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) configdir.MakePath(confDir) configdir.MakePath(cacheDir) - sessionPath := path.Join(confDir, sessionDir) - if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil { + cli, err := ipc.Connect() + if err == nil { log.Println("Another instance is running. Reactivating it...") - reactivateFile := path.Join(sessionPath, sessionActivateFile) - if f, err := os.Create(reactivateFile); err == nil { - f.Close() - } - time.Sleep(750 * time.Millisecond) - if _, err := os.Stat(reactivateFile); err == nil { - log.Println("No other instance responded. Starting as normal...") - os.RemoveAll(sessionPath) - } else { - return nil, ErrAnotherInstance - } + cli.Show() + return nil, ErrAnotherInstance } + /* + sessionPath := path.Join(confDir, sessionDir) + if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil { + log.Println("Another instance is running. Reactivating it...") + reactivateFile := path.Join(sessionPath, sessionActivateFile) + if f, err := os.Create(reactivateFile); err == nil { + f.Close() + } + time.Sleep(750 * time.Millisecond) + if _, err := os.Stat(reactivateFile); err == nil { + log.Println("No other instance responded. Starting as normal...") + os.RemoveAll(sessionPath) + } else { + return nil, ErrAnotherInstance + } + } + */ + log.Printf("Starting %s...", appName) log.Printf("Using config dir: %s", confDir) log.Printf("Using cache dir: %s", cacheDir) @@ -114,16 +124,18 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) a.readConfig() a.startConfigWriter(a.bgrndCtx) - if !a.Config.Application.AllowMultiInstance { - log.Println("Creating session lock file") - os.MkdirAll(sessionPath, 0770) - if f, err := os.Create(path.Join(sessionPath, sessionLockFile)); err == nil { - f.Close() - } else { - log.Printf("error creating session file: %s", err.Error()) + /* + if !a.Config.Application.AllowMultiInstance { + log.Println("Creating session lock file") + os.MkdirAll(sessionPath, 0770) + if f, err := os.Create(path.Join(sessionPath, sessionLockFile)); err == nil { + f.Close() + } else { + log.Printf("error creating session file: %s", err.Error()) + } + a.startSessionWatcher(sessionPath) } - a.startSessionWatcher(sessionPath) - } + */ a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion) a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour) @@ -143,6 +155,9 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) { _, _ = a.ImageManager.GetCoverThumbnail(coverID) }) + listener, _ := ipc.Listen() + server := ipc.NewServer(a.PlaybackManager, nil) + go server.Serve(listener) // OS media center integrations a.setupMPRIS(displayAppName) diff --git a/backend/ipc/client.go b/backend/ipc/client.go index 134ed4f..d03eeb6 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "log" "net" "net/http" ) @@ -18,6 +19,7 @@ type Client struct { func Connect() (*Client, error) { conn, err := Dial() if err != nil { + log.Println("dial error") return nil, err } client := &Client{httpC: http.Client{ @@ -28,6 +30,7 @@ func Connect() (*Client, error) { }, }} if err := client.Ping(); err != nil { + log.Println("ping error") return nil, err } return client, nil @@ -60,17 +63,26 @@ func (c *Client) SeekBackOrPrevious() error { return c.makeSimpleRequest(http.MethodPost, NextPath) } +func (c *Client) Show() error { + return c.makeSimpleRequest(http.MethodPost, ShowPath) +} + +func (c *Client) Quit() error { + return c.makeSimpleRequest(http.MethodPost, QuitPath) +} + func (c *Client) makeSimpleRequest(method string, path string) error { var resp *http.Response var err error switch method { case http.MethodGet: - resp, err = c.httpC.Get(path) + resp, err = c.httpC.Get("http://supersonic/" + path) case http.MethodPost: - resp, err = c.httpC.Post(path, "application/json", nil) + resp, err = c.httpC.Post("http://supersonic/"+path, "application/json", nil) } if err != nil { + log.Printf("http err: %v\n", err) return err } defer resp.Body.Close() From 8e815c644a6df1e903487659f7013cce2b9fd21c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 12 Jun 2024 09:07:15 -0700 Subject: [PATCH 5/9] reactivate works through ipc now, at least on Mac --- backend/app.go | 101 ++++++++++-------------------------- backend/ipc/client.go | 7 +-- backend/ipc/conn_other.go | 9 +++- backend/ipc/conn_windows.go | 5 ++ backend/ipc/server.go | 33 ++++++++---- go.mod | 2 +- 6 files changed, 67 insertions(+), 90 deletions(-) diff --git a/backend/app.go b/backend/app.go index cd915ab..eecc85f 100644 --- a/backend/app.go +++ b/backend/app.go @@ -17,7 +17,6 @@ import ( "github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/util" - "github.com/fsnotify/fsnotify" "github.com/google/uuid" "github.com/20after4/configdir" @@ -25,13 +24,10 @@ import ( ) const ( - configFile = "config.toml" - portableDir = "supersonic_portable" - sessionDir = "session" - sessionLockFile = ".lock" - sessionActivateFile = ".activate" - savedQueueFile = "saved_queue.json" - themesDir = "themes" + configFile = "config.toml" + portableDir = "supersonic_portable" + savedQueueFile = "saved_queue.json" + themesDir = "themes" ) var ( @@ -47,6 +43,7 @@ type App struct { LocalPlayer *mpv.Player UpdateChecker UpdateChecker MPRISHandler *MPRISHandler + ipcServer ipc.IPCServer // UI callbacks to be set in main OnReactivate func() @@ -91,24 +88,6 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) return nil, ErrAnotherInstance } - /* - sessionPath := path.Join(confDir, sessionDir) - if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil { - log.Println("Another instance is running. Reactivating it...") - reactivateFile := path.Join(sessionPath, sessionActivateFile) - if f, err := os.Create(reactivateFile); err == nil { - f.Close() - } - time.Sleep(750 * time.Millisecond) - if _, err := os.Stat(reactivateFile); err == nil { - log.Println("No other instance responded. Starting as normal...") - os.RemoveAll(sessionPath) - } else { - return nil, ErrAnotherInstance - } - } - */ - log.Printf("Starting %s...", appName) log.Printf("Using config dir: %s", confDir) log.Printf("Using cache dir: %s", cacheDir) @@ -124,19 +103,6 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) a.readConfig() a.startConfigWriter(a.bgrndCtx) - /* - if !a.Config.Application.AllowMultiInstance { - log.Println("Creating session lock file") - os.MkdirAll(sessionPath, 0770) - if f, err := os.Create(path.Join(sessionPath, sessionLockFile)); err == nil { - f.Close() - } else { - log.Printf("error creating session file: %s", err.Error()) - } - a.startSessionWatcher(sessionPath) - } - */ - a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion) a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour) @@ -155,9 +121,14 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) { _, _ = a.ImageManager.GetCoverThumbnail(coverID) }) - listener, _ := ipc.Listen() - server := ipc.NewServer(a.PlaybackManager, nil) - go server.Serve(listener) + + // Start IPC server + listener, err := ipc.Listen() + if err == nil { + a.ipcServer = ipc.NewServer(a.PlaybackManager, a.callOnReactivate, + func() { _ = a.callOnExit() }) + go a.ipcServer.Serve(listener) + } // OS media center integrations a.setupMPRIS(displayAppName) @@ -211,26 +182,6 @@ func (a *App) readConfig() { a.Config = cfg } -func (a *App) startSessionWatcher(sessionPath string) { - if sessionWatch, err := fsnotify.NewWatcher(); err == nil { - sessionWatch.Add(sessionPath) - go func() { - for { - select { - case <-a.bgrndCtx.Done(): - return - case <-sessionWatch.Events: - activatePath := path.Join(sessionPath, sessionActivateFile) - if _, err := os.Stat(activatePath); err == nil { - os.Remove(path.Join(sessionPath, sessionActivateFile)) - a.callOnReactivate() - } - } - } - }() - } -} - // periodically save config file so abnormal exit won't lose settings func (a *App) startConfigWriter(ctx context.Context) { tick := time.NewTicker(2 * time.Minute) @@ -254,6 +205,17 @@ func (a *App) callOnReactivate() { } } +func (a *App) callOnExit() error { + if a.OnExit == nil { + return errors.New("no quit handler registered") + } + go func() { + time.Sleep(10 * time.Millisecond) + a.OnExit() + }() + return nil +} + func (a *App) initMPV() error { p := mpv.NewWithClientName(a.appName) c := a.Config.LocalPlayback @@ -329,16 +291,7 @@ func (a *App) setupMPRIS(mprisAppName string) { return a.ImageManager.GetCoverArtUrl(id) } a.MPRISHandler.OnRaise = func() error { a.callOnReactivate(); return nil } - a.MPRISHandler.OnQuit = func() error { - if a.OnExit == nil { - return errors.New("no quit handler registered") - } - go func() { - time.Sleep(10 * time.Millisecond) - a.OnExit() - }() - return nil - } + a.MPRISHandler.OnQuit = a.callOnExit a.MPRISHandler.Start() } @@ -361,6 +314,9 @@ func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error { } func (a *App) Shutdown() { + if a.ipcServer != nil { + a.ipcServer.Shutdown(a.bgrndCtx) + } a.MPRISHandler.Shutdown() a.PlaybackManager.DisableCallbacks() if a.Config.Application.SavePlayQueue { @@ -377,7 +333,6 @@ func (a *App) Shutdown() { a.cancel() a.LocalPlayer.Destroy() a.Config.WriteConfigFile(a.configFilePath()) - os.RemoveAll(path.Join(a.configDir, sessionDir)) } func (a *App) LoadSavedPlayQueue() error { diff --git a/backend/ipc/client.go b/backend/ipc/client.go index d03eeb6..34276bb 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -17,15 +17,10 @@ type Client struct { // Connect attempts to connect to the IPC socket as client. func Connect() (*Client, error) { - conn, err := Dial() - if err != nil { - log.Println("dial error") - return nil, err - } client := &Client{httpC: http.Client{ Transport: &http.Transport{ DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { - return conn, nil + return Dial() }, }, }} diff --git a/backend/ipc/conn_other.go b/backend/ipc/conn_other.go index 62fdc9e..0dd4e90 100644 --- a/backend/ipc/conn_other.go +++ b/backend/ipc/conn_other.go @@ -2,7 +2,10 @@ package ipc -import "net" +import ( + "net" + "os" +) func Dial() (net.Conn, error) { // TODO - use XDG runtime dir, also handle portable mode @@ -12,3 +15,7 @@ func Dial() (net.Conn, error) { func Listen() (net.Listener, error) { return net.Listen("unix", "/tmp/supersonic.sock") } + +func DestroyConn() error { + return os.Remove("/tmp/supersonic.sock") +} diff --git a/backend/ipc/conn_windows.go b/backend/ipc/conn_windows.go index 59c0fad..8ddf9c1 100644 --- a/backend/ipc/conn_windows.go +++ b/backend/ipc/conn_windows.go @@ -17,3 +17,8 @@ func Dial() (net.Conn, error) { func Listen() (net.Listener, error) { return winio.ListenPipe("supersonic", nil) } + +func DestroyConn() error { + // Windows named pipes automatically clean up + return nil +} diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 3242e9d..960c20e 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -1,7 +1,9 @@ package ipc import ( + "context" "encoding/json" + "net" "net/http" ) @@ -17,21 +19,34 @@ type PlaybackHandler interface { SetVolume(int) error } -type WindowHandler interface { - Show() - Quit() +type IPCServer interface { + Serve(net.Listener) error + Shutdown(context.Context) error } type serverImpl struct { + server *http.Server pbHandler PlaybackHandler - wdHandler WindowHandler + showFn func() + quitFn func() } -func NewServer(pbHandler PlaybackHandler, wdHandler WindowHandler) *http.Server { - s := serverImpl{pbHandler: pbHandler, wdHandler: wdHandler} - return &http.Server{ +func NewServer(pbHandler PlaybackHandler, showFn, quitFn func()) IPCServer { + s := &serverImpl{pbHandler: pbHandler, showFn: showFn, quitFn: quitFn} + s.server = &http.Server{ Handler: s.createHandler(), } + return s +} + +func (s *serverImpl) Serve(listener net.Listener) error { + return s.server.Serve(listener) +} + +func (s *serverImpl) Shutdown(ctx context.Context) error { + err := s.server.Shutdown(ctx) + DestroyConn() + return err } func (s *serverImpl) createHandler() http.Handler { @@ -42,11 +57,11 @@ func (s *serverImpl) createHandler() http.Handler { }) m.HandleFunc(PingPath, s.makeSimpleEndpointHandler(func() error { return nil })) m.HandleFunc(ShowPath, s.makeSimpleEndpointHandler(func() error { - s.wdHandler.Show() + s.showFn() return nil })) m.HandleFunc(QuitPath, s.makeSimpleEndpointHandler(func() error { - go s.wdHandler.Quit() + s.quitFn() return nil })) m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.pbHandler.Continue)) diff --git a/go.mod b/go.mod index 40e90e4..dc95f66 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/dweymouth/go-jellyfin v0.0.0-20240517151952-5ceca61cb645 github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee github.com/dweymouth/go-subsonic v0.0.0-20240603150834-605046e7c78a - github.com/fsnotify/fsnotify v1.7.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 github.com/pelletier/go-toml/v2 v2.0.8 @@ -30,6 +29,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/disintegration/imaging v1.6.2 // indirect github.com/fredbi/uri v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect github.com/fyne-io/glfw-js v0.0.0-20240101223322-6e1efdc71b7a // indirect github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect From 280030c858e46613cc7c45758ed32765b96e3026 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 12 Jun 2024 17:21:15 -0700 Subject: [PATCH 6/9] add cmdline flags and some fixes to the IPC layer --- backend/app.go | 56 ++++++++++++++++++++++++++++++--------- backend/cmdlineoptions.go | 41 ++++++++++++++++++++++++++++ backend/ipc/api.go | 54 ++++++++++++++----------------------- backend/ipc/client.go | 36 ++++++++++++------------- backend/ipc/conn_other.go | 28 +++++++++++++++++--- backend/ipc/server.go | 22 +++++++-------- main.go | 30 +++++++++++++-------- ui/mainwindow.go | 13 +++++++-- 8 files changed, 187 insertions(+), 93 deletions(-) create mode 100644 backend/cmdlineoptions.go diff --git a/backend/app.go b/backend/app.go index eecc85f..93c4d9d 100644 --- a/backend/app.go +++ b/backend/app.go @@ -81,17 +81,6 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) configdir.MakePath(confDir) configdir.MakePath(cacheDir) - cli, err := ipc.Connect() - if err == nil { - log.Println("Another instance is running. Reactivating it...") - cli.Show() - return nil, ErrAnotherInstance - } - - log.Printf("Starting %s...", appName) - log.Printf("Using config dir: %s", confDir) - log.Printf("Using cache dir: %s", cacheDir) - a := &App{ appName: appName, appVersionTag: appVersionTag, @@ -101,7 +90,20 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) } a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) a.readConfig() - a.startConfigWriter(a.bgrndCtx) + + if HaveCommandLineOptions() || !a.Config.Application.AllowMultiInstance { + connected, err := a.checkCLIFlagsAndSendIPCMsg() + if err != nil { + log.Printf("error sending IPC message: %s", err.Error()) + } + if connected /* we reached the other instance at all */ { + return nil, ErrAnotherInstance + } + } + + log.Printf("Starting %s...", appName) + log.Printf("Using config dir: %s", confDir) + log.Printf("Using cache dir: %s", cacheDir) a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion) a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour) @@ -137,6 +139,8 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) return a.ImageManager.GetCoverArtUrl(id) }) + a.startConfigWriter(a.bgrndCtx) + return a, nil } @@ -367,6 +371,34 @@ func (a *App) SaveConfigFile() { a.lastWrittenCfg = *a.Config } +func (a *App) checkCLIFlagsAndSendIPCMsg() (connected bool, err error) { + cli, err := ipc.Connect() + if err != nil { + return false, err + } + + switch { + case *FlagPlay: + err = cli.Play() + case *FlagPause: + err = cli.Pause() + case *FlagPlayPause: + err = cli.PlayPause() + case *FlagPrevious: + err = cli.SeekBackOrPrevious() + case *FlagNext: + err = cli.SeekNext() + case VolumeCLIArg >= 0: + err = cli.SetVolume(VolumeCLIArg) + case SeekToCLIArg >= 0: + err = cli.SeekSeconds(SeekToCLIArg) + default: + log.Println("Another instance is running. Reactivating it...") + err = cli.Show() + } + return true, err +} + func (a *App) configFilePath() string { return path.Join(a.configDir, configFile) } diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go new file mode 100644 index 0000000..5e189ed --- /dev/null +++ b/backend/cmdlineoptions.go @@ -0,0 +1,41 @@ +package backend + +import ( + "flag" + "strconv" +) + +var ( + VolumeCLIArg int = -1 + SeekToCLIArg float64 = -1 + + FlagPlay = flag.Bool("play", false, "unpause or begin playback") + FlagPause = flag.Bool("pause", false, "pause playback") + FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state") + FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current") + FlagNext = flag.Bool("next", false, "seek to next track") + FlagVersion = flag.Bool("version", false, "print app version and exit") + FlagHelp = flag.Bool("help", false, "print command line options and exit") +) + +func init() { + flag.Func("volume", "sets the playback volume (0-100)", func(s string) error { + v, err := strconv.Atoi(s) + VolumeCLIArg = v + return err + }) + + flag.Func("seek-to", "seeks to the given position in seconds in the current file (0.0 - )", func(s string) error { + v, err := strconv.ParseFloat(s, 64) + SeekToCLIArg = v + return err + }) +} + +func HaveCommandLineOptions() bool { + visitedAny := false + flag.Visit(func(*flag.Flag) { + visitedAny = true + }) + return visitedAny +} diff --git a/backend/ipc/api.go b/backend/ipc/api.go index be82574..fbea445 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -1,43 +1,29 @@ package ipc +import "fmt" + const ( - // GET - PingPath = "/ping" - - // POST - PlayPath = "/transport/play" - // POST + PingPath = "/ping" + PlayPath = "/transport/play" PlayPausePath = "/transport/playpause" - // POST - PausePath = "/transport/pause" - // POST - StopPath = "/transport/stop" - // POST - PreviousPath = "/transport/previous" - // POST - NextPath = "/transport/next" - // POST(TimePos) - TimePosPath = "/transport/timepos" - // POST to seek - PlayTrackPath = "/queue/playtrack" - // GET -> Volume - // POST(Volume) - VolumePath = "/volume" - - // POST - ShowPath = "/window/show" - // POST - QuitPath = "/window/quit" + PausePath = "/transport/pause" + StopPath = "/transport/stop" + PreviousPath = "/transport/previous" + NextPath = "/transport/next" + TimePosPath = "/transport/timepos" // ?s= + VolumePath = "/volume" // ?v= + ShowPath = "/window/show" + QuitPath = "/window/quit" ) -type TimePos struct { - Seconds float64 `json:"seconds"` -} - -type Volume struct { - Volume int `json:"volume"` -} - type Response struct { Error string `json:"error"` } + +func SetVolumePath(vol int) string { + return fmt.Sprintf("%s?v=%d", VolumePath, vol) +} + +func SeekToSecondsPath(secs float64) string { + return fmt.Sprintf("%s?s=%0.2f", TimePosPath, secs) +} diff --git a/backend/ipc/client.go b/backend/ipc/client.go index 34276bb..3b31ff1 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -25,56 +25,56 @@ func Connect() (*Client, error) { }, }} if err := client.Ping(); err != nil { - log.Println("ping error") return nil, err } return client, nil } func (c *Client) Ping() error { - if c.makeSimpleRequest(http.MethodGet, PingPath) != nil { + if c.sendRequest(PingPath) != nil { return ErrPingFail } return nil } func (c *Client) Play() error { - return c.makeSimpleRequest(http.MethodPost, PlayPath) + return c.sendRequest(PlayPath) } func (c *Client) Pause() error { - return c.makeSimpleRequest(http.MethodPost, PausePath) + return c.sendRequest(PausePath) } func (c *Client) PlayPause() error { - return c.makeSimpleRequest(http.MethodPost, PlayPausePath) + return c.sendRequest(PlayPausePath) } func (c *Client) SeekNext() error { - return c.makeSimpleRequest(http.MethodPost, NextPath) + return c.sendRequest(NextPath) } func (c *Client) SeekBackOrPrevious() error { - return c.makeSimpleRequest(http.MethodPost, NextPath) + return c.sendRequest(PreviousPath) +} + +func (c *Client) SeekSeconds(secs float64) error { + return c.sendRequest(SeekToSecondsPath(secs)) +} + +func (c *Client) SetVolume(vol int) error { + return c.sendRequest(SetVolumePath(vol)) } func (c *Client) Show() error { - return c.makeSimpleRequest(http.MethodPost, ShowPath) + return c.sendRequest(ShowPath) } func (c *Client) Quit() error { - return c.makeSimpleRequest(http.MethodPost, QuitPath) + return c.sendRequest(QuitPath) } -func (c *Client) makeSimpleRequest(method string, path string) error { - var resp *http.Response - var err error - switch method { - case http.MethodGet: - resp, err = c.httpC.Get("http://supersonic/" + path) - case http.MethodPost: - resp, err = c.httpC.Post("http://supersonic/"+path, "application/json", nil) - } +func (c *Client) sendRequest(path string) error { + resp, err := c.httpC.Get("http://supersonic/" + path) if err != nil { log.Printf("http err: %v\n", err) diff --git a/backend/ipc/conn_other.go b/backend/ipc/conn_other.go index 0dd4e90..9fd6ca7 100644 --- a/backend/ipc/conn_other.go +++ b/backend/ipc/conn_other.go @@ -3,19 +3,41 @@ package ipc import ( + "fmt" "net" "os" + "os/user" + "path" + "runtime" ) +var socketPath = "/tmp/supersonic.sock" + +func init() { + if runtime.GOOS == "darwin" { + if home, err := os.UserHomeDir(); err == nil { + socketPath = path.Join(home, "Library", "Caches", "supersonic", "supersonic.sock") + } else if user, err := user.Current(); err == nil { + socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Name) + } + } else { + if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { + socketPath = path.Join(runtime, "supersonic.sock") + } else if user, err := user.Current(); err == nil { + socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Name) + } + } +} + func Dial() (net.Conn, error) { // TODO - use XDG runtime dir, also handle portable mode - return net.Dial("unix", "/tmp/supersonic.sock") + return net.Dial("unix", socketPath) } func Listen() (net.Listener, error) { - return net.Listen("unix", "/tmp/supersonic.sock") + return net.Listen("unix", socketPath) } func DestroyConn() error { - return os.Remove("/tmp/supersonic.sock") + return os.Remove(socketPath) } diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 960c20e..fc62ed3 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net" "net/http" + "strconv" ) type PlaybackHandler interface { @@ -71,25 +72,20 @@ func (s *serverImpl) createHandler() http.Handler { m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious)) m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext)) m.HandleFunc(TimePosPath, func(w http.ResponseWriter, r *http.Request) { - var t TimePos - if err := json.NewDecoder(r.Response.Body).Decode(&t); err != nil { + _s := r.URL.Query().Get("s") + if secs, err := strconv.ParseFloat(_s, 64); err == nil { + s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(secs)) + } else { s.writeErr(w, err) - return } - s.writeSimpleResponse(w, s.pbHandler.SeekSeconds(t.Seconds)) }) m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) { - if r.Method == http.MethodGet { - msg, _ := json.Marshal(Volume{Volume: s.pbHandler.Volume()}) - w.Write(msg) - return - } - var v Volume - if err := json.NewDecoder(r.Response.Body).Decode(&v); err != nil { + v := r.URL.Query().Get("v") + if vol, err := strconv.Atoi(v); err == nil { + s.writeSimpleResponse(w, s.pbHandler.SetVolume(vol)) + } else { s.writeErr(w, err) - return } - s.writeSimpleResponse(w, s.pbHandler.SetVolume(v.Volume)) }) return m } diff --git a/main.go b/main.go index 212d0ae..513bef5 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "flag" + "fmt" "log" "os" "runtime" @@ -10,14 +12,28 @@ import ( "github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/ui" - "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" ) func main() { + // parse cmd line flags - see backend/cmdlineoptions.go + flag.Parse() + if *backend.FlagVersion { + fmt.Println(res.AppVersion) + return + } + if *backend.FlagHelp { + flag.Usage() + return + } + // rest of flag actions are handled in backend.StartupApp + myApp, err := backend.StartupApp(res.AppName, res.DisplayName, res.AppVersionTag, res.LatestReleaseURL) if err != nil { - log.Fatalf("fatal startup error: %v", err.Error()) + if err != backend.ErrAnotherInstance { + log.Fatalf("fatal startup error: %v", err.Error()) + } + return } if myApp.Config.Application.UIScaleSize == "Smaller" { @@ -29,15 +45,7 @@ func main() { fyneApp := app.New() fyneApp.SetIcon(res.ResAppicon256Png) - w := float32(myApp.Config.Application.WindowWidth) - if w <= 1 { - w = 1000 - } - h := float32(myApp.Config.Application.WindowHeight) - if h <= 1 { - h = 800 - } - mainWindow := ui.NewMainWindow(fyneApp, res.AppName, res.DisplayName, res.AppVersion, myApp, fyne.NewSize(w, h)) + mainWindow := ui.NewMainWindow(fyneApp, res.AppName, res.DisplayName, res.AppVersion, myApp) myApp.OnReactivate = mainWindow.Show myApp.OnExit = mainWindow.Quit diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 1c24141..10b2111 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -60,7 +60,7 @@ type MainWindow struct { radioBtn *widget.Button } -func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App, size fyne.Size) MainWindow { +func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, app *backend.App) MainWindow { m := MainWindow{ App: app, Window: fyneApp.NewWindow(displayAppName), @@ -89,7 +89,16 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m.BottomPanel = NewBottomPanel(app.PlaybackManager, app.ImageManager, m.Controller) m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) m.Window.SetContent(m.container) - m.Window.Resize(size) + + w := float32(app.Config.Application.WindowWidth) + if w <= 1 { + w = 1000 + } + h := float32(app.Config.Application.WindowHeight) + if h <= 1 { + h = 800 + } + m.Window.Resize(fyne.NewSize(w, h)) app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { if item == nil { m.Window.SetTitle(displayAppName) From bfb869fadcca1f87b1aa0ce4a82c3238c64ee815 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 12 Jun 2024 18:08:22 -0700 Subject: [PATCH 7/9] a few fixes --- backend/app.go | 12 ++++++++++-- backend/ipc/client.go | 2 -- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/app.go b/backend/app.go index 93c4d9d..88e75f4 100644 --- a/backend/app.go +++ b/backend/app.go @@ -91,10 +91,13 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) a.readConfig() - if HaveCommandLineOptions() || !a.Config.Application.AllowMultiInstance { + if haveCLI := HaveCommandLineOptions(); haveCLI || !a.Config.Application.AllowMultiInstance { connected, err := a.checkCLIFlagsAndSendIPCMsg() if err != nil { - log.Printf("error sending IPC message: %s", err.Error()) + if haveCLI { + // we were supposed to control another instance and couldn't + log.Fatalf("error sending IPC message: %s", err.Error()) + } } if connected /* we reached the other instance at all */ { return nil, ErrAnotherInstance @@ -125,11 +128,16 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) }) // Start IPC server + if !a.lastWrittenCfg.Application.AllowMultiInstance { + ipc.DestroyConn() // cleanup socket possibly orphaned by crashed process + } listener, err := ipc.Listen() if err == nil { a.ipcServer = ipc.NewServer(a.PlaybackManager, a.callOnReactivate, func() { _ = a.callOnExit() }) go a.ipcServer.Serve(listener) + } else { + log.Printf("error starting IPC server: %s", err.Error()) } // OS media center integrations diff --git a/backend/ipc/client.go b/backend/ipc/client.go index 3b31ff1..b342382 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "errors" - "log" "net" "net/http" ) @@ -77,7 +76,6 @@ func (c *Client) sendRequest(path string) error { resp, err := c.httpC.Get("http://supersonic/" + path) if err != nil { - log.Printf("http err: %v\n", err) return err } defer resp.Body.Close() From 8570bcd25af490c67af33e089306f3e8f2d87bf0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 12 Jun 2024 18:39:07 -0700 Subject: [PATCH 8/9] if AllowMultiInstance, don't have second instance try to start IPC server --- backend/app.go | 66 ++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/backend/app.go b/backend/app.go index 88e75f4..4c5e9cc 100644 --- a/backend/app.go +++ b/backend/app.go @@ -91,17 +91,17 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) a.readConfig() - if haveCLI := HaveCommandLineOptions(); haveCLI || !a.Config.Application.AllowMultiInstance { - connected, err := a.checkCLIFlagsAndSendIPCMsg() - if err != nil { - if haveCLI { - // we were supposed to control another instance and couldn't - log.Fatalf("error sending IPC message: %s", err.Error()) - } - } - if connected /* we reached the other instance at all */ { - return nil, ErrAnotherInstance + cli, _ := ipc.Connect() + if HaveCommandLineOptions() { + if err := a.checkFlagsAndSendIPCMsg(cli); err != nil { + // we were supposed to control another instance and couldn't + log.Fatalf("error sending IPC message: %s", err.Error()) } + return nil, ErrAnotherInstance + } else if cli != nil && !a.Config.Application.AllowMultiInstance { + log.Println("Another instance is running. Reactivating it...") + cli.Show() + return nil, ErrAnotherInstance } log.Printf("Starting %s...", appName) @@ -127,17 +127,17 @@ func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) _, _ = a.ImageManager.GetCoverThumbnail(coverID) }) - // Start IPC server - if !a.lastWrittenCfg.Application.AllowMultiInstance { + // Start IPC server if another not already running in a different instance + if cli == nil { ipc.DestroyConn() // cleanup socket possibly orphaned by crashed process - } - listener, err := ipc.Listen() - if err == nil { - a.ipcServer = ipc.NewServer(a.PlaybackManager, a.callOnReactivate, - func() { _ = a.callOnExit() }) - go a.ipcServer.Serve(listener) - } else { - log.Printf("error starting IPC server: %s", err.Error()) + listener, err := ipc.Listen() + if err == nil { + a.ipcServer = ipc.NewServer(a.PlaybackManager, a.callOnReactivate, + func() { _ = a.callOnExit() }) + go a.ipcServer.Serve(listener) + } else { + log.Printf("error starting IPC server: %s", err.Error()) + } } // OS media center integrations @@ -379,32 +379,28 @@ func (a *App) SaveConfigFile() { a.lastWrittenCfg = *a.Config } -func (a *App) checkCLIFlagsAndSendIPCMsg() (connected bool, err error) { - cli, err := ipc.Connect() - if err != nil { - return false, err +func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error { + if cli == nil { + return errors.New("no IPC connection") } - switch { case *FlagPlay: - err = cli.Play() + return cli.Play() case *FlagPause: - err = cli.Pause() + return cli.Pause() case *FlagPlayPause: - err = cli.PlayPause() + return cli.PlayPause() case *FlagPrevious: - err = cli.SeekBackOrPrevious() + return cli.SeekBackOrPrevious() case *FlagNext: - err = cli.SeekNext() + return cli.SeekNext() case VolumeCLIArg >= 0: - err = cli.SetVolume(VolumeCLIArg) + return cli.SetVolume(VolumeCLIArg) case SeekToCLIArg >= 0: - err = cli.SeekSeconds(SeekToCLIArg) + return cli.SeekSeconds(SeekToCLIArg) default: - log.Println("Another instance is running. Reactivating it...") - err = cli.Show() + return nil } - return true, err } func (a *App) configFilePath() string { From c0e120a3979610ca38c6ee502ee212520c5351a0 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 13 Jun 2024 07:55:57 -0700 Subject: [PATCH 9/9] use valid pipe name on Windows --- backend/ipc/conn_other.go | 4 ++-- backend/ipc/conn_windows.go | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/backend/ipc/conn_other.go b/backend/ipc/conn_other.go index 9fd6ca7..5ac6a45 100644 --- a/backend/ipc/conn_other.go +++ b/backend/ipc/conn_other.go @@ -18,13 +18,13 @@ func init() { if home, err := os.UserHomeDir(); err == nil { socketPath = path.Join(home, "Library", "Caches", "supersonic", "supersonic.sock") } else if user, err := user.Current(); err == nil { - socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Name) + socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Uid) } } else { if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" { socketPath = path.Join(runtime, "supersonic.sock") } else if user, err := user.Current(); err == nil { - socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Name) + socketPath = fmt.Sprintf("/tmp/supersonic-%s.sock", user.Uid) } } } diff --git a/backend/ipc/conn_windows.go b/backend/ipc/conn_windows.go index 8ddf9c1..26de3d7 100644 --- a/backend/ipc/conn_windows.go +++ b/backend/ipc/conn_windows.go @@ -4,18 +4,26 @@ package ipc import ( "net" - "time" + "os/user" + "regexp" "github.com/Microsoft/go-winio" ) +var pipeName = `\\.\pipe\supersonic` + +func init() { + if user, err := user.Current(); err == nil { + pipeName += regexp.MustCompile(`[^a-zA-Z0-9]+`).ReplaceAllString(user.Name, "") + } +} + func Dial() (net.Conn, error) { - timeout := 300 * time.Millisecond - return winio.DialPipe("supersonic", &timeout) + return winio.DialPipe(pipeName, nil) } func Listen() (net.Listener, error) { - return winio.ListenPipe("supersonic", nil) + return winio.ListenPipe(pipeName, nil) } func DestroyConn() error {