• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender-artifact / 1979486890

12 Aug 2025 09:38AM UTC coverage: 76.223% (-0.02%) from 76.243%
1979486890

push

gitlab-ci

web-flow
Merge pull request #727 from michalkopczan/MEN-8429_mender-artifact-waits-indefinitely-if-ssh-connection-fails-silently

fix: mender-artifact hangs when ssh connection fails silently

0 of 2 new or added lines in 1 file covered. (0.0%)

2 existing lines in 1 file now uncovered.

5982 of 7848 relevant lines covered (76.22%)

138.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/cli/util/ssh.go
1
// Copyright 2025 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package util
16

17
import (
18
        "bufio"
19
        "context"
20
        "io"
21
        "os"
22
        "os/exec"
23
        "os/signal"
24
        "strings"
25
        "syscall"
26
        "time"
27

28
        "github.com/pkg/errors"
29
        "github.com/urfave/cli"
30
)
31

32
type SSHCommand struct {
33
        Cmd     *exec.Cmd
34
        ctx     context.Context
35
        Stdout  io.ReadCloser
36
        cancel  context.CancelFunc
37
        sigChan chan os.Signal
38
        errChan chan error
39
}
40

41
func StartSSHCommand(c *cli.Context,
42
        _ctx context.Context,
43
        cancel context.CancelFunc,
44
        command string,
45
        sshConnectedToken string,
46
) (*SSHCommand, error) {
×
47

×
48
        var userAtHost string
×
49
        var sigChan chan os.Signal
×
50
        var errChan chan error
×
51
        port := "22"
×
52
        host := strings.TrimPrefix(c.String("file"), "ssh://")
×
53

×
54
        if remotePort := strings.Split(host, ":"); len(remotePort) == 2 {
×
55
                port = remotePort[1]
×
56
                userAtHost = remotePort[0]
×
57
        } else {
×
58
                userAtHost = host
×
59
        }
×
60

61
        args := c.StringSlice("ssh-args")
×
62
        // Check if port is specified explicitly with the --ssh-args flag
×
63
        addPort := true
×
64
        for _, arg := range args {
×
65
                if strings.Contains(arg, "-p") {
×
66
                        addPort = false
×
67
                        break
×
68
                }
69
        }
70
        if addPort {
×
71
                args = append(args, "-p", port)
×
72
        }
×
73
        args = append(args, userAtHost)
×
74
        args = append(
×
75
                args,
×
NEW
76
                "-o ServerAliveInterval=30",
×
NEW
77
                "-o ServerAliveCountMax=1",
×
78
                "/bin/sh",
×
79
                "-c",
×
80
                command)
×
81

×
82
        cmd := exec.Command("ssh", args...)
×
83

×
84
        // Simply connect stdin/stderr
×
85
        cmd.Stdin = os.Stdin
×
86
        cmd.Stderr = os.Stderr
×
87
        stdout, err := cmd.StdoutPipe()
×
88
        if err != nil {
×
89
                return nil, errors.New("Error redirecting stdout on exec")
×
90
        }
×
91

92
        // Disable tty echo before starting
93
        term, err := DisableEcho(int(os.Stdin.Fd()))
×
94
        if err == nil {
×
95
                sigChan = make(chan os.Signal, 1)
×
96
                errChan = make(chan error, 1)
×
97
                // Make sure that echo is enabled if the process gets
×
98
                // interrupted
×
99
                signal.Notify(sigChan)
×
100
                go EchoSigHandler(_ctx, sigChan, errChan, term)
×
101
        } else if err != syscall.ENOTTY {
×
102
                return nil, err
×
103
        }
×
104

105
        if err := cmd.Start(); err != nil {
×
106
                return nil, err
×
107
        }
×
108

109
        // Wait for 120 seconds for ssh to establish connection
110
        err = waitForBufferSignal(stdout, os.Stdout, sshConnectedToken, 2*time.Minute)
×
111
        if err != nil {
×
112
                _ = cmd.Process.Kill()
×
113
                return nil, errors.Wrap(err,
×
114
                        "Error waiting for ssh session to be established.")
×
115
        }
×
116
        return &SSHCommand{
×
117
                ctx:     _ctx,
×
118
                Cmd:     cmd,
×
119
                Stdout:  stdout,
×
120
                cancel:  cancel,
×
121
                sigChan: sigChan,
×
122
                errChan: errChan,
×
123
        }, nil
×
124
}
125

126
func (s *SSHCommand) EndSSHCommand() error {
×
127
        if s.Cmd.ProcessState != nil && s.Cmd.ProcessState.Exited() {
×
128
                return errors.New("SSH session closed unexpectedly")
×
129
        }
×
130

131
        if err := s.Cmd.Wait(); err != nil {
×
132
                return errors.Wrap(err,
×
133
                        "SSH session closed with error")
×
134
        }
×
135

136
        if s.sigChan != nil {
×
137
                signal.Stop(s.sigChan)
×
138
                s.cancel()
×
139
                if err := <-s.errChan; err != nil {
×
140
                        return err
×
141
                }
×
142
        } else {
×
143
                s.cancel()
×
144
        }
×
145

146
        return nil
×
147
}
148

149
// Reads from src waiting for the string specified by signal, writing all other
150
// output appearing at src to sink. The function returns an error if occurs
151
// reading from the stream or the deadline exceeds.
152
func waitForBufferSignal(src io.Reader, sink io.Writer,
153
        signal string, deadline time.Duration) error {
×
154

×
155
        var err error
×
156
        errChan := make(chan error)
×
157

×
158
        go func() {
×
159
                stdoutRdr := bufio.NewReader(src)
×
160
                for {
×
161
                        line, err := stdoutRdr.ReadString('\n')
×
162
                        if err != nil {
×
163
                                errChan <- err
×
164
                                break
×
165
                        }
166
                        if strings.Contains(line, signal) {
×
167
                                errChan <- nil
×
168
                                break
×
169
                        }
170
                        _, err = sink.Write([]byte(line + "\n"))
×
171
                        if err != nil {
×
172
                                errChan <- err
×
173
                                break
×
174
                        }
175
                }
176
        }()
177

178
        select {
×
179
        case err = <-errChan:
×
180
                // Error from goroutine
181
        case <-time.After(deadline):
×
182
                err = errors.New("Input deadline exceeded")
×
183
        }
184
        return err
×
185
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc