pax_global_header00006660000000000000000000000064131410564540014515gustar00rootroot0000000000000052 comment=6a197d5ea61168f2ac821de2b7f011b250904900 golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/000077500000000000000000000000001314105645400225765ustar00rootroot00000000000000golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/.gitignore000066400000000000000000000004431314105645400245670ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # popular temporaries .err .out .diff # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/.gitmodules000066400000000000000000000001311314105645400247460ustar00rootroot00000000000000[submodule "git-hooks"] path = git-hooks url = https://github.com/nightlyone/git-hooks golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/.travis.yml000066400000000000000000000002631314105645400247100ustar00rootroot00000000000000language: go go: - 1.4.3 - 1.6.2 - tip # Only test commits to production branch and all pull requests branches: only: - master matrix: allow_failures: - go: tip golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/LICENSE000066400000000000000000000020361314105645400236040ustar00rootroot00000000000000Copyright (c) 2012 Ingo Oeser Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/README.md000066400000000000000000000022401314105645400240530ustar00rootroot00000000000000lockfile ========= Handle locking via pid files. [![Build Status Unix][1]][2] [![Build status Windows][3]][4] [1]: https://secure.travis-ci.org/nightlyone/lockfile.png [2]: https://travis-ci.org/nightlyone/lockfile [3]: https://ci.appveyor.com/api/projects/status/7mojkmauj81uvp8u/branch/master?svg=true [4]: https://ci.appveyor.com/project/nightlyone/lockfile/branch/master install ------- Install [Go 1][5], either [from source][6] or [with a prepackaged binary][7]. For Windows suport, Go 1.4 or newer is required. Then run go get github.com/nightlyone/lockfile [5]: http://golang.org [6]: http://golang.org/doc/install/source [7]: http://golang.org/doc/install LICENSE ------- MIT documentation ------------- [package documentation at godoc.org](http://godoc.org/github.com/nightlyone/lockfile) install ------------------- go get github.com/nightlyone/lockfile contributing ============ Contributions are welcome. Please open an issue or send me a pull request for a dedicated branch. Make sure the git commit hooks show it works. git commit hooks ----------------------- enable commit hooks via cd .git ; rm -rf hooks; ln -s ../git-hooks hooks ; cd .. golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/appveyor.yml000066400000000000000000000002741314105645400251710ustar00rootroot00000000000000clone_folder: c:\gopath\src\github.com\nightlyone\lockfile environment: GOPATH: c:\gopath install: - go version - go env - go get -v -t ./... build_script: - go test -v ./... golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/git-hooks/000077500000000000000000000000001314105645400245025ustar00rootroot00000000000000golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/lockfile.go000066400000000000000000000133751314105645400247260ustar00rootroot00000000000000// Package lockfile handles pid file based locking. // While a sync.Mutex helps against concurrency issues within a single process, // this package is designed to help against concurrency issues between cooperating processes // or serializing multiple invocations of the same process. You can also combine sync.Mutex // with Lockfile in order to serialize an action between different goroutines in a single program // and also multiple invocations of this program. package lockfile import ( "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" ) // Lockfile is a pid file which can be locked type Lockfile string // TemporaryError is a type of error where a retry after a random amount of sleep should help to mitigate it. type TemporaryError string func (t TemporaryError) Error() string { return string(t) } // Temporary returns always true. // It exists, so you can detect it via // if te, ok := err.(interface{ Temporary() bool }); ok { // fmt.Println("I am a temporay error situation, so wait and retry") // } func (t TemporaryError) Temporary() bool { return true } // Various errors returned by this package var ( ErrBusy = TemporaryError("Locked by other process") // If you get this, retry after a short sleep might help ErrNotExist = TemporaryError("Lockfile created, but doesn't exist") // If you get this, retry after a short sleep might help ErrNeedAbsPath = errors.New("Lockfiles must be given as absolute path names") ErrInvalidPid = errors.New("Lockfile contains invalid pid for system") ErrDeadOwner = errors.New("Lockfile contains pid of process not existent on this system anymore") ErrRogueDeletion = errors.New("Lockfile owned by me has been removed unexpectedly") ) // New describes a new filename located at the given absolute path. func New(path string) (Lockfile, error) { if !filepath.IsAbs(path) { return Lockfile(""), ErrNeedAbsPath } return Lockfile(path), nil } // GetOwner returns who owns the lockfile. func (l Lockfile) GetOwner() (*os.Process, error) { name := string(l) // Ok, see, if we have a stale lockfile here content, err := ioutil.ReadFile(name) if err != nil { return nil, err } // try hard for pids. If no pid, the lockfile is junk anyway and we delete it. pid, err := scanPidLine(content) if err != nil { return nil, err } running, err := isRunning(pid) if err != nil { return nil, err } if running { proc, err := os.FindProcess(pid) if err != nil { return nil, err } return proc, nil } return nil, ErrDeadOwner } // TryLock tries to own the lock. // It Returns nil, if successful and and error describing the reason, it didn't work out. // Please note, that existing lockfiles containing pids of dead processes // and lockfiles containing no pid at all are simply deleted. func (l Lockfile) TryLock() error { name := string(l) // This has been checked by New already. If we trigger here, // the caller didn't use New and re-implemented it's functionality badly. // So panic, that he might find this easily during testing. if !filepath.IsAbs(name) { panic(ErrNeedAbsPath) } tmplock, err := ioutil.TempFile(filepath.Dir(name), filepath.Base(name)+".") if err != nil { return err } cleanup := func() { _ = tmplock.Close() _ = os.Remove(tmplock.Name()) } defer cleanup() if err := writePidLine(tmplock, os.Getpid()); err != nil { return err } // EEXIST and similiar error codes, caught by os.IsExist, are intentionally ignored, // as it means that someone was faster creating this link // and ignoring this kind of error is part of the algorithm. // The we will probably fail the pid owner check later, if this process is still alive. // We cannot ignore ALL errors, since failure to support hard links, disk full // as well as many other errors can happen to a filesystem operation // and we really want to abort on those. if err := os.Link(tmplock.Name(), name); err != nil { if !os.IsExist(err) { return err } } fiTmp, err := os.Lstat(tmplock.Name()) if err != nil { return err } fiLock, err := os.Lstat(name) if err != nil { // tell user that a retry would be a good idea if os.IsNotExist(err) { return ErrNotExist } return err } // Success if os.SameFile(fiTmp, fiLock) { return nil } proc, err := l.GetOwner() switch err { default: // Other errors -> defensively fail and let caller handle this return err case nil: if proc.Pid != os.Getpid() { return ErrBusy } case ErrDeadOwner, ErrInvalidPid: // cases we can fix below } // clean stale/invalid lockfile err = os.Remove(name) if err != nil { // If it doesn't exist, then it doesn't matter who removed it. if !os.IsNotExist(err) { return err } } // now that the stale lockfile is gone, let's recurse return l.TryLock() } // Unlock a lock again, if we owned it. Returns any error that happend during release of lock. func (l Lockfile) Unlock() error { proc, err := l.GetOwner() switch err { case ErrInvalidPid, ErrDeadOwner: return ErrRogueDeletion case nil: if proc.Pid == os.Getpid() { // we really own it, so let's remove it. return os.Remove(string(l)) } // Not owned by me, so don't delete it. return ErrRogueDeletion default: // This is an application error or system error. // So give a better error for logging here. if os.IsNotExist(err) { return ErrRogueDeletion } // Other errors -> defensively fail and let caller handle this return err } } func writePidLine(w io.Writer, pid int) error { _, err := io.WriteString(w, fmt.Sprintf("%d\n", pid)) return err } func scanPidLine(content []byte) (int, error) { if len(content) == 0 { return 0, ErrInvalidPid } var pid int if _, err := fmt.Sscanln(string(content), &pid); err != nil { return 0, ErrInvalidPid } if pid <= 0 { return 0, ErrInvalidPid } return pid, nil } golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/lockfile_test.go000066400000000000000000000126411314105645400257600ustar00rootroot00000000000000package lockfile import ( "fmt" "io/ioutil" "math/rand" "os" "path/filepath" "strconv" "testing" ) func ExampleLockfile() { lock, err := New(filepath.Join(os.TempDir(), "lock.me.now.lck")) if err != nil { fmt.Printf("Cannot init lock. reason: %v", err) panic(err) // handle properly please! } err = lock.TryLock() // Error handling is essential, as we only try to get the lock. if err != nil { fmt.Printf("Cannot lock %q, reason: %v", lock, err) panic(err) // handle properly please! } defer lock.Unlock() fmt.Println("Do stuff under lock") // Output: Do stuff under lock } func TestBasicLockUnlock(t *testing.T) { path, err := filepath.Abs("test_lockfile.pid") if err != nil { panic(err) } lf, err := New(path) if err != nil { t.Fail() fmt.Println("Error making lockfile: ", err) return } err = lf.TryLock() if err != nil { t.Fail() fmt.Println("Error locking lockfile: ", err) return } err = lf.Unlock() if err != nil { t.Fail() fmt.Println("Error unlocking lockfile: ", err) return } } func GetDeadPID() int { // I have no idea how windows handles large PIDs, or if they even exist. // So limit it to be less or equal to 4096 to be safe. const maxPid = 4095 // limited iteration, so we finish one day seen := map[int]bool{} for len(seen) < maxPid { pid := rand.Intn(maxPid + 1) // see https://godoc.org/math/rand#Intn why if seen[pid] { continue } seen[pid] = true running, err := isRunning(pid) if err != nil { fmt.Println("Error checking PID: ", err) continue } if !running { return pid } } panic(fmt.Sprintf("all pids lower %d are used, cannot test this", maxPid)) } func TestBusy(t *testing.T) { path, err := filepath.Abs("test_lockfile.pid") if err != nil { t.Fatal(err) return } pid := os.Getppid() if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil { t.Fatal(err) return } defer os.Remove(path) lf, err := New(path) if err != nil { t.Fatal(err) return } got := lf.TryLock() if got != ErrBusy { t.Fatalf("expected error %q, got %v", ErrBusy, got) return } } func TestRogueDeletion(t *testing.T) { path, err := filepath.Abs("test_lockfile.pid") if err != nil { t.Fatal(err) return } lf, err := New(path) if err != nil { t.Fatal(err) return } err = lf.TryLock() if err != nil { t.Fatal(err) return } err = os.Remove(path) if err != nil { t.Fatal(err) return } got := lf.Unlock() if got != ErrRogueDeletion { t.Fatalf("unexpected error: %v", got) return } } func TestRogueDeletionDeadPid(t *testing.T) { path, err := filepath.Abs("test_lockfile.pid") if err != nil { t.Fatal(err) return } lf, err := New(path) if err != nil { t.Fatal(err) return } err = lf.TryLock() if err != nil { t.Fatal(err) return } pid := GetDeadPID() if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil { t.Fatal(err) return } defer os.Remove(path) err = lf.Unlock() if err != ErrRogueDeletion { t.Fatalf("unexpected error: %v", err) return } if _, err := os.Stat(path); os.IsNotExist(err) { t.Fatal("lockfile should not be deleted by us, if we didn't create it") } else { if err != nil { t.Fatalf("unexpected error %v", err) } } } func TestRemovesStaleLockOnDeadOwner(t *testing.T) { path, err := filepath.Abs("test_lockfile.pid") if err != nil { t.Fatal(err) return } lf, err := New(path) if err != nil { t.Fatal(err) return } pid := GetDeadPID() if err := ioutil.WriteFile(path, []byte(strconv.Itoa(pid)+"\n"), 0666); err != nil { t.Fatal(err) return } err = lf.TryLock() if err != nil { t.Fatal(err) return } if err := lf.Unlock(); err != nil { t.Fatal(err) return } } func TestInvalidPidLeadToReplacedLockfileAndSuccess(t *testing.T) { path, err := filepath.Abs("test_lockfile.pid") if err != nil { t.Fatal(err) return } if err := ioutil.WriteFile(path, []byte("\n"), 0666); err != nil { t.Fatal(err) return } defer os.Remove(path) lf, err := New(path) if err != nil { t.Fatal(err) return } if err := lf.TryLock(); err != nil { t.Fatalf("unexpected error: %v", err) return } // now check if file exists and contains the correct content got, err := ioutil.ReadFile(path) if err != nil { t.Fatalf("unexpected error %v", err) return } want := fmt.Sprintf("%d\n", os.Getpid()) if string(got) != want { t.Fatalf("got %q, want %q", got, want) } } func TestScanPidLine(t *testing.T) { tests := [...]struct { input []byte pid int xfail error }{ { xfail: ErrInvalidPid, }, { input: []byte(""), xfail: ErrInvalidPid, }, { input: []byte("\n"), xfail: ErrInvalidPid, }, { input: []byte("-1\n"), xfail: ErrInvalidPid, }, { input: []byte("0\n"), xfail: ErrInvalidPid, }, { input: []byte("a\n"), xfail: ErrInvalidPid, }, { input: []byte("1\n"), pid: 1, }, } // test positive cases first for step, tc := range tests { if tc.xfail != nil { continue } want := tc.pid got, err := scanPidLine(tc.input) if err != nil { t.Fatalf("%d: unexpected error %v", step, err) } if got != want { t.Errorf("%d: expected pid %d, got %d", step, want, got) } } // test negative cases now for step, tc := range tests { if tc.xfail == nil { continue } want := tc.xfail _, got := scanPidLine(tc.input) if got != want { t.Errorf("%d: expected error %v, got %v", step, want, got) } } } golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/lockfile_unix.go000066400000000000000000000005131314105645400257570ustar00rootroot00000000000000// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris package lockfile import ( "os" "syscall" ) func isRunning(pid int) (bool, error) { proc, err := os.FindProcess(pid) if err != nil { return false, err } if err := proc.Signal(syscall.Signal(0)); err != nil { return false, nil } return true, nil } golang-github-nightlyone-lockfile-0.0~git20170804.6a197d5/lockfile_windows.go000066400000000000000000000011221314105645400264630ustar00rootroot00000000000000package lockfile import ( "syscall" ) //For some reason these consts don't exist in syscall. const ( error_invalid_parameter = 87 code_still_active = 259 ) func isRunning(pid int) (bool, error) { procHnd, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, true, uint32(pid)) if err != nil { if scerr, ok := err.(syscall.Errno); ok { if uintptr(scerr) == error_invalid_parameter { return false, nil } } } var code uint32 err = syscall.GetExitCodeProcess(procHnd, &code) if err != nil { return false, err } return code == code_still_active, nil }