From 2f40f86294f55ff0da25e7b1e89af084be14e0a2 Mon Sep 17 00:00:00 2001
From: Erik Brakkee
Date: Mon, 22 Jul 2024 19:34:26 +0200
Subject: [PATCH] cross compilation on windows working. pty.Start() is not
supported on windows
---
Dockerfile | 9 ++-
Makefile | 4 ++
cmd/agent/agent.go | 16 ++---
cmd/agent/agent_linux.go | 12 ++++
cmd/agent/agent_windows.go | 7 +++
cmd/agent/open_process_windows.go | 98 ++++++++++++++++++++++++++++++
cmd/tcpclient/sshclient.go | 16 -----
cmd/tcpclient/sshclient_linux.go | 26 ++++++++
cmd/tcpclient/sshclient_windows.go | 7 +++
cmd/tcpserver/sshserver.go | 14 +----
cmd/tcpserver/sshserver_linux.go | 12 ++++
cmd/tcpserver/sshserver_windows.go | 7 +++
compose.yaml | 2 +-
go.mod | 4 +-
pkg/agent/session.go | 6 +-
static/index.html | 16 ++---
16 files changed, 204 insertions(+), 52 deletions(-)
create mode 100755 cmd/agent/agent_linux.go
create mode 100755 cmd/agent/agent_windows.go
create mode 100644 cmd/agent/open_process_windows.go
create mode 100644 cmd/tcpclient/sshclient_linux.go
create mode 100644 cmd/tcpclient/sshclient_windows.go
create mode 100644 cmd/tcpserver/sshserver_linux.go
create mode 100644 cmd/tcpserver/sshserver_windows.go
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 @@
About
- 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.
Local clients: using ssh proxy command
@@ -84,7 +84,7 @@
server.
- Next step is to run a local SSH of SFTP client:
+ Next step is to run a local SSH or SFTP client: