agent/fileutil/filelock/filelock.go (128 lines of code) (raw):
// Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may not
// use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
// either express or implied. See the License for the specific language governing
// permissions and limitations under the License.
package filelock
import (
"fmt"
"math/rand"
"os"
"strings"
"time"
"github.com/aws/amazon-ssm-agent/agent/fileutil"
)
const (
writeVerifyMilliseconds = 1000
)
type FileLocker interface {
Lock(lockPath string, ownerId string, timeoutSeconds int) (locked bool, err error)
Unlock(lockPath string, ownerId string) (hadLock bool, err error)
}
// TODO: Write platform-specific implementations instead.
type fileLocker struct {
}
func NewFileLocker() FileLocker {
return &fileLocker{}
}
func GetOwnerIdForProcess() string {
pid := os.Getpid()
gid := os.Getgid()
return fmt.Sprintf("pid-%d-gid-%d", pid, gid)
}
func (fl *fileLocker) Lock(lockPath string, ownerId string, timeoutSeconds int) (locked bool, err error) {
locked, err = LockFile(lockPath, ownerId, timeoutSeconds)
return
}
func (fl *fileLocker) Unlock(lockPath string, ownerId string) (hadLock bool, err error) {
hadLock, err = UnlockFile(lockPath, ownerId)
return
}
func statLockFile(lockPath string) (exists bool, modificationTime time.Time, err error) {
var fi os.FileInfo
if fi, err = os.Stat(lockPath); err != nil {
if os.IsNotExist(err) {
return false, time.Time{}, nil
}
return true, time.Time{}, err
}
modificationTime = fi.ModTime()
return true, modificationTime, nil
}
func writeLockFile(lockPath string, contents string) (created bool, err error) {
var f *os.File
// Using os.O_EXCL because on most systems it will be atomic operation.
if f, err = os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
if os.IsExist(err) {
return false, nil
}
return false, fmt.Errorf("Unable to open lock file for writing. %v", err)
}
defer f.Close()
if _, err = f.WriteString(contents); err != nil {
return false, fmt.Errorf("Error writing to lock file. %v", err)
}
return true, nil
}
func readLockFile(lockPath string) (contents string, err error) {
if contents, err = fileutil.ReadAllText(lockPath); err != nil {
return contents, fmt.Errorf("Error reading from lock file %v. %v", lockPath, err)
}
return strings.TrimSpace(contents), err
}
func isLockFileExist(lockPath string) (exist bool, err error) {
var exists bool
if exists, _, err = statLockFile(lockPath); err != nil {
return false, err
}
return exists, nil
}
func expireIfLockFileTimeout(lockPath string, timeoutSeconds int) (err error) {
var modificationTime time.Time
var exists bool
if exists, modificationTime, err = statLockFile(lockPath); err != nil {
return err
}
if !exists {
return nil
}
startTime := modificationTime.Add(writeVerifyMilliseconds * time.Millisecond)
timeoutTime := startTime.Add(time.Second * time.Duration(timeoutSeconds))
timeout := timeoutTime.Before(time.Now())
// Existing lock timed out, just quickly delete lockfile.
if timeout {
os.Remove(lockPath)
}
return nil
}
func LockFile(lockPath string, ownerId string, timeoutSeconds int) (locked bool, err error) {
// Sleep a little to desynchronize processes
time.Sleep(time.Duration(rand.Int63n(1000000)) * time.Microsecond)
// Check if lock can be expired.
if err = expireIfLockFileTimeout(lockPath, timeoutSeconds); err != nil {
err := fmt.Errorf("Error checking lock file for timeout: %v. %v", lockPath, err)
return false, err
}
var exist bool
if exist, err = isLockFileExist(lockPath); err != nil {
err := fmt.Errorf("Error checking whether lock file exists: %v. %v", lockPath, err)
return false, err
}
if exist {
// Lock file exists, another process must have the lock.
return false, nil
}
// Try to create lock file
var created bool
if created, err = writeLockFile(lockPath, ownerId); err != nil {
err := fmt.Errorf("Error locking file: %v. %v", lockPath, err)
return false, err
}
if !created {
// Another process must have gotten the lock.
return false, nil
}
// Wait a little
time.Sleep(writeVerifyMilliseconds * time.Millisecond)
var contentsRead string
if contentsRead, err = readLockFile(lockPath); err != nil && contentsRead != ownerId {
// Content doesn't match, another process must have gotten the lock.
return false, nil
}
return true, nil
}
func UnlockFile(lockPath string, ownerId string) (hadLock bool, err error) {
var contentsRead string
if contentsRead, _ = readLockFile(lockPath); contentsRead != ownerId {
// Content doesn't match, another process must have gotten the lock.
return false, nil
}
if err = os.Remove(lockPath); err != nil {
return false, err
}
return true, nil
}