diff --git a/console/console_windows.go b/console/console_windows.go index da765e0..9e9e2d5 100644 --- a/console/console_windows.go +++ b/console/console_windows.go @@ -9,33 +9,43 @@ import ( ) type Console struct { - stdout io.Writer - commandLine string + stdout io.Writer // stdout is the writer where the output will be written to. + commandLine string // commandLine is the command that will be executed. conPTY *conpty.ConPty - ExitCode uint32 + exitCode uint32 + Parser *Parser } func New(exePath string, stdout io.Writer) *Console { + p := NewParser() + return &Console{ commandLine: exePath, stdout: stdout, + Parser: p, } } -func (c *Console) Run() error { +// Run executes the command in steamcmd and returns the exit code. Exit code does not need to be 0 to return no errors (error is for executing the pseudoconsole) +func (c *Console) Run() (uint32, error) { var err error c.conPTY, err = conpty.Start(c.commandLine) if err != nil { - return err + return 1, err } defer c.conPTY.Close() - go io.Copy(c.stdout, c.conPTY) + d := &duplicateWriter{ + writer1: c.Parser, + writer2: c.stdout, + } + + go io.Copy(d, c.conPTY) - c.ExitCode, err = c.conPTY.Wait(context.Background()) + c.exitCode, err = c.conPTY.Wait(context.Background()) if err != nil { - return err + return 1, err } - return nil + return c.exitCode, nil } diff --git a/console/parser.go b/console/parser.go new file mode 100644 index 0000000..17bf2bf --- /dev/null +++ b/console/parser.go @@ -0,0 +1,111 @@ +package console + +import ( + "fmt" + "io" + "regexp" + "strconv" + "strings" +) + +type Action int + +const ( + Downloading Action = iota + Verifying + Preallocating +) + +// Parser is a parser for the steamcmd console output. Right now it only parses the progress of downloading, verifying and preallocating. To capture other output use the stdout. +type Parser struct { + OnInformationReceived func(action Action, progress float64, currentWritten, total uint64) +} + +// NewParser creates a new Parser instance. this must be used, if not the parser will not work. +func NewParser() *Parser { + // Set empty function because otherwise it will panic, because not every user will use this. + return &Parser{ + OnInformationReceived: func(action Action, progress float64, currentWritten, total uint64) {}, + } +} + +func (p *Parser) Write(data []byte) (int, error) { + switch { + case strings.Contains(string(data), "verifying"): + progress, current, total, err := extractProgressAndNumbers(string(data)) + if err != nil { + return 0, err + } + go p.OnInformationReceived(Verifying, progress, current, total) + case strings.Contains(string(data), "downloading"): + progress, current, total, err := extractProgressAndNumbers(string(data)) + if err != nil { + return 0, err + } + go p.OnInformationReceived(Downloading, progress, current, total) + case strings.Contains(string(data), "preallocating"): + progress, current, total, err := extractProgressAndNumbers(string(data)) + if err != nil { + return 0, err + } + go p.OnInformationReceived(Preallocating, progress, current, total) + } + return len(data), nil +} + +func extractProgressAndNumbers(input string) (progress float64, currentWritten, total uint64, err error) { + progressPattern := `progress:\s+(\d+\.\d+)\s+\((\d+)\s+/\s+(\d+)\)` + + re := regexp.MustCompile(progressPattern) + match := re.FindStringSubmatch(input) + + if len(match) != 4 { + err = fmt.Errorf("Progress not found in the provided line.") + return + } + + progressStr := match[1] + currentStr := match[2] + totalStr := match[3] + + progress, err = strconv.ParseFloat(progressStr, 64) + if err != nil { + err = fmt.Errorf("Error parsing progress value: %w", err) + return + } + + currentWritten, err = strconv.ParseUint(currentStr, 10, 64) + if err != nil { + err = fmt.Errorf("Error parsing currentWritten value: %w", err) + return + } + + total, err = strconv.ParseUint(totalStr, 10, 64) + if err != nil { + err = fmt.Errorf("Error parsing total value: %w", err) + return + } + + return progress, currentWritten, total, nil +} + +// region duplicateWriter + +// duplicateWriter duplicates the output to two writers. Io.MultiWriter does not work for some reason. (prob because parser takes longer) +type duplicateWriter struct { + writer1 io.Writer + writer2 io.Writer +} + +// Write writes the data to both writers 1 and 2. +func (d *duplicateWriter) Write(p []byte) (n int, err error) { + go func() { + d.writer1.Write(p) + }() + go func() { + d.writer2.Write(p) + }() + return len(p), nil +} + +// endregion diff --git a/steamcmd.go b/steamcmd.go index df2ae55..2763844 100644 --- a/steamcmd.go +++ b/steamcmd.go @@ -6,34 +6,33 @@ import ( ) type SteamCMD struct { - // Prompts contains all the commands that will be executed. - Prompts []*Prompt - console *console.Console + // prompts contains all the commands that will be executed. + prompts []*Prompt + Console *console.Console Stdout io.Writer } // New creates a new SteamCMD instance. -func New(stdout io.Writer) *SteamCMD { - return &SteamCMD{ - Prompts: make([]*Prompt, 0), +func New(stdout io.Writer, prompts []*Prompt) *SteamCMD { + + s := &SteamCMD{ + prompts: prompts, Stdout: stdout, } -} -// Run puts all the prompts together and executes them. -func (s *SteamCMD) Run() error { + //prepare command cmd := "steamcmd" - - for _, prompt := range s.Prompts { + for _, prompt := range s.prompts { cmd += " +" + prompt.FullPrompt } - cmd += " +quit" - s.console = console.New(cmd, s.Stdout) - err := s.console.Run() - if err != nil { - return err - } - return nil + s.Console = console.New(cmd, s.Stdout) + + return s +} + +// Run executes the SteamCMD instance. +func (s *SteamCMD) Run() (uint32, error) { + return s.Console.Run() }