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 // f may be dead at the moment after we access f.cleanup, 98 // so the cleanup can fire before Stop completes. Keep f 99 // alive while we call Stop. See the documentation for 100 // runtime.Cleanup.Stop. 101 runtime.KeepAlive(f) 102 return err 103 } 104 105 // Read opens the named file with a read-lock and returns its contents. 106 func Read(name string) ([]byte, error) { 107 f, err := Open(name) 108 if err != nil { 109 return nil, err 110 } 111 defer f.Close() 112 113 return io.ReadAll(f) 114 } 115 116 // Write opens the named file (creating it with the given permissions if needed), 117 // then write-locks it and overwrites it with the given content. 118 func Write(name string, content io.Reader, perm fs.FileMode) (err error) { 119 f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 120 if err != nil { 121 return err 122 } 123 124 _, err = io.Copy(f, content) 125 if closeErr := f.Close(); err == nil { 126 err = closeErr 127 } 128 return err 129 } 130 131 // Transform invokes t with the result of reading the named file, with its lock 132 // still held. 133 // 134 // If t returns a nil error, Transform then writes the returned contents back to 135 // the file, making a best effort to preserve existing contents on error. 136 // 137 // t must not modify the slice passed to it. 138 func Transform(name string, t func([]byte) ([]byte, error)) (err error) { 139 f, err := Edit(name) 140 if err != nil { 141 return err 142 } 143 defer f.Close() 144 145 old, err := io.ReadAll(f) 146 if err != nil { 147 return err 148 } 149 150 new, err := t(old) 151 if err != nil { 152 return err 153 } 154 155 if len(new) > len(old) { 156 // The overall file size is increasing, so write the tail first: if we're 157 // about to run out of space on the disk, we would rather detect that 158 // failure before we have overwritten the original contents. 159 if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil { 160 // Make a best effort to remove the incomplete tail. 161 f.Truncate(int64(len(old))) 162 return err 163 } 164 } 165 166 // We're about to overwrite the old contents. In case of failure, make a best 167 // effort to roll back before we close the file. 168 defer func() { 169 if err != nil { 170 if _, err := f.WriteAt(old, 0); err == nil { 171 f.Truncate(int64(len(old))) 172 } 173 } 174 }() 175 176 if len(new) >= len(old) { 177 if _, err := f.WriteAt(new[:len(old)], 0); err != nil { 178 return err 179 } 180 } else { 181 if _, err := f.WriteAt(new, 0); err != nil { 182 return err 183 } 184 // The overall file size is decreasing, so shrink the file to its final size 185 // after writing. We do this after writing (instead of before) so that if 186 // the write fails, enough filesystem space will likely still be reserved 187 // to contain the previous contents. 188 if err := f.Truncate(int64(len(new))); err != nil { 189 return err 190 } 191 } 192 193 return nil 194 } 195