Server: Move process handling and shutdown to separate package #4767

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-02-04 19:05:26 +01:00
parent 8e16a7a67e
commit ae5f35259c
10 changed files with 123 additions and 42 deletions

View File

@@ -2,16 +2,15 @@ package api
import (
"net/http"
"os"
"syscall"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/internal/server/process"
)
// StopServer shuts down the server.
// StopServer initiates a server restart if the user is authorized.
//
// POST /api/v1/server/stop
func StopServer(router *gin.RouterGroup) {
@@ -25,16 +24,11 @@ func StopServer(router *gin.RouterGroup) {
return
}
process, err := os.FindProcess(os.Getpid())
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, NewResponse(http.StatusInternalServerError, err, ""))
return
} else {
// Trigger restart.
//
// Note that this requires an entrypoint script or other process to
// spawns a new instance when the server exists with status code 1.
c.JSON(http.StatusOK, conf.Options())
}
if err = process.Signal(syscall.SIGTERM); err != nil {
log.Errorf("server: %s", err)
}
process.Restart()
})
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism/backup"
"github.com/photoprism/photoprism/internal/server"
"github.com/photoprism/photoprism/internal/server/process"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/internal/workers/auto"
"github.com/photoprism/photoprism/pkg/clean"
@@ -144,9 +145,9 @@ func startAction(ctx *cli.Context) error {
// Start auto-indexing background worker.
auto.Start(conf)
// Wait for signal to initiate server shutdown.
signal.Notify(server.Signal, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
sig := <-server.Signal
// Wait for signal to trigger server shutdown or restart.
signal.Notify(process.Signal, os.Interrupt, syscall.SIGTERM, syscall.SIGUSR1)
sig := <-process.Signal
// Stop all background activity.
auto.Shutdown()
@@ -165,7 +166,10 @@ func startAction(ctx *cli.Context) error {
time.Sleep(2 * time.Second)
conf.Shutdown()
// Don't exit with 0 if SIGUSR1 was received to avoid restarts.
// Exit with status code 1 if the shutdown was initiated with SIGUSR1 to request a restart.
//
// Note that this requires an entrypoint script or other process to
// spawns a new instance when the server exists with status code 1.
if sig == syscall.SIGUSR1 {
os.Exit(1)
return nil

14
internal/server/fail.go Normal file
View File

@@ -0,0 +1,14 @@
package server
import (
"github.com/photoprism/photoprism/internal/server/process"
)
// Fail logs an error and then initiates a server shutdown.
func Fail(err string, params ...interface{}) {
if err != "" {
log.Errorf(err, params...)
}
process.Shutdown()
}

View File

@@ -0,0 +1,27 @@
package process
import (
"fmt"
"os"
"path/filepath"
"github.com/photoprism/photoprism/pkg/fs"
)
// ID is the process ID under which the server is running.
var ID = os.Getpid()
// WritePID writes the process ID to a file, if specified.
func WritePID(fileName string) error {
if fileName == "" {
return nil
}
if pidDir := filepath.Dir(fileName); !fs.Writable(pidDir) {
return fmt.Errorf("%s is not writable", pidDir)
} else if err := fs.WriteString(fileName, fmt.Sprintf("%d", ID)); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,14 @@
package process
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestID(t *testing.T) {
t.Run("Matches", func(t *testing.T) {
assert.Equal(t, os.Getpid(), ID)
})
}

View File

@@ -0,0 +1,25 @@
/*
Package process provides server process information and handling.
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
This program is free software: you can redistribute it and/or modify
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
<https://docs.photoprism.app/license/agpl>
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
The AGPL is supplemented by our Trademark and Brand Guidelines,
which describe how our Brand Assets may be used:
<https://www.photoprism.app/trademark>
Feel free to send an email to hello@photoprism.app if you have questions,
want to support our work, or just want to say hello.
Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package process

View File

@@ -0,0 +1,22 @@
package process
import (
"os"
"syscall"
)
// Signal channel for initiating the server shutdown.
var Signal = make(chan os.Signal)
// Restart gracefully restarts the server.
//
// Note that this requires an entrypoint script or other process to
// spawns a new instance when the server exists with status code 1.
func Restart() {
Signal <- syscall.SIGUSR1
}
// Shutdown gracefully stops the server.
func Shutdown() {
Signal <- syscall.SIGTERM
}

View File

@@ -1,23 +0,0 @@
package server
import (
"os"
"syscall"
)
// Signal channel for initiating the shutdown.
var Signal = make(chan os.Signal)
// Fail reports an error and shuts down the server.
func Fail(err string, params ...interface{}) {
if err != "" {
log.Errorf(err, params...)
}
Shutdown()
}
// Shutdown gracefully stops the server.
func Shutdown() {
Signal <- syscall.SIGINT
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/server/process"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/net/header"
@@ -33,6 +34,9 @@ func Start(ctx context.Context, conf *config.Config) {
start := time.Now()
// Log the server process ID for troubleshooting purposes.
log.Infof("server: started as pid %d", process.ID)
// Set web server mode.
if conf.HttpMode() != "" {
gin.SetMode(conf.HttpMode())

2
scripts/dist/cmd.sh vendored
View File

@@ -88,7 +88,7 @@ echo "backup path...: ${PHOTOPRISM_BACKUP_PATH:-default}"
echo "import path...: ${PHOTOPRISM_IMPORT_PATH:-default}"
echo "originals path: ${PHOTOPRISM_ORIGINALS_PATH:-default}"
# error code of the last executed command
# exit status code of the last command executed
ret=0
# change to another user and group on request