| // Copyright 2017 Google Inc. 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. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License 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 build |
| |
| import ( |
| "errors" |
| "fmt" |
| "math" |
| "os" |
| "path/filepath" |
| "syscall" |
| "time" |
| |
| "android/soong/ui/logger" |
| ) |
| |
| // This file provides cross-process synchronization methods |
| // i.e. making sure only one Soong process is running for a given output directory |
| |
| func BecomeSingletonOrFail(ctx Context, config Config) (lock *fileLock) { |
| lockingInfo, err := newLock(config.OutDir()) |
| if err != nil { |
| ctx.Logger.Fatal(err) |
| } |
| lockfilePollDuration := time.Second |
| lockfileTimeout := time.Second * 10 |
| if envTimeout := os.Getenv("SOONG_LOCK_TIMEOUT"); envTimeout != "" { |
| lockfileTimeout, err = time.ParseDuration(envTimeout) |
| if err != nil { |
| ctx.Logger.Fatalf("failure parsing SOONG_LOCK_TIMEOUT %q: %s", envTimeout, err) |
| } |
| } |
| err = lockSynchronous(*lockingInfo, newSleepWaiter(lockfilePollDuration, lockfileTimeout), ctx.Logger) |
| if err != nil { |
| ctx.Logger.Fatal(err) |
| } |
| return lockingInfo |
| } |
| |
| type lockable interface { |
| tryLock() error |
| Unlock() error |
| description() string |
| } |
| |
| var _ lockable = (*fileLock)(nil) |
| |
| type fileLock struct { |
| File *os.File |
| } |
| |
| func (l fileLock) description() (path string) { |
| return l.File.Name() |
| } |
| func (l fileLock) tryLock() (err error) { |
| return syscall.Flock(int(l.File.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) |
| } |
| func (l fileLock) Unlock() (err error) { |
| return l.File.Close() |
| } |
| |
| func lockSynchronous(lock lockable, waiter waiter, logger logger.Logger) (err error) { |
| |
| waited := false |
| |
| for { |
| err = lock.tryLock() |
| if err == nil { |
| if waited { |
| // If we had to wait at all, then when the wait is done, we inform the user |
| logger.Printf("Acquired lock on %v; previous Soong process must have completed. Continuing...\n", lock.description()) |
| } |
| return nil |
| } |
| |
| done, description := waiter.checkDeadline() |
| |
| if !waited { |
| 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()) |
| } |
| |
| waited = true |
| |
| if done { |
| return fmt.Errorf("Tried to lock %s, but timed out %s . Make sure no other Soong process is using it", |
| lock.description(), waiter.summarize()) |
| } else { |
| waiter.wait() |
| } |
| } |
| } |
| |
| func newLock(basedir string) (lock *fileLock, err error) { |
| lockPath := filepath.Join(basedir, ".lock") |
| |
| os.MkdirAll(basedir, 0777) |
| lockfileDescriptor, err := os.OpenFile(lockPath, os.O_RDWR|os.O_CREATE, 0666) |
| if err != nil { |
| return nil, errors.New("failed to open " + lockPath) |
| } |
| lockingInfo := &fileLock{File: lockfileDescriptor} |
| |
| return lockingInfo, nil |
| } |
| |
| type waiter interface { |
| wait() |
| checkDeadline() (done bool, remainder string) |
| summarize() (summary string) |
| } |
| |
| type sleepWaiter struct { |
| sleepInterval time.Duration |
| deadline time.Time |
| |
| totalWait time.Duration |
| } |
| |
| var _ waiter = (*sleepWaiter)(nil) |
| |
| func newSleepWaiter(interval time.Duration, duration time.Duration) (waiter *sleepWaiter) { |
| return &sleepWaiter{interval, time.Now().Add(duration), duration} |
| } |
| |
| func (s sleepWaiter) wait() { |
| time.Sleep(s.sleepInterval) |
| } |
| func (s *sleepWaiter) checkDeadline() (done bool, remainder string) { |
| remainingSleep := s.deadline.Sub(time.Now()) |
| numSecondsRounded := math.Floor(remainingSleep.Seconds()*10+0.5) / 10 |
| if remainingSleep > 0 { |
| return false, fmt.Sprintf("%vs", numSecondsRounded) |
| } else { |
| return true, "" |
| } |
| } |
| func (s sleepWaiter) summarize() (summary string) { |
| return fmt.Sprintf("polling every %v until %v", s.sleepInterval, s.totalWait) |
| } |