github.com/mutagen-io/mutagen@v0.18.0-rc1/cmd/mutagen/sync/monitor.go (about) 1 package sync 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "path" 9 "time" 10 11 "github.com/spf13/cobra" 12 13 "github.com/fatih/color" 14 15 "github.com/dustin/go-humanize" 16 17 "github.com/mutagen-io/mutagen/cmd" 18 "github.com/mutagen-io/mutagen/cmd/mutagen/common" 19 "github.com/mutagen-io/mutagen/cmd/mutagen/common/templating" 20 "github.com/mutagen-io/mutagen/cmd/mutagen/daemon" 21 22 synchronizationmodels "github.com/mutagen-io/mutagen/pkg/api/models/synchronization" 23 "github.com/mutagen-io/mutagen/pkg/grpcutil" 24 "github.com/mutagen-io/mutagen/pkg/platform/terminal" 25 selectionpkg "github.com/mutagen-io/mutagen/pkg/selection" 26 synchronizationsvc "github.com/mutagen-io/mutagen/pkg/service/synchronization" 27 "github.com/mutagen-io/mutagen/pkg/synchronization" 28 "github.com/mutagen-io/mutagen/pkg/synchronization/rsync" 29 ) 30 31 // computeMonitorStatusLine constructs a monitoring status line for a 32 // synchronization session. 33 func computeMonitorStatusLine(state *synchronization.State) string { 34 // Build the status line. 35 var status string 36 if state.Session.Paused { 37 status += color.YellowString("[Paused]") 38 } else { 39 // Add a conflict flag if there are conflicts. 40 if len(state.Conflicts) > 0 { 41 status += color.YellowString("[C] ") 42 } 43 44 // Add a problems flag if there are problems. 45 haveProblems := len(state.AlphaState.ScanProblems) > 0 || 46 len(state.BetaState.ScanProblems) > 0 || 47 len(state.AlphaState.TransitionProblems) > 0 || 48 len(state.BetaState.TransitionProblems) > 0 49 if haveProblems { 50 status += color.YellowString("[!] ") 51 } 52 53 // Add an error flag if there is one present. 54 if state.LastError != "" { 55 status += color.RedString("[X] ") 56 } 57 58 // Handle the formatting based on status. If we're in a staging mode, 59 // then extract the relevant progress information. Despite not having a 60 // built-in mechanism for knowing the total expected size of a staging 61 // operation, we do know the number of files that the staging operation 62 // is performing, so if that's equal to the number of files on the 63 // source endpoint, then we know that we can use the total file size on 64 // the source endpoint as an estimate for the total staging size. 65 var stagingProgress *rsync.ReceiverState 66 var totalExpectedSize uint64 67 if state.Status == synchronization.Status_StagingAlpha { 68 status += "[←] " 69 stagingProgress = state.AlphaState.StagingProgress 70 if stagingProgress == nil { 71 status += "Preparing to stage files on alpha" 72 } else if stagingProgress.ExpectedFiles == state.BetaState.Files { 73 totalExpectedSize = state.BetaState.TotalFileSize 74 } 75 } else if state.Status == synchronization.Status_StagingBeta { 76 status += "[→] " 77 stagingProgress = state.BetaState.StagingProgress 78 if stagingProgress == nil { 79 status += "Preparing to stage files on beta" 80 } else if stagingProgress.ExpectedFiles == state.AlphaState.Files { 81 totalExpectedSize = state.AlphaState.TotalFileSize 82 } 83 } else { 84 status += state.Status.Description() 85 } 86 87 // Print staging progress, if available. 88 if stagingProgress != nil { 89 var fractionComplete float32 90 var totalSizeDenominator string 91 if totalExpectedSize != 0 { 92 fractionComplete = float32(stagingProgress.TotalReceivedSize) / float32(totalExpectedSize) 93 totalSizeDenominator = "/" + humanize.Bytes(totalExpectedSize) 94 } else { 95 fractionComplete = float32(stagingProgress.ReceivedFiles) / float32(stagingProgress.ExpectedFiles) 96 } 97 status += fmt.Sprintf("[%d/%d - %s%s - %.0f%%] %s (%s/%s)", 98 stagingProgress.ReceivedFiles, stagingProgress.ExpectedFiles, 99 humanize.Bytes(stagingProgress.TotalReceivedSize), totalSizeDenominator, 100 100.0*fractionComplete, 101 terminal.NeutralizeControlCharacters(path.Base(stagingProgress.Path)), 102 humanize.Bytes(stagingProgress.ReceivedSize), humanize.Bytes(stagingProgress.ExpectedSize), 103 ) 104 } 105 } 106 107 // Done. 108 return status 109 } 110 111 // monitorMain is the entry point for the monitor command. 112 func monitorMain(_ *cobra.Command, arguments []string) error { 113 // Create the session selection specification that will select our initial 114 // batch of sessions. 115 selection := &selectionpkg.Selection{ 116 All: len(arguments) == 0 && monitorConfiguration.labelSelector == "", 117 Specifications: arguments, 118 LabelSelector: monitorConfiguration.labelSelector, 119 } 120 if err := selection.EnsureValid(); err != nil { 121 return fmt.Errorf("invalid session selection specification: %w", err) 122 } 123 124 // Load the formatting template (if any has been specified). 125 template, err := monitorConfiguration.TemplateFlags.LoadTemplate() 126 if err != nil { 127 return fmt.Errorf("unable to load formatting template: %w", err) 128 } 129 130 // Determine the listing mode. 131 mode := common.SessionDisplayModeMonitor 132 if monitorConfiguration.long { 133 mode = common.SessionDisplayModeMonitorLong 134 } 135 136 // Connect to the daemon and defer closure of the connection. 137 daemonConnection, err := daemon.Connect(true, true) 138 if err != nil { 139 return fmt.Errorf("unable to connect to daemon: %w", err) 140 } 141 defer daemonConnection.Close() 142 143 // Create a session service client. 144 synchronizationService := synchronizationsvc.NewSynchronizationClient(daemonConnection) 145 146 // Create the list request that we'll use. 147 request := &synchronizationsvc.ListRequest{ 148 Selection: selection, 149 } 150 151 // If no template has been specified, then create a status line printer with 152 // bold text and defer a line break operation. 153 var statusLinePrinter *cmd.StatusLinePrinter 154 if template == nil { 155 statusLinePrinter = &cmd.StatusLinePrinter{ 156 Color: color.New(color.Bold), 157 } 158 defer statusLinePrinter.BreakIfPopulated() 159 } 160 161 // Track the last update time. 162 var lastUpdateTime time.Time 163 164 // Track whether or not we've identified an individual session in the 165 // non-templated case. 166 var identifiedSingleTargetSession bool 167 168 // Loop and print monitoring information indefinitely. 169 for { 170 // Regulate the update frequency (and tame CPU usage in both the monitor 171 // command and the daemon) by enforcing a minimum update cycle interval. 172 now := time.Now() 173 timeSinceLastUpdate := now.Sub(lastUpdateTime) 174 if timeSinceLastUpdate < common.MinimumMonitorUpdateInterval { 175 time.Sleep(common.MinimumMonitorUpdateInterval - timeSinceLastUpdate) 176 } 177 lastUpdateTime = now 178 179 // Perform a list operation. 180 response, err := synchronizationService.List(context.Background(), request) 181 if err != nil { 182 return fmt.Errorf("list failed: %w", grpcutil.PeelAwayRPCErrorLayer(err)) 183 } else if err = response.EnsureValid(); err != nil { 184 return fmt.Errorf("invalid list response received: %w", err) 185 } 186 187 // Update the state tracking index. 188 request.PreviousStateIndex = response.StateIndex 189 190 // If a template has been specified, then use that to format output with 191 // public model types. No validation is necessary here since we don't 192 // require any specific number of sessions. 193 if template != nil { 194 sessions := synchronizationmodels.ExportSessions(response.SessionStates) 195 if err := template.Execute(os.Stdout, sessions); err != nil { 196 return fmt.Errorf("unable to execute formatting template: %w", err) 197 } 198 continue 199 } 200 201 // No template has been specified, but our command line monitoring 202 // interface only supports dynamic status displays for a single session 203 // at a time, so we choose the newest session identified by the initial 204 // criteria and update our selection to target it specifically. 205 var state *synchronization.State 206 if !identifiedSingleTargetSession { 207 if len(response.SessionStates) == 0 { 208 err = errors.New("no matching sessions exist") 209 } else { 210 // Select the most recently created session matching the 211 // selection criteria (which are ordered by creation date). 212 state = response.SessionStates[len(response.SessionStates)-1] 213 214 // Update the selection criteria to target only that session. 215 request.Selection = &selectionpkg.Selection{ 216 Specifications: []string{state.Session.Identifier}, 217 } 218 219 // Print session information. 220 printSession(state, mode) 221 222 // Record that we've identified our target session. 223 identifiedSingleTargetSession = true 224 } 225 } else if len(response.SessionStates) != 1 { 226 err = errors.New("invalid list response") 227 } else { 228 state = response.SessionStates[0] 229 } 230 if err != nil { 231 return err 232 } 233 234 // Compute the status line. 235 statusLine := computeMonitorStatusLine(state) 236 237 // Print the status line. 238 statusLinePrinter.Print(statusLine) 239 } 240 } 241 242 // monitorCommand is the monitor command. 243 var monitorCommand = &cobra.Command{ 244 Use: "monitor [<session>...]", 245 Short: "Display streaming session status information", 246 RunE: monitorMain, 247 SilenceUsage: true, 248 } 249 250 // monitorConfiguration stores configuration for the monitor command. 251 var monitorConfiguration struct { 252 // help indicates whether or not to show help information and exit. 253 help bool 254 // long indicates whether or not to use long-format monitoring. 255 long bool 256 // labelSelector encodes a label selector to be used in identifying which 257 // sessions should be paused. 258 labelSelector string 259 // TemplateFlags store custom templating behavior. 260 templating.TemplateFlags 261 } 262 263 func init() { 264 // Grab a handle for the command line flags. 265 flags := monitorCommand.Flags() 266 267 // Disable alphabetical sorting of flags in help output. 268 flags.SortFlags = false 269 270 // Manually add a help flag to override the default message. Cobra will 271 // still implement its logic automatically. 272 flags.BoolVarP(&monitorConfiguration.help, "help", "h", false, "Show help information") 273 274 // Wire up monitor flags. 275 flags.BoolVarP(&monitorConfiguration.long, "long", "l", false, "Show detailed session information") 276 flags.StringVar(&monitorConfiguration.labelSelector, "label-selector", "", "Monitor the most recently created session matching the specified label selector") 277 278 // Wire up templating flags. 279 monitorConfiguration.TemplateFlags.Register(flags) 280 }