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

mendersoftware / mender-artifact / 1980017761

12 Aug 2025 02:01PM UTC coverage: 76.088% (-0.1%) from 76.223%
1980017761

push

gitlab-ci

web-flow
Merge pull request #728 from michalkopczan/MEN-8428-mender-artifact-does-not-reenable-echo

fix: mender-artifact does not reenable echo on ssh error

0 of 50 new or added lines in 3 files covered. (0.0%)

4 existing lines in 2 files now uncovered.

5982 of 7862 relevant lines covered (76.09%)

137.91 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
×
NEW
51
        s := &SSHCommand{
×
NEW
52
                ctx:    _ctx,
×
NEW
53
                cancel: cancel,
×
NEW
54
        }
×
NEW
55

×
56
        port := "22"
×
57
        host := strings.TrimPrefix(c.String("file"), "ssh://")
×
58

×
59
        if remotePort := strings.Split(host, ":"); len(remotePort) == 2 {
×
60
                port = remotePort[1]
×
61
                userAtHost = remotePort[0]
×
62
        } else {
×
63
                userAtHost = host
×
64
        }
×
65

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

×
87
        cmd := exec.Command("ssh", args...)
×
NEW
88
        s.Cmd = cmd
×
89

×
90
        // Simply connect stdin/stderr
×
91
        cmd.Stdin = os.Stdin
×
92
        cmd.Stderr = os.Stderr
×
93
        stdout, err := cmd.StdoutPipe()
×
94
        if err != nil {
×
NEW
95
                return s, errors.New("Error redirecting stdout on exec")
×
96
        }
×
NEW
97
        s.Stdout = stdout
×
UNCOV
98

×
UNCOV
99
        // Disable tty echo before starting
×
100
        term, err := DisableEcho(int(os.Stdin.Fd()))
×
101
        if err == nil {
×
102
                sigChan = make(chan os.Signal, 1)
×
103
                errChan = make(chan error, 1)
×
NEW
104
                s.sigChan = sigChan
×
NEW
105
                s.errChan = errChan
×
106
                // Make sure that echo is enabled if the process gets
×
107
                // interrupted
×
108
                signal.Notify(sigChan)
×
109
                go EchoSigHandler(_ctx, sigChan, errChan, term)
×
110
        } else if err != syscall.ENOTTY {
×
NEW
111
                return s, err
×
112
        }
×
113

114
        if err := cmd.Start(); err != nil {
×
NEW
115
                return s, err
×
116
        }
×
117

118
        // Wait for 120 seconds for ssh to establish connection
119
        err = waitForBufferSignal(stdout, os.Stdout, sshConnectedToken, 2*time.Minute)
×
120
        if err != nil {
×
121
                _ = cmd.Process.Kill()
×
NEW
122
                return s, errors.Wrap(err,
×
123
                        "Error waiting for ssh session to be established.")
×
124
        }
×
NEW
125
        return s, nil
×
126
}
127

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

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

138
        return nil
×
139
}
140

141
// Reads from src waiting for the string specified by signal, writing all other
142
// output appearing at src to sink. The function returns an error if occurs
143
// reading from the stream or the deadline exceeds.
144
func waitForBufferSignal(src io.Reader, sink io.Writer,
145
        signal string, deadline time.Duration) error {
×
146

×
147
        var err error
×
148
        errChan := make(chan error)
×
149

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

170
        select {
×
171
        case err = <-errChan:
×
172
                // Error from goroutine
173
        case <-time.After(deadline):
×
174
                err = errors.New("Input deadline exceeded")
×
175
        }
176
        return err
×
177
}
178

NEW
179
func (s *SSHCommand) WaitForEchoRestore() error {
×
NEW
180
        if s.sigChan != nil {
×
NEW
181
                signal.Stop(s.sigChan)
×
NEW
182
                s.cancel()
×
NEW
183
                if err := <-s.errChan; err != nil {
×
NEW
184
                        return err
×
NEW
185
                }
×
186
        }
NEW
187
        return nil
×
188
}
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