From 470d8b196997421c9356b7d41dc50ca3063886f7 Mon Sep 17 00:00:00 2001 From: Nicolas Ruflin Date: Tue, 1 Nov 2016 00:14:56 +0100 Subject: [PATCH] Improve prospector state handling (#2840) Previously each prospector was holding old states initially loaded by the registry file. This was changed to that each prospector only loads the states with a path that matches the glob pattern. This reduces the number of states handled by each prospector and reduces overlap. One consequence of this is that in case a log file "moves" from one prospector to an other through renaming during the runtime of filebeat, no state will be found. But this behaviour is not expected and would lead to other issues already now. The expectation is that a file stays inside the prospector over its full lifetime. In case of a filebeat restart including a config change, it can happen that some states are not managed anymore by a prospector. These "obsolete" states will stay in the registrar until a further config change when a glob pattern would again include these states. As an example: Filebeat is started with the following config. ``` filebeat.prospectors: - paths: ['*.log'] clean_removed: true ``` There is a log file `a.log` and `b.log`. Both files are harvested and states for `a.log` and `b.log` are persisted. Then filebeat is stopped and the config file is modified to. ``` filebeat.prospectors: - paths: ['a.log'] clean_removed: true ``` Filebeat is started again. The prospector will now only handle the state for `a.log`. In case `b.log` is removed from disk, the state for `b.log` will stay in the registry file. In case the config file was with `clean_inactive: 5min`, all TTL are reset on restart to `-2` by the registry and `-1` the prospector or the new `clean_inactive` value. Using `-2` in the registrar can be useful in the future to detect which states are not managed anymore by any prospector. As all TTL are reset on restart, persisting of the TTL is not required anymore but can become useful for cleanup so the information is kept in the registry file. Further changes: * Add tests for matchFile method * Add tests for prospector state init filtering * states are passed to `Prospector.Init` on startup instead of setting it directly in the object * `Prospector.Init` and `Prospectorer.Init` now return an error and filebeat exits on startup problems * Remove lastClean as not needed anymore * Have one log file for each bom file test * Update to new vagrant box with Golang 1.7.3 --- CHANGELOG.asciidoc | 3 +- filebeat/input/file/state.go | 8 +- filebeat/prospector/prospector.go | 13 +- filebeat/prospector/prospector_log.go | 64 +++-- .../prospector/prospector_log_other_test.go | 151 ++++++++++++ .../prospector/prospector_log_windows_test.go | 47 ++++ filebeat/prospector/prospector_stdin.go | 3 +- filebeat/prospector/prospector_test.go | 4 +- filebeat/registrar/registrar.go | 7 +- filebeat/tests/system/test_harvester.py | 2 +- filebeat/tests/system/test_registrar.py | 221 +++++++++++++++++- libbeat/scripts/install-go.ps1 | 6 +- 12 files changed, 471 insertions(+), 58 deletions(-) create mode 100644 filebeat/prospector/prospector_log_other_test.go create mode 100644 filebeat/prospector/prospector_log_windows_test.go diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 5c0c411fcee..04ed2e55324 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -69,8 +69,9 @@ https://github.com/elastic/beats/compare/v5.0.0...master[Check the HEAD diff] *Topbeat* *Filebeat* - - Add command line option -once to run filebeat only once and then close. {pull}2456[2456] +- Only load matching states into prospector to improve state handling {pull}2840[2840] +- Reset all states ttl on startup to make sure it is overwritten by new config {pull}2840[2840] *Winlogbeat* diff --git a/filebeat/input/file/state.go b/filebeat/input/file/state.go index 811652d3072..3f2becb4caf 100644 --- a/filebeat/input/file/state.go +++ b/filebeat/input/file/state.go @@ -27,7 +27,7 @@ func NewState(fileInfo os.FileInfo, path string) State { Finished: false, FileStateOS: GetOSState(fileInfo), Timestamp: time.Now(), - TTL: -1 * time.Second, // By default, state does have an infinite ttl + TTL: -1, // By default, state does have an infinite ttl } } @@ -102,11 +102,11 @@ func (s *States) Cleanup() int { for _, state := range s.states { - ttl := state.TTL + expired := (state.TTL > 0 && currentTime.Sub(state.Timestamp) > state.TTL) - if ttl == 0 || (ttl > 0 && currentTime.Sub(state.Timestamp) > ttl) { + if state.TTL == 0 || expired { if state.Finished { - logp.Debug("state", "State removed for %v because of older: %v", state.Source, ttl) + logp.Debug("state", "State removed for %v because of older: %v", state.Source, state.TTL) continue // drop state } else { logp.Err("State for %s should have been dropped, but couldn't as state is not finished.", state.Source) diff --git a/filebeat/prospector/prospector.go b/filebeat/prospector/prospector.go index 5f2c22efc3f..ddb3545f7a4 100644 --- a/filebeat/prospector/prospector.go +++ b/filebeat/prospector/prospector.go @@ -34,7 +34,7 @@ type Prospector struct { } type Prospectorer interface { - Init() + Init(states []file.State) error Run() } @@ -49,8 +49,8 @@ func NewProspector(cfg *common.Config, states file.States, outlet Outlet) (*Pros outlet: outlet, harvesterChan: make(chan *input.Event), done: make(chan struct{}), - states: states.Copy(), wg: sync.WaitGroup{}, + states: &file.States{}, channelWg: sync.WaitGroup{}, } @@ -61,7 +61,7 @@ func NewProspector(cfg *common.Config, states file.States, outlet Outlet) (*Pros return nil, err } - err := prospector.Init() + err := prospector.Init(states.GetStates()) if err != nil { return nil, err } @@ -72,7 +72,7 @@ func NewProspector(cfg *common.Config, states file.States, outlet Outlet) (*Pros } // Init sets up default config for prospector -func (p *Prospector) Init() error { +func (p *Prospector) Init(states []file.State) error { var prospectorer Prospectorer var err error @@ -90,7 +90,10 @@ func (p *Prospector) Init() error { return err } - prospectorer.Init() + err = prospectorer.Init(states) + if err != nil { + return err + } p.prospectorer = prospectorer // Create empty harvester to check if configs are fine diff --git a/filebeat/prospector/prospector_log.go b/filebeat/prospector/prospector_log.go index 91b2251bd63..8853f1532a5 100644 --- a/filebeat/prospector/prospector_log.go +++ b/filebeat/prospector/prospector_log.go @@ -1,12 +1,13 @@ package prospector import ( + "expvar" "os" "path/filepath" + "runtime" + "strings" "time" - "expvar" - "github.com/elastic/beats/filebeat/harvester" "github.com/elastic/beats/filebeat/input" "github.com/elastic/beats/filebeat/input/file" @@ -21,7 +22,6 @@ var ( type ProspectorLog struct { Prospector *Prospector config prospectorConfig - lastClean time.Time } func NewProspectorLog(p *Prospector) (*ProspectorLog, error) { @@ -34,37 +34,27 @@ func NewProspectorLog(p *Prospector) (*ProspectorLog, error) { return prospectorer, nil } -func (p *ProspectorLog) Init() { +// Init sets up the prospector +// It goes through all states coming from the registry. Only the states which match the glob patterns of +// the prospector will be loaded and updated. All other states will not be touched. +func (p *ProspectorLog) Init(states []file.State) error { logp.Debug("prospector", "exclude_files: %s", p.config.ExcludeFiles) - logp.Info("Load previous states from registry into memory") - fileStates := p.Prospector.states.GetStates() - - // Make sure all states are set as finished - for _, state := range fileStates { - + for _, state := range states { // Check if state source belongs to this prospector. If yes, update the state. - if p.hasFile(state.Source) { - // Set all states again to infinity TTL to make sure only removed if config still same - // clean_inactive / clean_removed could have been changed between restarts + if p.matchesFile(state.Source) { state.TTL = -1 - // Update prospector states and send new states to registry err := p.Prospector.updateState(input.NewEvent(state)) if err != nil { logp.Err("Problem putting initial state: %+v", err) + return err } - } else { - // Only update internal state, do not report it to registry - // Having all states could be useful in case later a file is moved into this prospector - // TODO: Think about if this is expected or unexpected - p.Prospector.states.Update(state) } } - p.lastClean = time.Now() - - logp.Info("Previous states loaded: %v", p.Prospector.states.Count()) + logp.Info("Prospector with previous states loaded: %v", p.Prospector.states.Count()) + return nil } func (p *ProspectorLog) Run() { @@ -168,26 +158,26 @@ func (p *ProspectorLog) getFiles() map[string]os.FileInfo { return paths } -// hasFile returns true in case the given filePath is part of this prospector -func (p *ProspectorLog) hasFile(filePath string) bool { +// matchesFile returns true in case the given filePath is part of this prospector, means matches its glob patterns +func (p *ProspectorLog) matchesFile(filePath string) bool { for _, glob := range p.config.Paths { - // Evaluate the path as a wildcards/shell glob - matches, err := filepath.Glob(glob) + + if runtime.GOOS == "windows" { + // Windows allows / slashes which makes glob patterns with / work + // But for match we need paths with \ as only file names are compared and no lookup happens + glob = strings.Replace(glob, "/", "\\", -1) + } + + // Evaluate if glob matches filePath + match, err := filepath.Match(glob, filePath) if err != nil { + logp.Debug("prospector", "Error matching glob: %s", err) continue } - // Check any matched files to see if we need to start a harvester - for _, file := range matches { - - // check if the file is in the exclude_files list - if p.isFileExcluded(file) { - continue - } - - if filePath == file { - return true - } + // Check if file is not excluded + if match && !p.isFileExcluded(filePath) { + return true } } return false diff --git a/filebeat/prospector/prospector_log_other_test.go b/filebeat/prospector/prospector_log_other_test.go new file mode 100644 index 00000000000..06990455b53 --- /dev/null +++ b/filebeat/prospector/prospector_log_other_test.go @@ -0,0 +1,151 @@ +// +build !windows + +package prospector + +import ( + "regexp" + "testing" + + "github.com/elastic/beats/filebeat/input" + "github.com/elastic/beats/filebeat/input/file" + + "github.com/stretchr/testify/assert" +) + +var matchTests = []struct { + file string + paths []string + excludeFiles []*regexp.Regexp + result bool +}{ + { + "test/test.log", + []string{"test/*"}, + nil, + true, + }, + { + "notest/test.log", + []string{"test/*"}, + nil, + false, + }, + { + "test/test.log", + []string{"test/*.log"}, + nil, + true, + }, + { + "test/test.log", + []string{"test/*.nolog"}, + nil, + false, + }, + { + "test/test.log", + []string{"test/*"}, + []*regexp.Regexp{regexp.MustCompile("test.log")}, + false, + }, + { + "test/test.log", + []string{"test/*"}, + []*regexp.Regexp{regexp.MustCompile("test2.log")}, + true, + }, +} + +func TestMatchFile(t *testing.T) { + + for _, test := range matchTests { + + p := ProspectorLog{ + config: prospectorConfig{ + Paths: test.paths, + ExcludeFiles: test.excludeFiles, + }, + } + + assert.Equal(t, test.result, p.matchesFile(test.file)) + } +} + +var initStateTests = []struct { + states []file.State // list of states + paths []string // prospector glob + count int // expected states in prospector +}{ + { + []file.State{ + file.State{Source: "test"}, + }, + []string{"test"}, + 1, + }, + { + []file.State{ + file.State{Source: "notest"}, + }, + []string{"test"}, + 0, + }, + { + []file.State{ + file.State{Source: "test1.log", FileStateOS: file.StateOS{Inode: 1}}, + file.State{Source: "test2.log", FileStateOS: file.StateOS{Inode: 2}}, + }, + []string{"*.log"}, + 2, + }, + { + []file.State{ + file.State{Source: "test1.log", FileStateOS: file.StateOS{Inode: 1}}, + file.State{Source: "test2.log", FileStateOS: file.StateOS{Inode: 2}}, + }, + []string{"test1.log"}, + 1, + }, + { + []file.State{ + file.State{Source: "test1.log", FileStateOS: file.StateOS{Inode: 1}}, + file.State{Source: "test2.log", FileStateOS: file.StateOS{Inode: 2}}, + }, + []string{"test.log"}, + 0, + }, + { + []file.State{ + file.State{Source: "test1.log", FileStateOS: file.StateOS{Inode: 1}}, + file.State{Source: "test2.log", FileStateOS: file.StateOS{Inode: 1}}, + }, + []string{"*.log"}, + 1, // Expecting only 1 state because of some inode (this is only a theoretical case) + }, +} + +// TestInit checks that the correct states are in a prospector after the init phase +// This means only the ones that match the glob and not exclude files +func TestInit(t *testing.T) { + + for _, test := range initStateTests { + p := ProspectorLog{ + Prospector: &Prospector{ + states: &file.States{}, + outlet: TestOutlet{}, + }, + config: prospectorConfig{ + Paths: test.paths, + }, + } + err := p.Init(test.states) + assert.NoError(t, err) + assert.Equal(t, test.count, p.Prospector.states.Count()) + } + +} + +// TestOutlet is an empty outlet for testing +type TestOutlet struct{} + +func (o TestOutlet) OnEvent(event *input.Event) bool { return true } diff --git a/filebeat/prospector/prospector_log_windows_test.go b/filebeat/prospector/prospector_log_windows_test.go new file mode 100644 index 00000000000..a86c4095478 --- /dev/null +++ b/filebeat/prospector/prospector_log_windows_test.go @@ -0,0 +1,47 @@ +// +build windows + +package prospector + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +var matchTestsWindows = []struct { + file string + paths []string + excludeFiles []*regexp.Regexp + result bool +}{ + { + "C:\\\\hello\\test\\test.log", // Path are always in windows format + []string{"C:\\\\hello/test/*.log"}, // Globs can also be with forward slashes + nil, + true, + }, + { + "C:\\\\hello\\test\\test.log", // Path are always in windows format + []string{"C:\\\\hello\\test/*.log"}, // Globs can also be mixed + nil, + true, + }, +} + +// TestMatchFileWindows test if match works correctly on windows +// Separate test are needed on windows because of automated path conversion +func TestMatchFileWindows(t *testing.T) { + + for _, test := range matchTestsWindows { + + p := ProspectorLog{ + config: prospectorConfig{ + Paths: test.paths, + ExcludeFiles: test.excludeFiles, + }, + } + + assert.Equal(t, test.result, p.matchesFile(test.file)) + } +} diff --git a/filebeat/prospector/prospector_stdin.go b/filebeat/prospector/prospector_stdin.go index 37b86ff9d4b..10b671d6b73 100644 --- a/filebeat/prospector/prospector_stdin.go +++ b/filebeat/prospector/prospector_stdin.go @@ -29,8 +29,9 @@ func NewProspectorStdin(p *Prospector) (*ProspectorStdin, error) { return prospectorer, nil } -func (p *ProspectorStdin) Init() { +func (p *ProspectorStdin) Init(states []file.State) error { p.started = false + return nil } func (p *ProspectorStdin) Run() { diff --git a/filebeat/prospector/prospector_test.go b/filebeat/prospector/prospector_test.go index 4638333e5bf..096daf27a19 100644 --- a/filebeat/prospector/prospector_test.go +++ b/filebeat/prospector/prospector_test.go @@ -6,6 +6,8 @@ import ( "regexp" "testing" + "github.com/elastic/beats/filebeat/input/file" + "github.com/stretchr/testify/assert" ) @@ -15,7 +17,7 @@ func TestProspectorInitInputTypeLogError(t *testing.T) { config: prospectorConfig{}, } - err := prospector.Init() + err := prospector.Init([]file.State{}) // Error should be returned because no path is set assert.Error(t, err) } diff --git a/filebeat/registrar/registrar.go b/filebeat/registrar/registrar.go index 4b58ac12d3a..bc2a98c82aa 100644 --- a/filebeat/registrar/registrar.go +++ b/filebeat/registrar/registrar.go @@ -118,9 +118,12 @@ func (r *Registrar) loadStates() error { return fmt.Errorf("Error decoding states: %s", err) } - // Set all states to finished on restart + // Set all states to finished and disable TTL on restart + // For all states covered by a prospector, TTL will be overwritten with the prospector value for key, state := range states { state.Finished = true + // Set ttl to -2 to easily spot which states are not managed by a prospector + state.TTL = -2 states[key] = state } @@ -248,7 +251,7 @@ func (r *Registrar) Run() { statesCleanup.Add(int64(cleanedStates)) logp.Debug("registrar", - "Registrar states cleaned up. Before: %d , After: %d", + "Registrar states cleaned up. Before: %d, After: %d", beforeCount, beforeCount-cleanedStates) if err := r.writeRegistry(); err != nil { diff --git a/filebeat/tests/system/test_harvester.py b/filebeat/tests/system/test_harvester.py index 03ada4ec2c5..162a59ebc57 100644 --- a/filebeat/tests/system/test_harvester.py +++ b/filebeat/tests/system/test_harvester.py @@ -482,7 +482,7 @@ def test_boms(self): content = message + '\n' file.write(content) - filebeat = self.start_beat() + filebeat = self.start_beat(output=config[0] + ".log") self.wait_until( lambda: self.output_has(lines=1, output_file="output/" + config[0]), diff --git a/filebeat/tests/system/test_registrar.py b/filebeat/tests/system/test_registrar.py index daba757c385..accddd759ce 100644 --- a/filebeat/tests/system/test_registrar.py +++ b/filebeat/tests/system/test_registrar.py @@ -1028,7 +1028,6 @@ def test_restart_state(self): path=os.path.abspath(self.working_dir) + "/log/*", close_inactive="1s", ignore_older="3s", - clean_inactive="5s", ) os.mkdir(self.working_dir + "/log/") @@ -1053,6 +1052,13 @@ def test_restart_state(self): filebeat.check_kill_and_wait() + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/*", + close_inactive="1s", + ignore_older="3s", + clean_inactive="5s", + ) + filebeat = self.start_beat(output="filebeat2.log") # Write additional file @@ -1061,12 +1067,221 @@ def test_restart_state(self): # Make sure all 4 states are persisted self.wait_until( - lambda: self.log_contains("Before: 4, After: 4", logfile="filebeat2.log"), + lambda: self.log_contains("Prospector states cleaned up. Before: 4, After: 4", logfile="filebeat2.log"), max_timeout=10) # Wait until registry file is cleaned self.wait_until( - lambda: self.log_contains("Before: 0, After: 0", logfile="filebeat2.log"), + lambda: self.log_contains("Prospector states cleaned up. Before: 0, After: 0", logfile="filebeat2.log"), + max_timeout=10) + + filebeat.check_kill_and_wait() + + + def test_restart_state_reset(self): + """ + Test that ttl is set to -1 after restart and no prospector covering it + """ + + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/*", + clean_inactive="10s", + ignore_older="5s" + ) + os.mkdir(self.working_dir + "/log/") + + testfile = self.working_dir + "/log/test.log" + + with open(testfile, 'w') as file: + file.write("Hello World\n") + + filebeat = self.start_beat() + + # Wait until state written + self.wait_until( + lambda: self.output_has(lines=1), + max_timeout=30) + + filebeat.check_kill_and_wait() + + # Check that ttl > 0 was set because of clean_inactive + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] > 0 + + # No config file which does not match the exisitng state + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test2.log", + clean_inactive="10s", + ignore_older="5s", + ) + + filebeat = self.start_beat(output="filebeat2.log") + + # Wait until prospectors are started + self.wait_until( + lambda: self.log_contains_count("Starting prospector of type: log", logfile="filebeat2.log") >= 1, + max_timeout=10) + + filebeat.check_kill_and_wait() + + # Check that ttl was reset correctly + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == -2 + + def test_restart_state_reset_ttl(self): + """ + Test that ttl is reset after restart if clean_inactive changes + """ + + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test.log", + clean_inactive="10s", + ignore_older="5s" + ) + os.mkdir(self.working_dir + "/log/") + + testfile = self.working_dir + "/log/test.log" + + with open(testfile, 'w') as file: + file.write("Hello World\n") + + filebeat = self.start_beat() + + # Wait until state written + self.wait_until( + lambda: self.output_has(lines=1), + max_timeout=30) + + filebeat.check_kill_and_wait() + + # Check that ttl > 0 was set because of clean_inactive + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == 10 * 1000 * 1000 * 1000 + + # No config file which does not match the existing state + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test.log", + clean_inactive="20s", + ignore_older="5s", + ) + + filebeat = self.start_beat(output="filebeat2.log") + + # Wait until new state is written + self.wait_until( + lambda: self.log_contains("Flushing spooler because of timeout. Events flushed: ", logfile="filebeat2.log"), + max_timeout=10) + + filebeat.check_kill_and_wait() + + # Check that ttl was reset correctly + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == 20 * 1000 * 1000 * 1000 + + def test_restart_state_reset_ttl_with_space(self): + """ + Test that ttl is reset after restart if clean_inactive changes + This time it is tested with a space in the filename to see if everything is loaded as expected + """ + + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test file.log", + clean_inactive="10s", + ignore_older="5s" + ) + os.mkdir(self.working_dir + "/log/") + + testfile = self.working_dir + "/log/test file.log" + + with open(testfile, 'w') as file: + file.write("Hello World\n") + + filebeat = self.start_beat() + + # Wait until state written + self.wait_until( + lambda: self.output_has(lines=1), + max_timeout=30) + + filebeat.check_kill_and_wait() + + # Check that ttl > 0 was set because of clean_inactive + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == 10 * 1000 * 1000 * 1000 + + # new config file whith other clean_inactive + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test file.log", + clean_inactive="20s", + ignore_older="5s", + ) + + filebeat = self.start_beat(output="filebeat2.log") + + # Wait until new state is written + self.wait_until( + lambda: self.log_contains("Flushing spooler because of timeout. Events flushed: ", logfile="filebeat2.log"), + max_timeout=10) + + filebeat.check_kill_and_wait() + + # Check that ttl was reset correctly + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == 20 * 1000 * 1000 * 1000 + + + def test_restart_state_reset_ttl_no_clean_inactive(self): + """ + Test that ttl is reset after restart if clean_inactive is disabled + """ + + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test.log", + clean_inactive="10s", + ignore_older="5s" + ) + os.mkdir(self.working_dir + "/log/") + + testfile = self.working_dir + "/log/test.log" + + with open(testfile, 'w') as file: + file.write("Hello World\n") + + filebeat = self.start_beat() + + # Wait until state written + self.wait_until( + lambda: self.output_has(lines=1), + max_timeout=30) + + filebeat.check_kill_and_wait() + + # Check that ttl > 0 was set because of clean_inactive + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == 10 * 1000 * 1000 * 1000 + + # New config without clean_inactive + self.render_config_template( + path=os.path.abspath(self.working_dir) + "/log/test.log", + ) + + filebeat = self.start_beat(output="filebeat2.log") + + # Wait until prospectors are started + self.wait_until( + lambda: self.log_contains("Flushing spooler because of timeout. Events flushed: ", logfile="filebeat2.log"), max_timeout=10) filebeat.check_kill_and_wait() + + # Check that ttl was reset correctly + data = self.get_registry() + assert len(data) == 1 + assert data[0]["ttl"] == -1 diff --git a/libbeat/scripts/install-go.ps1 b/libbeat/scripts/install-go.ps1 index 167a3d4b9fa..9034291eab2 100644 --- a/libbeat/scripts/install-go.ps1 +++ b/libbeat/scripts/install-go.ps1 @@ -1,10 +1,10 @@ # Installs golang on Windows. # # # Run script: -# .\install-go.ps1 -version 1.5.3 +# .\install-go.ps1 -version 1.7.3 # # # Download and run script: -# $env:GOVERSION = '1.5.3' +# $env:GOVERSION = '1.7.3' # iex ((new-object net.webclient).DownloadString('SCRIPT_URL_HERE')) Param( [String]$version, @@ -20,7 +20,7 @@ Download and install Golang on Windows. It sets the GOROOT environment variable and adds GOROOT\bin to the PATH environment variable. Usage: - $SCRIPT -version 1.5.3 + $SCRIPT -version 1.7.3 Options: -h | -help Print the help menu.