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

mendersoftware / mender-connect / 1143626594

18 Jan 2024 09:42PM UTC coverage: 77.922% (-22.1%) from 100.0%
1143626594

Pull #128

gitlab-ci

danielskinstad
chore: removed the text "mender-connect version" from --version output to be consistent

Changelog: --version no longer outputs the text "mender-connect version".

Ticket: MEN-6965

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
Pull Request #128: chore: removed the text "mender-connect version" from --version outpu…

2467 of 3166 relevant lines covered (77.92%)

6.8 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 {
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

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

239
func MenderSessionTerminateAll() (shellCount int, sessionCount int, err error) {
1✔
240
        shellCount = 0
1✔
241
        sessionCount = 0
1✔
242
        for id, s := range sessionsMap {
3✔
243
                e := s.StopShell()
2✔
244
                if e == nil {
4✔
245
                        shellCount++
2✔
246
                } else {
2✔
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)
2✔
255
                if e == nil {
4✔
256
                        sessionCount++
2✔
257
                } else {
2✔
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
1✔
264
}
265

266
func MenderSessionTerminateExpired() (
267
        shellCount int,
268
        sessionCount int,
269
        totalExpiredLeft int,
270
        err error,
271
) {
4✔
272
        shellCount = 0
4✔
273
        sessionCount = 0
4✔
274
        totalExpiredLeft = 0
4✔
275
        for id, s := range sessionsMap {
8✔
276
                if s.IsExpired(false) {
6✔
277
                        e := s.StopShell()
2✔
278
                        if e == nil {
4✔
279
                                shellCount++
2✔
280
                        } else {
2✔
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)
2✔
289
                        if e == nil {
4✔
290
                                sessionCount++
2✔
291
                        } else {
2✔
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
4✔
300
}
301

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

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

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

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

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

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

330
        pid, pseudoTTY, cmd, err := shell.ExecuteShell(
11✔
331
                terminal.Uid,
11✔
332
                terminal.Gid,
11✔
333
                terminal.HomeDir,
11✔
334
                terminal.Shell,
11✔
335
                terminal.TerminalString,
11✔
336
                terminal.Height,
11✔
337
                terminal.Width,
11✔
338
                terminal.ShellArguments)
11✔
339
        if err != nil {
12✔
340
                return err
1✔
341
        }
1✔
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)
10✔
347
        s.shell = shell.NewMenderShell(sessionId, pseudoTTY, pseudoTTY)
10✔
348
        s.shell.Start()
10✔
349

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

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

10✔
362
        return nil
10✔
363
}
364

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

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

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

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

10✔
389
        for {
20✔
390
                select {
10✔
391
                case <-s.stop:
6✔
392
                        return
6✔
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 {
2✔
433
        s.activeAt = timeNow()
2✔
434
        data := m.Body
2✔
435
        commandLine := string(data)
2✔
436
        n, err := s.writer.Write(data)
2✔
437
        if err != nil && n != len(data) {
3✔
438
                err = shell.ErrExecWriteBytesShort
1✔
439
        }
1✔
440
        if err != nil {
3✔
441
                log.Debugf("error: '%s' while running '%s'.", err.Error(), commandLine)
1✔
442
        } else {
2✔
443
                log.Debugf("executed: '%s'", commandLine)
1✔
444
        }
1✔
445
        return err
2✔
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) {
7✔
453
        log.Infof("session %s status:%d stopping shell", s.id, s.status)
7✔
454
        if s.status != ActiveSession && s.status != HangedSession {
8✔
455
                return ErrSessionShellNotRunning
1✔
456
        }
1✔
457

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

6✔
463
        p, err := os.FindProcess(s.shellPid)
6✔
464
        if err != nil {
6✔
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)
6✔
474
        if err != nil {
6✔
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()
6✔
479

6✔
480
        err = procps.TerminateAndWait(s.shellPid, s.command, 2*time.Second)
6✔
481
        if err != nil {
6✔
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
6✔
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