mirror of
https://github.com/kairos-io/kairos-agent.git
synced 2025-08-19 16:57:02 +00:00
246 lines
6.8 KiB
Go
246 lines
6.8 KiB
Go
package agent
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/kairos-io/kairos-sdk/types"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// Install Process Page
|
|
type installProcessPage struct {
|
|
progress int
|
|
step string
|
|
steps []string
|
|
done chan bool // Channel to signal when installation is complete
|
|
output chan string // Channel to receive output from the installer
|
|
cmd *exec.Cmd // Reference to the running installer command
|
|
}
|
|
|
|
func newInstallProcessPage() *installProcessPage {
|
|
return &installProcessPage{
|
|
progress: 0,
|
|
step: InstallDefaultStep,
|
|
steps: []string{
|
|
InstallDefaultStep,
|
|
InstallPartitionStep,
|
|
InstallBeforeInstallStep,
|
|
InstallActiveStep,
|
|
InstallBootloaderStep,
|
|
InstallRecoveryStep,
|
|
InstallPassiveStep,
|
|
InstallAfterInstallStep,
|
|
InstallCompleteStep,
|
|
},
|
|
done: make(chan bool),
|
|
output: make(chan string),
|
|
}
|
|
}
|
|
|
|
func (p *installProcessPage) Init() tea.Cmd {
|
|
oldLog := mainModel.log
|
|
cc := NewInteractiveInstallConfig(&mainModel)
|
|
// Create a new logger to track the install process output
|
|
// TODO: Maybe do a dual logger or something? So we can still see the output in the old logger decently
|
|
logBuffer := bytes.Buffer{}
|
|
bufferLog := types.NewBufferLogger(&logBuffer)
|
|
cc.Logger = bufferLog
|
|
|
|
// Start the installer in a goroutine
|
|
go func() {
|
|
defer close(p.done)
|
|
err := RunInstall(cc)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
// Track the log buffer and send mapped steps to p.output
|
|
go func() {
|
|
lastLen := 0
|
|
for {
|
|
time.Sleep(100 * time.Millisecond)
|
|
buf := logBuffer.Bytes()
|
|
if len(buf) > lastLen {
|
|
newLogs := buf[lastLen:]
|
|
lines := bytes.Split(newLogs, []byte("\n"))
|
|
for _, line := range lines {
|
|
strLine := string(line)
|
|
if len(strLine) == 0 {
|
|
continue
|
|
}
|
|
|
|
oldLog.Print(strLine)
|
|
// Parse log line as JSON and extract the message field
|
|
var logEntry map[string]interface{}
|
|
msg := strLine
|
|
if err := json.Unmarshal([]byte(strLine), &logEntry); err == nil {
|
|
// Log the message to the old logger still so we have it there
|
|
if m, ok := logEntry["message"].(string); ok {
|
|
msg = m
|
|
}
|
|
}
|
|
|
|
if strings.Contains(msg, AgentPartitionLog) {
|
|
p.output <- StepPrefix + InstallPartitionStep
|
|
} else if strings.Contains(msg, AgentBeforeInstallLog) {
|
|
p.output <- StepPrefix + InstallBeforeInstallStep
|
|
} else if strings.Contains(msg, AgentActiveLog) {
|
|
p.output <- StepPrefix + InstallActiveStep
|
|
} else if strings.Contains(msg, AgentBootloaderLog) {
|
|
p.output <- StepPrefix + InstallBootloaderStep
|
|
} else if strings.Contains(msg, AgentRecoveryLog) {
|
|
p.output <- StepPrefix + InstallRecoveryStep
|
|
} else if strings.Contains(msg, AgentPassiveLog) {
|
|
p.output <- StepPrefix + InstallPassiveStep
|
|
} else if strings.Contains(msg, AgentAfterInstallLog) && !strings.Contains(msg, "chroot") {
|
|
p.output <- StepPrefix + InstallAfterInstallStep
|
|
} else if strings.Contains(msg, AgentCompleteLog) {
|
|
p.output <- StepPrefix + InstallCompleteStep
|
|
}
|
|
}
|
|
lastLen = len(buf)
|
|
}
|
|
select {
|
|
case <-p.done:
|
|
return
|
|
default:
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Return a command that will check for output from the installer
|
|
return func() tea.Msg {
|
|
return CheckInstallerMsg{}
|
|
}
|
|
}
|
|
|
|
// CheckInstallerMsg Message type to check for installer output
|
|
type CheckInstallerMsg struct{}
|
|
|
|
func (p *installProcessPage) Update(msg tea.Msg) (Page, tea.Cmd) {
|
|
switch msg.(type) {
|
|
case CheckInstallerMsg:
|
|
// Check for new output from the installer
|
|
select {
|
|
case output, ok := <-p.output:
|
|
if !ok {
|
|
// Channel closed, installer is done
|
|
return p, nil
|
|
}
|
|
|
|
// Process the output
|
|
if strings.HasPrefix(output, StepPrefix) {
|
|
// This is a step change notification
|
|
stepName := strings.TrimPrefix(output, StepPrefix)
|
|
|
|
// Find the index of the step
|
|
for i, s := range p.steps {
|
|
if s == stepName {
|
|
p.progress = i
|
|
p.step = stepName
|
|
break
|
|
}
|
|
}
|
|
} else if strings.HasPrefix(output, ErrorPrefix) {
|
|
// Handle error
|
|
errorMsg := strings.TrimPrefix(output, ErrorPrefix)
|
|
p.step = "Error: " + errorMsg
|
|
return p, nil
|
|
}
|
|
|
|
// Continue checking for output
|
|
return p, func() tea.Msg { return CheckInstallerMsg{} }
|
|
|
|
case <-p.done:
|
|
// Installer is finished
|
|
p.progress = len(p.steps) - 1
|
|
p.step = p.steps[len(p.steps)-1]
|
|
return p, nil
|
|
|
|
default:
|
|
// No new output yet, check again after a short delay
|
|
return p, tea.Tick(time.Millisecond*100, func(_ time.Time) tea.Msg {
|
|
return CheckInstallerMsg{}
|
|
})
|
|
}
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
func (p *installProcessPage) View() string {
|
|
s := "Installation in Progress\n\n"
|
|
|
|
// Progress bar
|
|
totalSteps := len(p.steps)
|
|
progressPercent := (p.progress * 100) / (totalSteps - 1)
|
|
barWidth := 40 // Make progress bar wider
|
|
filled := barWidth * progressPercent / 100
|
|
progressBar := lipgloss.NewStyle().Foreground(kairosHighlight2).Background(kairosBg).Render(strings.Repeat("█", filled)) +
|
|
lipgloss.NewStyle().Foreground(kairosBorder).Background(kairosBg).Render(strings.Repeat("░", barWidth-filled))
|
|
|
|
s += "Progress:" + progressBar + lipgloss.NewStyle().Background(kairosBg).Render(" ")
|
|
s += lipgloss.NewStyle().Foreground(kairosText).Background(kairosBg).Bold(true).Render(fmt.Sprintf("%d%%", progressPercent))
|
|
s += "\n\n"
|
|
s += fmt.Sprintf("Current step: %s\n\n", p.step)
|
|
|
|
// Show completed steps
|
|
s += "Completed steps:\n"
|
|
tick := lipgloss.NewStyle().Foreground(kairosAccent).Render(checkMark)
|
|
for i := 0; i < p.progress; i++ {
|
|
s += fmt.Sprintf("%s %s\n", tick, p.steps[i])
|
|
}
|
|
|
|
if p.progress < len(p.steps)-1 {
|
|
// Make the warning message red
|
|
warning := lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")).Bold(true).Render("[!] Do not power off the system during installation!")
|
|
s += "\n" + warning
|
|
} else {
|
|
// Make the completion message green
|
|
complete := lipgloss.NewStyle().Foreground(lipgloss.Color("#00FF00")).Bold(true).Render("Installation completed successfully!\nYou can now reboot your system.")
|
|
s += "\n" + complete
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
func (p *installProcessPage) Title() string {
|
|
return "Installing"
|
|
}
|
|
|
|
func (p *installProcessPage) Help() string {
|
|
if p.progress >= len(p.steps)-1 {
|
|
return "Press any key to exit"
|
|
}
|
|
return "Installation in progress - Use ctrl+c to abort"
|
|
}
|
|
|
|
func (p *installProcessPage) ID() string { return "install_process" }
|
|
|
|
// Abort aborts the running installer process and cleans up
|
|
func (p *installProcessPage) Abort() {
|
|
if p.cmd != nil && p.cmd.Process != nil {
|
|
_ = p.cmd.Process.Kill()
|
|
mainModel.log.Info("Installer process aborted by user")
|
|
}
|
|
// Close output channel if not already closed
|
|
select {
|
|
case <-p.done:
|
|
// already closed
|
|
default:
|
|
close(p.done)
|
|
}
|
|
// Optionally, send a message to output channel
|
|
select {
|
|
case p.output <- ErrorPrefix + "Installation aborted by user":
|
|
default:
|
|
}
|
|
}
|