From 768160b05e9929bf5d82359b7768e67ffffeb9b6 Mon Sep 17 00:00:00 2001 From: Rob Emery Date: Tue, 1 Oct 2024 23:40:53 +0000 Subject: [PATCH] feat: Windows MSI installer and service support (#3125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * First version/rough layout of the required wix to build an MSI that embeds everything * Don't need revision number * produced exe from existing build process is navidrome not Navidrome * Adding Kardianos wrapper around Cobra so the callbacks are handled automatically (this is basically only for windows) * Adding pointless check to shut up lint for now * make format * Revert disabling npm tidy * Using Kardianos always will result in the application hanging so it needs only be wrapped to handle the callbacks if it's being used in the service context, otherwise use cobra directly * Copying in service installation etc from https://github.com/navidrome/navidrome/pull/2295 * Under Linux this installs a user service (I don't think this is correct, but lets get this working first). User units/services cannot depends on system units, so previously this bombed out with Exit Code 5. * Under Windows we can install both the x86 and x64 builds, they will install to different folders, but previously they would overwrite the service as they were both called Navidrome. Now, it will install 2 services. This will still be weird/broken as they will attempt to listen on the same port, however uninstalling the "wrong" arch will not cause the "right" one to be partially uninstalled anymore * Reverting changes to the context as they don't really seem necessary anyway * Need to consistently name the service * Fixing broken context * The included files should be removed when the app is uninstalled * Reverting back to the original context here, I don't think it makes any difference to running under kardianos * Let's see what we have immediately available * OK, the build takes ages so let's just try and do the whole thing in one go, maybe we'll get lucky * Need -r on directory copy, plus we'll probably need to install wixl * No sudo cmd, so I assume this runs as root * WORKSPACE! * Moving the version to be a single variable, we'll probably be able to pull it from the github tag or whatever * Might as well put the msi in the right folder, it's tidier * Writing the version number into the msi, from the output of goreleaser * Using jq to parse the goreleaser metadata, so need to install it * MSI only supports numerical version numbers, so I'll make the "snapshot" version .1 minor patch greater * -r or --raw (on newer versions) means we don't get the "" around the value * Running as a user service I think makes limited sense for this * Will now ask for configuration settings during install. MSI/WiX only supports writing out INI files, Toml is almost INI compatable, except that the INI needs to write out a section first, so we need to have a script to strip that off. We are forced to display a License.rtf file by the UI so I think the build process should probably rename the default licence file and that will suffice. Uninstalling works cleanly, howvever upgrades seem to leave the old version installed in "programs and features" currently. Adding the UI has introduced a requirement for WiX 0.103 * Updating the build to include --ext ui for the new config ui * Configuration dialog should not display for upgrades as the config file is already written * Making description consistent with the systemd service and making the build process produce the required License.rtf * Fixing " non-constant format string in call to fmt.Errorf (govet)" * Its a string, not an int; read better. * Wixl 103 is required for --ext ui, so we need 24.04 * OK this is still installing Wix 0.101, maybe it all needs to be 24.04? * Switching the builds back to ubuntu-latest (22.04 at current) as it runs on a custom container, it's actually debian anyway Moving msi build into its own job so it can run on 24.04 so we have access to wixl 0.103 for --ext ui support * Forcing build * Whitespace fix * Adding sudo I guess * Gotta checkout as well * Adding debugging for when there's soemthing wrong with the paths * Adding more ls to see if the output has worked * The msi's are in subdirs * Actually they're in the ./wix directory * Still can't find these msi's? * I think that was being treated literally previously * No idea why this isn't working, give it a relative path instead? * Making explicit on the dialogue that the configuration file will be where the installation dir is * The lint keeps failing and it's just getting in the way so I'll turn this off for now and we'll edit out this commit from the merge * Cutting more out of the build to get more stuff out of the way * Need to increase the width to fit the text in * Calling everything License.rtf, presumably one of them is correct * I am pretty sure the License.rtf loading is broken under Wixl; so let's just bypass the EULA from the UI which is a nicer experience for the users anyway * This needs to be after WelcomeDlg now the Eula isn't displayed * You're supposed to be able to use to override the location that the bmp's are loaded from, I can't get this to work under wixl so I'm guessing given that the ui extension is new, it hasn't been implemented with that in mind. So we'll hack it by overwriting the files installed with the package. * We should make this less brittle so when wixl is updated it still works * Re-enabling the lint and tests etc * Improving the scaling quality and removing borders from images to tidy them up a tad * Pretty sure this isn't necessary as MY_PROPERTY will always be false * Without publishing this event, we can't continue to the next dialogue however I think we should be able to get away without the property * Refactoring out the duplication so we only have one service defined and we can run that either way * Pushing the Interactive check into the root commmand? Feels like it is probably getting closer to the right place at least * go tidy * OK this didn't work under windows, I'm guessing it's because it's lacking all the metadata about the service it needs to report back to Windows on. * We need to run service execute now so that the windows service will behave (hopefully)! * Lint * go tidy * Renaming service to "navidrome" rather than "Navidrome" as this is the filename that systemd writes and it's unusual to have capital letters in service names under Linux. Switching to use service execute for Linux to mirror Windows * Need to provide the arguments to append * Without passing the context around, the DB isn't closed gracefully so we end up with with .db-shm and .db-wal files for recovery * We should log fatal rather than outputting directly to stdout * go tidy * refactor: small nitpicks * fix: terminate service gracefully --------- Co-authored-by: Deluan Quintão --- .github/workflows/pipeline.yml | 46 +++++++- cmd/root.go | 14 +-- cmd/svc.go | 209 +++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 3 + wix/Navidrome_UI_Flow.wxs | 41 +++++++ wix/SettingsDlg.wxs | 44 +++++++ wix/bmp/banner.bmp | Bin 0 -> 85894 bytes wix/bmp/dialogue.bmp | Bin 0 -> 461814 bytes wix/convertIniToToml.vbs | 17 +++ wix/navidrome.wxs | 93 +++++++++++++++ 11 files changed, 459 insertions(+), 9 deletions(-) create mode 100644 cmd/svc.go create mode 100644 wix/Navidrome_UI_Flow.wxs create mode 100644 wix/SettingsDlg.wxs create mode 100644 wix/bmp/banner.bmp create mode 100644 wix/bmp/dialogue.bmp create mode 100644 wix/convertIniToToml.vbs create mode 100644 wix/navidrome.wxs diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index faa06a7f7..9b1f943ea 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -39,7 +39,6 @@ jobs: echo 'To fix this check, run "make format" and commit the changes' exit 1 fi - go: name: Test Go code runs-on: ubuntu-latest @@ -156,6 +155,51 @@ jobs: !dist/*.zip retention-days: 7 + msi: + name: Build and publish MSI files + needs: [binaries] + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: binaries + path: dist + + - name: Build MSI + run: | + sudo apt-get install -y wixl jq + + NAVIDROME_BUILD_VERSION=$(jq -r '.version' < $GITHUB_WORKSPACE/dist/metadata.json | sed -e 's/^v//' -e 's/-SNAPSHOT/.1/' ) + + mkdir -p $GITHUB_WORKSPACE/wix/386 + cp $GITHUB_WORKSPACE/LICENSE $GITHUB_WORKSPACE/wix/386 + cp $GITHUB_WORKSPACE/README.md $GITHUB_WORKSPACE/wix/386 + + cp -r $GITHUB_WORKSPACE/wix/386 $GITHUB_WORKSPACE/wix/amd64 + + cp $GITHUB_WORKSPACE/dist/navidrome_windows_386_windows_386/navidrome.exe $GITHUB_WORKSPACE/wix/386 + cp $GITHUB_WORKSPACE/dist/navidrome_windows_amd64_windows_amd64_v1/navidrome.exe $GITHUB_WORKSPACE/wix/amd64 + + # workaround for wixl WixVariable not working to override bmp locations + sudo cp $GITHUB_WORKSPACE/wix/bmp/banner.bmp /usr/share/wixl-*/ext/ui/bitmaps/bannrbmp.bmp + sudo cp $GITHUB_WORKSPACE/wix/bmp/dialogue.bmp /usr/share/wixl-*/ext/ui/bitmaps/dlgbmp.bmp + + cd $GITHUB_WORKSPACE/wix/386 + wixl ../navidrome.wxs -D Version=$NAVIDROME_BUILD_VERSION -D Platform=x86 --arch x86 --ext ui --output ../navidrome_386.msi + + cd $GITHUB_WORKSPACE/wix/amd64 + wixl ../navidrome.wxs -D Version=$NAVIDROME_BUILD_VERSION -D Platform=x64 --arch x64 --ext ui --output ../navidrome_amd64.msi + + ls -la $GITHUB_WORKSPACE/wix/*.msi + + - uses: actions/upload-artifact@v4 + with: + name: installers + path: wix/*.msi + retention-days: 7 + docker: name: Build and publish Docker images needs: [binaries] diff --git a/cmd/root.go b/cmd/root.go index b6aa0d708..f623b408f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "fmt" "os" "os/signal" "strings" @@ -37,7 +36,7 @@ Complete documentation is available at https://www.navidrome.org/docs`, preRun() }, Run: func(cmd *cobra.Command, args []string) { - runNavidrome() + runNavidrome(cmd.Context()) }, PostRun: func(cmd *cobra.Command, args []string) { postRun() @@ -50,8 +49,7 @@ Complete documentation is available at https://www.navidrome.org/docs`, func Execute() { rootCmd.SetVersionTemplate(`{{println .Version}}`) if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - os.Exit(1) + log.Fatal(err) } } @@ -69,10 +67,10 @@ func postRun() { // runNavidrome is the main entry point for the Navidrome server. It starts all the services and blocks. // If any of the services returns an error, it will log it and exit. If the process receives a signal to exit, // it will cancel the context and exit gracefully. -func runNavidrome() { +func runNavidrome(ctx context.Context) { defer db.Init()() - ctx, cancel := mainContext() + ctx, cancel := mainContext(ctx) defer cancel() g, ctx := errgroup.WithContext(ctx) @@ -88,8 +86,8 @@ func runNavidrome() { } // mainContext returns a context that is cancelled when the process receives a signal to exit. -func mainContext() (context.Context, context.CancelFunc) { - return signal.NotifyContext(context.Background(), +func mainContext(ctx context.Context) (context.Context, context.CancelFunc) { + return signal.NotifyContext(ctx, os.Interrupt, syscall.SIGHUP, syscall.SIGTERM, diff --git a/cmd/svc.go b/cmd/svc.go new file mode 100644 index 000000000..21c9b64cc --- /dev/null +++ b/cmd/svc.go @@ -0,0 +1,209 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + "time" + + "github.com/kardianos/service" + "github.com/navidrome/navidrome/conf" + "github.com/navidrome/navidrome/log" + "github.com/spf13/cobra" +) + +var ( + svcStatusLabels = map[service.Status]string{ + service.StatusUnknown: "Unknown", + service.StatusStopped: "Stopped", + service.StatusRunning: "Running", + } +) + +func init() { + svcCmd.AddCommand(buildInstallCmd()) + svcCmd.AddCommand(buildUninstallCmd()) + svcCmd.AddCommand(buildStartCmd()) + svcCmd.AddCommand(buildStopCmd()) + svcCmd.AddCommand(buildStatusCmd()) + svcCmd.AddCommand(buildExecuteCmd()) + rootCmd.AddCommand(svcCmd) +} + +var svcCmd = &cobra.Command{ + Use: "service", + Aliases: []string{"svc"}, + Short: "Manage Navidrome as a service", + Long: fmt.Sprintf("Manage Navidrome as a service, using the OS service manager (%s)", service.Platform()), + Run: runServiceCmd, +} + +type svcControl struct { + ctx context.Context + cancel context.CancelFunc + done chan struct{} +} + +func (p *svcControl) Start(service.Service) error { + p.done = make(chan struct{}) + p.ctx, p.cancel = context.WithCancel(context.Background()) + go func() { + runNavidrome(p.ctx) + close(p.done) + }() + return nil +} + +func (p *svcControl) Stop(service.Service) error { + log.Info("Stopping service") + p.cancel() + select { + case <-p.done: + log.Info("Service stopped gracefully") + case <-time.After(10 * time.Second): + log.Error("Service did not stop in time. Killing it.") + } + return nil +} + +var svcInstance = sync.OnceValue(func() service.Service { + options := make(service.KeyValue) + options["Restart"] = "on-success" + options["SuccessExitStatus"] = "1 2 8 SIGKILL" + options["UserService"] = false + options["LogDirectory"] = conf.Server.DataFolder + svcConfig := &service.Config{ + Name: "navidrome", + DisplayName: "Navidrome", + Description: "Your Personal Streaming Service", + Dependencies: []string{ + "Requires=", + "After="}, + WorkingDirectory: executablePath(), + Option: options, + } + arguments := []string{"service", "execute"} + if conf.Server.ConfigFile != "" { + arguments = append(arguments, "-c", conf.Server.ConfigFile) + } + svcConfig.Arguments = arguments + + prg := &svcControl{} + svc, err := service.New(prg, svcConfig) + if err != nil { + log.Fatal(err) + } + return svc +}) + +func runServiceCmd(cmd *cobra.Command, _ []string) { + _ = cmd.Help() +} + +func executablePath() string { + ex, err := os.Executable() + if err != nil { + log.Fatal(err) + } + return filepath.Dir(ex) +} + +func buildInstallCmd() *cobra.Command { + runInstallCmd := func(_ *cobra.Command, _ []string) { + var err error + println("Installing service with:") + println(" working directory: " + executablePath()) + println(" music folder: " + conf.Server.MusicFolder) + println(" data folder: " + conf.Server.DataFolder) + println(" logs folder: " + conf.Server.DataFolder) + if cfgFile != "" { + conf.Server.ConfigFile, err = filepath.Abs(cfgFile) + if err != nil { + log.Fatal(err) + } + println(" config file: " + conf.Server.ConfigFile) + } + err = svcInstance().Install() + if err != nil { + log.Fatal(err) + } + println("Service installed. Use 'navidrome svc start' to start it.") + } + + return &cobra.Command{ + Use: "install", + Short: "Install Navidrome service.", + Run: runInstallCmd, + } +} + +func buildUninstallCmd() *cobra.Command { + return &cobra.Command{ + Use: "uninstall", + Short: "Uninstall Navidrome service. Does not delete the music or data folders", + Run: func(cmd *cobra.Command, args []string) { + err := svcInstance().Uninstall() + if err != nil { + log.Fatal(err) + } + println("Service uninstalled. Music and data folders are still intact.") + }, + } +} + +func buildStartCmd() *cobra.Command { + return &cobra.Command{ + Use: "start", + Short: "Start Navidrome service", + Run: func(cmd *cobra.Command, args []string) { + err := svcInstance().Start() + if err != nil { + log.Fatal(err) + } + println("Service started. Use 'navidrome svc status' to check its status.") + }, + } +} + +func buildStopCmd() *cobra.Command { + return &cobra.Command{ + Use: "stop", + Short: "Stop Navidrome service", + Run: func(cmd *cobra.Command, args []string) { + err := svcInstance().Stop() + if err != nil { + log.Fatal(err) + } + println("Service stopped. Use 'navidrome svc status' to check its status.") + }, + } +} + +func buildStatusCmd() *cobra.Command { + return &cobra.Command{ + Use: "status", + Short: "Show Navidrome service status", + Run: func(cmd *cobra.Command, args []string) { + status, err := svcInstance().Status() + if err != nil { + log.Fatal(err) + } + fmt.Printf("Navidrome is %s.\n", svcStatusLabels[status]) + }, + } +} + +func buildExecuteCmd() *cobra.Command { + return &cobra.Command{ + Use: "execute", + Short: "Run navidrome as a service in the foreground (it is very unlikely you want to run this, you are better off running just navidrome)", + Run: func(cmd *cobra.Command, args []string) { + err := svcInstance().Run() + if err != nil { + log.Fatal(err) + } + }, + } +} diff --git a/go.mod b/go.mod index 44a90c477..396e96e88 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/google/wire v0.6.0 github.com/hashicorp/go-multierror v1.1.1 github.com/jellydator/ttlcache/v3 v3.3.0 + github.com/kardianos/service v1.2.2 github.com/kr/pretty v0.3.1 github.com/lestrrat-go/jwx/v2 v2.1.1 github.com/matoous/go-nanoid/v2 v2.1.0 diff --git a/go.sum b/go.sum index 1413e2529..c1a05298c 100644 --- a/go.sum +++ b/go.sum @@ -96,6 +96,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= +github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -255,6 +257,7 @@ golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/wix/Navidrome_UI_Flow.wxs b/wix/Navidrome_UI_Flow.wxs new file mode 100644 index 000000000..2ea38e172 --- /dev/null +++ b/wix/Navidrome_UI_Flow.wxs @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wix/SettingsDlg.wxs b/wix/SettingsDlg.wxs new file mode 100644 index 000000000..4a83f91da --- /dev/null +++ b/wix/SettingsDlg.wxs @@ -0,0 +1,44 @@ + + + + + + + + {\WixUI_Font_Title}Configuration + + + Please enter configuration settings + + + + + + + + + + + + + + + + + + + + + + + + + CostingComplete = 1 + + + + 1 + + + + diff --git a/wix/bmp/banner.bmp b/wix/bmp/banner.bmp new file mode 100644 index 0000000000000000000000000000000000000000..bbaa0fc2b02014abff05c00e8b225ae4dc13d6fc GIT binary patch literal 85894 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 Zh=2%;PXf~M@%h|z*CHSSB9J9u{tNYKC+q+K literal 0 HcmV?d00001 diff --git a/wix/bmp/dialogue.bmp b/wix/bmp/dialogue.bmp new file mode 100644 index 0000000000000000000000000000000000000000..56f3db55b78db4289ec1f244b9f9063b87993bb6 GIT binary patch literal 461814 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 literal 0 HcmV?d00001 diff --git a/wix/convertIniToToml.vbs b/wix/convertIniToToml.vbs new file mode 100644 index 000000000..1feb7d6d5 --- /dev/null +++ b/wix/convertIniToToml.vbs @@ -0,0 +1,17 @@ +Const ForReading = 1 +Const ForWriting = 2 + +sSourceFilename = Wscript.Arguments(0) +sTargetFilename = Wscript.Arguments(1) + +Set oFSO = CreateObject("Scripting.FileSystemObject") +Set oFile = oFSO.OpenTextFile(sSourceFilename, ForReading) +sFileContent = oFile.ReadAll +oFile.Close + +sNewFileContent = Replace(sFileContent, "[MSI_PLACEHOLDER_SECTION]" & vbCrLf, "") +If Not ( oFSO.FileExists(sTargetFilename) ) Then + Set oFile = oFSO.CreateTextFile(sTargetFilename) + oFile.Write sNewFileContent + oFile.Close +End If diff --git a/wix/navidrome.wxs b/wix/navidrome.wxs new file mode 100644 index 000000000..ad923a0a4 --- /dev/null +++ b/wix/navidrome.wxs @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Not Installed AND NOT WIX_UPGRADE_DETECTED + + + + NOT Installed AND NOT REMOVE + + + + + + + + + + +