Skip to content

Commit

Permalink
added parser
Browse files Browse the repository at this point in the history
Added parser that parses output from:
Downloading
Validating
Preallocating

Also, there is a "hook" available to run logic on the parsing.

Refactored code
  • Loading branch information
JensvandeWiel committed Aug 3, 2023
1 parent 51074c8 commit c4258aa
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 27 deletions.
28 changes: 19 additions & 9 deletions console/console_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
111 changes: 111 additions & 0 deletions console/parser.go
Original file line number Diff line number Diff line change
@@ -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
35 changes: 17 additions & 18 deletions steamcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

0 comments on commit c4258aa

Please sign in to comment.