From cb6dab3c5f83e4cd9d947bc90b4aec0839058635 Mon Sep 17 00:00:00 2001 From: EnameEtavir <84581448+EnameEtavir@users.noreply.github.com> Date: Tue, 24 Aug 2021 07:18:30 +0200 Subject: [PATCH] Fix: config race conditions with RWMutex (#1645) * Fix: config race conditions with RWMutex Added RWMutex to config.Instance which read or write locks all instances where viper is used. Refactored checksum manager to only use config and not viper directly anymore. All stash viper operations are now "behind" the config.Instance and thus mutex "protected". --- pkg/manager/checksum.go | 7 +- pkg/manager/config/config.go | 177 +++++++++++++++++- pkg/manager/config/config_concurrency_test.go | 100 ++++++++++ .../src/components/Changelog/versions/v090.md | 1 + 4 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 pkg/manager/config/config_concurrency_test.go diff --git a/pkg/manager/checksum.go b/pkg/manager/checksum.go index a545008b6..bc41ddfe1 100644 --- a/pkg/manager/checksum.go +++ b/pkg/manager/checksum.go @@ -4,7 +4,6 @@ import ( "context" "errors" - "github.com/spf13/viper" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" @@ -26,16 +25,12 @@ func setInitialMD5Config(txnManager models.TransactionManager) { usingMD5 := count != 0 defaultAlgorithm := models.HashAlgorithmOshash - if usingMD5 { defaultAlgorithm = models.HashAlgorithmMd5 } - // TODO - this should use the config instance - viper.SetDefault(config.VideoFileNamingAlgorithm, defaultAlgorithm) - viper.SetDefault(config.CalculateMD5, usingMD5) - config := config.GetInstance() + config.SetChecksumDefaultValues(defaultAlgorithm, usingMD5) if err := config.Write(); err != nil { logger.Errorf("Error while writing configuration file: %s", err.Error()) } diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 4a62c7cbc..eb1db30dc 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -9,6 +9,9 @@ import ( "runtime" "strings" + "sync" + //"github.com/sasha-s/go-deadlock" // if you have deadlock issues + "golang.org/x/crypto/bcrypt" "github.com/spf13/viper" @@ -165,6 +168,8 @@ func HasTLSConfig() bool { type Instance struct { cpuProfilePath string isNewSystem bool + sync.RWMutex + //deadlock.RWMutex // for deadlock testing/issues } var instance *Instance @@ -181,6 +186,8 @@ func (i *Instance) IsNewSystem() bool { } func (i *Instance) SetConfigFile(fn string) { + i.Lock() + defer i.Unlock() viper.SetConfigFile(fn) } @@ -192,6 +199,8 @@ func (i *Instance) GetCPUProfilePath() string { } func (i *Instance) Set(key string, value interface{}) { + i.Lock() + defer i.Unlock() viper.Set(key, value) } @@ -205,11 +214,15 @@ func (i *Instance) SetPassword(value string) { } func (i *Instance) Write() error { + i.Lock() + defer i.Unlock() return viper.WriteConfig() } // GetConfigFile returns the full path to the used configuration file. func (i *Instance) GetConfigFile() string { + i.RLock() + defer i.RUnlock() return viper.ConfigFileUsed() } @@ -226,6 +239,8 @@ func (i *Instance) GetDefaultDatabaseFilePath() string { } func (i *Instance) GetStashPaths() []*models.StashConfig { + i.RLock() + defer i.RUnlock() var ret []*models.StashConfig if err := viper.UnmarshalKey(Stash, &ret); err != nil || len(ret) == 0 { // fallback to legacy format @@ -243,30 +258,44 @@ func (i *Instance) GetStashPaths() []*models.StashConfig { } func (i *Instance) GetConfigFilePath() string { + i.RLock() + defer i.RUnlock() return viper.ConfigFileUsed() } func (i *Instance) GetCachePath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Cache) } func (i *Instance) GetGeneratedPath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Generated) } func (i *Instance) GetMetadataPath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Metadata) } func (i *Instance) GetDatabasePath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Database) } func (i *Instance) GetJWTSignKey() []byte { + i.RLock() + defer i.RUnlock() return []byte(viper.GetString(JWTSignKey)) } func (i *Instance) GetSessionStoreKey() []byte { + i.RLock() + defer i.RUnlock() return []byte(viper.GetString(SessionStoreKey)) } @@ -279,14 +308,20 @@ func (i *Instance) GetDefaultScrapersPath() string { } func (i *Instance) GetExcludes() []string { + i.RLock() + defer i.RUnlock() return viper.GetStringSlice(Exclude) } func (i *Instance) GetImageExcludes() []string { + i.RLock() + defer i.RUnlock() return viper.GetStringSlice(ImageExclude) } func (i *Instance) GetVideoExtensions() []string { + i.RLock() + defer i.RUnlock() ret := viper.GetStringSlice(VideoExtensions) if ret == nil { ret = defaultVideoExtensions @@ -295,6 +330,8 @@ func (i *Instance) GetVideoExtensions() []string { } func (i *Instance) GetImageExtensions() []string { + i.RLock() + defer i.RUnlock() ret := viper.GetStringSlice(ImageExtensions) if ret == nil { ret = defaultImageExtensions @@ -303,6 +340,8 @@ func (i *Instance) GetImageExtensions() []string { } func (i *Instance) GetGalleryExtensions() []string { + i.RLock() + defer i.RUnlock() ret := viper.GetStringSlice(GalleryExtensions) if ret == nil { ret = defaultGalleryExtensions @@ -311,10 +350,14 @@ func (i *Instance) GetGalleryExtensions() []string { } func (i *Instance) GetCreateGalleriesFromFolders() bool { + i.RLock() + defer i.RUnlock() return viper.GetBool(CreateGalleriesFromFolders) } func (i *Instance) GetLanguage() string { + i.RLock() + defer i.RUnlock() ret := viper.GetString(Language) // default to English @@ -328,12 +371,16 @@ func (i *Instance) GetLanguage() string { // IsCalculateMD5 returns true if MD5 checksums should be generated for // scene video files. func (i *Instance) IsCalculateMD5() bool { + i.RLock() + defer i.RUnlock() return viper.GetBool(CalculateMD5) } // GetVideoFileNamingAlgorithm returns what hash algorithm should be used for // naming generated scene video files. func (i *Instance) GetVideoFileNamingAlgorithm() models.HashAlgorithm { + i.RLock() + defer i.RUnlock() ret := viper.GetString(VideoFileNamingAlgorithm) // default to oshash @@ -345,22 +392,30 @@ func (i *Instance) GetVideoFileNamingAlgorithm() models.HashAlgorithm { } func (i *Instance) GetScrapersPath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(ScrapersPath) } func (i *Instance) GetScraperUserAgent() string { + i.RLock() + defer i.RUnlock() return viper.GetString(ScraperUserAgent) } // GetScraperCDPPath gets the path to the Chrome executable or remote address // to an instance of Chrome. func (i *Instance) GetScraperCDPPath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(ScraperCDPPath) } // GetScraperCertCheck returns true if the scraper should check for insecure // certificates when fetching an image or a page. func (i *Instance) GetScraperCertCheck() bool { + i.RLock() + defer i.RUnlock() ret := true if viper.IsSet(ScraperCertCheck) { ret = viper.GetBool(ScraperCertCheck) @@ -370,6 +425,8 @@ func (i *Instance) GetScraperCertCheck() bool { } func (i *Instance) GetScraperExcludeTagPatterns() []string { + i.RLock() + defer i.RUnlock() var ret []string if viper.IsSet(ScraperExcludeTagPatterns) { ret = viper.GetStringSlice(ScraperExcludeTagPatterns) @@ -379,6 +436,8 @@ func (i *Instance) GetScraperExcludeTagPatterns() []string { } func (i *Instance) GetStashBoxes() []*models.StashBox { + i.RLock() + defer i.RUnlock() var boxes []*models.StashBox viper.UnmarshalKey(StashBoxes, &boxes) return boxes @@ -392,34 +451,48 @@ func (i *Instance) GetDefaultPluginsPath() string { } func (i *Instance) GetPluginsPath() string { + i.RLock() + defer i.RUnlock() return viper.GetString(PluginsPath) } func (i *Instance) GetHost() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Host) } func (i *Instance) GetPort() int { + i.RLock() + defer i.RUnlock() return viper.GetInt(Port) } func (i *Instance) GetExternalHost() string { + i.RLock() + defer i.RUnlock() return viper.GetString(ExternalHost) } // GetPreviewSegmentDuration returns the duration of a single segment in a // scene preview file, in seconds. func (i *Instance) GetPreviewSegmentDuration() float64 { + i.RLock() + defer i.RUnlock() return viper.GetFloat64(PreviewSegmentDuration) } // GetParallelTasks returns the number of parallel tasks that should be started // by scan or generate task. func (i *Instance) GetParallelTasks() int { + i.RLock() + defer i.RUnlock() return viper.GetInt(ParallelTasks) } func (i *Instance) GetParallelTasksWithAutoDetection() int { + i.RLock() + defer i.RUnlock() parallelTasks := viper.GetInt(ParallelTasks) if parallelTasks <= 0 { parallelTasks = (runtime.NumCPU() / 4) + 1 @@ -428,11 +501,15 @@ func (i *Instance) GetParallelTasksWithAutoDetection() int { } func (i *Instance) GetPreviewAudio() bool { + i.RLock() + defer i.RUnlock() return viper.GetBool(PreviewAudio) } // GetPreviewSegments returns the amount of segments in a scene preview file. func (i *Instance) GetPreviewSegments() int { + i.RLock() + defer i.RUnlock() return viper.GetInt(PreviewSegments) } @@ -443,6 +520,8 @@ func (i *Instance) GetPreviewSegments() int { // in the preview. If the value is suffixed with a '%' character (for example // '2%'), then it is interpreted as a proportion of the total video duration. func (i *Instance) GetPreviewExcludeStart() string { + i.RLock() + defer i.RUnlock() return viper.GetString(PreviewExcludeStart) } @@ -452,12 +531,16 @@ func (i *Instance) GetPreviewExcludeStart() string { // when generating previews. If the value is suffixed with a '%' character, // then it is interpreted as a proportion of the total video duration. func (i *Instance) GetPreviewExcludeEnd() string { + i.RLock() + defer i.RUnlock() return viper.GetString(PreviewExcludeEnd) } // GetPreviewPreset returns the preset when generating previews. Defaults to // Slow. func (i *Instance) GetPreviewPreset() models.PreviewPreset { + i.RLock() + defer i.RUnlock() ret := viper.GetString(PreviewPreset) // default to slow @@ -469,6 +552,8 @@ func (i *Instance) GetPreviewPreset() models.PreviewPreset { } func (i *Instance) GetMaxTranscodeSize() models.StreamingResolutionEnum { + i.RLock() + defer i.RUnlock() ret := viper.GetString(MaxTranscodeSize) // default to original @@ -480,6 +565,8 @@ func (i *Instance) GetMaxTranscodeSize() models.StreamingResolutionEnum { } func (i *Instance) GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum { + i.RLock() + defer i.RUnlock() ret := viper.GetString(MaxStreamingTranscodeSize) // default to original @@ -491,19 +578,27 @@ func (i *Instance) GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum } func (i *Instance) GetAPIKey() string { + i.RLock() + defer i.RUnlock() return viper.GetString(ApiKey) } func (i *Instance) GetUsername() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Username) } func (i *Instance) GetPasswordHash() string { + i.RLock() + defer i.RUnlock() return viper.GetString(Password) } func (i *Instance) GetCredentials() (string, string) { if i.HasCredentials() { + i.RLock() + defer i.RUnlock() return viper.GetString(Username), viper.GetString(Password) } @@ -511,12 +606,14 @@ func (i *Instance) GetCredentials() (string, string) { } func (i *Instance) HasCredentials() bool { + i.RLock() + defer i.RUnlock() if !viper.IsSet(Username) || !viper.IsSet(Password) { return false } - username := i.GetUsername() - pwHash := i.GetPasswordHash() + username := viper.GetString(Username) + pwHash := viper.GetString(Password) return username != "" && pwHash != "" } @@ -565,6 +662,8 @@ func (i *Instance) ValidateStashBoxes(boxes []*models.StashBoxInput) error { // GetMaxSessionAge gets the maximum age for session cookies, in seconds. // Session cookie expiry times are refreshed every request. func (i *Instance) GetMaxSessionAge() int { + i.Lock() + defer i.Unlock() viper.SetDefault(MaxSessionAge, DefaultMaxSessionAge) return viper.GetInt(MaxSessionAge) } @@ -572,15 +671,21 @@ func (i *Instance) GetMaxSessionAge() int { // GetCustomServedFolders gets the map of custom paths to their applicable // filesystem locations func (i *Instance) GetCustomServedFolders() URLMap { + i.RLock() + defer i.RUnlock() return viper.GetStringMapString(CustomServedFolders) } func (i *Instance) GetCustomUILocation() string { + i.RLock() + defer i.RUnlock() return viper.GetString(CustomUILocation) } // Interface options func (i *Instance) GetMenuItems() []string { + i.RLock() + defer i.RUnlock() if viper.IsSet(MenuItems) { return viper.GetStringSlice(MenuItems) } @@ -588,46 +693,63 @@ func (i *Instance) GetMenuItems() []string { } func (i *Instance) GetSoundOnPreview() bool { + i.RLock() + defer i.RUnlock() return viper.GetBool(SoundOnPreview) } func (i *Instance) GetWallShowTitle() bool { + i.Lock() + defer i.Unlock() viper.SetDefault(WallShowTitle, true) return viper.GetBool(WallShowTitle) } func (i *Instance) GetCustomPerformerImageLocation() string { - // don't set the default, as it causes race condition crashes - // viper.SetDefault(CustomPerformerImageLocation, "") + i.Lock() + defer i.Unlock() + viper.SetDefault(CustomPerformerImageLocation, "") return viper.GetString(CustomPerformerImageLocation) } func (i *Instance) GetWallPlayback() string { + i.Lock() + defer i.Unlock() viper.SetDefault(WallPlayback, "video") return viper.GetString(WallPlayback) } func (i *Instance) GetMaximumLoopDuration() int { + i.Lock() + defer i.Unlock() viper.SetDefault(MaximumLoopDuration, 0) return viper.GetInt(MaximumLoopDuration) } func (i *Instance) GetAutostartVideo() bool { + i.Lock() + defer i.Unlock() viper.SetDefault(AutostartVideo, false) return viper.GetBool(AutostartVideo) } func (i *Instance) GetShowStudioAsText() bool { + i.Lock() + defer i.Unlock() viper.SetDefault(ShowStudioAsText, false) return viper.GetBool(ShowStudioAsText) } func (i *Instance) GetSlideshowDelay() int { + i.Lock() + defer i.Unlock() viper.SetDefault(SlideshowDelay, 5000) return viper.GetInt(SlideshowDelay) } func (i *Instance) GetCSSPath() string { + i.RLock() + defer i.RUnlock() // use custom.css in the same directory as the config file configFileUsed := viper.ConfigFileUsed() configDir := filepath.Dir(configFileUsed) @@ -655,6 +777,8 @@ func (i *Instance) GetCSS() string { } func (i *Instance) SetCSS(css string) { + i.RLock() + defer i.RUnlock() fn := i.GetCSSPath() buf := []byte(css) @@ -663,39 +787,53 @@ func (i *Instance) SetCSS(css string) { } func (i *Instance) GetCSSEnabled() bool { + i.RLock() + defer i.RUnlock() return viper.GetBool(CSSEnabled) } func (i *Instance) GetHandyKey() string { + i.RLock() + defer i.RUnlock() return viper.GetString(HandyKey) } // GetDLNAServerName returns the visible name of the DLNA server. If empty, // "stash" will be used. func (i *Instance) GetDLNAServerName() string { + i.RLock() + defer i.RUnlock() return viper.GetString(DLNAServerName) } // GetDLNADefaultEnabled returns true if the DLNA is enabled by default. func (i *Instance) GetDLNADefaultEnabled() bool { + i.RLock() + defer i.RUnlock() return viper.GetBool(DLNADefaultEnabled) } // GetDLNADefaultIPWhitelist returns a list of IP addresses/wildcards that // are allowed to use the DLNA service. func (i *Instance) GetDLNADefaultIPWhitelist() []string { + i.RLock() + defer i.RUnlock() return viper.GetStringSlice(DLNADefaultIPWhitelist) } // GetDLNAInterfaces returns a list of interface names to expose DLNA on. If // empty, runs on all interfaces. func (i *Instance) GetDLNAInterfaces() []string { + i.RLock() + defer i.RUnlock() return viper.GetStringSlice(DLNAInterfaces) } // GetLogFile returns the filename of the file to output logs to. // An empty string means that file logging will be disabled. func (i *Instance) GetLogFile() string { + i.RLock() + defer i.RUnlock() return viper.GetString(LogFile) } @@ -703,6 +841,8 @@ func (i *Instance) GetLogFile() string { // in addition to writing to a log file. Logging will be output to the // terminal if file logging is disabled. Defaults to true. func (i *Instance) GetLogOut() bool { + i.RLock() + defer i.RUnlock() ret := true if viper.IsSet(LogOut) { ret = viper.GetBool(LogOut) @@ -714,6 +854,8 @@ func (i *Instance) GetLogOut() bool { // GetLogLevel returns the lowest log level to write to the log. // Should be one of "Debug", "Info", "Warning", "Error" func (i *Instance) GetLogLevel() string { + i.RLock() + defer i.RUnlock() const defaultValue = "Info" value := viper.GetString(LogLevel) @@ -727,6 +869,8 @@ func (i *Instance) GetLogLevel() string { // GetLogAccess returns true if http requests should be logged to the terminal. // HTTP requests are not logged to the log file. Defaults to true. func (i *Instance) GetLogAccess() bool { + i.RLock() + defer i.RUnlock() ret := true if viper.IsSet(LogAccess) { ret = viper.GetBool(LogAccess) @@ -737,6 +881,8 @@ func (i *Instance) GetLogAccess() bool { // Max allowed graphql upload size in megabytes func (i *Instance) GetMaxUploadSize() int64 { + i.RLock() + defer i.RUnlock() ret := int64(1024) if viper.IsSet(MaxUploadSize) { ret = viper.GetInt64(MaxUploadSize) @@ -745,6 +891,8 @@ func (i *Instance) GetMaxUploadSize() int64 { } func (i *Instance) Validate() error { + i.RLock() + defer i.RUnlock() mandatoryPaths := []string{ Database, Generated, @@ -767,7 +915,22 @@ func (i *Instance) Validate() error { return nil } +func (i *Instance) SetChecksumDefaultValues(defaultAlgorithm models.HashAlgorithm, usingMD5 bool) { + i.Lock() + defer i.Unlock() + viper.SetDefault(VideoFileNamingAlgorithm, defaultAlgorithm) + viper.SetDefault(CalculateMD5, usingMD5) +} + func (i *Instance) setDefaultValues() error { + + // read data before write lock scope + defaultDatabaseFilePath := i.GetDefaultDatabaseFilePath() + defaultScrapersPath := i.GetDefaultScrapersPath() + defaultPluginsPath := i.GetDefaultPluginsPath() + + i.Lock() + defer i.Unlock() viper.SetDefault(ParallelTasks, parallelTasksDefault) viper.SetDefault(PreviewSegmentDuration, previewSegmentDurationDefault) viper.SetDefault(PreviewSegments, previewSegmentsDefault) @@ -776,14 +939,14 @@ func (i *Instance) setDefaultValues() error { viper.SetDefault(PreviewAudio, previewAudioDefault) viper.SetDefault(SoundOnPreview, false) - viper.SetDefault(Database, i.GetDefaultDatabaseFilePath()) + viper.SetDefault(Database, defaultDatabaseFilePath) // Set generated to the metadata path for backwards compat viper.SetDefault(Generated, viper.GetString(Metadata)) // Set default scrapers and plugins paths - viper.SetDefault(ScrapersPath, i.GetDefaultScrapersPath()) - viper.SetDefault(PluginsPath, i.GetDefaultPluginsPath()) + viper.SetDefault(ScrapersPath, defaultScrapersPath) + viper.SetDefault(PluginsPath, defaultPluginsPath) return viper.WriteConfig() } diff --git a/pkg/manager/config/config_concurrency_test.go b/pkg/manager/config/config_concurrency_test.go new file mode 100644 index 000000000..2f86f332e --- /dev/null +++ b/pkg/manager/config/config_concurrency_test.go @@ -0,0 +1,100 @@ +package config + +import ( + "sync" + "testing" +) + +// should be run with -race +func TestConcurrentConfigAccess(t *testing.T) { + i := GetInstance() + + const workers = 8 + //const loops = 1000 + const loops = 200 + var wg sync.WaitGroup + for t := 0; t < workers; t++ { + wg.Add(1) + go func() { + for l := 0; l < loops; l++ { + i.SetInitialConfig() + + i.HasCredentials() + i.GetCPUProfilePath() + i.GetConfigFile() + i.GetConfigPath() + i.GetDefaultDatabaseFilePath() + i.GetStashPaths() + i.GetConfigFilePath() + i.Set(Cache, i.GetCachePath()) + i.Set(Generated, i.GetGeneratedPath()) + i.Set(Metadata, i.GetMetadataPath()) + i.Set(Database, i.GetDatabasePath()) + i.Set(JWTSignKey, i.GetJWTSignKey()) + i.Set(SessionStoreKey, i.GetSessionStoreKey()) + i.GetDefaultScrapersPath() + i.Set(Exclude, i.GetExcludes()) + i.Set(ImageExclude, i.GetImageExcludes()) + i.Set(VideoExtensions, i.GetVideoExtensions()) + i.Set(ImageExtensions, i.GetImageExtensions()) + i.Set(GalleryExtensions, i.GetGalleryExtensions()) + i.Set(CreateGalleriesFromFolders, i.GetCreateGalleriesFromFolders()) + i.Set(Language, i.GetLanguage()) + i.Set(VideoFileNamingAlgorithm, i.GetVideoFileNamingAlgorithm()) + i.Set(ScrapersPath, i.GetScrapersPath()) + i.Set(ScraperUserAgent, i.GetScraperUserAgent()) + i.Set(ScraperCDPPath, i.GetScraperCDPPath()) + i.Set(ScraperCertCheck, i.GetScraperCertCheck()) + i.Set(ScraperExcludeTagPatterns, i.GetScraperExcludeTagPatterns()) + i.Set(StashBoxes, i.GetStashBoxes()) + i.GetDefaultPluginsPath() + i.Set(PluginsPath, i.GetPluginsPath()) + i.Set(Host, i.GetHost()) + i.Set(Port, i.GetPort()) + i.Set(ExternalHost, i.GetExternalHost()) + i.Set(PreviewSegmentDuration, i.GetPreviewSegmentDuration()) + i.Set(ParallelTasks, i.GetParallelTasks()) + i.Set(ParallelTasks, i.GetParallelTasksWithAutoDetection()) + i.Set(PreviewAudio, i.GetPreviewAudio()) + i.Set(PreviewSegments, i.GetPreviewSegments()) + i.Set(PreviewExcludeStart, i.GetPreviewExcludeStart()) + i.Set(PreviewExcludeEnd, i.GetPreviewExcludeEnd()) + i.Set(PreviewPreset, i.GetPreviewPreset()) + i.Set(MaxTranscodeSize, i.GetMaxTranscodeSize()) + i.Set(MaxStreamingTranscodeSize, i.GetMaxStreamingTranscodeSize()) + i.Set(ApiKey, i.GetAPIKey()) + i.Set(Username, i.GetUsername()) + i.Set(Password, i.GetPasswordHash()) + i.GetCredentials() + i.Set(MaxSessionAge, i.GetMaxSessionAge()) + i.Set(CustomServedFolders, i.GetCustomServedFolders()) + i.Set(CustomUILocation, i.GetCustomUILocation()) + i.Set(MenuItems, i.GetMenuItems()) + i.Set(SoundOnPreview, i.GetSoundOnPreview()) + i.Set(WallShowTitle, i.GetWallShowTitle()) + i.Set(CustomPerformerImageLocation, i.GetCustomPerformerImageLocation()) + i.Set(WallPlayback, i.GetWallPlayback()) + i.Set(MaximumLoopDuration, i.GetMaximumLoopDuration()) + i.Set(AutostartVideo, i.GetAutostartVideo()) + i.Set(ShowStudioAsText, i.GetShowStudioAsText()) + i.Set(SlideshowDelay, i.GetSlideshowDelay()) + i.GetCSSPath() + i.GetCSS() + i.Set(CSSEnabled, i.GetCSSEnabled()) + i.Set(HandyKey, i.GetHandyKey()) + i.Set(DLNAServerName, i.GetDLNAServerName()) + i.Set(DLNADefaultEnabled, i.GetDLNADefaultEnabled()) + i.Set(DLNADefaultIPWhitelist, i.GetDLNADefaultIPWhitelist()) + i.Set(DLNAInterfaces, i.GetDLNAInterfaces()) + i.Set(LogFile, i.GetLogFile()) + i.Set(LogOut, i.GetLogOut()) + i.Set(LogLevel, i.GetLogLevel()) + i.Set(LogAccess, i.GetLogAccess()) + i.Set(MaxUploadSize, i.GetMaxUploadSize()) + } + wg.Done() + }() + } + + wg.Wait() +} diff --git a/ui/v2.5/src/components/Changelog/versions/v090.md b/ui/v2.5/src/components/Changelog/versions/v090.md index 7dfbbdcc1..929556c08 100644 --- a/ui/v2.5/src/components/Changelog/versions/v090.md +++ b/ui/v2.5/src/components/Changelog/versions/v090.md @@ -25,6 +25,7 @@ * Added de-DE language option. ([#1578](https://github.com/stashapp/stash/pull/1578)) ### 🐛 Bug fixes +* Fix race condition panic when reading and writing config concurrently. ([#1645](https://github.com/stashapp/stash/issues/1343)) * Fix performance issue on Studios page getting studio image count. ([#1643](https://github.com/stashapp/stash/pull/1643)) * Regenerate scene phash if overwrite flag is set. ([#1633](https://github.com/stashapp/stash/pull/1633)) * Create .stash directory in $HOME only if required. ([#1623](https://github.com/stashapp/stash/pull/1623))