mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-15 13:00:08 -05:00
226 lines
5.7 KiB
Go
226 lines
5.7 KiB
Go
package db
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"syscall"
|
|
"time"
|
|
|
|
psutils "github.com/shirou/gopsutil/process"
|
|
"github.com/turbot/steampipe/constants"
|
|
"github.com/turbot/steampipe/display"
|
|
|
|
"github.com/turbot/steampipe/utils"
|
|
)
|
|
|
|
// StopStatus :: pseudoEnum for service stop result
|
|
type StopStatus int
|
|
|
|
const (
|
|
// ServiceStopped :: StopStatus - service was stopped
|
|
ServiceStopped StopStatus = iota
|
|
// ServiceNotRunning :: StopStatus - service was not running
|
|
ServiceNotRunning
|
|
// ServiceStopFailed :: StopStatus - service could not be stopped
|
|
ServiceStopFailed
|
|
// ServiceStopTimedOut :: StopStatus - service stop attempt timed out
|
|
ServiceStopTimedOut
|
|
)
|
|
|
|
// Shutdown :: closes the client connection and stops the
|
|
// database instance if the given `invoker` matches
|
|
func Shutdown(client *Client, invoker Invoker) {
|
|
utils.LogTime("db.Shutdown start")
|
|
defer utils.LogTime("db.Shutdown end")
|
|
|
|
if client != nil {
|
|
client.Close()
|
|
}
|
|
|
|
status, _ := GetStatus()
|
|
|
|
// is the service running?
|
|
if status != nil {
|
|
if status.Invoker == InvokerService {
|
|
// if the service was invoked by `steampipe service`,
|
|
// then we don't shut it down
|
|
return
|
|
}
|
|
|
|
count, _ := GetCountOfConnectedClients()
|
|
|
|
if count > 0 {
|
|
// there are other clients connected to the database
|
|
// we can't stop the DB.
|
|
return
|
|
}
|
|
|
|
// we can shutdown the database
|
|
status, err := StopDB(false, invoker)
|
|
if err != nil {
|
|
utils.ShowError(err)
|
|
}
|
|
if status != ServiceStopped {
|
|
StopDB(true, invoker)
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetCountOfConnectedClients() (int, error) {
|
|
rootClient, err := createSteampipeRootDbClient()
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
row := rootClient.QueryRow("select count(*) from pg_stat_activity where client_port IS NOT NULL and application_name='steampipe' and backend_type='client backend';")
|
|
count := 0
|
|
row.Scan(&count)
|
|
rootClient.Close()
|
|
return (count - 1 /* deduct the existing client */), nil
|
|
}
|
|
|
|
// StopDB :: search and stop the running instance. Does nothing if an instance was not found
|
|
func StopDB(force bool, invoker Invoker) (StopStatus, error) {
|
|
log.Println("[TRACE] StopDB", force)
|
|
|
|
utils.LogTime("db.StopDB start")
|
|
defer utils.LogTime("db.StopDB end")
|
|
|
|
if force {
|
|
// remove this file regardless of whether
|
|
// we could stop the service or not
|
|
// so that the next time it starts,
|
|
// all previous instances are nuked
|
|
defer os.Remove(runningInfoFilePath())
|
|
}
|
|
info, err := GetStatus()
|
|
if err != nil {
|
|
return ServiceStopFailed, err
|
|
}
|
|
|
|
if force {
|
|
// check if we have a process from another install-dir
|
|
checkedPreviousInstances := make(chan bool, 1)
|
|
s := display.StartSpinnerAfterDelay("Checking for running instances...", constants.SpinnerShowTimeout, checkedPreviousInstances)
|
|
defer func() {
|
|
close(checkedPreviousInstances)
|
|
display.StopSpinner(s)
|
|
}()
|
|
killInstanceIfAny()
|
|
return ServiceStopped, nil
|
|
}
|
|
|
|
if info == nil {
|
|
// we do not have a info file
|
|
// assume that the service is not running
|
|
return ServiceNotRunning, nil
|
|
}
|
|
|
|
// GetStatus has made sure that the process exists
|
|
process, err := psutils.NewProcess(int32(info.Pid))
|
|
if err != nil {
|
|
return ServiceStopFailed, err
|
|
}
|
|
|
|
// we need to do this since the standard
|
|
// cmd.Process.Kill() sends a SIGKILL which
|
|
// makes PG terminate immediately without saving state.
|
|
// refer: https://www.postgresql.org/docs/12/server-shutdown.html
|
|
// refer: https://golang.org/src/os/exec_posix.go?h=kill#L65
|
|
err = process.SendSignal(syscall.SIGTERM)
|
|
|
|
if err != nil {
|
|
return ServiceStopFailed, err
|
|
}
|
|
|
|
processKilledChannel := make(chan bool, 1)
|
|
spinner := display.StartSpinnerAfterDelay("Shutting down...", constants.SpinnerShowTimeout, processKilledChannel)
|
|
defer func() {
|
|
close(processKilledChannel)
|
|
display.StopSpinner(spinner)
|
|
}()
|
|
|
|
err = doThreeStepProcessExit(process)
|
|
if err != nil {
|
|
// we couldn't stop it still.
|
|
// timeout
|
|
return ServiceStopTimedOut, err
|
|
}
|
|
|
|
return ServiceStopped, nil
|
|
}
|
|
|
|
/**
|
|
Postgres has two more levels of shutdown:
|
|
* SIGTERM - Smart Shutdown : Wait for children to end normally - exit self
|
|
* SIGINT - Fast Shutdown : SIGTERM children - wait for them to exit - exit self
|
|
* SIGQUIT - Immediate Shutdown : SIGQUIT children - wait at most 5 seconds,
|
|
send SIGKILL to children - exit self immediately
|
|
|
|
Postgres recommended shutdown is to send a SIGTERM - which initiates
|
|
a Smart-Shutdown sequence.
|
|
|
|
https://www.postgresql.org/docs/12/server-shutdown.html
|
|
|
|
By the time we actually try to run this sequence, we will have
|
|
checked that the service can indeed shutdown gracefully,
|
|
the sequence is there only as a backup.
|
|
|
|
We cannot do a simple os.Process().Kill(), since that sends a SIGKILL,
|
|
which makes PG terminate immediately without saving any state.
|
|
Its the BIG RED Button.
|
|
**/
|
|
func doThreeStepProcessExit(process *psutils.Process) error {
|
|
var err error
|
|
var exitSuccessful bool
|
|
|
|
// send a SIGTERM
|
|
err = process.SendSignal(syscall.SIGTERM)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exitSuccessful = waitForProcessExit(process)
|
|
if !exitSuccessful {
|
|
// process didn't quit
|
|
// try a SIGINT
|
|
err = process.SendSignal(syscall.SIGINT)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exitSuccessful = waitForProcessExit(process)
|
|
}
|
|
if !exitSuccessful {
|
|
// process didn't quit
|
|
// desperation prevails
|
|
err = process.SendSignal(syscall.SIGQUIT)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
exitSuccessful = waitForProcessExit(process)
|
|
}
|
|
|
|
if !exitSuccessful {
|
|
return fmt.Errorf("service shutdown timed out")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func waitForProcessExit(process *psutils.Process) bool {
|
|
checkTimer := time.NewTicker(50 * time.Millisecond)
|
|
timeoutAt := time.After(2 * time.Second)
|
|
|
|
for {
|
|
select {
|
|
case <-checkTimer.C:
|
|
pEx, _ := PidExists(int(process.Pid))
|
|
if pEx {
|
|
continue
|
|
}
|
|
return true
|
|
case <-timeoutAt:
|
|
return false
|
|
}
|
|
}
|
|
}
|