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  }