// Forwards the local server listener to the specified target address (format host:port) using the SSH connection as tunnel. // What this method does is the same as "ssh -L $ANY-PORT:jenkins-host:$TARGET-PORT" jenkins-host. func (self *SSHTunnelEstablisher) forwardLocalConnectionsTo(config *util.Config, ssh *ssh.Client, listener net.Listener, targetAddress string) { transfer := func(source io.ReadCloser, target io.Writer) { defer source.Close() _, _ = io.Copy(target, source) } establishBIDITransport := func(source net.Conn, target net.Conn) { go transfer(source, target) go transfer(target, source) } sshAddress := ssh.Conn.RemoteAddr().String() localAddress := listener.Addr().String() util.GOut("ssh-tunnel", "Forwarding local connections on '%v' to '%v' via '%v'.", localAddress, targetAddress, sshAddress) for { if sourceConnection, err := listener.Accept(); err == nil { if targetConnection, err := ssh.Dial("tcp", targetAddress); err == nil { establishBIDITransport(sourceConnection, targetConnection) } else { util.GOut("ssh-tunnel", "ERROR: Failed forwarding incoming local connection on '%v' to '%v' via '%v'.", localAddress, targetAddress, sshAddress) } } else { util.GOut("ssh-tunnel", "Stop forwarding local connections on '%v' to '%v'.", localAddress, targetAddress) return } } }
func (self *LocationCleaner) cleanupLocations(dirsToKeepClean, exclusions []string, mode string, maxTTL time.Duration) { for _, rootDir := range dirsToKeepClean { rootDir = filepath.Clean(rootDir) dirToEmptyMap := map[string]bool{} expiredTimeOffset := time.Now().Add(-maxTTL) if mode == ModeTTLPerLocation { util.GOut("cleanup", "Checking %v for expiration.", rootDir) exclusionCount := self.cleanupFiles(rootDir, expiredTimeOffset, true, exclusions, dirToEmptyMap) if exclusionCount > 0 { return } util.GOut("cleanup", "Cleaning %v", rootDir) } else { util.GOut("cleanup", "Cleaning expired files in %v", rootDir) } // Handling outdated temporary files _ = self.cleanupFiles(rootDir, expiredTimeOffset, false, exclusions, dirToEmptyMap) // Handling all directories that are known to be empty for dirPath, emptyDir := range dirToEmptyMap { // Root-Dir is only cleaned for "TTLPerLocation". if mode != ModeTTLPerLocation && rootDir == dirPath { continue } if emptyDir { if err := os.Remove(dirPath); err == nil { util.GOut("cleanup", "\x1b[39mRemoved empty directory: %v", dirPath) } } } } }
func (self *ServerMode) execute(privateKey ssh.Signer) { // An SSH server is represented by a ServerConfig, which holds // certificate details and handles authentication of ServerConns. config := &ssh.ServerConfig{ PasswordCallback: func(c ssh.ConnMetadata, pass []byte) (*ssh.Permissions, error) { // Slowing down password check to make BF attacks more difficult. time.Sleep(time.Second * 1) if c.User() == self.config.SSHUsername && string(pass) == self.config.SSHPassword { return nil, nil } else { return nil, fmt.Errorf("SSH: Password rejected for %q", c.User()) } }, AuthLogCallback: func(c ssh.ConnMetadata, method string, err error) { if err == nil { util.GOut("SSH", "Authentication succeeded '%v' using '%v'", c.User(), method) } else { util.GOut("SSH", "Failed attempt to authenticate '%v' using '%v' ; Caused by: %v", c.User(), method, err) } }, } config.AddHostKey(privateKey) // Once a ServerConfig has been configured, connections can be // accepted. address := fmt.Sprintf("%v:%v", self.config.SSHListenAddress, self.config.SSHListenPort) util.GOut("SSH", "Starting to listen @ %v", address) listener, err := net.Listen("tcp", address) if err != nil { panic("SSH: Failed to listen @ " + address) } else { defer func() { listener.Close() self.status.Set(ModeStopped) }() } go func() { for { connection, err := listener.Accept() if err != nil { util.GOut("SSH", "Failed to accept next incoming SSH connection, assuming connection was closed.") return } // Handling only one connection at a time should be enough. self.handleSSHRequest(&connection, config) } }() // Entering main loop and remain there until the terminal is stopped and the deferred channel close is triggered. self.status.Set(ModeStarted) for self.status.Get() == ModeStarted { time.Sleep(time.Millisecond * 100) } }
// Implements Java installation for Windows func (self *JavaDownloader) InstallJava(config *util.Config) error { util.GOut("DOWNLOAD", "Getting %v", config.CIHostURI) util.GOut("INSTALL", "Installing %v", config.CIHostURI) // TODO: Implement like done here: // TODO: https://github.com/jenkinsci/jenkins/blob/main/core/src/main/java/hudson/tools/JDKInstaller.java return fmt.Errorf("Installing Java is not implemented yet. Install it manually.") }
func (self *JenkinsClientDownloader) downloadJar(config *util.Config) error { util.GOut("DOWNLOAD", "Getting latest Jenkins client %v", (config.CIHostURI + "/" + ClientJarURL)) // Create the HTTP request. request, err := config.CIRequest("GET", ClientJarURL, nil) if err != nil { return err } if fi, err := os.Stat(ClientJarName); err == nil { request.Header.Add("If-Modified-Since", fi.ModTime().Format(http.TimeFormat)) } // Perform the HTTP request. var source io.ReadCloser sourceTime := time.Now() if response, err := config.CIClient().Do(request); err == nil { defer response.Body.Close() source = response.Body if response.StatusCode == 304 { util.GOut("DOWNLOAD", "Jenkins client is up-to-date, no need to download.") return nil } else if response.StatusCode != 200 { return fmt.Errorf("Failed downloading jenkins client. Cause: HTTP-%v %v", response.StatusCode, response.Status) } if value := response.Header.Get("Last-Modified"); value != "" { if time, err := http.ParseTime(value); err == nil { sourceTime = time } } } else { return fmt.Errorf("Failed downloading jenkins client. Connect failed. Cause: %v", err) } target, err := os.Create(ClientJarDownloadName) defer target.Close() if err != nil { return fmt.Errorf("Failed downloading jenkins client. Cannot create local file. Cause: %v", err) } if _, err = io.Copy(target, source); err == nil { target.Close() if err = os.Remove(ClientJarName); err == nil || os.IsNotExist(err) { if err = os.Rename(ClientJarDownloadName, ClientJarName); err == nil { os.Chtimes(ClientJarName, sourceTime, sourceTime) } } return err } else { return fmt.Errorf("Failed downloading jenkins client. Transfer failed. Cause: %v", err) } }
// Opens a new local server socket. func (self *SSHTunnelEstablisher) newLocalServerListener() (serverListener net.Listener, err error) { serverListener, err = net.Listen("tcp", "localhost:0") if err == nil { self.closables = append(self.closables, serverListener) util.GOut("ssh-tunnel", "Opened local listener on '%v'.", serverListener.Addr()) } else { util.GOut("ssh-tunnel", "ERROR: Failed opening local listener. Cause: %v", err) } return }
func (self *SSHTunnelEstablisher) IsConfigAcceptable(config *util.Config) bool { if config.CITunnelSSHEnabled && config.CITunnelSSHAddress == "" { util.GOut("ssh-tunnel", "WARN: SSH tunnel is enabled but SSH server address is empty.") return false } if config.CITunnelSSHAddress != "" && !config.HasCIConnection() { util.GOut("ssh-tunnel", "WARN: No Jenkins URI defined. SSH tunnel settings are not enough to connect to Jenkins.") return false } return true }
func (self *FullGCInvoker) invokeSystemGC(config *util.Config) { // curl -d "script=System.gc()" -X POST http://user:password@jenkins-host/ci/computer/%s/scriptText postBody := strings.NewReader(fmt.Sprintf(FullGCPostBody, url.QueryEscape(FullGCScript))) request, err := config.CIRequest("POST", fmt.Sprintf(FullGCURL, config.ClientName), postBody) if err == nil { if response, err := config.CIClient().Do(request); err == nil { response.Body.Close() if response.StatusCode != 200 { util.GOut("gc", "ERROR: Failed invoking full GC as node request in Jenkins failed with %s", response.Status) } } else { util.GOut("gc", "ERROR: Failed invoking full GC as Jenkins cannot be contacted. Cause: %v", err) } } }
func (self *ClientMode) IsConfigAcceptable(config *util.Config) bool { if !config.HasCIConnection() { util.GOut(self.Name(), "ERROR: No Jenkins URI defined. Cannot connect to the CI server.") return false } if config.SecretKey == "" && !self.isAuthCredentialsPassedViaCommandline(config) { if config.SecretKey = self.getSecretFromJenkins(config); config.SecretKey == "" { util.GOut(self.Name(), "ERROR: No secret key set for node %v and the attempt to fetch it from Jenkins failed.", config.ClientName) return false } } return true }
// Monitors that the tunnel is alive by periodically querying the node status off Jenkins. // Timeout, hanging connections or connection errors lead to a restart of the current execution mode (which implicitly closes SSH tunnel as well). func (self *SSHTunnelEstablisher) startAliveStateMonitoring(config *util.Config) { // Periodically check the node status and increment lastAliveTick on success go func() { for _ = range self.aliveTicker.C { if !self.tunnelConnected.Get() { continue } if _, err := GetJenkinsNodeStatus(config); err == nil { self.lastAliveTick.Set(self.expectedAliveTick.Get()) } } }() // Periodically check that lastAliveTick was incremented. go func() { for _ = range self.aliveTickEvaluator.C { if !self.tunnelConnected.Get() { continue } if math.Abs(float64(self.expectedAliveTick.Get()-self.lastAliveTick.Get())) > 1 { util.GOut("ssh-tunnel", "WARN: The SSH tunnel appears to be dead or Jenkins is gone. Forcing restart of client and SSH tunnel.") modes.GetConfiguredMode(config).Stop() } else { self.expectedAliveTick.AddAndGet(1) } } }() }
// Creates a new tunnel establisher. func NewSSHTunnelEstablisher(registerInMode bool) *SSHTunnelEstablisher { self := new(SSHTunnelEstablisher) self.closables = []io.Closer{} self.ciHostURL = nil self.aliveTicker, self.aliveTickEvaluator = time.NewTicker(nodeSshTunnelAliveMonitoringInterval), time.NewTicker(nodeSshTunnelAliveMonitoringInterval) self.expectedAliveTick, self.lastAliveTick = util.NewAtomicInt32(), util.NewAtomicInt32() self.tunnelConnected = util.NewAtomicBoolean() if registerInMode { modes.RegisterModeListener(func(mode modes.ExecutableMode, nextStatus int32, config *util.Config) { if !config.CITunnelSSHEnabled || config.CITunnelSSHAddress == "" || mode.Name() != "client" || !config.HasCIConnection() { return } if nextStatus == modes.ModeStarting { var err error if self.ciHostURL, err = url.Parse(config.CIHostURI); err != nil { util.GOut("ssh-tunnel", "ERROR: Failed parsing Jenkins URI. Cannot tunnel connections to Jenkins. Cause: %v", err) return } self.setupSSHTunnel(config) } else if nextStatus == modes.ModeStopped { self.tearDownSSHTunnel(config) } }) } return self }
func (self *JenkinsNodeMonitor) IsConfigAcceptable(config *util.Config) bool { if config.ClientMonitorStateOnServer && !config.HasCIConnection() { util.GOut("monitor", "No Jenkins URI defined. Cannot monitor this node within Jenkins.") return false } return true }
func (self *FullGCInvoker) IsConfigAcceptable(config *util.Config) bool { if config.ForceFullGC && !config.HasCIConnection() { util.GOut("gc", "WARN: No Jenkins URI defined. System.GC() cannot be called inside the Jenkins client.") return false } return true }
func (self *PeriodicRestarter) waitForIdleIfRequired(config *util.Config) { if config.PeriodicClientRestartOnlyWhenIDLE { for !util.NodeIsIdle.Get() { util.GOut("periodic", "Waiting for node to become IDLE before triggering a restart.") time.Sleep(time.Minute * 5) } } }
// Checks if Jenkins shows this node as connected and returns the node's IDLE state as second return value. func (self *JenkinsNodeMonitor) isServerSideConnected(config *util.Config) (connected bool, idle bool, serverReachable bool) { if status, err := GetJenkinsNodeStatus(config); err == nil { return !status.Offline, status.Idle, true } else { util.GOut("monitor", "ERROR: Failed to monitor node %v using %v. Cause: %v", config.ClientName, config.CIHostURI, err) return false, true, false } }
// Performs registration & deregistration for autostart on windows. func (self *AutostartHandler) Prepare(config *util.Config) { cwd, _ := os.Getwd() self.commandline = fmt.Sprintf("\"%s\" \"-directory=%s\"", os.Args[0], cwd) if config.Autostart { util.GOut("autostart", "Registering %v for autostart.", self.commandline) err := self.register() if err != nil { util.GOut("autostart", "ERROR: FAILED to register for autostart. Cause: %v", err) } } else { if self.isRegistered() { util.GOut("autostart", "Unregistering %v from autostart.", self.commandline) err := self.unregister() if err != nil { util.GOut("autostart", "ERROR: FAILED to unregister from autostart. Cause: %v", err) } } } }
func (self *ClientMode) getSecretFromJenkins(config *util.Config) string { response, err := config.CIGet(fmt.Sprintf("computer/%s/", config.ClientName)) if err == nil { defer response.Body.Close() if response.StatusCode == 200 { var content []byte if content, err = ioutil.ReadAll(response.Body); err == nil { return self.extractSecret(content) } } else { util.GOut("client", "ERROR: Failed fetching secret key from Jenkins. Cause: %v", response.Status) } } if err != nil { util.GOut("client", "ERROR: Failed fetching secret key from Jenkins. Cause: %v", err) } return "" }
func (self *ClientMode) getCustomizedAgentJnlp(config *util.Config) []byte { response, err := config.CIGet(fmt.Sprintf("computer/%s/slave-agent.jnlp", config.ClientName)) if err == nil { defer response.Body.Close() if response.StatusCode == 200 { var content []byte if content, err = ioutil.ReadAll(response.Body); err == nil { return self.applyCustomJnlpArgs(config, content) } } else { util.GOut("client", "ERROR: Failed JNLP config from Jenkins. Cause: %v", response.Status) } } if err != nil { util.GOut("client", "ERROR: Failed JNLP config from Jenkins. Cause: %v", err) } return nil }
// Checks if java is installed and if the java version is greater or equals the required version. func (self *JavaDownloader) javaIsInstalled() bool { if java, err := exec.LookPath("java"); err == nil { // Set absolute path to java util.Java = java // Check the version if output, err := exec.Command(java, "-version").CombinedOutput(); err == nil { if pattern, err := regexp.Compile(`(?i)java version "([^"]+)"`); err == nil { if matches := pattern.FindSubmatch(output); matches != nil && len(matches) == 2 { javaVersion := string(matches[1]) if version.Compare(javaVersion, MinJavaVersion, ">=") { util.GOut("java", "Found java version %v, no need to install a newer version.", javaVersion) return true } util.GOut("java", "Found java version %v. A newer version is required to run the Jenkins client.", javaVersion) } } } } return false }
func (self *NodeNameHandler) Prepare(config *util.Config) { if !config.HasCIConnection() { return } if foundNode, err := self.verifyNodeName(config); err == nil { if !foundNode { if config.CreateClientIfMissing { if err := self.createNode(config); err == nil { util.GOut("naming", "Created node '%s' in Jenkins.", config.ClientName) } else { util.GOut("naming", "ERROR: Failed to create node '%s' in Jenkins. Cause: %v", config.ClientName, err) } foundNode, _ = self.verifyNodeName(config) } else { util.GOut("naming", "Will not attempt to auto generate node '%s' in Jenkins. Enable this with '-create' or within the configuration.", config.ClientName) } } if foundNode { util.GOut("naming", "Found client node name in Jenkins, using '%v'.", config.ClientName) } else { util.GOut("naming", "WARN: Client node name '%v' was %s in Jenkins. Likely the next operations will fail.", config.ClientName, "NOT FOUND") } } else { util.GOut("nameing", "ERROR: Failed to verify the client node name in Jenkins. Cause: %v", err) } }
func (self *ClientMode) redirectConsoleOutput(config *util.Config, input io.ReadCloser, output io.Writer, outputMutex *sync.Mutex) { defer input.Close() reader := bufio.NewReader(input) holdsLock := false lock, unlock := func() { if !holdsLock { outputMutex.Lock() holdsLock = true } }, func() { if holdsLock { outputMutex.Unlock() holdsLock = false } } defer unlock() restartTriggered := false for { line, isPrefix, err := reader.ReadLine() if len(line) > 0 || holdsLock { lock() // Send to output if len(line) > 0 { output.Write(line) } if !isPrefix { output.Write([]byte("\n")) unlock() } if config.ClientMonitorConsole && !restartTriggered && config.ConsoleMonitor.IsRestartTriggered(string(line)) { go func() { time.Sleep(time.Second * 1) go self.Stop() util.GOut("client", "WARN: %s found in console output. Client state may be invalid, forced a restart.", "RESTART TOKEN") }() restartTriggered = true } } if err != nil { break } } }
// Checks if both, this side and the remote side show the node as connected and increments a offline count if not. // Forces a restart of the connector when offline count reaches the threshold. func (self *JenkinsNodeMonitor) monitor(config *util.Config) { if self.isThisSideConnected(config) { if connected, idle, serverReachable := self.isServerSideConnected(config); connected { util.NodeIsIdle.Set(idle) self.offlineCount = 0 if !self.onlineShown { util.GOut("monitor", "Node is online in Jenkins.") self.onlineShown = true } } else { util.NodeIsIdle.Set(true) if serverReachable { self.offlineCount++ } if self.offlineCount > 3*maxOfflineCountBeforeRestart { self.offlineCount = maxOfflineCountBeforeRestart } if self.offlineCount == maxOfflineCountBeforeRestart { self.forceReconnect(config) } util.GOut("monitor", "WARN: Node is OFFLINE in Jenkins.") self.onlineShown = false } } else { util.NodeIsIdle.Set(true) self.offlineCount = 0 if self.onlineShown { util.GOut("monitor", "WARN: Node went OFFLINE locally.") self.onlineShown = false } } }
func (self *JenkinsClientDownloader) Prepare(config *util.Config) { util.ClientJar, _ = filepath.Abs(ClientJarName) modes.RegisterModeListener(func(mode modes.ExecutableMode, nextStatus int32, config *util.Config) { if mode.Name() == "client" && nextStatus == modes.ModeStarting && config.HasCIConnection() { if err := self.downloadJar(config); err != nil { jar, e := os.Open(ClientJarName) defer jar.Close() if os.IsNotExist(e) { panic(fmt.Sprintf("No jenkins client: %s", err)) } else { util.GOut("DOWNLOAD", "%s", err) } } } }) }
func (self *PeriodicRestarter) Prepare(config *util.Config) { if self.ticker != nil { self.ticker.Stop() } if !config.PeriodicClientRestartEnabled || config.PeriodicClientRestartIntervalHours <= 0 { return } self.ticker = time.NewTicker(time.Hour * time.Duration(config.PeriodicClientRestartIntervalHours)) go func() { // Run in schedule for time := range self.ticker.C { util.GOut("periodic", "Triggering periodic restart.", time) self.waitForIdleIfRequired(config) // Stopping the mode as this will automatically do a restart. modes.GetConfiguredMode(config).Stop() } }() }
func (self *LocationCleaner) cleanupFiles(rootDir string, expiredTimeOffset time.Time, dryRun bool, exclusions []string, dirToEmptyMap map[string]bool) (exclusionCount int64) { filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { if err == nil { dirToEmptyMap[filepath.Dir(path)] = false if info.IsDir() { dirToEmptyMap[filepath.Clean(path)] = true } else { fileIsToRemove := true if fileIsToRemove && util.GetFileLastTouched(info).After(expiredTimeOffset) { fileIsToRemove = false } if fileIsToRemove && len(exclusions) > 0 { for _, pattern := range exclusions { if matchesExclusionPattern, _ := filepath.Match(pattern, path); matchesExclusionPattern { fileIsToRemove = false break } } } if fileIsToRemove { if !dryRun { if err := os.Remove(path); err == nil { util.GOut("cleanup", "\x1b[39mRemoved expired: %v", path) } } } else { exclusionCount++ } } } return err }) return }
func (self *FullGCInvoker) Prepare(config *util.Config) { if self.tickers != nil { for _, ticker := range self.tickers { ticker.Stop() } } else { self.tickers = []*time.Ticker{} } if !config.ForceFullGC { return } util.GOut("gc", "Periodic forced full GC is enabled.") if config.ForceFullGCIntervalMinutes > 0 { self.tickers = append(self.tickers, self.scheduleGCInvoker(config, config.ForceFullGCIntervalMinutes, false)) } if config.ForceFullGCIDLEIntervalMinutes > 0 { self.tickers = append(self.tickers, self.scheduleGCInvoker(config, config.ForceFullGCIDLEIntervalMinutes, true)) } }
func (self *LocationCleaner) waitForIdle() { for !util.NodeIsIdle.Get() { util.GOut("cleanup", "Waiting for node to become IDLE before cleaning configured locations.") time.Sleep(time.Minute * 5) } }
// Forces a reconnect with Jenkins by stopping the current mode. func (self *JenkinsNodeMonitor) forceReconnect(config *util.Config) { if self.isThisSideConnected(config) { util.GOut("monitor", "WARN: This node appears dead in Jenkins, forcing a reconnect.") modes.GetConfiguredMode(config).Stop() } }
// Opens a new SSH connection, local server ports (JNLP, HTTP) and forwards it to the corresponding ports on Jenkins. func (self *SSHTunnelEstablisher) setupSSHTunnel(config *util.Config) { if self.ciHostURL == nil { return } if !config.PassCIAuth && config.SecretKey != "" { util.GOut("ssh-tunnel", "WARN: Secret key is not supported in combination with SSH tunnel. Implicitly setting %v to %v", "client>passAuth", "true") config.PassCIAuth = true } // Ensure no other SSL connections are still open. self.tearDownSSHTunnel(config) // Configuring the SSH client clientConfig := &ssh.ClientConfig{ User: config.CITunnelSSHUsername, Auth: []ssh.AuthMethod{ ssh.Password(config.CITunnelSSHPassword), }, HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { expected, actual := config.CITunnelSSHFingerprint, self.formatHostFingerprint(key) if actual != expected && expected != "-" { if expected == "" { return fmt.Errorf("The host fingerprint of '%v' is '%v'. Please add this to the configuration in order to connect.", hostname, actual) } else { return fmt.Errorf("The host fingerprint of '%v' is '%v' while '%v' was expected. Connection aborted.", hostname, actual, expected) } } return nil }, } // Connecting to the SSH host if config.CITunnelSSHPort == 0 { config.CITunnelSSHPort = 22 } sshAddress := fmt.Sprintf("%v:%v", config.CITunnelSSHAddress, config.CITunnelSSHPort) sshClient, err := ssh.Dial("tcp", sshAddress, clientConfig) if err == nil { self.closables = append(self.closables, sshClient) util.GOut("ssh-tunnel", "Successfully connected with '%v'.", sshAddress) } else { util.GOut("ssh-tunnel", "ERROR: Failed connecting with %v. Cause: %v", sshAddress, err) return } // Fetching target ports jnlpTargetAddress, err := self.formatJNLPHostAndPort(config) if err != nil { util.GOut("ssh-tunnel", "ERROR: Failed fetching JNLP port from '%v'. Cause: %v.", config.CIHostURI, err) return } httpTargetAddress := self.formatHttpHostAndPort() // Creating a local server listeners to use for port forwarding. httpListener, err1 := self.newLocalServerListener() jnlpListener, err2 := self.newLocalServerListener() if err1 != nil || err2 != nil { self.tearDownSSHTunnel(config) return } // Forward local connections to the HTTP(S)/JNLP ports. go self.forwardLocalConnectionsTo(config, sshClient, httpListener, httpTargetAddress) go self.forwardLocalConnectionsTo(config, sshClient, jnlpListener, jnlpTargetAddress) // Apply the tunnel configuration localCiURL, _ := url.Parse(self.ciHostURL.String()) localCiURL.Host = httpListener.Addr().String() config.CIHostURI = localCiURL.String() util.JnlpArgs["-url"] = localCiURL.String() util.JnlpArgs["-tunnel"] = jnlpListener.Addr().String() // Mark tunnel as connected when we passed this line. self.tunnelConnected.Set(true) }
func (self *ClientMode) execute(config *util.Config) { commandline := []string{} commandline = append(commandline, util.JavaArgs...) commandline = append(commandline, config.JavaArgs...) if config.JavaMaxMemory != "" { commandline = append(commandline, "-Xmx"+config.JavaMaxMemory) } commandline = append(commandline, "-jar", util.ClientJar) if len(util.JnlpArgs) > 0 { if err := ioutil.WriteFile("~slave-agent.jnlp", self.getCustomizedAgentJnlp(config), os.ModeTemporary); err == nil { defer os.Remove("~slave-agent.jnlp") commandline = append(commandline, "-jnlpUrl", "file:./~slave-agent.jnlp") } else { util.GOut("client", "ERROR: Failed creating customized JNLP config. Cause: %v", err) } } else { commandline = append(commandline, "-jnlpUrl", fmt.Sprintf("%v/computer/%v/slave-agent.jnlp", config.CIHostURI, config.ClientName)) if config.SecretKey != "" && !self.isAuthCredentialsPassedViaCommandline(config) { commandline = append(commandline, "-secret", config.SecretKey) } } if config.CIAcceptAnyCert { commandline = append(commandline, "-noCertificateCheck") } if config.HandleReconnectsInLauncher { commandline = append(commandline, "-noReconnect") } if self.isAuthCredentialsPassedViaCommandline(config) { commandline = append(commandline, "-auth", fmt.Sprintf("%s:%s", config.CIUsername, config.CIPassword)) commandline = append(commandline, "-jnlpCredentials", fmt.Sprintf("%s:%s", config.CIUsername, config.CIPassword)) } stoppingClient, clientStopped := make(chan bool), make(chan bool) go func() { command := exec.Command(util.Java, commandline...) if pOut, err := command.StdoutPipe(); err == nil { go self.redirectConsoleOutput(config, pOut, os.Stdout, util.OutputMutex) } else { panic("Failed connecting stdout with console") } if pErr, err := command.StderrPipe(); err == nil { go self.redirectConsoleOutput(config, pErr, os.Stderr, util.OutputMutex) } else { panic("Failed connecting stderr with console") } util.GOut("client", "Starting: %s", self.createFilteredCommands(commandline)) if err := command.Start(); err != nil { util.GOut("client", "ERROR: Jenkins client failed to start with %v", err) } else { util.GOut("client", "Jenkins client was started.") go func() { <-stoppingClient command.Process.Kill() time.Sleep(time.Second * 1) }() if err := command.Wait(); err != nil { util.GOut("client", "WARN: Jenkins client quit with %v", err) } else { util.GOut("client", "Jenkins client was stopped.") } self.status.Set(ModeStopped) clientStopped <- true } }() // Entering main loop self.status.Set(ModeStarted) for self.status.Get() == ModeStarted { time.Sleep(time.Millisecond * 100) } stoppingClient <- true <-clientStopped self.status.Set(ModeStopped) }