forked from vmware/vic
-
Notifications
You must be signed in to change notification settings - Fork 0
/
tether_linux.go
376 lines (312 loc) · 10.9 KB
/
tether_linux.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
// Copyright 2016-2017 VMware, Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tether
import (
"fmt"
"io"
"os"
"os/signal"
"path/filepath"
"runtime/debug"
"strings"
"syscall"
log "github.com/Sirupsen/logrus"
"github.com/kr/pty"
"github.com/vmware/vic/pkg/trace"
)
const (
//https://github.com/golang/go/blob/master/src/syscall/zerrors_linux_arm64.go#L919
SetChildSubreaper = 0x24
// in sync with lib/apiservers/portlayer/handlers/interaction_handler.go
// 115200 bps is 14.4 KB/s so use that
ioCopyBufferSize = 14 * 1024
)
// Mkdev will hopefully get rolled into go.sys at some point
func Mkdev(majorNumber int, minorNumber int) int {
return (majorNumber << 8) | (minorNumber & 0xff) | ((minorNumber & 0xfff00) << 12)
}
// ReloadConfig signals the current process, which triggers the signal handler
// to reload the config.
func ReloadConfig() error {
defer trace.End(trace.Begin(""))
p, err := os.FindProcess(os.Getpid())
if err != nil {
return err
}
if err = p.Signal(syscall.SIGHUP); err != nil {
return err
}
return nil
}
// childReaper is used to handle events from child processes, including child exit.
// If running as pid=1 then this means it handles zombie process reaping for orphaned children
// as well as direct child processes.
func (t *tether) childReaper() error {
signal.Notify(t.incoming, syscall.SIGCHLD)
/*
PR_SET_CHILD_SUBREAPER (since Linux 3.4)
If arg2 is nonzero, set the "child subreaper" attribute of the
calling process; if arg2 is zero, unset the attribute. When a
process is marked as a child subreaper, all of the children
that it creates, and their descendants, will be marked as
having a subreaper. In effect, a subreaper fulfills the role
of init(1) for its descendant processes. Upon termination of
a process that is orphaned (i.e., its immediate parent has
already terminated) and marked as having a subreaper, the
nearest still living ancestor subreaper will receive a SIGCHLD
signal and be able to wait(2) on the process to discover its
termination status.
*/
// TODO: update to check /proc/sys/kernel/osrelease and use this only on supported kernel versions
if _, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, SetChildSubreaper, uintptr(1), 0); err != 0 {
// for now just log the error
log.Errorf("Unable to configure child subreaper - should not matter when run as pid1: %s", err)
}
log.Info("Started reaping child processes")
go func() {
var status syscall.WaitStatus
flag := syscall.WNOHANG | syscall.WUNTRACED | syscall.WCONTINUED
for range t.incoming {
func() {
// general resiliency
defer func() {
if r := recover(); r != nil {
fmt.Fprintf(os.Stderr, "Recovered in childReaper: %s\n%s", r, debug.Stack())
}
}()
// reap until no more children to process
for {
log.Debugf("Inspecting children with status change")
select {
case <-t.ctx.Done():
log.Warnf("Someone called shutdown, returning from child reaper")
return
default:
}
pid, err := syscall.Wait4(-1, &status, flag, nil)
// pid 0 means no processes wish to report status
if pid == 0 || err == syscall.ECHILD {
log.Debug("No more child processes to reap")
break
}
if err != nil {
log.Warnf("Wait4 got error: %v\n", err)
break
}
if !status.Exited() && !status.Signaled() {
log.Debugf("Received notifcation about non-exit status change for %d: %d", pid, status)
// no reaping or exit handling required
continue
}
exitCode := status.ExitStatus()
log.Debugf("Reaped process %d, return code: %d", pid, exitCode)
// TODO: we will likely need additional handling here to distinguish in the Session state machine whether the session was signaled or we exited. See some of the exec/session documentation added by @matthewavery
session, ok := t.removeChildPid(pid)
if ok {
log.Debugf("Removed child pid: %d", pid)
session.Lock()
session.ExitStatus = exitCode
session.Unlock()
// Don't hold the lock while waiting for the file descriptors
// to close as these can be held open by child processes
log.Debugf("Waiting on session.wait")
session.wait.Wait()
log.Debugf("Wait on session.wait completed")
session.Lock()
t.handleSessionExit(session)
session.Unlock()
continue
}
ok = t.ops.HandleUtilityExit(pid, exitCode)
if ok {
log.Debugf("Remove utility pid: %d", pid)
continue
}
log.Infof("Reaped zombie process PID %d", pid)
}
}()
}
log.Info("Stopped reaping child processes")
}()
return nil
}
func (t *tether) stopReaper() {
defer trace.End(trace.Begin("Shutting down child reaping"))
// Ordering is important otherwise we may one goroutine closing, and the other goroutine is trying to write afterwards
log.Debugf("Removing the signal notifier")
signal.Reset(syscall.SIGCHLD)
// just closing the incoming channel is not going to stop the iteration
// so we use the context cancellation to signal it
t.cancel()
log.Debugf("Closing the reapers signal channel")
close(t.incoming)
}
func (t *tether) triggerReaper() {
defer trace.End(trace.Begin("Triggering child reaping"))
t.incoming <- syscall.SIGCHLD
}
func findExecutable(file string) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return os.ErrPermission
}
// lookPath searches for an executable binary named file in the directories
// specified by the path argument.
// This is a direct modification of the unix os/exec core library impl
func lookPath(file string, env []string, dir string) (string, error) {
// if it starts with a ./ or ../ it's a relative path
// need to check explicitly to allow execution of .hidden files
if strings.HasPrefix(file, "./") || strings.HasPrefix(file, "../") {
file = fmt.Sprintf("%s%c%s", dir, os.PathSeparator, file)
err := findExecutable(file)
if err == nil {
return filepath.Clean(file), nil
}
return "", err
}
// check if it's already a path spec
if strings.Contains(file, "/") {
err := findExecutable(file)
if err == nil {
return filepath.Clean(file), nil
}
return "", err
}
// extract path from the env
var pathenv string
for _, value := range env {
if strings.HasPrefix(value, "PATH=") {
pathenv = value
break
}
}
pathval := strings.TrimPrefix(pathenv, "PATH=")
dirs := filepath.SplitList(pathval)
for _, dir := range dirs {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := dir + "/" + file
if err := findExecutable(path); err == nil {
return filepath.Clean(path), nil
}
}
return "", fmt.Errorf("%s: no such executable in PATH", file)
}
func establishPty(session *SessionConfig) error {
defer trace.End(trace.Begin("initializing pty handling for session " + session.ID))
// pty.Start creates a process group anyway so no change needed to kill all descendants
var err error
session.Pty, err = pty.Start(&session.Cmd)
if err != nil {
return err
}
session.wait.Add(1)
go func() {
_, gerr := io.CopyBuffer(session.Outwriter, session.Pty, make([]byte, ioCopyBufferSize))
log.Debugf("PTY stdout copy: %s", gerr)
session.wait.Done()
}()
go func() {
_, gerr := io.CopyBuffer(session.Pty, session.Reader, make([]byte, ioCopyBufferSize))
log.Debugf("PTY stdin copy: %s", gerr)
// ensure that an EOT is delivered to the process - this makes the behaviour on EOF at this layer
// consistent between tty and non-tty cases
n, gerr := session.Pty.Write([]byte("\x04"))
if n != 1 || gerr != nil {
log.Errorf("Failed to write EOT to pty, closing directly: %s", gerr)
session.Pty.Close()
}
log.Debug("Written EOT to pty")
}()
return nil
}
func establishNonPty(session *SessionConfig) error {
defer trace.End(trace.Begin("initializing nonpty handling for session " + session.ID))
var err error
// configure a process group so we can kill any descendants
if session.Cmd.SysProcAttr == nil {
session.Cmd.SysProcAttr = &syscall.SysProcAttr{}
}
session.Cmd.SysProcAttr.Setsid = true
if session.OpenStdin {
log.Debugf("Setting StdinPipe")
if session.StdinPipe, err = session.Cmd.StdinPipe(); err != nil {
log.Errorf("StdinPipe failed with %s", err)
return err
}
}
log.Debugf("Setting StdoutPipe")
if session.StdoutPipe, err = session.Cmd.StdoutPipe(); err != nil {
log.Errorf("Setting StdoutPipe failed with %s", err)
return err
}
log.Debugf("Setting StderrPipe")
if session.StderrPipe, err = session.Cmd.StderrPipe(); err != nil {
log.Errorf("Setting StderrPipe failed with %s", err)
return err
}
if session.OpenStdin {
go func() {
_, gerr := io.CopyBuffer(session.StdinPipe, session.Reader, make([]byte, ioCopyBufferSize))
log.Debugf("Reader stdin returned: %s", gerr)
if gerr == nil {
if cerr := session.StdinPipe.Close(); cerr != nil {
log.Errorf("(stdin): Close StdinPipe failed with %s", cerr)
}
}
}()
}
// Add 2 for Std{out|err}
session.wait.Add(2)
go func() {
_, gerr := io.CopyBuffer(session.Outwriter, session.StdoutPipe, make([]byte, ioCopyBufferSize))
log.Debugf("Writer goroutine for stdout returned: %s", gerr)
if session.StdinPipe != nil {
log.Debugf("(stdout): Writing zero byte to stdin pipe")
n, werr := session.StdinPipe.Write([]byte{})
if n == 0 && werr != nil && werr.Error() == "write |1: bad file descriptor" {
log.Debugf("(stdout): Closing stdin pipe")
if cerr := session.StdinPipe.Close(); cerr != nil {
log.Errorf("Close failed with %s", cerr)
}
}
}
log.Debugf("Writer goroutine for stdout exiting")
session.wait.Done()
}()
go func() {
_, gerr := io.CopyBuffer(session.Errwriter, session.StderrPipe, make([]byte, ioCopyBufferSize))
log.Debugf("Writer goroutine for stderr returned: %s", gerr)
if session.StdinPipe != nil {
log.Debugf("(stderr): Writing zero byte to stdin pipe")
n, werr := session.StdinPipe.Write([]byte{})
if n == 0 && werr != nil && werr.Error() == "write |1: bad file descriptor" {
log.Debugf("(stderr): Closing stdin pipe")
if cerr := session.StdinPipe.Close(); cerr != nil {
log.Errorf("Close failed with %s", cerr)
}
}
}
log.Debugf("Writer goroutine for stderr exiting")
session.wait.Done()
}()
return session.Cmd.Start()
}