diff --git a/Dockerfile b/Dockerfile index 9b99222..9a04d82 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,18 @@ WORKDIR /opt/converge RUN go mod download COPY . /opt/converge/ RUN go build -ldflags "-linkmode 'external' -extldflags '-static'" -o bin ./cmd/... +RUN GOOS=windows GOARCH=amd64 go build -o bin ./cmd/... FROM scratch COPY --from=builder /opt/converge/bin/converge /opt/converge/bin/ -COPY --from=builder /opt/converge/bin/agent /opt/converge/bin/tcptows /opt/converge/bin/wsproxy /opt/converge/docs/ +COPY --from=builder /opt/converge/bin/agent \ + /opt/converge/bin/tcptows \ + /opt/converge/bin/wsproxy \ + /opt/converge/bin/agent.exe \ + /opt/converge/bin/tcptows.exe \ + /opt/converge/bin/wsproxy.exe \ + /opt/converge/docs/ COPY --from=builder /opt/converge/static/ /opt/converge/docs/ ENTRYPOINT ["/opt/converge/bin/converge", "/opt/converge/docs" ] diff --git a/Makefile b/Makefile index f0ef146..e598960 100644 --- a/Makefile +++ b/Makefile @@ -13,6 +13,10 @@ build: vet mkdir -p bin go build -o bin ./cmd/... +buildwin: + mkdir -p bin + GOOS=windows GOARCH=amd64 go build -o bin ./cmd/... + clean: rm -rf bin diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index 68d618e..878845a 100755 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -6,7 +6,11 @@ import ( "converge/pkg/iowrappers" "converge/pkg/websocketutil" "fmt" + "github.com/creack/pty" + "github.com/gliderlabs/ssh" "github.com/gorilla/websocket" + "github.com/hashicorp/yamux" + "github.com/pkg/sftp" "io" "log" "net" @@ -14,14 +18,7 @@ import ( "os" "os/exec" "strings" - "syscall" "time" - "unsafe" - - "github.com/creack/pty" - "github.com/gliderlabs/ssh" - "github.com/hashicorp/yamux" - "github.com/pkg/sftp" _ "embed" ) @@ -55,11 +52,6 @@ func passwordAuth(ctx ssh.Context, password string) bool { return ctx.User() == "abc" && password == "123" } -func setWinsize(f *os.File, w, h int) { - syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), - uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) -} - func sshServer(hostKeyFile string, shellCommand string) *ssh.Server { ssh.Handle(func(s ssh.Session) { diff --git a/cmd/agent/agent_linux.go b/cmd/agent/agent_linux.go new file mode 100755 index 0000000..998014f --- /dev/null +++ b/cmd/agent/agent_linux.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + "syscall" + "unsafe" +) + +func setWinsize(f *os.File, w, h int) { + syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) +} diff --git a/cmd/agent/agent_windows.go b/cmd/agent/agent_windows.go new file mode 100755 index 0000000..8120a10 --- /dev/null +++ b/cmd/agent/agent_windows.go @@ -0,0 +1,7 @@ +package main + +import "os" + +func setWinsize(f *os.File, w, h int) { + // Empty +} diff --git a/cmd/agent/open_process_windows.go b/cmd/agent/open_process_windows.go new file mode 100644 index 0000000..0378ba4 --- /dev/null +++ b/cmd/agent/open_process_windows.go @@ -0,0 +1,98 @@ +package main + +import ( + "fmt" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +func example() { + // Create pipes for stdin and stdout + var stdInRead, stdInWrite, stdOutRead, stdOutWrite windows.Handle + sa := &windows.SecurityAttributes{Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})), InheritHandle: 1} + + err := windows.CreatePipe(&stdInRead, &stdInWrite, sa, 0) + if err != nil { + fmt.Println("Error creating stdin pipe:", err) + return + } + defer windows.CloseHandle(stdInRead) + defer windows.CloseHandle(stdInWrite) + + err = windows.CreatePipe(&stdOutRead, &stdOutWrite, sa, 0) + if err != nil { + fmt.Println("Error creating stdout pipe:", err) + return + } + defer windows.CloseHandle(stdOutRead) + defer windows.CloseHandle(stdOutWrite) + + // Set the pipe to non-blocking mode + mode := uint32(windows.PIPE_NOWAIT) + err = windows.SetNamedPipeHandleState(stdInWrite, &mode, nil, nil) + if err != nil { + fmt.Println("Error setting stdin pipe to non-blocking:", err) + return + } + err = windows.SetNamedPipeHandleState(stdOutRead, &mode, nil, nil) + if err != nil { + fmt.Println("Error setting stdout pipe to non-blocking:", err) + return + } + + // Prepare process startup info + si := &windows.StartupInfo{ + Cb: uint32(unsafe.Sizeof(windows.StartupInfo{})), + Flags: windows.STARTF_USESTDHANDLES, + StdInput: stdInRead, + StdOutput: stdOutWrite, + StdErr: stdOutWrite, + } + pi := &windows.ProcessInformation{} + + // Create the process + cmd := "cmd.exe" + err = windows.CreateProcess( + nil, + syscall.StringToUTF16Ptr(cmd), + nil, + nil, + true, + 0, + nil, + nil, + si, + pi, + ) + if err != nil { + fmt.Println("Error creating process:", err) + return + } + defer windows.CloseHandle(pi.Process) + defer windows.CloseHandle(pi.Thread) + + // Write to the process + message := "echo Hello, World!\r\n" + var written uint32 + err = windows.WriteFile(stdInWrite, []byte(message), &written, nil) + if err != nil { + fmt.Println("Error writing to process:", err) + return + } + + // Read from the process + buffer := make([]byte, 1024) + var read uint32 + err = windows.ReadFile(stdOutRead, buffer, &read, nil) + if err != nil && err != windows.ERROR_NO_DATA { + fmt.Println("Error reading from process:", err) + return + } + + fmt.Printf("Output: %s", buffer[:read]) + + // Wait for the process to finish + windows.WaitForSingleObject(pi.Process, windows.INFINITE) +} diff --git a/cmd/tcpclient/sshclient.go b/cmd/tcpclient/sshclient.go index 2a08017..e8384e6 100755 --- a/cmd/tcpclient/sshclient.go +++ b/cmd/tcpclient/sshclient.go @@ -116,19 +116,3 @@ func runSSHClient(user, password string, netConn net.Conn) error { return nil } - -func handleWindowChange(session *ssh.Session, fd int) { - sigwinchCh := make(chan os.Signal, 1) - signal.Notify(sigwinchCh, syscall.SIGWINCH) - - for range sigwinchCh { - width, height, err := term.GetSize(fd) - if err != nil { - log.Printf("Failed to get window size: %v\n", err) - continue - } - if err := session.WindowChange(height, width); err != nil { - log.Printf("Failed to send window change request: %v\n", err) - } - } -} diff --git a/cmd/tcpclient/sshclient_linux.go b/cmd/tcpclient/sshclient_linux.go new file mode 100644 index 0000000..2ad9694 --- /dev/null +++ b/cmd/tcpclient/sshclient_linux.go @@ -0,0 +1,26 @@ +package main + +import ( + "golang.org/x/crypto/ssh" + "golang.org/x/term" + "log" + "os" + "os/signal" + "syscall" +) + +func handleWindowChange(session *ssh.Session, fd int) { + sigwinchCh := make(chan os.Signal, 1) + signal.Notify(sigwinchCh, syscall.SIGWINCH) + + for range sigwinchCh { + width, height, err := term.GetSize(fd) + if err != nil { + log.Printf("Failed to get window size: %v\n", err) + continue + } + if err := session.WindowChange(height, width); err != nil { + log.Printf("Failed to send window change request: %v\n", err) + } + } +} diff --git a/cmd/tcpclient/sshclient_windows.go b/cmd/tcpclient/sshclient_windows.go new file mode 100644 index 0000000..0a271ba --- /dev/null +++ b/cmd/tcpclient/sshclient_windows.go @@ -0,0 +1,7 @@ +package main + +import "golang.org/x/crypto/ssh" + +func handleWindowChange(session *ssh.Session, fd int) { + // Empty +} diff --git a/cmd/tcpserver/sshserver.go b/cmd/tcpserver/sshserver.go index 9baf1b4..7ddd5f5 100755 --- a/cmd/tcpserver/sshserver.go +++ b/cmd/tcpserver/sshserver.go @@ -2,16 +2,13 @@ package main import ( "fmt" + "github.com/creack/pty" + "github.com/gliderlabs/ssh" + "github.com/pkg/sftp" "io" "log" "os" "os/exec" - "syscall" - "unsafe" - - "github.com/creack/pty" - "github.com/gliderlabs/ssh" - "github.com/pkg/sftp" ) func SftpHandler(sess ssh.Session) { @@ -40,11 +37,6 @@ func passwordAuth(ctx ssh.Context, password string) bool { return ctx.User() == "abc" && password == "123" } -func setWinsize(f *os.File, w, h int) { - syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), - uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) -} - func main() { ssh.Handle(func(s ssh.Session) { cmd := exec.Command("bash") diff --git a/cmd/tcpserver/sshserver_linux.go b/cmd/tcpserver/sshserver_linux.go new file mode 100644 index 0000000..998014f --- /dev/null +++ b/cmd/tcpserver/sshserver_linux.go @@ -0,0 +1,12 @@ +package main + +import ( + "os" + "syscall" + "unsafe" +) + +func setWinsize(f *os.File, w, h int) { + syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ), + uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0}))) +} diff --git a/cmd/tcpserver/sshserver_windows.go b/cmd/tcpserver/sshserver_windows.go new file mode 100644 index 0000000..8120a10 --- /dev/null +++ b/cmd/tcpserver/sshserver_windows.go @@ -0,0 +1,7 @@ +package main + +import "os" + +func setWinsize(f *os.File, w, h int) { + // Empty +} diff --git a/compose.yaml b/compose.yaml index c68855e..71ed4b8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,7 @@ services: converge: - image: your.repo.com/converge:1.0 + image: $REGISTRY/converge:1.0 build: context: . ports: diff --git a/go.mod b/go.mod index 2b78cd4..9f65ada 100755 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ -module converge +module converge -go 1.18 +go 1.21 require ( github.com/creack/pty v1.1.21 diff --git a/pkg/agent/session.go b/pkg/agent/session.go index f1ac5c0..8fbfb6b 100644 --- a/pkg/agent/session.go +++ b/pkg/agent/session.go @@ -30,6 +30,8 @@ type AgentState struct { // map of unique session id to a session sessions map[int]*AgentSession + + agentUsed bool } type AgentSession struct { @@ -61,6 +63,7 @@ func ConfigureAgent(advanceWarningTime, agentExpiryTime, tickerInterval time.Dur tickerInterval: tickerInterval, ticker: time.NewTicker(tickerInterval), sessions: make(map[int]*AgentSession), + agentUsed: false, } go func() { @@ -97,6 +100,7 @@ func Login(sessionId int, sshSession ssh.Session) { sshSession: sshSession, } state.sessions[sessionId] = &agentSession + state.agentUsed = true LogStatus() } @@ -178,7 +182,7 @@ func check() { } } - if !fileExists(holdFilename) && len(state.sessions) == 0 { + if state.agentUsed && !fileExists(holdFilename) && len(state.sessions) == 0 { log.Printf("All clients disconnected and no '%s' file found, exiting", holdFilename) os.Exit(0) } diff --git a/static/index.html b/static/index.html index 2684c8c..acffb94 100644 --- a/static/index.html +++ b/static/index.html @@ -17,7 +17,7 @@
- Converge is a utility for troubleshooting builds on continuous integration serves. + Converge is a utility for troubleshooting builds on continuous integration servers. It solves a common problem where the cause of job failure is difficult to determine. This is complicated further by the fact that build jobs are usually run on a build farm where there is no access to the build agents or in more modern envrionments when @@ -28,7 +28,7 @@ With Converge it is possible to get remote shell access to such jobs. This works by configuring the build job to connect to a Converge server using an agent program. The agent program can be downloaded from within the CI job using curl or wget. - Next, an end-use can connect to the Converge server, a rendez-vous server, that connects + Next, an end-user can connect to the Converge server, a rendez-vous server, that connects the client and server together.
@@ -44,12 +44,12 @@There is a timeout mechanism in the agent such that jobs do not hang indefinitely waiting - for a connection. This mechanism is useful to make sure build agents do not wait - indefinitely for a user session. By default, the agent exits with status 0 when + for a connection. This mechanism is useful to make sure build agents do not keep + build agents occupied for a long time. By default, the agent exits with status 0 when the first client exits after logging in. This behavior as well as general expiry can be controlled from within a shell session by touching a .hold file. After logging in, the user can control expiry of the session as instructed by messages in the ssh session. - Then the timeout of a session is near the user is informed about this with messages + When the timeout of a session is near the user is informed about this with messages in the shell.
@@ -69,8 +69,8 @@ Above, ID is a unique id for the job. This should not conflict with other ids. - This connects the agent to the converge server. Clients can now connect to converge - to establish a connection to the CI job through converge. + This connects the agent to the converge server. Clients can now connect to the Converge + server to establish a connection to the CI job through converge.- Next step is to run a local SSH of SFTP client: + Next step is to run a local SSH or SFTP client: