Skip to content

Commit

Permalink
Merge pull request #5137 from twz123/supervisor-prochandle
Browse files Browse the repository at this point in the history
Introduce supervisor.procHandle interface
  • Loading branch information
twz123 authored Oct 23, 2024
2 parents 87c369f + ceaa0f9 commit ba5c8d1
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 144 deletions.
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.

0 comments on commit ba5c8d1

Please sign in to comment.