Source file src/cmd/go/internal/lockedfile/lockedfile.go
1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package lockedfile creates and manipulates files whose contents should only 6 // change atomically. 7 package lockedfile 8 9 import ( 10 "fmt" 11 "io" 12 "io/fs" 13 "os" 14 "runtime" 15 ) 16 17 // A File is a locked *os.File. 18 // 19 // Closing the file releases the lock. 20 // 21 // If the program exits while a file is locked, the operating system releases 22 // the lock but may not do so promptly: callers must ensure that all locked 23 // files are closed before exiting. 24 type File struct { 25 osFile 26 closed bool 27 // cleanup panics when the file is no longer referenced and it has not been closed. 28 cleanup runtime.Cleanup 29 } 30 31 // osFile embeds a *os.File while keeping the pointer itself unexported. 32 // (When we close a File, it must be the same file descriptor that we opened!) 33 type osFile struct { 34 *os.File 35 } 36 37 // OpenFile is like os.OpenFile, but returns a locked file. 38 // If flag includes os.O_WRONLY or os.O_RDWR, the file is write-locked; 39 // otherwise, it is read-locked. 40 func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) { 41 var ( 42 f = new(File) 43 err error 44 ) 45 f.osFile.File, err = openFile(name, flag, perm) 46 if err != nil { 47 return nil, err 48 } 49 50 // Although the operating system will drop locks for open files when the go 51 // command exits, we want to hold locks for as little time as possible, and we 52 // especially don't want to leave a file locked after we're done with it. Our 53 // Close method is what releases the locks, so use a cleanup to report 54 // missing Close calls on a best-effort basis. 55 f.cleanup = runtime.AddCleanup(f, func(fileName string) { 56 panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", fileName)) 57 }, f.Name()) 58 59 return f, nil 60 } 61 62 // Open is like os.Open, but returns a read-locked file. 63 func Open(name string) (*File, error) { 64 return OpenFile(name, os.O_RDONLY, 0) 65 } 66 67 // Create is like os.Create, but returns a write-locked file. 68 func Create(name string) (*File, error) { 69 return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) 70 } 71 72 // Edit creates the named file with mode 0666 (before umask), 73 // but does not truncate existing contents. 74 // 75 // If Edit succeeds, methods on the returned File can be used for I/O. 76 // The associated file descriptor has mode O_RDWR and the file is write-locked. 77 func Edit(name string) (*File, error) { 78 return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666) 79 } 80 81 // Close unlocks and closes the underlying file. 82 // 83 // Close may be called multiple times; all calls after the first will return a 84 // non-nil error. 85 func (f *File) Close() error { 86 if f.closed { 87 return &fs.PathError{ 88 Op: "close", 89 Path: f.Name(), 90 Err: fs.ErrClosed, 91 } 92 } 93 f.closed = true 94 95 err := closeFile(f.osFile.File) 96 f.cleanup.Stop() 97 return err 98 } 99 100 // Read opens the named file with a read-lock and returns its contents. 101 func Read(name string) ([]byte, error) { 102 f, err := Open(name) 103 if err != nil { 104 return nil, err 105 } 106 defer f.Close() 107 108 return io.ReadAll(f) 109 } 110 111 // Write opens the named file (creating it with the given permissions if needed), 112 // then write-locks it and overwrites it with the given content. 113 func Write(name string, content io.Reader, perm fs.FileMode) (err error) { 114 f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 115 if err != nil { 116 return err 117 } 118 119 _, err = io.Copy(f, content) 120 if closeErr := f.Close(); err == nil { 121 err = closeErr 122 } 123 return err 124 } 125 126 // Transform invokes t with the result of reading the named file, with its lock 127 // still held. 128 // 129 // If t returns a nil error, Transform then writes the returned contents back to 130 // the file, making a best effort to preserve existing contents on error. 131 // 132 // t must not modify the slice passed to it. 133 func Transform(name string, t func([]byte) ([]byte, error)) (err error) { 134 f, err := Edit(name) 135 if err != nil { 136 return err 137 } 138 defer f.Close() 139 140 old, err := io.ReadAll(f) 141 if err != nil { 142 return err 143 } 144 145 new, err := t(old) 146 if err != nil { 147 return err 148 } 149 150 if len(new) > len(old) { 151 // The overall file size is increasing, so write the tail first: if we're 152 // about to run out of space on the disk, we would rather detect that 153 // failure before we have overwritten the original contents. 154 if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil { 155 // Make a best effort to remove the incomplete tail. 156 f.Truncate(int64(len(old))) 157 return err 158 } 159 } 160 161 // We're about to overwrite the old contents. In case of failure, make a best 162 // effort to roll back before we close the file. 163 defer func() { 164 if err != nil { 165 if _, err := f.WriteAt(old, 0); err == nil { 166 f.Truncate(int64(len(old))) 167 } 168 } 169 }() 170 171 if len(new) >= len(old) { 172 if _, err := f.WriteAt(new[:len(old)], 0); err != nil { 173 return err 174 } 175 } else { 176 if _, err := f.WriteAt(new, 0); err != nil { 177 return err 178 } 179 // The overall file size is decreasing, so shrink the file to its final size 180 // after writing. We do this after writing (instead of before) so that if 181 // the write fails, enough filesystem space will likely still be reserved 182 // to contain the previous contents. 183 if err := f.Truncate(int64(len(new))); err != nil { 184 return err 185 } 186 } 187 188 return nil 189 } 190