Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce supervisor.procHandle interface #5137

Merged
merged 8 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions pkg/supervisor/prochandle.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
Copyright 2024 k0s authors

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 supervisor

// A handle to a running process. May be used to inspect the process properties
// and terminate it.
type procHandle interface {
// Reads and returns the process's command line.
cmdline() ([]string, error)

// Reads and returns the process's environment.
environ() ([]string, error)

// Terminates the process gracefully.
terminateGracefully() error

// Terminates the process forcibly.
terminateForcibly() error
}
67 changes: 67 additions & 0 deletions pkg/supervisor/prochandle_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//go:build unix

/*
Copyright 2022 k0s authors

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 supervisor

import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
)

type unixPID int

func newProcHandle(pid int) (procHandle, error) {
return unixPID(pid), nil
}

func (pid unixPID) cmdline() ([]string, error) {
cmdline, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(int(pid)), "cmdline"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %w", syscall.ESRCH, err)
}
return nil, fmt.Errorf("failed to read process cmdline: %w", err)
}

return strings.Split(string(cmdline), "\x00"), nil
}

func (pid unixPID) environ() ([]string, error) {
env, err := os.ReadFile(filepath.Join("/proc", strconv.Itoa(int(pid)), "environ"))
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("%w: %w", syscall.ESRCH, err)
}
return nil, fmt.Errorf("failed to read process environ: %w", err)
}

return strings.Split(string(env), "\x00"), nil
}

func (pid unixPID) terminateGracefully() error {
return syscall.Kill(int(pid), syscall.SIGTERM)
}

func (pid unixPID) terminateForcibly() error {
return syscall.Kill(int(pid), syscall.SIGKILL)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ limitations under the License.

package supervisor

// maybeKillPidFile checks kills the process in the pidFile if it's has
// the same binary as the supervisor's. This function does not delete
// the old pidFile as this is done by the caller.
func (s *Supervisor) maybeKillPidFile() error {
s.log.Warnf("maybeKillPidFile is not implemented on Windows")
return nil
import (
"syscall"
)

// newProcHandle is not implemented on Windows.
func newProcHandle(int) (procHandle, error) {
return nil, syscall.EWINDOWS
}
112 changes: 111 additions & 1 deletion pkg/supervisor/supervisor.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ package supervisor

import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path"
"runtime"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -140,7 +142,11 @@ func (s *Supervisor) Supervise() error {
}

if err := s.maybeKillPidFile(); err != nil {
return err
if !errors.Is(err, errors.ErrUnsupported) {
return err
}

s.log.WithError(err).Warn("Old process cannot be terminated")
}

var ctx context.Context
Expand Down Expand Up @@ -242,6 +248,110 @@ func (s *Supervisor) Stop() {
}
}

// maybeKillPidFile checks kills the process in the pidFile if it's has
// the same binary as the supervisor's and also checks that the env
// `_KOS_MANAGED=yes`. This function does not delete the old pidFile as
// this is done by the caller.
func (s *Supervisor) maybeKillPidFile() error {
pid, err := os.ReadFile(s.PidFile)
if os.IsNotExist(err) {
return nil
} else if err != nil {
return fmt.Errorf("failed to read PID file %s: %w", s.PidFile, err)
}

p, err := strconv.Atoi(strings.TrimSuffix(string(pid), "\n"))
if err != nil {
return fmt.Errorf("failed to parse PID file %s: %w", s.PidFile, err)
}

ph, err := newProcHandle(p)
if err != nil {
return fmt.Errorf("cannot interact with PID %d from PID file %s: %w", p, s.PidFile, err)
}

if err := s.killProcess(ph); err != nil {
return fmt.Errorf("failed to kill PID %d from PID file %s: %w", p, s.PidFile, err)
}

return nil
}

const exitCheckInterval = 200 * time.Millisecond

// Tries to terminate a process gracefully. If it's still running after
// s.TimeoutStop, the process is forcibly terminated.
func (s *Supervisor) killProcess(ph procHandle) error {
// Kill the process pid
deadlineTicker := time.NewTicker(s.TimeoutStop)
defer deadlineTicker.Stop()
checkTicker := time.NewTicker(exitCheckInterval)
defer checkTicker.Stop()

Loop:
for {
select {
case <-checkTicker.C:
shouldKill, err := s.shouldKillProcess(ph)
if err != nil {
return err
}
if !shouldKill {
return nil
}

err = ph.terminateGracefully()
if errors.Is(err, syscall.ESRCH) {
return nil
} else if err != nil {
return fmt.Errorf("failed to terminate gracefully: %w", err)
}
case <-deadlineTicker.C:
break Loop
}
}

shouldKill, err := s.shouldKillProcess(ph)
if err != nil {
return err
}
if !shouldKill {
return nil
}

err = ph.terminateForcibly()
if errors.Is(err, syscall.ESRCH) {
return nil
} else if err != nil {
return fmt.Errorf("failed to terminate forcibly: %w", err)
}
return nil
}

func (s *Supervisor) shouldKillProcess(ph procHandle) (bool, error) {
// only kill process if it has the expected cmd
if cmd, err := ph.cmdline(); err != nil {
if errors.Is(err, syscall.ESRCH) {
return false, nil
}
return false, err
} else if len(cmd) > 0 && cmd[0] != s.BinPath {
return false, nil
}

//only kill process if it has the _KOS_MANAGED env set
if env, err := ph.environ(); err != nil {
if errors.Is(err, syscall.ESRCH) {
return false, nil
}
return false, err
} else if !slices.Contains(env, k0sManaged) {
return false, nil
}

return true, nil
}

// Prepare the env for exec:
// - handle component specific env
// - inject k0s embedded bins into path
Expand Down
137 changes: 0 additions & 137 deletions pkg/supervisor/supervisor_unix.go

This file was deleted.

Loading