| Jeff Gaston | 3615fe8 | 2017-05-24 13:14:34 -0700 | [diff] [blame] | 1 | // Copyright 2017 Google Inc. All rights reserved. | 
|  | 2 | // | 
|  | 3 | // Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | 4 | // you may not use this file except in compliance with the License. | 
|  | 5 | // You may obtain a copy of the License at | 
|  | 6 | // | 
|  | 7 | //     http://www.apache.org/licenses/LICENSE-2.0 | 
|  | 8 | // | 
|  | 9 | // Unless required by applicable law or agreed to in writing, software | 
|  | 10 | // distributed under the License is distributed on an "AS IS" BASIS, | 
|  | 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | 12 | // See the License for the specific language governing permissions and | 
|  | 13 | // limitations under the License. | 
|  | 14 |  | 
|  | 15 | package build | 
|  | 16 |  | 
|  | 17 | import ( | 
|  | 18 | "errors" | 
|  | 19 | "fmt" | 
|  | 20 | "math" | 
|  | 21 | "os" | 
|  | 22 | "path/filepath" | 
|  | 23 | "syscall" | 
|  | 24 | "time" | 
|  | 25 |  | 
|  | 26 | "android/soong/ui/logger" | 
|  | 27 | ) | 
|  | 28 |  | 
|  | 29 | // This file provides cross-process synchronization methods | 
|  | 30 | // i.e. making sure only one Soong process is running for a given output directory | 
|  | 31 |  | 
|  | 32 | func BecomeSingletonOrFail(ctx Context, config Config) (lock *fileLock) { | 
|  | 33 | lockingInfo, err := newLock(config.OutDir()) | 
|  | 34 | if err != nil { | 
|  | 35 | ctx.Logger.Fatal(err) | 
|  | 36 | } | 
|  | 37 | err = lockSynchronous(*lockingInfo, newSleepWaiter(lockfilePollDuration, lockfileTimeout), ctx.Logger) | 
|  | 38 | if err != nil { | 
|  | 39 | ctx.Logger.Fatal(err) | 
|  | 40 | } | 
|  | 41 | return lockingInfo | 
|  | 42 | } | 
|  | 43 |  | 
|  | 44 | var lockfileTimeout = time.Second * 10 | 
|  | 45 | var lockfilePollDuration = time.Second | 
|  | 46 |  | 
|  | 47 | type lockable interface { | 
|  | 48 | tryLock() error | 
|  | 49 | Unlock() error | 
|  | 50 | description() string | 
|  | 51 | } | 
|  | 52 |  | 
|  | 53 | var _ lockable = (*fileLock)(nil) | 
|  | 54 |  | 
|  | 55 | type fileLock struct { | 
|  | 56 | File *os.File | 
|  | 57 | } | 
|  | 58 |  | 
|  | 59 | func (l fileLock) description() (path string) { | 
|  | 60 | return l.File.Name() | 
|  | 61 | } | 
|  | 62 | func (l fileLock) tryLock() (err error) { | 
|  | 63 | return syscall.Flock(int(l.File.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) | 
|  | 64 | } | 
|  | 65 | func (l fileLock) Unlock() (err error) { | 
|  | 66 | return l.File.Close() | 
|  | 67 | } | 
|  | 68 |  | 
|  | 69 | func lockSynchronous(lock lockable, waiter waiter, logger logger.Logger) (err error) { | 
|  | 70 |  | 
|  | 71 | waited := false | 
|  | 72 |  | 
|  | 73 | for { | 
|  | 74 | err = lock.tryLock() | 
|  | 75 | if err == nil { | 
|  | 76 | if waited { | 
|  | 77 | // If we had to wait at all, then when the wait is done, we inform the user | 
|  | 78 | logger.Printf("Acquired lock on %v; previous Soong process must have completed. Continuing...\n", lock.description()) | 
|  | 79 | } | 
|  | 80 | return nil | 
|  | 81 | } | 
|  | 82 |  | 
|  | 83 | waited = true | 
|  | 84 |  | 
|  | 85 | done, description := waiter.checkDeadline() | 
|  | 86 |  | 
|  | 87 | if done { | 
|  | 88 | return fmt.Errorf("Tried to lock %s, but timed out %s . Make sure no other Soong process is using it", | 
|  | 89 | lock.description(), waiter.summarize()) | 
|  | 90 | } else { | 
|  | 91 | logger.Printf("Waiting up to %s to lock %v to ensure no other Soong process is running in the same output directory\n", description, lock.description()) | 
|  | 92 | waiter.wait() | 
|  | 93 | } | 
|  | 94 | } | 
|  | 95 | } | 
|  | 96 |  | 
|  | 97 | func newLock(basedir string) (lock *fileLock, err error) { | 
|  | 98 | lockPath := filepath.Join(basedir, ".lock") | 
|  | 99 |  | 
|  | 100 | os.MkdirAll(basedir, 0777) | 
|  | 101 | lockfileDescriptor, err := os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE, 0666) | 
|  | 102 | if err != nil { | 
|  | 103 | return nil, errors.New("failed to open " + lockPath) | 
|  | 104 | } | 
|  | 105 | lockingInfo := &fileLock{File: lockfileDescriptor} | 
|  | 106 |  | 
|  | 107 | return lockingInfo, nil | 
|  | 108 | } | 
|  | 109 |  | 
|  | 110 | type waiter interface { | 
|  | 111 | wait() | 
|  | 112 | checkDeadline() (done bool, remainder string) | 
|  | 113 | summarize() (summary string) | 
|  | 114 | } | 
|  | 115 |  | 
|  | 116 | type sleepWaiter struct { | 
|  | 117 | sleepInterval time.Duration | 
|  | 118 | deadline      time.Time | 
|  | 119 |  | 
|  | 120 | totalWait time.Duration | 
|  | 121 | } | 
|  | 122 |  | 
|  | 123 | var _ waiter = (*sleepWaiter)(nil) | 
|  | 124 |  | 
|  | 125 | func newSleepWaiter(interval time.Duration, duration time.Duration) (waiter *sleepWaiter) { | 
|  | 126 | return &sleepWaiter{interval, time.Now().Add(duration), duration} | 
|  | 127 | } | 
|  | 128 |  | 
|  | 129 | func (s sleepWaiter) wait() { | 
|  | 130 | time.Sleep(s.sleepInterval) | 
|  | 131 | } | 
|  | 132 | func (s *sleepWaiter) checkDeadline() (done bool, remainder string) { | 
|  | 133 | remainingSleep := s.deadline.Sub(time.Now()) | 
|  | 134 | numSecondsRounded := math.Floor(remainingSleep.Seconds()*10+0.5) / 10 | 
|  | 135 | if remainingSleep > 0 { | 
|  | 136 | return false, fmt.Sprintf("%vs", numSecondsRounded) | 
|  | 137 | } else { | 
|  | 138 | return true, "" | 
|  | 139 | } | 
|  | 140 | } | 
|  | 141 | func (s sleepWaiter) summarize() (summary string) { | 
|  | 142 | return fmt.Sprintf("polling every %v until %v", s.sleepInterval, s.totalWait) | 
|  | 143 | } |