Skip to content
Closed
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
110 changes: 107 additions & 3 deletions crates/wake-cli/src/commands/shell.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
use anyhow::{Context, Result};
use chrono::Utc;
use portable_pty::{native_pty_system, CommandBuilder, PtySize};
use std::io::{Read, Write};
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::net::UnixListener;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use wake_core::{Config, Database, GitCache, HookMessage, OutputBuffer, SummarizationConfig};

/// Get the user's shell, trying multiple sources:
/// 1. SHELL environment variable
/// 2. /etc/passwd entry for current user
/// 3. Fall back to /bin/sh
fn get_user_shell() -> String {
// Try SHELL env var first
if let Ok(shell) = std::env::var("SHELL") {
return shell;
}

// Try to read from /etc/passwd
if let Some(shell) = get_shell_from_passwd() {
return shell;
}

// Final fallback
"/bin/sh".to_string()
}

/// Read the current user's shell from /etc/passwd
fn get_shell_from_passwd() -> Option<String> {
let uid = unsafe { libc::getuid() };
let file = std::fs::File::open("/etc/passwd").ok()?;
let reader = BufReader::new(file);

for line in reader.lines().map_while(Result::ok) {
// Format: username:password:uid:gid:gecos:home:shell
let fields: Vec<&str> = line.split(':').collect();
if fields.len() >= 7 {
if let Ok(entry_uid) = fields[2].parse::<u32>() {
if entry_uid == uid {
let shell = fields[6].trim();
if !shell.is_empty() {
return Some(shell.to_string());
}
}
}
}
}

None
}

struct PendingCommand {
id: i64,
command: String,
Expand Down Expand Up @@ -38,8 +81,8 @@ pub async fn run() -> Result<()> {
// Clean up any stale socket
let _ = std::fs::remove_file(&socket_path);

// Get user's shell
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
// Get user's shell (env var -> /etc/passwd -> /bin/sh fallback)
let shell = get_user_shell();

// Detect project root
let cwd = std::env::current_dir().ok();
Expand Down Expand Up @@ -397,3 +440,64 @@ impl Drop for RawModeGuard {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_get_shell_from_passwd_returns_valid_shell() {
// This test runs as the current user, so it should find a shell
let shell = get_shell_from_passwd();
assert!(shell.is_some(), "Should find current user's shell in /etc/passwd");
let shell = shell.unwrap();
assert!(shell.starts_with('/'), "Shell should be an absolute path");
assert!(
shell.contains("sh") || shell.contains("zsh") || shell.contains("fish"),
"Shell should be a known shell type: {}",
shell
);
}

#[test]
fn test_get_user_shell_returns_shell() {
let shell = get_user_shell();
assert!(shell.starts_with('/'), "Shell should be an absolute path");
assert!(!shell.is_empty(), "Shell should not be empty");
}

#[test]
fn test_get_user_shell_prefers_env_var() {
// Save original
let original = std::env::var("SHELL").ok();

// Set a custom SHELL
std::env::set_var("SHELL", "/bin/custom-shell");
let shell = get_user_shell();
assert_eq!(shell, "/bin/custom-shell");

// Restore original
match original {
Some(val) => std::env::set_var("SHELL", val),
None => std::env::remove_var("SHELL"),
}
}

#[test]
fn test_get_user_shell_falls_back_to_passwd() {
// Save original
let original = std::env::var("SHELL").ok();

// Remove SHELL env var
std::env::remove_var("SHELL");

let shell = get_user_shell();
// Should get shell from passwd, not /bin/sh (unless that's actually the user's shell)
assert!(shell.starts_with('/'), "Shell should be an absolute path");

// Restore original
if let Some(val) = original {
std::env::set_var("SHELL", val);
}
}
}
82 changes: 82 additions & 0 deletions scripts/repro-issue-10.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/bin/bash
# Reproduction script for https://github.com/joemckenney/wake/issues/10
# Issue: No commands recorded on immutable Linux OS (Fedora Atomic)
#
# Root cause: SHELL env var is not exported on Fedora, so wake falls back
# to /bin/sh which doesn't source .bashrc, so hooks never load.

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(dirname "$SCRIPT_DIR")"

# Create Dockerfile
DOCKERFILE=$(mktemp)
cat > "$DOCKERFILE" << 'DOCKERFILE_CONTENT'
FROM fedora:43

# Install utilities
RUN dnf install -y util-linux-script which && dnf clean all

# Mimic Fedora Atomic home directory structure
RUN mkdir -p /var/home/testuser && \
rm -rf /home && \
ln -s /var/home /home && \
useradd -d /var/home/testuser -s /bin/bash testuser && \
chown -R testuser:testuser /var/home/testuser

# Install wake binary
COPY target/release/wake /usr/local/bin/wake
RUN chmod +x /usr/local/bin/wake

# Set up bashrc with wake hooks
USER testuser
WORKDIR /var/home/testuser
RUN echo 'eval "$(wake init bash)"' >> ~/.bashrc

CMD ["/bin/bash"]
DOCKERFILE_CONTENT

echo "=== Building container ==="
docker build -f "$DOCKERFILE" -t wake-fedora-test "$REPO_ROOT"

echo ""
echo "=== Test 1: Confirm SHELL is not exported ==="
docker run --rm wake-fedora-test bash -c '
echo "SHELL variable: $SHELL"
echo "SHELL in env: $(printenv SHELL || echo "NOT EXPORTED")"
'

echo ""
echo "=== Test 2: Reproduce the issue ==="
docker run --rm wake-fedora-test script -q -c '
wake shell << INNER
echo "Running inside wake shell..."
echo "Hook defined: \$(type __wake_preexec 2>/dev/null && echo YES || echo NO)"
ls /tmp > /dev/null
exit
INNER
echo ""
echo "Commands recorded:"
wake log -c 3
' /dev/null 2>&1 | strings

echo ""
echo "=== Test 3: Confirm workaround (export SHELL) ==="
docker run --rm wake-fedora-test script -q -c '
export SHELL # THE FIX
wake shell << INNER
echo "Hook defined: \$(type __wake_preexec 2>/dev/null && echo YES || echo NO)"
ls /tmp > /dev/null
exit
INNER
echo ""
echo "Commands recorded:"
wake log -c 3
' /dev/null 2>&1 | strings

# Cleanup
rm -f "$DOCKERFILE"

echo ""
echo "=== Done ==="