github.com/release-engineering/exodus-rsync@v1.11.2/internal/cmd/mixed.go (about) 1 package cmd 2 3 import ( 4 "bufio" 5 "context" 6 "io" 7 "os/exec" 8 "sync" 9 10 "github.com/release-engineering/exodus-rsync/internal/args" 11 "github.com/release-engineering/exodus-rsync/internal/conf" 12 "github.com/release-engineering/exodus-rsync/internal/log" 13 "github.com/release-engineering/exodus-rsync/internal/rsync" 14 ) 15 16 // Mixed publish mode, publishing both via exodus and rsync. 17 // If either one fails, it kills/cancels the other. 18 func mixedMain(ctx context.Context, cfg conf.Config, args args.Config) int { 19 logger := log.FromContext(ctx) 20 21 rsyncCmd, err := ext.rsync.Command(ctx, rsync.Arguments(ctx, args)) 22 if err != nil { 23 logger.F("error", err).Error("Failed to generate rsync command") 24 return 25 25 } 26 27 ctx, cancelFn := context.WithCancel(ctx) 28 29 wg := sync.WaitGroup{} 30 wg.Add(2) 31 32 defer wg.Wait() 33 defer cancelFn() 34 35 // If either one of the publishes fails, we call this function to bail out. 36 // 37 // The main point of this is to ensure that the most relevant log message appears 38 // last. If we just let cancel & return happen "naturally", then e.g. if rsync 39 // fails, the last few error messages will be about the cancellation of exodus publish 40 // and a non-expert reader will probably wrongly conclude that *this* was the problem 41 // and miss the error message relating to rsync. 42 bailOut := func(cancelMessage, errorMessage string, exitCode int) int { 43 logger.Warn(cancelMessage) 44 45 // If one of rsync/exodus is still in progress, cancel it. 46 cancelFn() 47 48 // Wait for both to complete/fail. 49 wg.Wait() 50 51 // Then log the message. 52 logger.Error(errorMessage) 53 54 return exitCode 55 } 56 57 var lastCode *chan int 58 rsyncCode := make(chan int, 1) 59 exodusCode := make(chan int, 1) 60 61 // Let rsync & exodus publishes run in their own goroutines. 62 go func() { 63 defer wg.Done() 64 exodusCode <- exodusMain(ctx, cfg, args) 65 }() 66 67 go func() { 68 defer wg.Done() 69 rsyncCode <- doRsyncCommand(ctx, rsyncCmd) 70 }() 71 72 select { 73 case code := <-exodusCode: 74 if code != 0 { 75 return bailOut( 76 "Cancelling rsync due to errors in exodus publish...", 77 "Publish via exodus-gw failed", code) 78 } 79 logger.Info("Finished exodus publish, waiting on rsync...") 80 lastCode = &rsyncCode 81 case code := <-rsyncCode: 82 if code != 0 { 83 return bailOut( 84 "Cancelling exodus publish due to errors in rsync...", 85 "Publish via rsync failed", code) 86 } 87 logger.Info("Finished rsync publish, waiting on exodus...") 88 lastCode = &exodusCode 89 } 90 91 return <-*lastCode 92 } 93 94 func doRsyncCommand(ctx context.Context, cmd *exec.Cmd) int { 95 logger := log.FromContext(ctx) 96 97 var outPipe, errPipe io.ReadCloser 98 99 outPipe, err := cmd.StdoutPipe() 100 if err == nil { 101 errPipe, err = cmd.StderrPipe() 102 } 103 if err != nil { 104 logger.F("error", err).Error("Can't connect pipes to rsync") 105 return 39 106 } 107 108 outScanner := bufio.NewScanner(outPipe) 109 errScanner := bufio.NewScanner(errPipe) 110 111 err = cmd.Start() 112 if err != nil { 113 logger.F("error", err).Error("Failed to run rsync") 114 return 25 115 } 116 117 pid := cmd.Process.Pid 118 119 wg := sync.WaitGroup{} 120 wg.Add(2) 121 122 entry := logger.F("rsync", pid) 123 type logFunc func(string) 124 125 piper := func(scanner *bufio.Scanner, log logFunc) { 126 defer wg.Done() 127 for { 128 if !scanner.Scan() { 129 return 130 } 131 log(scanner.Text()) 132 } 133 } 134 135 go piper(outScanner, entry.Info) 136 go piper(errScanner, entry.Warn) 137 wg.Wait() 138 139 err = cmd.Wait() 140 if err != nil { 141 logger.F("error", err).Error("rsync failed") 142 return 130 143 } 144 145 return 0 146 }