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

mendersoftware / mender-connect / 792377892

pending completion
792377892

Pull #97

gitlab-ci

GitHub
chore: bump github.com/stretchr/testify from 1.8.1 to 1.8.2
Pull Request #97: chore: bump github.com/stretchr/testify from 1.8.1 to 1.8.2

2440 of 3151 relevant lines covered (77.44%)

13.35 hits per line

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

74.56
/session/shell.go
1
// Copyright 2021 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 session
16

17
import (
18
        "errors"
19
        "io"
20
        "os"
21
        "os/exec"
22
        "syscall"
23
        "time"
24

25
        log "github.com/sirupsen/logrus"
26

27
        "github.com/mendersoftware/go-lib-micro/ws"
28
        wsshell "github.com/mendersoftware/go-lib-micro/ws/shell"
29

30
        "github.com/mendersoftware/mender-connect/connectionmanager"
31
        "github.com/mendersoftware/mender-connect/procps"
32
        "github.com/mendersoftware/mender-connect/shell"
33
)
34

35
type MenderSessionType int
36

37
const (
38
        ShellInteractiveSession MenderSessionType = iota
39
        MonitoringSession
40
        RemoteDebugSession
41
        ConfigurationSession
42
)
43

44
type MenderSessionStatus int
45

46
const (
47
        ActiveSession MenderSessionStatus = iota
48
        ExpiredSession
49
        IdleSession
50
        HangedSession
51
        EmptySession
52
        NewSession
53
)
54

55
const (
56
        NoExpirationTimeout = time.Second * 0
57
)
58

59
var (
60
        ErrSessionShellAlreadyRunning         = errors.New("shell is already running")
61
        ErrSessionShellNotRunning             = errors.New("shell is not running")
62
        ErrSessionShellTooManySessionsPerUser = errors.New("user has too many open sessions")
63
        ErrSessionNotFound                    = errors.New("session not found")
64
        ErrSessionTooManyShellsAlreadyRunning = errors.New("too many shells spawned")
65
)
66

67
var (
68
        defaultSessionExpiredTimeout     = 1024 * time.Second
69
        defaultSessionIdleExpiredTimeout = NoExpirationTimeout
70
        defaultTimeFormat                = "Mon Jan 2 15:04:05 -0700 MST 2006"
71
        MaxUserSessions                  = 1
72
        healthcheckInterval              = time.Second * 60
73
        healthcheckTimeout               = time.Second * 5
74
)
75

76
type MenderShellTerminalSettings struct {
77
        Uid            uint32
78
        Gid            uint32
79
        Shell          string
80
        HomeDir        string
81
        TerminalString string
82
        Height         uint16
83
        Width          uint16
84
        ShellArguments []string
85
}
86

87
type MenderShellSession struct {
88
        //mender shell represents a process of passing data between a running shell
89
        //subprocess running
90
        shell *shell.MenderShell
91
        //session id, generated
92
        id string
93
        //user id given with the MessageTypeSpawnShell message
94
        userId string
95
        //time at which session was created
96
        createdAt time.Time
97
        //time after which session is considered to be expired
98
        expiresAt time.Time
99
        //time of a last received message used to determine if the session is active
100
        activeAt time.Time
101
        //type of the session
102
        sessionType MenderSessionType
103
        //status of the session
104
        status MenderSessionStatus
105
        //terminal settings, for reference, usually it does not change
106
        //in theory size of the terminal can change
107
        terminal MenderShellTerminalSettings
108
        //the pid of the shell process mainly used for stopping the shell
109
        shellPid int
110
        //reader and writer are connected to the terminal stdio where the shell is running
111
        reader io.Reader
112
        //reader and writer are connected to the terminal stdio where the shell is running
113
        writer    io.Writer
114
        pseudoTTY *os.File
115
        command   *exec.Cmd
116
        // stop channel
117
        stop chan struct{}
118
        // pong channel
119
        pong chan struct{}
120
        // healthcheck
121
        healthcheckTimeout time.Time
122
}
123

124
var sessionsMap = map[string]*MenderShellSession{}
125
var sessionsByUserIdMap = map[string][]*MenderShellSession{}
126

127
func timeNow() time.Time {
88✔
128
        return time.Now().UTC()
88✔
129
}
88✔
130

131
func NewMenderShellSession(
132
        sessionId string,
133
        userId string,
134
        expireAfter time.Duration,
135
        expireAfterIdle time.Duration,
136
) (s *MenderShellSession, err error) {
48✔
137
        if userSessions, ok := sessionsByUserIdMap[userId]; ok {
64✔
138
                log.Debugf("user %s has %d sessions.", userId, len(userSessions))
16✔
139
                if len(userSessions) >= MaxUserSessions {
18✔
140
                        return nil, ErrSessionShellTooManySessionsPerUser
2✔
141
                }
2✔
142
        } else {
32✔
143
                sessionsByUserIdMap[userId] = []*MenderShellSession{}
32✔
144
        }
32✔
145

146
        if expireAfter == NoExpirationTimeout {
48✔
147
                expireAfter = defaultSessionExpiredTimeout
2✔
148
        }
2✔
149

150
        if expireAfterIdle != NoExpirationTimeout {
48✔
151
                defaultSessionIdleExpiredTimeout = expireAfterIdle
2✔
152
        }
2✔
153

154
        createdAt := timeNow()
46✔
155
        s = &MenderShellSession{
46✔
156
                id:          sessionId,
46✔
157
                userId:      userId,
46✔
158
                createdAt:   createdAt,
46✔
159
                expiresAt:   createdAt.Add(expireAfter),
46✔
160
                sessionType: ShellInteractiveSession,
46✔
161
                status:      NewSession,
46✔
162
                stop:        make(chan struct{}),
46✔
163
                pong:        make(chan struct{}),
46✔
164
        }
46✔
165
        sessionsMap[sessionId] = s
46✔
166
        sessionsByUserIdMap[userId] = append(sessionsByUserIdMap[userId], s)
46✔
167
        return s, nil
46✔
168
}
169

170
func MenderShellSessionGetCount() int {
2✔
171
        return len(sessionsMap)
2✔
172
}
2✔
173

174
func MenderShellSessionGetSessionIds() []string {
2✔
175
        keys := make([]string, 0, len(sessionsMap))
2✔
176
        for k := range sessionsMap {
6✔
177
                keys = append(keys, k)
4✔
178
        }
4✔
179

180
        return keys
2✔
181
}
182

183
func MenderShellSessionGetById(id string) *MenderShellSession {
60✔
184
        if v, ok := sessionsMap[id]; ok {
98✔
185
                return v
38✔
186
        } else {
60✔
187
                return nil
22✔
188
        }
22✔
189
}
190

191
func MenderShellDeleteById(id string) error {
22✔
192
        if v, ok := sessionsMap[id]; ok {
38✔
193
                userSessions := sessionsByUserIdMap[v.userId]
16✔
194
                for i, s := range userSessions {
32✔
195
                        if s.id == id {
32✔
196
                                sessionsByUserIdMap[v.userId] = append(userSessions[:i], userSessions[i+1:]...)
16✔
197
                                break
16✔
198
                        }
199
                }
200
                delete(sessionsMap, id)
16✔
201
                return nil
16✔
202
        } else {
6✔
203
                return ErrSessionNotFound
6✔
204
        }
6✔
205
}
206

207
func MenderShellSessionsGetByUserId(userId string) []*MenderShellSession {
12✔
208
        if v, ok := sessionsByUserIdMap[userId]; ok {
20✔
209
                return v
8✔
210
        } else {
12✔
211
                return nil
4✔
212
        }
4✔
213
}
214

215
func MenderShellStopByUserId(userId string) (count uint, err error) {
4✔
216
        a := sessionsByUserIdMap[userId]
4✔
217
        log.Debugf("stopping all shells of user %s.", userId)
4✔
218
        if len(a) == 0 {
6✔
219
                return 0, ErrSessionNotFound
2✔
220
        }
2✔
221
        count = 0
2✔
222
        err = nil
2✔
223
        for _, s := range a {
6✔
224
                if s.shell == nil {
4✔
225
                        continue
×
226
                }
227
                e := s.StopShell()
4✔
228
                if e != nil && procps.ProcessExists(s.shellPid) {
4✔
229
                        err = e
×
230
                        continue
×
231
                }
232
                delete(sessionsMap, s.id)
4✔
233
                count++
4✔
234
        }
235
        delete(sessionsByUserIdMap, userId)
2✔
236
        return count, err
2✔
237
}
238

239
func MenderSessionTerminateAll() (shellCount int, sessionCount int, err error) {
2✔
240
        shellCount = 0
2✔
241
        sessionCount = 0
2✔
242
        for id, s := range sessionsMap {
6✔
243
                e := s.StopShell()
4✔
244
                if e == nil {
8✔
245
                        shellCount++
4✔
246
                } else {
4✔
247
                        log.Debugf(
×
248
                                "terminate sessions: failed to stop shell for session: %s: %s",
×
249
                                id,
×
250
                                e.Error(),
×
251
                        )
×
252
                        err = e
×
253
                }
×
254
                e = MenderShellDeleteById(id)
4✔
255
                if e == nil {
8✔
256
                        sessionCount++
4✔
257
                } else {
4✔
258
                        log.Debugf("terminate sessions: failed to remove session: %s: %s", id, e.Error())
×
259
                        err = e
×
260
                }
×
261
        }
262

263
        return shellCount, sessionCount, err
2✔
264
}
265

266
func MenderSessionTerminateExpired() (
267
        shellCount int,
268
        sessionCount int,
269
        totalExpiredLeft int,
270
        err error,
271
) {
8✔
272
        shellCount = 0
8✔
273
        sessionCount = 0
8✔
274
        totalExpiredLeft = 0
8✔
275
        for id, s := range sessionsMap {
16✔
276
                if s.IsExpired(false) {
12✔
277
                        e := s.StopShell()
4✔
278
                        if e == nil {
8✔
279
                                shellCount++
4✔
280
                        } else {
4✔
281
                                log.Debugf(
×
282
                                        "expire sessions: failed to stop shell for session: %s: %s",
×
283
                                        id,
×
284
                                        e.Error(),
×
285
                                )
×
286
                                err = e
×
287
                        }
×
288
                        e = MenderShellDeleteById(id)
4✔
289
                        if e == nil {
8✔
290
                                sessionCount++
4✔
291
                        } else {
4✔
292
                                log.Debugf("expire sessions: failed to delete session: %s: %s", id, e.Error())
×
293
                                totalExpiredLeft++
×
294
                                err = e
×
295
                        }
×
296
                }
297
        }
298

299
        return shellCount, sessionCount, totalExpiredLeft, err
8✔
300
}
301

302
func (s *MenderShellSession) GetStatus() MenderSessionStatus {
4✔
303
        return s.status
4✔
304
}
4✔
305

306
func (s *MenderShellSession) GetStartedAtFmt() string {
4✔
307
        return s.createdAt.Format(defaultTimeFormat)
4✔
308
}
4✔
309

310
func (s *MenderShellSession) GetExpiresAtFmt() string {
4✔
311
        return s.expiresAt.Format(defaultTimeFormat)
4✔
312
}
4✔
313

314
func (s *MenderShellSession) GetActiveAtFmt() string {
4✔
315
        return s.activeAt.Format(defaultTimeFormat)
4✔
316
}
4✔
317

318
func (s *MenderShellSession) GetShellCommandPath() string {
2✔
319
        return s.command.Path
2✔
320
}
2✔
321

322
func (s *MenderShellSession) StartShell(
323
        sessionId string,
324
        terminal MenderShellTerminalSettings,
325
) error {
24✔
326
        if s.status == ActiveSession || s.status == HangedSession {
26✔
327
                return ErrSessionShellAlreadyRunning
2✔
328
        }
2✔
329

330
        pid, pseudoTTY, cmd, err := shell.ExecuteShell(
22✔
331
                terminal.Uid,
22✔
332
                terminal.Gid,
22✔
333
                terminal.HomeDir,
22✔
334
                terminal.Shell,
22✔
335
                terminal.TerminalString,
22✔
336
                terminal.Height,
22✔
337
                terminal.Width,
22✔
338
                terminal.ShellArguments)
22✔
339
        if err != nil {
24✔
340
                return err
2✔
341
        }
2✔
342

343
        //MenderShell represents a process of passing messages between backend
344
        //and the shell subprocess (started above via shell.ExecuteShell) over
345
        //the websocket connection
346
        log.Infof("mender-connect starting shell command passing process, pid: %d", pid)
20✔
347
        s.shell = shell.NewMenderShell(sessionId, pseudoTTY, pseudoTTY)
20✔
348
        s.shell.Start()
20✔
349

20✔
350
        s.shellPid = pid
20✔
351
        s.reader = pseudoTTY
20✔
352
        s.writer = pseudoTTY
20✔
353
        s.status = ActiveSession
20✔
354
        s.terminal = terminal
20✔
355
        s.pseudoTTY = pseudoTTY
20✔
356
        s.command = cmd
20✔
357
        s.activeAt = timeNow()
20✔
358

20✔
359
        // start the healthcheck go-routine
20✔
360
        go s.healthcheck()
20✔
361

20✔
362
        return nil
20✔
363
}
364

365
func (s *MenderShellSession) GetId() string {
104✔
366
        return s.id
104✔
367
}
104✔
368

369
func (s *MenderShellSession) GetShellPid() int {
4✔
370
        return s.shellPid
4✔
371
}
4✔
372

373
func (s *MenderShellSession) IsExpired(setStatus bool) bool {
16✔
374
        if defaultSessionIdleExpiredTimeout != NoExpirationTimeout {
22✔
375
                idleTimeoutReached := s.activeAt.Add(defaultSessionIdleExpiredTimeout)
6✔
376
                return timeNow().After(idleTimeoutReached)
6✔
377
        }
6✔
378
        e := timeNow().After(s.expiresAt)
10✔
379
        if e && setStatus {
12✔
380
                s.status = ExpiredSession
2✔
381
        }
2✔
382
        return e
10✔
383
}
384

385
func (s *MenderShellSession) healthcheck() {
20✔
386
        nextHealthcheckPing := time.Now().Add(healthcheckInterval)
20✔
387
        s.healthcheckTimeout = time.Now().Add(healthcheckInterval + healthcheckTimeout)
20✔
388

20✔
389
        for {
40✔
390
                select {
20✔
391
                case <-s.stop:
12✔
392
                        return
12✔
393
                case <-s.pong:
×
394
                        s.healthcheckTimeout = time.Now().Add(healthcheckInterval + healthcheckTimeout)
×
395
                case <-time.After(time.Until(s.healthcheckTimeout)):
×
396
                        if s.healthcheckTimeout.Before(time.Now()) {
×
397
                                log.Errorf("session %s, health check failed, connection with the client lost", s.id)
×
398
                                s.expiresAt = time.Now()
×
399
                                return
×
400
                        }
×
401
                case <-time.After(time.Until(nextHealthcheckPing)):
×
402
                        s.healthcheckPing()
×
403
                        nextHealthcheckPing = time.Now().Add(healthcheckInterval)
×
404
                }
405
        }
406
}
407

408
func (s *MenderShellSession) healthcheckPing() {
×
409
        msg := &ws.ProtoMsg{
×
410
                Header: ws.ProtoHdr{
×
411
                        Proto:     ws.ProtoTypeShell,
×
412
                        MsgType:   wsshell.MessageTypePingShell,
×
413
                        SessionID: s.id,
×
414
                        Properties: map[string]interface{}{
×
415
                                "timeout": int(healthcheckInterval.Seconds() + healthcheckTimeout.Seconds()),
×
416
                                "status":  wsshell.ControlMessage,
×
417
                        },
×
418
                },
×
419
                Body: nil,
×
420
        }
×
421
        log.Debugf("session %s healthcheck ping", s.id)
×
422
        err := connectionmanager.Write(ws.ProtoTypeShell, msg)
×
423
        if err != nil {
×
424
                log.Debugf("error on write: %s", err.Error())
×
425
        }
×
426
}
427

428
func (s *MenderShellSession) HealthcheckPong() {
×
429
        s.pong <- struct{}{}
×
430
}
×
431

432
func (s *MenderShellSession) ShellCommand(m *ws.ProtoMsg) error {
4✔
433
        s.activeAt = timeNow()
4✔
434
        data := m.Body
4✔
435
        commandLine := string(data)
4✔
436
        n, err := s.writer.Write(data)
4✔
437
        if err != nil && n != len(data) {
6✔
438
                err = shell.ErrExecWriteBytesShort
2✔
439
        }
2✔
440
        if err != nil {
6✔
441
                log.Debugf("error: '%s' while running '%s'.", err.Error(), commandLine)
2✔
442
        } else {
4✔
443
                log.Debugf("executed: '%s'", commandLine)
2✔
444
        }
2✔
445
        return err
4✔
446
}
447

448
func (s *MenderShellSession) ResizeShell(height, width uint16) {
×
449
        shell.ResizeShell(s.pseudoTTY, height, width)
×
450
}
×
451

452
func (s *MenderShellSession) StopShell() (err error) {
14✔
453
        log.Infof("session %s status:%d stopping shell", s.id, s.status)
14✔
454
        if s.status != ActiveSession && s.status != HangedSession {
16✔
455
                return ErrSessionShellNotRunning
2✔
456
        }
2✔
457

458
        close(s.stop)
12✔
459
        s.shell.Stop()
12✔
460
        s.terminal = MenderShellTerminalSettings{}
12✔
461
        s.status = EmptySession
12✔
462

12✔
463
        p, err := os.FindProcess(s.shellPid)
12✔
464
        if err != nil {
12✔
465
                log.Errorf(
×
466
                        "session %s, shell pid %d, find process error: %s",
×
467
                        s.id,
×
468
                        s.shellPid,
×
469
                        err.Error(),
×
470
                )
×
471
                return err
×
472
        }
×
473
        err = p.Signal(syscall.SIGINT)
12✔
474
        if err != nil {
12✔
475
                log.Errorf("session %s, shell pid %d, signal error: %s", s.id, s.shellPid, err.Error())
×
476
                return err
×
477
        }
×
478
        s.pseudoTTY.Close()
12✔
479

12✔
480
        err = procps.TerminateAndWait(s.shellPid, s.command, 2*time.Second)
12✔
481
        if err != nil {
12✔
482
                log.Errorf("session %s, shell pid %d, termination error: %s", s.id, s.shellPid, err.Error())
×
483
                return err
×
484
        }
×
485

486
        return nil
12✔
487
}
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