-
-
Notifications
You must be signed in to change notification settings - Fork 114
feat: Ability to view all logs with prefix when processes table header is selected #437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -66,6 +66,8 @@ type ProjectRunner struct { | |||||||||||||||||||||||||||||||
| procCompleteChannel chan int | ||||||||||||||||||||||||||||||||
| processTree *ProcessTree | ||||||||||||||||||||||||||||||||
| processScheduler *scheduler.Scheduler | ||||||||||||||||||||||||||||||||
| isRestartingAll bool | ||||||||||||||||||||||||||||||||
| restartAllMutex sync.Mutex | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // RestartCall represents an in-flight restart operation | ||||||||||||||||||||||||||||||||
|
|
@@ -161,6 +163,14 @@ func (p *ProjectRunner) Run() error { | |||||||||||||||||||||||||||||||
| case runProcCount := <-p.procCompleteChannel: | ||||||||||||||||||||||||||||||||
| log.Debug().Msgf("Remaining processes: %d", runProcCount) | ||||||||||||||||||||||||||||||||
| if runProcCount == 0 { | ||||||||||||||||||||||||||||||||
| // Check if a restart-all operation is in progress | ||||||||||||||||||||||||||||||||
| p.restartAllMutex.Lock() | ||||||||||||||||||||||||||||||||
| isRestarting := p.isRestartingAll | ||||||||||||||||||||||||||||||||
| p.restartAllMutex.Unlock() | ||||||||||||||||||||||||||||||||
| if isRestarting { | ||||||||||||||||||||||||||||||||
| log.Debug().Msg("Restart all in progress, not exiting") | ||||||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| if p.processScheduler == nil || len(p.processScheduler.GetScheduledProcesses()) == 0 { | ||||||||||||||||||||||||||||||||
| log.Info().Msg("Project completed") | ||||||||||||||||||||||||||||||||
| p.exitCodeMutex.Lock() | ||||||||||||||||||||||||||||||||
|
|
@@ -561,6 +571,106 @@ func (p *ProjectRunner) doRestart(name string) error { | |||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| func (p *ProjectRunner) RestartAllProcesses() error { | ||||||||||||||||||||||||||||||||
| log.Info().Msg("Restarting all processes") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Set flag to prevent main loop from exiting when process count reaches 0 | ||||||||||||||||||||||||||||||||
| p.restartAllMutex.Lock() | ||||||||||||||||||||||||||||||||
| p.isRestartingAll = true | ||||||||||||||||||||||||||||||||
| p.restartAllMutex.Unlock() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Build shutdown order | ||||||||||||||||||||||||||||||||
| p.runProcMutex.Lock() | ||||||||||||||||||||||||||||||||
| shutdownOrder := []*Process{} | ||||||||||||||||||||||||||||||||
| if p.isOrderedShutdown { | ||||||||||||||||||||||||||||||||
| err := p.project.WithProcesses([]string{}, func(process types.ProcessConfig) error { | ||||||||||||||||||||||||||||||||
| if runningProc, ok := p.runningProcesses[process.ReplicaName]; ok { | ||||||||||||||||||||||||||||||||
| shutdownOrder = append(shutdownOrder, runningProc) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||
| log.Error().Msgf("Failed to build project run order: %s", err.Error()) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| slices.Reverse(shutdownOrder) | ||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||
| for _, proc := range p.runningProcesses { | ||||||||||||||||||||||||||||||||
| shutdownOrder = append(shutdownOrder, proc) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| p.runProcMutex.Unlock() | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| var nameOrder []string | ||||||||||||||||||||||||||||||||
| for _, v := range shutdownOrder { | ||||||||||||||||||||||||||||||||
| nameOrder = append(nameOrder, v.getName()) | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| log.Debug().Msgf("Stopping %d processes for restart. Order: %q", len(shutdownOrder), nameOrder) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Prepare all processes for shutdown (prevents auto-restart) | ||||||||||||||||||||||||||||||||
| for _, proc := range shutdownOrder { | ||||||||||||||||||||||||||||||||
| proc.prepareForShutDown() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Stop all processes | ||||||||||||||||||||||||||||||||
| p.shutDownAndWait(shutdownOrder) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Clear done processes map | ||||||||||||||||||||||||||||||||
| p.doneProcMutex.Lock() | ||||||||||||||||||||||||||||||||
| p.doneProcesses = make(map[string]*Process) | ||||||||||||||||||||||||||||||||
|
Comment on lines
+617
to
+619
|
||||||||||||||||||||||||||||||||
| // Clear done processes map | |
| p.doneProcMutex.Lock() | |
| p.doneProcesses = make(map[string]*Process) | |
| // Clear done processes map (in-place to avoid changing map identity) | |
| p.doneProcMutex.Lock() | |
| for name := range p.doneProcesses { | |
| delete(p.doneProcesses, name) | |
| } |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The process state reset sets all states to Pending and clears runtime information, but it doesn't reset other fields like SystemTime, Health, Mem, CPU, or Restarts. This could lead to stale information being displayed in the UI immediately after restart. Consider whether these fields should also be reset, or if there's a mechanism to refresh them when processes start.
| state.IsRunning = false | |
| state.IsRunning = false | |
| state.SystemTime = "" | |
| state.Health = "" | |
| state.Mem = 0 | |
| state.CPU = 0 | |
| state.Restarts = 0 |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an error occurs while building the run order (line 641), the restart flag is properly cleared, but the system is left in an inconsistent state - all processes have been stopped but won't be restarted. Consider whether partial recovery is possible (e.g., attempting to restart processes that were successfully configured) or if this error scenario should trigger a more graceful degradation.
| // Clear the restart flag on error | |
| p.restartAllMutex.Lock() | |
| p.isRestartingAll = false | |
| p.restartAllMutex.Unlock() | |
| return fmt.Errorf("failed to build project run order: %w", err) | |
| // If we could not collect any process at all, treat this as fatal | |
| if len(runOrder) == 0 { | |
| // Clear the restart flag on error | |
| p.restartAllMutex.Lock() | |
| p.isRestartingAll = false | |
| p.restartAllMutex.Unlock() | |
| return fmt.Errorf("failed to build project run order: %w", err) | |
| } | |
| // Otherwise, log the error but continue with the processes we did collect | |
| log.Error().Err(err).Msg("failed to fully build project run order; proceeding with partial run order") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package tui | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "hash/fnv" | ||
| "math" | ||
|
|
||
| "github.com/f1bonacc1/process-compose/src/pclog" | ||
| ) | ||
|
|
||
| // tviewColors is a list of tview color names for process name prefixes. | ||
| var tviewColors = []string{ | ||
| "red", | ||
| "green", | ||
| "yellow", | ||
| "blue", | ||
| "magenta", | ||
| "cyan", | ||
| "orange", | ||
| "pink", | ||
| "lime", | ||
| "aqua", | ||
| "violet", | ||
| "gold", | ||
| } | ||
|
|
||
| // getProcessColor returns a tview color name for the given process name. | ||
| func getProcessColor(name string) string { | ||
| hash := fnv.New32a() | ||
| hash.Write([]byte(name)) | ||
| return tviewColors[int(hash.Sum32())%len(tviewColors)] | ||
| } | ||
|
Comment on lines
+12
to
+32
|
||
|
|
||
| // AllLogsObserver wraps a LogView and prefixes log lines with a colored process name. | ||
| // It implements pclog.LogObserver interface. | ||
| type AllLogsObserver struct { | ||
| processName string | ||
| logView *LogView | ||
| color string | ||
| uniqueID string | ||
| } | ||
|
|
||
| // NewAllLogsObserver creates a new AllLogsObserver for the given process. | ||
| func NewAllLogsObserver(processName string, logView *LogView) *AllLogsObserver { | ||
| return &AllLogsObserver{ | ||
| processName: processName, | ||
| logView: logView, | ||
| color: getProcessColor(processName), | ||
| uniqueID: pclog.GenerateUniqueID(10), | ||
| } | ||
| } | ||
|
|
||
| // WriteString writes a log line prefixed with the colored process name. | ||
| func (o *AllLogsObserver) WriteString(line string) (n int, err error) { | ||
| return o.logView.WriteStringWithProcess(line, o.processName, o.color) | ||
| } | ||
|
|
||
| // SetLines sets multiple log lines, each prefixed with the colored process name. | ||
| func (o *AllLogsObserver) SetLines(lines []string) { | ||
| for _, line := range lines { | ||
| _, _ = o.WriteString(line) | ||
| } | ||
| } | ||
|
|
||
| // GetTailLength returns the tail length for log subscription. | ||
| func (o *AllLogsObserver) GetTailLength() int { | ||
| return math.MaxInt | ||
| } | ||
|
|
||
| // GetUniqueID returns the unique identifier for this observer. | ||
| func (o *AllLogsObserver) GetUniqueID() string { | ||
| return fmt.Sprintf("all-logs-%s-%s", o.processName, o.uniqueID) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The restartAllMutex is used to protect the isRestartingAll flag, but there's a potential race condition window. Between lines 601 and 609, the runProcMutex is released but processes haven't been shut down yet. If another operation tries to interact with processes during this window, it might see inconsistent state. Consider holding the runProcMutex for the entire shutdown phase, or document why this early release is safe.