Skip to content

Commit

Permalink
Implement events for install / script exec
Browse files Browse the repository at this point in the history
  • Loading branch information
evanpurkhiser committed Feb 22, 2020
1 parent 423f91b commit 27cf990
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 38 deletions.
9 changes: 8 additions & 1 deletion cmd/dots/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,23 @@ var installCmd = cobra.Command{
IsVerbose: verbose,
})

installConfig.EventLogger = installLogger.GetEventChan()

installLogger.InstallInfo()

if dryRun {
installLogger.DryrunInstall()
return nil
}

defer installLogger.LogEvents()()

installed := installer.InstallDotfiles(prepared, installConfig)
installer.RunInstallScripts(prepared, installConfig)
installer.FinalizeInstall(installed, installConfig)

// TODO Needs some error handling clenaup
// TODO Collect errors from installation, script execution, and
// finalization to determine exit code.

return nil
},
Expand Down
36 changes: 36 additions & 0 deletions events/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package events

type EventType string

// Available event types.
const (
// InstallStarting fires when an installation for a set of dotfiles has bugun
// processing.
InstallStarting EventType = "install_starting"

// InstallDone fires when all dotfiles have been installed. No scripts
// have been executed for the dotfiles yet.
InstallDone EventType = "install_completed"

// DotfileInstalled fires when a single dotfile has completed installation.
// This does not indicate that the insatllation was not a no-op.
DotfileInstalled EventType = "dotfile_installed"

// ScriptExecStarted fires when script execution has begun.
ScriptExecStarted EventType = "executing_all_scripts"

// ScriptExecDone fires when script execution has begun.
ScriptExecDone EventType = "executing_all_scripts"

// ScriptExecuting fires when a dotfile script is being executed.
ScriptExecuting EventType = "executing_script"

// ScriptCompleted fires when a dotfiles script has completed execution.
ScriptCompleted EventType = "script_completed"
)

// Event represents a output logging event.
type Event struct {
Type EventType
Object interface{}
}
50 changes: 40 additions & 10 deletions installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sync"

"go.evanpurkhiser.com/dots/config"
"go.evanpurkhiser.com/dots/events"
)

const separator = string(os.PathSeparator)
Expand All @@ -28,8 +29,10 @@ type InstallConfig struct {
// changed from its source. This implies that install scripts will be run.
ForceReinstall bool

// TODO We can probably add a channel here to pipe logging output so that we
// can output some logging
// EventLogger may be given data to output during the installation process.
// It is up to the logger service to decide if it can output anything for the
// given data or not.
EventLogger chan<- events.Event
}

// InstalledDotfile is a represents of the dotfile *after* it has been
Expand All @@ -41,12 +44,27 @@ type InstalledDotfile struct {
InstallError error
}

// WillInstallDotfile indicates weather the dotfile will be installed if
// InstallDotfile is given the prepared dotfile. This does not guarentee that
// errors will not occur during installation.
func WillInstallDotfile(dotfile *PreparedDotfile, config InstallConfig) bool {
// Skip dotfiles that we failed to prepare
if dotfile.PrepareError != nil {
return false
}

if !dotfile.IsChanged() && !config.ForceReinstall {
return false
}

return true
}

// InstallDotfile is given a prepared dotfile and installation configuration
// and will perform all the necessary actions to install the file into it's
// target location.
func InstallDotfile(dotfile *PreparedDotfile, config InstallConfig) error {
// Skip dotfiles that we failed to preapre
if dotfile.PrepareError != nil {
if !WillInstallDotfile(dotfile, config) {
return nil
}

Expand All @@ -56,10 +74,6 @@ func InstallDotfile(dotfile *PreparedDotfile, config InstallConfig) error {
installPath = config.OverrideInstallPath + separator + dotfile.Path
}

if !dotfile.IsChanged() && !config.ForceReinstall {
return nil
}

// Removed
if dotfile.Removed && !dotfile.RemovedNull {
return os.Remove(installPath)
Expand Down Expand Up @@ -114,20 +128,35 @@ func InstallDotfiles(install PreparedInstall, config InstallConfig) []*Installed
InstallError: err,
}

config.EventLogger <- events.Event{
Type: events.DotfileInstalled,
Object: installed[i],
}

waitGroup.Done()
}

config.EventLogger <- events.Event{
Type: events.InstallStarting,
Object: install,
}

for i, dotfile := range install.Dotfiles {
go doInstall(i, dotfile)
}

waitGroup.Wait()

config.EventLogger <- events.Event{
Type: events.InstallDone,
Object: installed,
}

return installed
}

// FinalizeInstall writes the updated lockfile after installation
func FinalizeInstall(installed []*InstalledDotfile, installConfig InstallConfig) {
func FinalizeInstall(installed []*InstalledDotfile, installConfig InstallConfig) error {
installedFiles := make([]string, 0, len(installed))

for _, dotfile := range installed {
Expand All @@ -143,5 +172,6 @@ func FinalizeInstall(installed []*InstalledDotfile, installConfig InstallConfig)

lockfile := installConfig.SourceLockfile
lockfile.InstalledFiles = installedFiles
config.WriteLockfile(lockfile, installConfig.SourceConfig)

return config.WriteLockfile(lockfile, installConfig.SourceConfig)
}
89 changes: 66 additions & 23 deletions installer/scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,84 @@ import (
"fmt"
"os"
"os/exec"

"go.evanpurkhiser.com/dots/events"
)

// ExecutedScript represents the completion of a script execution
type ExecutedScript struct {
*InstallScript

// ExecutionError represents any error that occured during script execution.
ExecutionError error
}

// RunInstallScript executes a single InstallScript.
func RunInstallScript(script *InstallScript, config InstallConfig) error {
if !script.ShouldInstall() && !config.ForceReinstall {
return nil
}

if !script.Executable {
return nil
}

command := exec.Command(script.Path)

// Execute the script in the installed path context
if config.OverrideInstallPath != "" {
command.Dir = config.OverrideInstallPath
} else {
command.Dir = config.SourceConfig.InstallPath
}

// Setup the environment
command.Env = append(
os.Environ(),
fmt.Sprintf("DOTS_SOURCE=%s", config.SourceConfig.SourcePath),
fmt.Sprintf("DOTS_FORCE_REINSTALL=%t", config.ForceReinstall),
)

command.Stdout = os.Stdout
command.Stderr = os.Stderr

return command.Run()
}

// RunInstallScripts executes the installation scripts of a PreparedInstall for
// files that have changed (unless ForceReinstall is enabled, in which case
// *all* scripts will be run).
func RunInstallScripts(install PreparedInstall, config InstallConfig) {
for _, script := range install.InstallScripts {
if !script.ShouldInstall() && !config.ForceReinstall {
continue
}
func RunInstallScripts(install PreparedInstall, config InstallConfig) []*ExecutedScript {
executedScripts := make([]*ExecutedScript, len(install.InstallScripts))

if !script.Executable {
continue
config.EventLogger <- events.Event{
Type: events.ScriptExecStarted,
Object: install,
}

for i, script := range install.InstallScripts {
config.EventLogger <- events.Event{
Type: events.ScriptCompleted,
Object: script,
}

command := exec.Command(script.Path)
err := RunInstallScript(script, config)

// Execute the script in the installed path cotnext
if config.OverrideInstallPath != "" {
command.Dir = config.OverrideInstallPath
} else {
command.Dir = config.SourceConfig.InstallPath
executedScripts[i] = &ExecutedScript{
InstallScript: script,
ExecutionError: err,
}

// Setup the environment
command.Env = append(
os.Environ(),
fmt.Sprintf("DOTS_SOURCE=%s", config.SourceConfig.SourcePath),
fmt.Sprintf("DOTS_FORCE_REINSTALL=%t", config.ForceReinstall),
)

command.Stdout = os.Stdout
command.Stderr = os.Stderr
config.EventLogger <- events.Event{
Type: events.ScriptCompleted,
Object: script,
}
}

command.Run()
config.EventLogger <- events.Event{
Type: events.ScriptExecDone,
Object: executedScripts,
}

return executedScripts
}
37 changes: 33 additions & 4 deletions output/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"strings"

"github.com/fatih/color"

"go.evanpurkhiser.com/dots/config"
"go.evanpurkhiser.com/dots/events"
"go.evanpurkhiser.com/dots/installer"
)

Expand Down Expand Up @@ -34,6 +36,7 @@ func New(config Config) *Output {
maxDotfileLength = len(d.Path)
}
}
logger.eventChan = make(chan events.Event)
logger.maxDotfileLength = maxDotfileLength

return logger
Expand All @@ -43,20 +46,46 @@ func New(config Config) *Output {
// dotfile installation operations.
type Output struct {
Config
eventChan chan events.Event
maxDotfileLength int
}

// shouldLogDotfile indicates if the dotfile should be logged given the current
// Output configuration.
func (l *Output) shouldLogDotfile(dotfile *installer.PreparedDotfile) bool {
return dotfile.PrepareError != nil ||
dotfile.IsChanged() ||
l.InstallConfig.ForceReinstall
return dotfile.PrepareError != nil || installer.WillInstallDotfile(dotfile, l.InstallConfig)
}

func (l *Output) logEvent(event events.Event) {
// Do something here
}

// GetEventChan returns the event channel that may be sent events to be
// outputted while the logger listening for events.
func (l *Output) GetEventChan() chan<- events.Event {
return l.eventChan
}

// LogEvents procese
func (l *Output) LogEvents() func() {
stop := make(chan bool)

go func() {
for {
select {
case <-stop:
return
case event := <-l.eventChan:
l.logEvent(event)
}
}
}()

return func() { stop <- true }
}

// DryrunInstall outputs the logging of a dryrun of the prepared dotfiles
func (l *Output) DryrunInstall() {
l.InstallInfo()
for _, dotfile := range l.PreparedInstall.Dotfiles {
l.DotfileInfo(dotfile)
}
Expand Down

0 comments on commit 27cf990

Please sign in to comment.