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

mendersoftware / mender-artifact / 1988835491

18 Aug 2025 07:47AM UTC coverage: 76.243% (+0.2%) from 76.088%
1988835491

Pull #732

gitlab-ci

danielskinstad
test test-homebrew-developer

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
Pull Request #732: testing

5982 of 7846 relevant lines covered (76.24%)

138.07 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,
×
76
                "/bin/sh",
×
77
                "-c",
×
78
                command)
×
79

×
80
        cmd := exec.Command("ssh", args...)
×
81

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

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

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

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

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

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

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

144
        return nil
×
145
}
146

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

×
153
        var err error
×
154
        errChan := make(chan error)
×
155

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

176
        select {
×
177
        case err = <-errChan:
×
178
                // Error from goroutine
179
        case <-time.After(deadline):
×
180
                err = errors.New("Input deadline exceeded")
×
181
        }
182
        return err
×
183
}
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

© 2026 Coveralls, Inc