trpc.group/trpc-go/trpc-go@v1.0.3/log/rollwriter/roll_writer_windows.go (about) 1 // 2 // 3 // Tencent is pleased to support the open source community by making tRPC available. 4 // 5 // Copyright (C) 2023 THL A29 Limited, a Tencent company. 6 // All rights reserved. 7 // 8 // If you have downloaded a copy of the tRPC source code from Tencent, 9 // please note that tRPC source code is licensed under the Apache 2.0 License, 10 // A copy of the Apache 2.0 License is included in this file. 11 // 12 // 13 14 //go:build windows 15 // +build windows 16 17 package rollwriter 18 19 import ( 20 "fmt" 21 "os" 22 "path" 23 "path/filepath" 24 "sync/atomic" 25 "time" 26 ) 27 28 const ( 29 tmpTimeFormat = "tmp-20060102-150405.00000" 30 bkTimeFormat = "bk-20060102-150405.00000" 31 ) 32 33 // doReopenFile reopens the file. 34 // 35 // On Windows, the given arguments no longer directly refer to concrete files. 36 // Instead, they are symbolic links to some well-formatted temporary files. 37 // After the new temporary file and new link are created, the old link will be 38 // removed. The old temporary file will be renamed exactly to the old link name. 39 // 40 // Generally, there are only two crucial cases involved: 41 // 1. This function is called because the log reaches the maximum size: 42 // The arguments are like: 43 // * `newLink` = "./trpc.log" 44 // * `oldLink` = "./trpc.log.bk-20230117-180000.00127" 45 // Then this function will create newLink which points to a new temporary file. 46 // And rename the old temporary file as `oldLink` (asynchronously). 47 // 2. This function is called because the log file needs to be reopened regularly 48 // to ensure that the underlying still exists: 49 // Two sub-cases are involved: 50 // 2.1. `newLink` == `oldLink`: 51 // In this case, we do not want to create a new temporary file (otherwise you 52 // would get tons of temporary files). Instead, we reopen the existing temporary 53 // file and do nothing with the link (because the link still refers to the same 54 // temporary file). 55 // 2.2. `newLink` != `oldLink`: 56 // In this case, the typical arguments are like: 57 // * `newLink` = "./trpc.log.2023011718" 58 // * `oldLink` = "./trpc.log.2023011717" 59 // Explanation: this function is called because of rolling by time. The newLink 60 // is present one hour later than the oldLink. 61 // Under this circumstance, we need to create a new temporary file and link it 62 // with the `newLink`. The old temporary file should be renamed as the `oldLink` 63 // name (asynchronously). Actually the behavior is the same as case 1, but the 64 // meaning and semantics of the arguments are different. 65 func (w *RollWriter) doReopenFile(newLink string, oldLink string) error { 66 atomic.StoreInt64(&w.openTime, time.Now().Unix()) 67 if w.tryResume(newLink, oldLink) { 68 return nil 69 } 70 // Case 2.1. `newLink` == `oldLink`: 71 if newLink == oldLink { 72 last := w.getCurrFile() 73 if last == nil { 74 return fmt.Errorf( 75 "w.getCurrFile should not be nil when newLink == oldLink (newLink now is %s)", 76 newLink) 77 } 78 f, err := w.os.OpenFile(last.Name(), os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 79 if err != nil { 80 return fmt.Errorf("os.OpenFile %s err: %w", last.Name(), err) 81 } 82 w.setCurrFile(f) 83 w.delayCloseAndRenameFile(&closeAndRenameFile{file: last}) // rename = "", so no renaming will happen. 84 return nil 85 } 86 87 // Case 1. and case 2.2. `newLink` != `oldLink`: 88 89 // Example tmp string: 90 // 1. roll by size = true/false, roll by time = false 91 // ./tmp-20230117-180000.00127.trpc.log 92 // 2. roll by time = true and this function is called by reopenFile 93 // ./tmp-20230117-180000.00127.trpc.log.2023011717 94 // Note: use base name of `newLink` as suffix instead of prefix to prevent 95 // temporary files from being recognized as valid backup files. 96 tmp := path.Join(w.currDir, time.Now().Format(tmpTimeFormat)+"."+filepath.Base(newLink)) 97 f, err := w.os.OpenFile(tmp, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 98 if err != nil { 99 return fmt.Errorf("os.OpenFile %s err: %w", tmp, err) 100 } 101 w.removeLink(newLink) 102 if err := os.Symlink(tmp, newLink); err != nil { 103 return fmt.Errorf("os.Symlink %s to %s err: %w", tmp, newLink, err) 104 } 105 w.removeLink(oldLink) 106 last := w.getCurrFile() 107 if last != nil { 108 w.delayCloseAndRenameFile(&closeAndRenameFile{file: last, rename: oldLink}) 109 } 110 w.setCurrFile(f) 111 return nil 112 } 113 114 func (w *RollWriter) tryResume(newLink, oldLink string) bool { 115 // Check if there exists `trpc.log -> tmp.xxxx.log`. 116 if oldLink != "" { 117 return false 118 } 119 st, err := os.Lstat(newLink) 120 if os.IsNotExist(err) { // The link `trpc.log` does not exist. 121 return false 122 } 123 if !isSymlink(st.Mode()) { // `trpc.log` exists, but it is not a link. 124 // Rename it to backup. 125 // If the directory contains trpc.log, the log cannot be written correctly. 126 // Because it is not possible to create a link with the same name. 127 w.os.Rename(newLink, path.Join(w.currDir, time.Now().Format(bkTimeFormat)+"."+filepath.Base(newLink))) 128 return false 129 } 130 131 // The following fixes the problem: 132 // When the service stops, the tmp log is not processed. After restarting, a new tmp file is generated 133 // and rolling continues, which can make it difficult to view the log properly. 134 fileName, err := os.Readlink(newLink) 135 if err != nil { 136 fmt.Printf("os.Readlink %s err: %+v\n", newLink, err) 137 return false 138 } 139 f, err := w.os.OpenFile(fileName, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666) 140 if err != nil { 141 fmt.Printf("w.os.OpenFile %s err: %+v\n", fileName, err) 142 return false 143 } 144 w.setCurrFile(f) 145 return true 146 } 147 148 // backupFile backs this file up and reopen a new one if file size is too large. 149 func (w *RollWriter) backupFile() { 150 if !(w.opts.MaxSize > 0 && atomic.LoadInt64(&w.currSize) >= w.opts.MaxSize) { 151 return 152 } 153 atomic.StoreInt64(&w.currSize, 0) 154 backup := w.currPath + "." + time.Now().Format(backupTimeFormat) 155 if err := w.doReopenFile(w.currPath, backup); err != nil { 156 fmt.Printf("w.doReopenFile %s err: %+v\n", w.currPath, err) 157 } 158 w.notify() 159 } 160 161 func (w *RollWriter) removeLink(path string) { 162 st, err := os.Lstat(path) 163 if err != nil { 164 return 165 } 166 if !isSymlink(st.Mode()) { 167 return 168 } 169 if err := w.os.Remove(path); err != nil { 170 fmt.Printf("os.Remove existing symlink %s err: %+v", path, err) 171 } 172 } 173 174 func isSymlink(m os.FileMode) bool { 175 return m&os.ModeSymlink != 0 176 }