Skip to content

Commit

Permalink
Merge pull request #13 from 0kate/feature/sigterm-support-for-gracefu…
Browse files Browse the repository at this point in the history
…l-shutdown

SIGTERM support for graceful shutdown
  • Loading branch information
jkfran authored May 2, 2023
2 parents b9346e7 + 22fe361 commit 937169e
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 13 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,18 @@ Kill multiple processes listening on ports 8045, 8046, and 8080:
```sh
killport 8045 8046 8080
```

Kill processes with specified signal:

```sh
killport -s sigkill 8080
```

### Flags

-s, --signal
Specify a signal name to be sent. (e.g. sigkill)

-v, --verbose
Increase the verbosity level. Use multiple times for more detailed output.

Expand Down
35 changes: 26 additions & 9 deletions src/linux.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::KillPortSignalOptions;

use log::{debug, info, warn};
use nix::sys::signal::{kill, Signal};
use nix::unistd::Pid;
Expand All @@ -15,14 +17,15 @@ use std::path::Path;
/// # Arguments
///
/// * `port` - A u16 value representing the port number.
pub fn kill_processes_by_port(port: u16) -> Result<bool, Error> {
/// * `signal` - A enum value representing the signal type.
pub fn kill_processes_by_port(port: u16, signal: KillPortSignalOptions) -> Result<bool, Error> {
let mut killed_any = false;

let target_inodes = find_target_inodes(port);

if !target_inodes.is_empty() {
for target_inode in target_inodes {
killed_any |= kill_processes_by_inode(target_inode)?;
killed_any |= kill_processes_by_inode(target_inode, signal)?;
}
}

Expand Down Expand Up @@ -77,7 +80,11 @@ fn find_target_inodes(port: u16) -> Vec<u64> {
/// # Arguments
///
/// * `target_inode` - A u64 value representing the target inode.
fn kill_processes_by_inode(target_inode: u64) -> Result<bool, Error> {
/// * `signal` - A enum value representing the signal type.
fn kill_processes_by_inode(
target_inode: u64,
signal: KillPortSignalOptions,
) -> Result<bool, Error> {
let processes = procfs::process::all_processes().unwrap();
let mut killed_any = false;

Expand All @@ -100,7 +107,7 @@ fn kill_processes_by_inode(target_inode: u64) -> Result<bool, Error> {
}
}

match kill_process_and_children(process.pid) {
match kill_process_and_children(process.pid, signal) {
Ok(_) => {
killed_any = true;
}
Expand Down Expand Up @@ -130,15 +137,19 @@ fn kill_processes_by_inode(target_inode: u64) -> Result<bool, Error> {
/// # Arguments
///
/// * `pid` - An i32 value representing the process ID.
fn kill_process_and_children(pid: i32) -> Result<(), std::io::Error> {
/// * `signal` - A enum value representing the signal type.
fn kill_process_and_children(
pid: i32,
signal: KillPortSignalOptions,
) -> Result<(), std::io::Error> {
let mut children_pids = Vec::new();
collect_child_pids(pid, &mut children_pids)?;

for child_pid in children_pids {
kill_process(child_pid)?;
kill_process(child_pid, signal)?;
}

kill_process(pid)?;
kill_process(pid, signal)?;

Ok(())
}
Expand Down Expand Up @@ -170,8 +181,14 @@ fn collect_child_pids(pid: i32, child_pids: &mut Vec<i32>) -> Result<(), std::io
/// # Arguments
///
/// * `pid` - An i32 value representing the process ID.
fn kill_process(pid: i32) -> Result<(), std::io::Error> {
/// * `signal` - A enum value representing the signal type.
fn kill_process(pid: i32, signal: KillPortSignalOptions) -> Result<(), std::io::Error> {
info!("Killing process with PID {}", pid);
let pid = Pid::from_raw(pid);
kill(pid, Signal::SIGKILL).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))

let system_signal = match signal {
KillPortSignalOptions::SIGKILL => Signal::SIGKILL,
KillPortSignalOptions::SIGTERM => Signal::SIGTERM,
};
kill(pid, system_signal).map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
}
11 changes: 9 additions & 2 deletions src/macos.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::KillPortSignalOptions;

use libproc::libproc::file_info::pidfdinfo;
use libproc::libproc::file_info::{ListFDs, ProcFDType};
use libproc::libproc::net_info::{SocketFDInfo, SocketInfoKind};
Expand Down Expand Up @@ -34,11 +36,12 @@ fn collect_proc() -> Vec<TaskAllInfo> {
/// # Arguments
///
/// * `port` - The port number to kill processes listening on.
/// * `signal` - A enum value representing the signal type.
///
/// # Returns
///
/// A `Result` containing a boolean value. If true, at least one process was killed; otherwise, false.
pub fn kill_processes_by_port(port: u16) -> Result<bool, io::Error> {
pub fn kill_processes_by_port(port: u16, signal: KillPortSignalOptions) -> Result<bool, io::Error> {
let process_infos = collect_proc();
let mut killed = false;

Expand Down Expand Up @@ -90,7 +93,11 @@ pub fn kill_processes_by_port(port: u16) -> Result<bool, io::Error> {
warn!("Warning: Found Docker. You might need to stop the container manually.");
} else {
info!("Killing process with PID {}", pid);
match signal::kill(pid, Signal::SIGKILL) {
let system_signal = match signal {
KillPortSignalOptions::SIGKILL => Signal::SIGKILL,
KillPortSignalOptions::SIGTERM => Signal::SIGTERM,
};
match signal::kill(pid, system_signal) {
Ok(_) => {
killed = true;
}
Expand Down
25 changes: 23 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,18 @@ use linux::kill_processes_by_port;
#[cfg(target_os = "macos")]
use macos::kill_processes_by_port;

use clap::Parser;
use clap::{Parser, ValueEnum};
use clap_verbosity_flag::{Verbosity, WarnLevel};
use log::error;
use std::process::exit;

/// The `KillPortSignalOptions` enum is used to specify signal types on the command-line arguments.
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum KillPortSignalOptions {
SIGKILL,
SIGTERM,
}

/// The `KillPortArgs` struct is used to parse command-line arguments for the
/// `killport` utility.
#[derive(Parser, Debug)]
Expand All @@ -32,6 +39,16 @@ struct KillPortArgs {
)]
ports: Vec<u16>,

/// An option to specify the type of signal to be sent.
#[arg(
long,
short = 's',
name = "SIG",
help = "SIG is a signal name",
default_value = "sigterm"
)]
signal: KillPortSignalOptions,

/// A verbosity flag to control the level of logging output.
#[command(flatten)]
verbose: Verbosity<WarnLevel>,
Expand Down Expand Up @@ -59,9 +76,13 @@ fn main() {
.filter_level(log_level)
.init();

// Determine a signal to be sent.
// If an option for signal number is added, we can determine a signal to be sent by signal number.
let signal = args.signal;

// Attempt to kill processes listening on specified ports
for port in args.ports {
match kill_processes_by_port(port) {
match kill_processes_by_port(port, signal) {
Ok(killed) => {
if killed {
println!("Successfully killed process listening on port {}", port);
Expand Down
18 changes: 18 additions & 0 deletions tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fn test_killport() {

assert!(status.success(), "Mock process compilation failed");

// Test killport execution without options
let mut mock_process = std::process::Command::new(tempdir_path.join("mock_process"))
.spawn()
.expect("Failed to run the mock process");
Expand All @@ -48,4 +49,21 @@ fn test_killport() {

// Cleanup: Terminate the mock process (if still running).
let _ = mock_process.kill();

// Test killport execution with -s option
let mut mock_process = std::process::Command::new(tempdir_path.join("mock_process"))
.spawn()
.expect("Failed to run the mock process");

// Test killport command with specifying a signal name
let mut cmd = Command::cargo_bin("killport").expect("Failed to find killport binary");
cmd.arg("8080")
.arg("-s")
.arg("sigterm")
.assert()
.success()
.stdout("Successfully killed process listening on port 8080\n");

// Cleanup: Terminate the mock process (if still running).
let _ = mock_process.kill();
}

0 comments on commit 937169e

Please sign in to comment.