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

mendersoftware / mender-connect / 1303473563

31 Jan 2024 07:18AM UTC coverage: 76.957% (+5.8%) from 71.188%
1303473563

push

gitlab-ci

web-flow
Merge pull request #132 from mendersoftware/cherry-2.1.x-MEN-6888-fix-ping-pong

[Cherry 2.1.x]: MEN-6888: Stop shell after health check fails

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

20 existing lines in 1 file now uncovered.

2448 of 3181 relevant lines covered (76.96%)

6.74 hits per line

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

70.86
/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 {
44✔
128
        return time.Now().UTC()
44✔
129
}
44✔
130

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

146
        if expireAfter == NoExpirationTimeout {
24✔
147
                expireAfter = defaultSessionExpiredTimeout
1✔
148
        }
1✔
149

150
        if expireAfterIdle != NoExpirationTimeout {
24✔
151
                defaultSessionIdleExpiredTimeout = expireAfterIdle
1✔
152
        }
1✔
153

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

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

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

180
        return keys
1✔
181
}
182

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

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

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

NEW
215
func MenderShellStopById(sessionId string) error {
×
NEW
216
        s := MenderShellSessionGetById(sessionId)
×
NEW
217
        if s.shell == nil {
×
NEW
218
                return ErrSessionNotFound
×
NEW
219
        }
×
NEW
220
        e := s.StopShell()
×
NEW
221
        if e != nil && procps.ProcessExists(s.shellPid) {
×
NEW
222
                return e
×
NEW
223
        }
×
NEW
224
        return MenderShellDeleteById(sessionId)
×
225
}
226

227
func MenderShellStopByUserId(userId string) (count uint, err error) {
2✔
228
        a := sessionsByUserIdMap[userId]
2✔
229
        log.Debugf("stopping all shells of user %s.", userId)
2✔
230
        if len(a) == 0 {
3✔
231
                return 0, ErrSessionNotFound
1✔
232
        }
1✔
233
        count = 0
1✔
234
        err = nil
1✔
235
        for _, s := range a {
3✔
236
                if s.shell == nil {
2✔
237
                        continue
×
238
                }
239
                e := s.StopShell()
2✔
240
                if e != nil && procps.ProcessExists(s.shellPid) {
2✔
241
                        err = e
×
242
                        continue
×
243
                }
244
                delete(sessionsMap, s.id)
2✔
245
                count++
2✔
246
        }
247
        delete(sessionsByUserIdMap, userId)
1✔
248
        return count, err
1✔
249
}
250

251
func MenderSessionTerminateAll() (shellCount int, sessionCount int, err error) {
1✔
252
        shellCount = 0
1✔
253
        sessionCount = 0
1✔
254
        for id, s := range sessionsMap {
3✔
255
                e := s.StopShell()
2✔
256
                if e == nil {
4✔
257
                        shellCount++
2✔
258
                } else {
2✔
259
                        log.Debugf(
×
260
                                "terminate sessions: failed to stop shell for session: %s: %s",
×
261
                                id,
×
262
                                e.Error(),
×
263
                        )
×
264
                        err = e
×
265
                }
×
266
                e = MenderShellDeleteById(id)
2✔
267
                if e == nil {
4✔
268
                        sessionCount++
2✔
269
                } else {
2✔
270
                        log.Debugf("terminate sessions: failed to remove session: %s: %s", id, e.Error())
×
271
                        err = e
×
272
                }
×
273
        }
274

275
        return shellCount, sessionCount, err
1✔
276
}
277

278
func MenderSessionTerminateExpired() (
279
        shellCount int,
280
        sessionCount int,
281
        totalExpiredLeft int,
282
        err error,
283
) {
4✔
284
        shellCount = 0
4✔
285
        sessionCount = 0
4✔
286
        totalExpiredLeft = 0
4✔
287
        for id, s := range sessionsMap {
8✔
288
                if s.IsExpired(false) {
6✔
289
                        e := s.StopShell()
2✔
290
                        if e == nil {
4✔
291
                                shellCount++
2✔
292
                        } else {
2✔
293
                                log.Debugf(
×
294
                                        "expire sessions: failed to stop shell for session: %s: %s",
×
295
                                        id,
×
296
                                        e.Error(),
×
297
                                )
×
298
                                err = e
×
299
                        }
×
300
                        e = MenderShellDeleteById(id)
2✔
301
                        if e == nil {
4✔
302
                                sessionCount++
2✔
303
                        } else {
2✔
304
                                log.Debugf("expire sessions: failed to delete session: %s: %s", id, e.Error())
×
305
                                totalExpiredLeft++
×
306
                                err = e
×
307
                        }
×
308
                }
309
        }
310

311
        return shellCount, sessionCount, totalExpiredLeft, err
4✔
312
}
313

314
func (s *MenderShellSession) GetStatus() MenderSessionStatus {
2✔
315
        return s.status
2✔
316
}
2✔
317

318
func (s *MenderShellSession) GetStartedAtFmt() string {
2✔
319
        return s.createdAt.Format(defaultTimeFormat)
2✔
320
}
2✔
321

322
func (s *MenderShellSession) GetExpiresAtFmt() string {
2✔
323
        return s.expiresAt.Format(defaultTimeFormat)
2✔
324
}
2✔
325

326
func (s *MenderShellSession) GetActiveAtFmt() string {
2✔
327
        return s.activeAt.Format(defaultTimeFormat)
2✔
328
}
2✔
329

330
func (s *MenderShellSession) GetShellCommandPath() string {
1✔
331
        return s.command.Path
1✔
332
}
1✔
333

334
func (s *MenderShellSession) StartShell(
335
        sessionId string,
336
        terminal MenderShellTerminalSettings,
337
) error {
12✔
338
        if s.status == ActiveSession || s.status == HangedSession {
13✔
339
                return ErrSessionShellAlreadyRunning
1✔
340
        }
1✔
341

342
        pid, pseudoTTY, cmd, err := shell.ExecuteShell(
11✔
343
                terminal.Uid,
11✔
344
                terminal.Gid,
11✔
345
                terminal.HomeDir,
11✔
346
                terminal.Shell,
11✔
347
                terminal.TerminalString,
11✔
348
                terminal.Height,
11✔
349
                terminal.Width,
11✔
350
                terminal.ShellArguments)
11✔
351
        if err != nil {
12✔
352
                return err
1✔
353
        }
1✔
354

355
        //MenderShell represents a process of passing messages between backend
356
        //and the shell subprocess (started above via shell.ExecuteShell) over
357
        //the websocket connection
358
        log.Infof("mender-connect starting shell command passing process, pid: %d", pid)
10✔
359
        s.shell = shell.NewMenderShell(sessionId, pseudoTTY, pseudoTTY)
10✔
360
        s.shell.Start()
10✔
361

10✔
362
        s.shellPid = pid
10✔
363
        s.reader = pseudoTTY
10✔
364
        s.writer = pseudoTTY
10✔
365
        s.status = ActiveSession
10✔
366
        s.terminal = terminal
10✔
367
        s.pseudoTTY = pseudoTTY
10✔
368
        s.command = cmd
10✔
369
        s.activeAt = timeNow()
10✔
370

10✔
371
        // start the healthcheck go-routine
10✔
372
        go s.healthcheck()
10✔
373

10✔
374
        return nil
10✔
375
}
376

377
func (s *MenderShellSession) GetId() string {
52✔
378
        return s.id
52✔
379
}
52✔
380

381
func (s *MenderShellSession) GetShellPid() int {
2✔
382
        return s.shellPid
2✔
383
}
2✔
384

385
func (s *MenderShellSession) IsExpired(setStatus bool) bool {
8✔
386
        if defaultSessionIdleExpiredTimeout != NoExpirationTimeout {
11✔
387
                idleTimeoutReached := s.activeAt.Add(defaultSessionIdleExpiredTimeout)
3✔
388
                return timeNow().After(idleTimeoutReached)
3✔
389
        }
3✔
390
        e := timeNow().After(s.expiresAt)
5✔
391
        if e && setStatus {
6✔
392
                s.status = ExpiredSession
1✔
393
        }
1✔
394
        return e
5✔
395
}
396

397
func (s *MenderShellSession) healthcheck() {
10✔
398
        nextHealthcheckPing := time.Now().Add(healthcheckInterval)
10✔
399
        s.healthcheckTimeout = time.Now().Add(healthcheckInterval + healthcheckTimeout)
10✔
400

10✔
401
        for {
20✔
402
                select {
10✔
403
                case <-s.stop:
6✔
404
                        return
6✔
405
                case <-s.pong:
×
406
                        s.healthcheckTimeout = time.Now().Add(healthcheckInterval + healthcheckTimeout)
×
407
                case <-time.After(time.Until(s.healthcheckTimeout)):
×
408
                        if s.healthcheckTimeout.Before(time.Now()) {
×
409
                                log.Errorf("session %s, health check failed, connection with the client lost", s.id)
×
410
                                s.expiresAt = time.Now()
×
NEW
411
                                err := MenderShellStopById(s.id)
×
NEW
412
                                if err != nil {
×
NEW
413
                                        log.Errorf("session %s, error stopping shell %s", s.id, err.Error())
×
NEW
414
                                } else {
×
NEW
415
                                        log.Infof("session %s successfully stopped", s.id)
×
NEW
416
                                }
×
417
                                return
×
418
                        }
419
                case <-time.After(time.Until(nextHealthcheckPing)):
×
420
                        s.healthcheckPing()
×
421
                        nextHealthcheckPing = time.Now().Add(healthcheckInterval)
×
422
                }
423
        }
424
}
425

426
func (s *MenderShellSession) healthcheckPing() {
×
427
        msg := &ws.ProtoMsg{
×
428
                Header: ws.ProtoHdr{
×
429
                        Proto:     ws.ProtoTypeShell,
×
430
                        MsgType:   wsshell.MessageTypePingShell,
×
431
                        SessionID: s.id,
×
432
                        Properties: map[string]interface{}{
×
433
                                "timeout": int(healthcheckInterval.Seconds() + healthcheckTimeout.Seconds()),
×
434
                                "status":  wsshell.ControlMessage,
×
435
                        },
×
436
                },
×
437
                Body: nil,
×
438
        }
×
439
        log.Debugf("session %s healthcheck ping", s.id)
×
440
        err := connectionmanager.Write(ws.ProtoTypeShell, msg)
×
441
        if err != nil {
×
442
                log.Debugf("error on write: %s", err.Error())
×
443
        }
×
444
}
445

446
func (s *MenderShellSession) HealthcheckPong() {
×
447
        s.pong <- struct{}{}
×
448
}
×
449

450
func (s *MenderShellSession) ShellCommand(m *ws.ProtoMsg) error {
2✔
451
        s.activeAt = timeNow()
2✔
452
        data := m.Body
2✔
453
        commandLine := string(data)
2✔
454
        n, err := s.writer.Write(data)
2✔
455
        if err != nil && n != len(data) {
3✔
456
                err = shell.ErrExecWriteBytesShort
1✔
457
        }
1✔
458
        if err != nil {
3✔
459
                log.Debugf("error: '%s' while running '%s'.", err.Error(), commandLine)
1✔
460
        } else {
2✔
461
                log.Debugf("executed: '%s'", commandLine)
1✔
462
        }
1✔
463
        return err
2✔
464
}
465

466
func (s *MenderShellSession) ResizeShell(height, width uint16) {
×
467
        shell.ResizeShell(s.pseudoTTY, height, width)
×
468
}
×
469

470
func (s *MenderShellSession) StopShell() (err error) {
7✔
471
        log.Infof("session %s status:%d stopping shell", s.id, s.status)
7✔
472
        if s.status != ActiveSession && s.status != HangedSession {
8✔
473
                return ErrSessionShellNotRunning
1✔
474
        }
1✔
475

476
        close(s.stop)
6✔
477
        s.shell.Stop()
6✔
478
        s.terminal = MenderShellTerminalSettings{}
6✔
479
        s.status = EmptySession
6✔
480

6✔
481
        p, err := os.FindProcess(s.shellPid)
6✔
482
        if err != nil {
6✔
483
                log.Errorf(
×
484
                        "session %s, shell pid %d, find process error: %s",
×
485
                        s.id,
×
486
                        s.shellPid,
×
487
                        err.Error(),
×
488
                )
×
489
                return err
×
490
        }
×
491
        err = p.Signal(syscall.SIGINT)
6✔
492
        if err != nil {
6✔
493
                log.Errorf("session %s, shell pid %d, signal error: %s", s.id, s.shellPid, err.Error())
×
494
                return err
×
495
        }
×
496
        s.pseudoTTY.Close()
6✔
497

6✔
498
        err = procps.TerminateAndWait(s.shellPid, s.command, 2*time.Second)
6✔
499
        if err != nil {
6✔
500
                log.Errorf("session %s, shell pid %d, termination error: %s", s.id, s.shellPid, err.Error())
×
501
                return err
×
502
        }
×
503

504
        return nil
6✔
505
}
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