Skip to content

Commit

Permalink
Use an atomic config write strategy on Windows
Browse files Browse the repository at this point in the history
- Introduce a util.WriteFilePseudoAtomic() func for consuming projects
- While the above func is multi-platform, limit to Windows usage for now

[NO NEW TESTS NEEDED]

Signed-off-by: Jason T. Greene <[email protected]>
  • Loading branch information
n1hility committed Apr 4, 2023
1 parent bdb4f5a commit 451e587
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 24 deletions.
22 changes: 0 additions & 22 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -1249,28 +1249,6 @@ func ReadCustomConfig() (*Config, error) {
return newConfig, nil
}

// Write writes the configuration to the default file
func (c *Config) Write() error {
var err error
path, err := customConfigFile()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
configFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer configFile.Close()
enc := toml.NewEncoder(configFile)
if err := enc.Encode(c); err != nil {
return err
}
return nil
}

// Reload clean the cached config and reloads the configuration from containers.conf files
// This function is meant to be used for long-running processes that need to reload potential changes made to
// the cached containers.conf files.
Expand Down
33 changes: 33 additions & 0 deletions pkg/config/config_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//go:build !windows
// +build !windows

package config

import (
"os"
"path/filepath"

"github.com/BurntSushi/toml"
)

// Write writes the configuration to the default file
func (c *Config) Write() error {
var err error
path, err := customConfigFile()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
configFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer configFile.Close()
enc := toml.NewEncoder(configFile)
if err := enc.Encode(c); err != nil {
return err
}
return nil
}
29 changes: 28 additions & 1 deletion pkg/config/config_windows.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
//go:build windows
// +build windows

package config

import "os"
import (
"io"
"os"
"path/filepath"

"github.com/BurntSushi/toml"
"github.com/containers/common/pkg/util"
)

const (
// OverrideContainersConfig holds the default config path overridden by the root user
Expand Down Expand Up @@ -29,3 +39,20 @@ func ifRootlessConfigPath() (string, error) {
var defaultHelperBinariesDir = []string{
"C:\\Program Files\\RedHat\\Podman",
}

// Write writes the configuration to the default file
func (c *Config) Write() error {
var err error
path, err := customConfigFile()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}

return util.WriteFilePseudoAtomic(path, 0o644, func(writer io.Writer) error {
enc := toml.NewEncoder(writer)
return enc.Encode(c)
})
}
60 changes: 59 additions & 1 deletion pkg/util/util.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package util

import "regexp"
import (
"io"
"os"
"path/filepath"
"regexp"
)

// StringInSlice determines if a string is in a string slice, returns bool
func StringInSlice(s string, sl []string) bool {
Expand All @@ -22,3 +27,56 @@ func StringMatchRegexSlice(s string, re []string) bool {
}
return false
}

func pathNoExt(path string) string {
l := len(filepath.Ext(path))
return path[:len(path)-l]
}

// WriteFilePseudoAtomic writes a file in a loosely atomic manner. It does not
// perform cooperative fs locking, or even application level locking. It simply
// ensures that a written file is fully flushed to disk before its content is
// published under the specified path name. Concurrent calls will overwrite
// the same resulting path.
func WriteFilePseudoAtomic(path string, mode os.FileMode, writeOp func(io.Writer) error) error {
tmpPattern := pathNoExt(filepath.Base(path)) + "-*.tmp"

// Create a unique tmp name in the same directory as the target file
// Note: We don't use O_SYNC since golang doesn't support it for Windows
// And even if it did use FILE_FLAG_WRITE_THROUGH, Windows IDE and SATA
// drives will ignore the corresponding Force Unit Access flag, so we need
// to manually flush the file instead
tmpFile, err := os.CreateTemp(filepath.Dir(path), tmpPattern)
if err != nil {
return err
}
defer tmpFile.Close()

if err := tmpFile.Chmod(mode); err != nil {
return err
}

// Perform caller provided write operation
if err := writeOp(tmpFile); err != nil {
return err
}

// Ensure file is flushed to disk before renaming
// Uses fsync() on Linux/Unix, fcntl(F_FULLFSYNC) on Darwin, and
// FlushFileBuffers on Windows, all of which ensure state is written to
// media (excluding bugs and bad drivers)
if err := tmpFile.Sync(); err != nil {
return err
}

// Eager cleanup is necessary on Windows (and harmless on other OSs)
// to prevent a lock being held
if err := tmpFile.Close(); err != nil {
return err
}

// os.Rename is atomic on Linux/Unix
// Triggers MoveFileEx with MOVEFILE_REPLACE_EXISTING, ensuring the change
// is a metadata write (since tmp file is on the same volume)
return os.Rename(tmpFile.Name(), path)
}

0 comments on commit 451e587

Please sign in to comment.