// Execute runs a command and optionally reloads it func (s *WatchStep) Execute(ctx context.Context, sess *core.Session) (int, error) { e, err := core.EmitterFromContext(ctx) if err != nil { return -1, err } // TODO(termie): PACKAGING make this a feature of session and remove // the calls into its struct // Start watching our stdout stopListening := make(chan struct{}) defer func() { stopListening <- struct{}{} }() go func() { for { select { case line := <-sess.Recv(): e.Emit(core.Logs, &core.LogsArgs{ // Hidden: sess.logsHidden, Logs: line, }) // We need to make sure we stop eating the stdout from the container // promiscuously when we finish out step case <-stopListening: return } } }() // cheating to get containerID // TODO(termie): we should deal with this eventually dt := sess.Transport().(*DockerTransport) containerID := dt.containerID // Set up a signal handler to end our step. finishedStep := make(chan struct{}) stopWatchHandler := &util.SignalHandler{ ID: "stop-watch", // Signal our stuff to stop and finish the step, return false to // signify that we've handled the signal and don't process further F: func() bool { s.logger.Println("Keyboard interrupt detected, finishing step") finishedStep <- struct{}{} return false }, } util.GlobalSigint().Add(stopWatchHandler) // NOTE(termie): I think the only way to exit this code is via this // signal handler and the signal monkey removes handlers // after it processes them, so this may be superfluous defer util.GlobalSigint().Remove(stopWatchHandler) // If we're not going to reload just run the thing once, synchronously if !s.reload { err := sess.Send(ctx, false, "set +e", s.Code) if err != nil { return 0, err } <-finishedStep // ignoring errors s.killProcesses(containerID, "INT") return 0, nil } f := &util.Formatter{s.options.GlobalOptions.ShowColors} s.logger.Info(f.Info("Reloading on file changes")) doCmd := func() { err := sess.Send(ctx, false, "set +e", s.Code) if err != nil { s.logger.Errorln(err) return } open, err := exposedPortMaps(s.dockerOptions.DockerHost, s.options.PublishPorts) if err != nil { s.logger.Warnf(f.Info("There was a problem parsing your docker host."), err) return } for _, uri := range open { s.logger.Infof(f.Info("Forwarding %s to %s on the container."), uri.HostURI, uri.ContainerPort) } } // Otherwise set up a watcher and do some magic watcher, err := s.watch(s.options.ProjectPath) if err != nil { return -1, err } debounce := util.NewDebouncer(2 * time.Second) done := make(chan struct{}) go func() { for { select { case event := <-watcher.Events: s.logger.Debugln("fsnotify event", event.String()) if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create || event.Op&fsnotify.Remove == fsnotify.Remove { if !strings.HasPrefix(filepath.Base(event.Name), ".") { s.logger.Debug(f.Info("Modified file", event.Name)) debounce.Trigger() } } case <-debounce.C: err := s.killProcesses(containerID, "INT") if err != nil { s.logger.Panic(err) return } s.logger.Info(f.Info("Reloading")) go doCmd() case err := <-watcher.Errors: s.logger.Error(err) done <- struct{}{} return case <-finishedStep: s.killProcesses(containerID, "INT") done <- struct{}{} return } } }() // Run build on first run debounce.Trigger() <-done return 0, nil }