9a0c26e5f6
- Add voting mechanism with term tracking and vote persistence - Implement election triggering logic with majority vote counting - Add primary/replica role transition handling with state management - Integrate health check failure detection for automatic elections - Refactor actor messaging system for distributed coordination - Update repository registration to query cluster for existing primary - Add broadcast mechanism for role change notifications - Implement proper term comparison and duplicate request filtering - Upgrade dependency versions including tokio-util for async utilities - Optimize code formatting and line wrapping for improved readability - Remove redundant blank lines and improve code structure consistency - Enhance error logging and trace information for debugging purposes
279 lines
8.4 KiB
Rust
279 lines
8.4 KiB
Rust
//! Hook execution runner.
|
|
//!
|
|
//! Executes server hooks, custom hooks, and gRPC callback hooks
|
|
//! in sequence for each git hook type (pre-receive, update, post-receive).
|
|
|
|
use std::io::Write;
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Output, Stdio};
|
|
use std::time::Duration;
|
|
|
|
/// Result of a hook execution.
|
|
#[derive(Debug)]
|
|
pub struct HookResult {
|
|
pub accepted: bool,
|
|
pub exit_code: i32,
|
|
pub stdout: String,
|
|
pub stderr: String,
|
|
}
|
|
|
|
impl HookResult {
|
|
pub fn accepted() -> Self {
|
|
Self {
|
|
accepted: true,
|
|
exit_code: 0,
|
|
stdout: String::new(),
|
|
stderr: String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn rejected(stderr: String) -> Self {
|
|
Self {
|
|
accepted: false,
|
|
exit_code: 1,
|
|
stdout: String::new(),
|
|
stderr,
|
|
}
|
|
}
|
|
|
|
pub fn from_output(output: &Output) -> Self {
|
|
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
|
|
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
|
|
Self {
|
|
accepted: output.status.success(),
|
|
exit_code: output.status.code().unwrap_or(-1),
|
|
stdout,
|
|
stderr,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Run all hook scripts in a directory (sorted alphabetically).
|
|
/// Returns the first rejection or accepted if all pass.
|
|
pub fn run_hook_dir(
|
|
hook_dir: &Path,
|
|
hook_type: &str,
|
|
stdin_data: &[u8],
|
|
timeout: Duration,
|
|
) -> HookResult {
|
|
if !hook_dir.exists() {
|
|
return HookResult::accepted();
|
|
}
|
|
|
|
let mut scripts: Vec<PathBuf> = Vec::new();
|
|
if let Ok(entries) = std::fs::read_dir(hook_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.is_file() {
|
|
let name = path.file_name().unwrap_or_default().to_string_lossy();
|
|
// Skip files starting with '.' (hidden files) or ending with '~' or '.sample'
|
|
if name.starts_with('.') || name.ends_with('~') || name.ends_with(".sample") {
|
|
continue;
|
|
}
|
|
scripts.push(path);
|
|
}
|
|
}
|
|
}
|
|
scripts.sort();
|
|
|
|
for script in &scripts {
|
|
tracing::debug!(
|
|
hook_type = %hook_type,
|
|
script = %script.display(),
|
|
"executing hook script"
|
|
);
|
|
let result = run_single_script(script, stdin_data, timeout);
|
|
if !result.accepted {
|
|
tracing::warn!(
|
|
hook_type = %hook_type,
|
|
script = %script.display(),
|
|
exit_code = result.exit_code,
|
|
stderr = %result.stderr,
|
|
"hook script rejected"
|
|
);
|
|
return result;
|
|
}
|
|
}
|
|
|
|
HookResult::accepted()
|
|
}
|
|
|
|
/// Run a single hook script with stdin data and timeout.
|
|
fn run_single_script(script_path: &Path, stdin_data: &[u8], timeout: Duration) -> HookResult {
|
|
let child = std::process::Command::new(script_path)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn();
|
|
|
|
match child {
|
|
Ok(mut c) => {
|
|
if let Some(ref mut stdin) = c.stdin {
|
|
let _ = stdin.write_all(stdin_data);
|
|
}
|
|
c.stdin = None;
|
|
|
|
let wait_result = c.wait_timeout(timeout);
|
|
match wait_result {
|
|
Ok(Some(status)) => {
|
|
// Process exited within timeout, get its output
|
|
// Note: We already have the status, so we need to construct output differently
|
|
// Since wait_with_output would fail after try_wait, we return status-only output
|
|
HookResult {
|
|
accepted: status.success(),
|
|
exit_code: status.code().unwrap_or(-1),
|
|
stdout: String::new(), // stdout was consumed by the process
|
|
stderr: String::new(), // stderr was consumed by the process
|
|
}
|
|
}
|
|
Ok(None) => {
|
|
tracing::warn!(
|
|
script = %script_path.display(),
|
|
timeout_secs = timeout.as_secs(),
|
|
"hook script timed out, killing"
|
|
);
|
|
let _ = c.kill();
|
|
// Explicitly wait to reap the zombie process
|
|
let _ = c.wait();
|
|
HookResult::rejected(format!(
|
|
"hook script timed out after {}s: {}",
|
|
timeout.as_secs(),
|
|
script_path.display()
|
|
))
|
|
}
|
|
Err(e) => {
|
|
let _ = c.kill();
|
|
// Explicitly wait to reap the zombie process
|
|
let _ = c.wait();
|
|
HookResult::rejected(format!("hook script wait error: {e}"))
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(
|
|
script = %script_path.display(),
|
|
error = %e,
|
|
"failed to spawn hook script"
|
|
);
|
|
// If the script can't be executed, treat as rejection
|
|
HookResult::rejected(format!("failed to spawn hook script: {e}"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Wait for a child process with timeout.
|
|
trait ChildWaitTimeout {
|
|
fn wait_timeout(
|
|
&mut self,
|
|
timeout: Duration,
|
|
) -> std::io::Result<Option<std::process::ExitStatus>>;
|
|
}
|
|
|
|
impl ChildWaitTimeout for std::process::Child {
|
|
fn wait_timeout(
|
|
&mut self,
|
|
timeout: Duration,
|
|
) -> std::io::Result<Option<std::process::ExitStatus>> {
|
|
let start = std::time::Instant::now();
|
|
loop {
|
|
match self.try_wait() {
|
|
Ok(Some(status)) => return Ok(Some(status)),
|
|
Ok(None) => {
|
|
if start.elapsed() > timeout {
|
|
return Ok(None); // timeout
|
|
}
|
|
std::thread::sleep(Duration::from_millis(100));
|
|
}
|
|
Err(e) => return Err(e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate the gitks hook runner script content.
|
|
/// This script is installed into each repository's hooks/ directory
|
|
/// and orchestrates the execution of server hooks, custom hooks, and gRPC callbacks.
|
|
pub fn generate_hook_runner_script(
|
|
hook_type: &str,
|
|
repo_relative_path: &str,
|
|
server_hooks_dir: Option<&str>,
|
|
hook_callback_addr: Option<&str>,
|
|
hook_timeout_secs: u64,
|
|
) -> String {
|
|
let server_hooks_section = if let Some(dir) = server_hooks_dir {
|
|
format!(
|
|
r#"
|
|
# Run server hooks
|
|
SERVER_HOOKS_DIR="{dir}/{hook_type}.d"
|
|
if [ -d "$SERVER_HOOKS_DIR" ]; then
|
|
for script in $(ls "$SERVER_HOOKS_DIR" | sort); do
|
|
skip=false
|
|
case "$script" in .*|*.sample|*~) skip=true;; esac
|
|
if [ "$skip" = "false" ]; then
|
|
"$SERVER_HOOKS_DIR/$script"
|
|
exit_code=$?
|
|
if [ $exit_code -ne 0 ]; then
|
|
exit $exit_code
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
"#
|
|
)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
let custom_hooks_section = r#"
|
|
# Run custom hooks (per-repository)
|
|
CUSTOM_HOOKS_DIR="$GIT_DIR/custom_hooks/$GITKS_HOOK_TYPE.d"
|
|
if [ -d "$CUSTOM_HOOKS_DIR" ]; then
|
|
for script in $(ls "$CUSTOM_HOOKS_DIR" | sort); do
|
|
skip=false
|
|
case "$script" in .*|*.sample|*~) skip=true;; esac
|
|
if [ "$skip" = "false" ]; then
|
|
"$CUSTOM_HOOKS_DIR/$script"
|
|
exit_code=$?
|
|
if [ $exit_code -ne 0 ]; then
|
|
exit $exit_code
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
"#;
|
|
|
|
let grpc_callback_section = if let Some(addr) = hook_callback_addr {
|
|
format!(
|
|
r#"
|
|
# gRPC callback to external HookService
|
|
if [ -n "{addr}" ]; then
|
|
# gRPC callback is handled by the gitks service directly
|
|
# The service will make the gRPC call after the git hook completes
|
|
# This section is a placeholder - actual gRPC callback is handled
|
|
# by the gitks receive_pack handler
|
|
fi
|
|
"#
|
|
)
|
|
} else {
|
|
String::new()
|
|
};
|
|
|
|
format!(
|
|
r#"#!/bin/sh
|
|
# gitks hook runner for {hook_type}
|
|
# Repository: {repo_relative_path}
|
|
# Auto-generated by gitks - do not modify manually
|
|
|
|
GITKS_HOOK_TYPE="{hook_type}"
|
|
GITKS_REPO_RELATIVE_PATH="{repo_relative_path}"
|
|
GITKS_HOOK_TIMEOUT="{hook_timeout_secs}"
|
|
|
|
{server_hooks_section}
|
|
{custom_hooks_section}
|
|
{grpc_callback_section}
|
|
|
|
exit 0
|
|
"#
|
|
)
|
|
}
|