Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .github/workflows/build-distributions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,24 @@ jobs:
"${run_as[@]}" "${smoke_scripts}/sqb" --version
"${run_as[@]}" "${smoke_scripts}/sqb" --help
"${run_as[@]}" "${smoke_scripts}/sqlbuild" --help
"${run_as[@]}" "${smoke_scripts}/sqlbuild" --version
# The sqlbuild forwarder returns sqb's own exit code; an unknown command is a usage error.
code=0
"${run_as[@]}" "${smoke_scripts}/sqlbuild" no-such-command > /dev/null 2>&1 || code=$?
if [[ "${code}" != "2" ]]; then
echo "sqlbuild no-such-command exited ${code}; sqb exits 2" >&2
exit 1
fi
exe="${{ runner.os == 'Windows' && '.exe' || '' }}"
{
echo "### Sizes (${{ matrix.platform.name }})"
echo
echo "| File | Bytes |"
echo "| --- | ---: |"
for file in "${GITHUB_WORKSPACE}"/dist/*.whl "${smoke_scripts}/sqb${exe}" "${smoke_scripts}/sqlbuild${exe}"; do
echo "| $(basename "${file}") | $(wc -c < "${file}" | tr -d ' ') |"
done
} >> "${GITHUB_STEP_SUMMARY}"
if [[ "${{ inputs.prebuilt-run-id == '' }}" == "true" ]]; then
cd "${GITHUB_WORKSPACE}"
"${run_as[@]}" "${smoke_python}" -m tests.integration.src.sqlbuild.compiler.compiled_project.publication.wheel.helpers \
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ jobs:
-l "${RUNNER_TEMP}/postgres.log" -w start
- name: Worker claim ownership, recovery and stop
run: cargo test --locked -p sqlbuild-process-ownership
- name: sqlbuild forwarder arguments, exit codes, streams, signals and missing sqb
run: cargo test --locked -p sqlbuild-cli --test launcher
- name: Control store, scheduler and recovery contracts on SQLite and PostgreSQL
env:
SQLBUILD_TEST_CONTROL_POSTGRES_URL: postgresql://postgres@127.0.0.1:5433/postgres
Expand Down
34 changes: 31 additions & 3 deletions crates/sqlbuild-cli/src/bin/sqlbuild.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
//! `sqlbuild`: the native SQLBuild command line, the same program as `sqb`.
//! `sqlbuild`: the same command line as `sqb`, by running the `sqb` installed beside it without linking the library.

#[path = "../launcher/_helpers/handoff.rs"]
mod handoff;

use std::ffi::OsString;
use std::io::Write;
use std::path::PathBuf;
use std::process::ExitCode;

use sqlbuild_cli::launcher::main::run_launcher::run_launcher;
use handoff::{beside, hand_off, process_exit};

fn main() -> ExitCode {
run_launcher()
let program: PathBuf = std::env::current_exe().unwrap_or_default();
let sqb_name = format!("sqb{}", std::env::consts::EXE_SUFFIX);
let Some(sqb) = beside(&program, &sqb_name) else {
let _ = writeln!(
std::io::stderr(),
"sqlbuild: cannot find {sqb_name} beside {}; reinstall sqlbuild-cli",
program.display()
);
return ExitCode::FAILURE;
};
let arguments: Vec<OsString> = std::env::args_os().skip(1).collect();
match hand_off(&sqb, &arguments) {
Ok(Some(code)) => process_exit(code),
Ok(None) => ExitCode::FAILURE,
Err(error) => {
let _ = writeln!(
std::io::stderr(),
"sqlbuild: cannot start {}: {error}",
sqb.display()
);
ExitCode::FAILURE
}
}
}
19 changes: 14 additions & 5 deletions crates/sqlbuild-cli/src/launcher/_helpers/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime};

use crate::launcher::_helpers::handoff::{hand_off, python_cli};
use crate::launcher::_helpers::handoff::{beside, hand_off};
use crate::launcher::_helpers::launch_key::{launch_key, launch_record_path};
use crate::launcher::_helpers::messages::{print_version, report_missing_python_cli};
use crate::launcher::_helpers::messages::{
print_version, report_handoff_failure, report_missing_python_cli,
};
use crate::launcher::constants::{
HANDOFF_FAILURE_EXIT_CODE, LAUNCH_RECORD_OPTION, VERSION_OPTIONS,
HANDOFF_FAILURE_EXIT_CODE, LAUNCH_RECORD_OPTION, PYTHON_CLI_SCRIPT, VERSION_OPTIONS,
};
use crate::native_commands::main::parse_native_invocation::parse_native_invocation;
use crate::native_commands::main::run_control_invocation::run_control_invocation;
Expand Down Expand Up @@ -64,7 +66,7 @@ pub(crate) fn launch(arguments: Vec<OsString>, started: (Instant, SystemTime)) -
return code;
}
let program: PathBuf = std::env::current_exe().unwrap_or_default();
let Some(script) = python_cli(&program) else {
let Some(script) = beside(&program, PYTHON_CLI_SCRIPT) else {
report_missing_python_cli(&program);
return HANDOFF_FAILURE_EXIT_CODE;
};
Expand Down Expand Up @@ -94,5 +96,12 @@ pub(crate) fn launch(arguments: Vec<OsString>, started: (Instant, SystemTime)) -
handed.push(option);
}
handed.extend(arguments);
hand_off(&script, &handed)
match hand_off(&script, &handed) {
Ok(Some(code)) => code,
Ok(None) => HANDOFF_FAILURE_EXIT_CODE,
Err(error) => {
report_handoff_failure(&script, &error);
HANDOFF_FAILURE_EXIT_CODE
}
}
}
43 changes: 21 additions & 22 deletions crates/sqlbuild-cli/src/launcher/_helpers/handoff.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
//! Find the Python CLI script installed beside this program and run it with the same arguments.
//! Find a program installed beside this one and run it with the same arguments and streams; the `sqlbuild` forwarder compiles this file into its own binary.

use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::{Command, ExitCode};

use crate::launcher::_helpers::messages::report_handoff_failure;
use crate::launcher::constants::{HANDOFF_FAILURE_EXIT_CODE, PYTHON_CLI_SCRIPT};

/// The Python CLI script beside this program or beside the file it links to.
pub(crate) fn python_cli(program: &Path) -> Option<PathBuf> {
/// The file `name` beside this program or beside the file it links to.
pub(crate) fn beside(program: &Path, name: &str) -> Option<PathBuf> {
let resolved = std::fs::canonicalize(program).unwrap_or_else(|_| program.to_path_buf());
[program, resolved.as_path()]
.into_iter()
.filter_map(Path::parent)
.map(|directory| directory.join(PYTHON_CLI_SCRIPT))
.map(|directory| directory.join(name))
.find(|candidate| candidate.is_file())
}

fn failure(script: &Path, error: &std::io::Error) -> i32 {
report_handoff_failure(script, error);
HANDOFF_FAILURE_EXIT_CODE
}

/// Replace this process with the Python CLI; returns only when it cannot start.
/// Replace this process with `target`; returns only the error that kept it from starting.
#[cfg(unix)]
pub(crate) fn hand_off(script: &Path, arguments: &[OsString]) -> i32 {
pub(crate) fn hand_off(target: &Path, arguments: &[OsString]) -> std::io::Result<Option<i32>> {
use std::os::unix::process::CommandExt;

let error = Command::new(script).args(arguments).exec();
failure(script, &error)
Err(Command::new(target).args(arguments).exec())
}

/// Run the Python CLI as a child that alone handles Ctrl+C, and return its exit code.
/// Run `target` as a child that alone handles Ctrl+C and Ctrl+Break, and return its exit code.
#[cfg(not(unix))]
pub(crate) fn hand_off(script: &Path, arguments: &[OsString]) -> i32 {
pub(crate) fn hand_off(target: &Path, arguments: &[OsString]) -> std::io::Result<Option<i32>> {
let _ = ctrlc::set_handler(|| {});
match Command::new(script).args(arguments).status() {
Ok(status) => status.code().unwrap_or(HANDOFF_FAILURE_EXIT_CODE),
Err(error) => failure(script, &error),
Command::new(target)
.args(arguments)
.status()
.map(|status| status.code())
}

/// End the process with `code`; a code beyond one byte ends it directly.
pub(crate) fn process_exit(code: i32) -> ExitCode {
match u8::try_from(code) {
Ok(code) => ExitCode::from(code),
Err(_) => std::process::exit(code),
}
}
9 changes: 3 additions & 6 deletions crates/sqlbuild-cli/src/launcher/main/run_launcher.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
//! The `sqb` and `sqlbuild` executables' whole process.
//! The `sqb` executable's whole process.

use std::ffi::OsString;
use std::process::ExitCode;
use std::time::{Instant, SystemTime};

use crate::launcher::_helpers::dispatch::launch;
use crate::launcher::_helpers::handoff::process_exit;

/// Run this process's invocation; an exit code beyond one byte ends the process directly.
pub fn run_launcher() -> ExitCode {
let started = (Instant::now(), SystemTime::now());
let arguments: Vec<OsString> = std::env::args_os().skip(1).collect();
let code: i32 = launch(arguments, started);
match u8::try_from(code) {
Ok(code) => ExitCode::from(code),
Err(_) => std::process::exit(code),
}
process_exit(launch(arguments, started))
}
6 changes: 6 additions & 0 deletions crates/sqlbuild-cli/tests/launcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[path = "launcher/helpers.rs"]
mod helpers;
#[path = "launcher/test_forwarder.rs"]
mod test_forwarder;
#[path = "launcher/test_types.rs"]
mod test_types;
149 changes: 149 additions & 0 deletions crates/sqlbuild-cli/tests/launcher/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#[cfg(unix)]
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::process::Child;
use std::process::Command;

const FAKE_SQB: &str = r#"
use std::io::{Read, Write};

fn main() {
let mut out = std::io::stdout();
for argument in std::env::args_os().skip(1) {
writeln!(out, "arg:{:?}", argument).unwrap();
}
if std::env::var_os("FAKE_STDIN").is_some() {
let mut input = String::new();
std::io::stdin().read_to_string(&mut input).unwrap();
write!(out, "stdin:{input}").unwrap();
}
out.flush().unwrap();
eprint!("fake stderr");
if std::env::var_os("FAKE_WAIT").is_some() {
#[cfg(windows)]
console::exit_on_break();
writeln!(out, "ready:{}", std::process::id()).unwrap();
out.flush().unwrap();
std::thread::sleep(std::time::Duration::from_secs(30));
}
let code: i32 = std::env::var("FAKE_EXIT").ok().and_then(|code| code.parse().ok()).unwrap_or(0);
std::process::exit(code);
}

#[cfg(windows)]
mod console {
type Handler = unsafe extern "system" fn(u32) -> i32;

#[link(name = "kernel32")]
extern "system" {
fn SetConsoleCtrlHandler(handler: Option<Handler>, add: i32) -> i32;
}

unsafe extern "system" fn on_break(_event: u32) -> i32 {
std::thread::sleep(std::time::Duration::from_millis(500));
std::process::exit(42)
}

pub fn exit_on_break() {
unsafe {
SetConsoleCtrlHandler(Some(on_break), 1);
}
}
}
"#;

#[cfg(windows)]
const BREAKER: &str = r#"
use std::io::BufRead;
use std::os::windows::process::CommandExt;
use std::process::{Command, Stdio};

#[link(name = "kernel32")]
extern "system" {
fn GenerateConsoleCtrlEvent(event: u32, group: u32) -> i32;
}

fn main() {
let forwarder = std::env::args_os().nth(1).unwrap();
let result = std::env::args_os().nth(2).unwrap();
let mut child = Command::new(forwarder)
.creation_flags(0x0000_0200)
.env("FAKE_WAIT", "1")
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.unwrap();
let mut ready = String::new();
std::io::BufReader::new(child.stdout.take().unwrap()).read_line(&mut ready).unwrap();
let sent = unsafe { GenerateConsoleCtrlEvent(1, child.id()) };
let status = child.wait().unwrap();
std::fs::write(result, format!("{sent} {}", status.code().unwrap())).unwrap();
}
"#;

fn compile(source: &str, directory: &Path, name: &str) -> PathBuf {
let path = directory.join(format!("{name}.rs"));
std::fs::write(&path, source).expect("program source");
let binary = directory.join(format!("{name}{}", std::env::consts::EXE_SUFFIX));
let output = Command::new(std::env::var("RUSTC").unwrap_or_else(|_| "rustc".into()))
.args(["--edition", "2021", "-o"])
.arg(&binary)
.arg(&path)
.output()
.expect("run rustc");
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
binary
}

/// The file name of `sqb` on this platform.
pub(crate) fn sqb_file() -> String {
format!("sqb{}", std::env::consts::EXE_SUFFIX)
}

/// A copy of the real `sqlbuild` forwarder, alone in `directory`; a copy works across volumes.
pub(crate) fn forwarder_in(directory: &Path) -> PathBuf {
let forwarder = directory.join(format!("sqlbuild{}", std::env::consts::EXE_SUFFIX));
std::fs::copy(env!("CARGO_BIN_EXE_sqlbuild"), &forwarder).expect("copy the forwarder");
forwarder
}

/// The real forwarder with a fake `sqb` beside it that reports what it received.
pub(crate) fn forwarder_with_fake_sqb(directory: &Path) -> PathBuf {
compile(FAKE_SQB, directory, "sqb");
forwarder_in(directory)
}

/// The pid the waiting fake `sqb` reports on its first stdout line.
#[cfg(unix)]
pub(crate) fn ready_pid(child: &mut Child) -> u32 {
let mut line = String::new();
BufReader::new(child.stdout.take().expect("piped stdout"))
.read_line(&mut line)
.expect("ready line");
line.trim()
.strip_prefix("ready:")
.and_then(|pid| pid.parse().ok())
.expect("the fake sqb reports its pid")
}

/// Ctrl+Break the forwarder's process group in a fresh console; returns `<sent> <forwarder exit code>`.
#[cfg(windows)]
pub(crate) fn break_forwarder(directory: &Path, forwarder: &Path) -> String {
use std::os::windows::process::CommandExt;

let breaker = compile(BREAKER, directory, "breaker");
let result = directory.join("result.txt");
let status = Command::new(breaker)
.arg(forwarder)
.arg(&result)
.creation_flags(0x0000_0010)
.status()
.expect("run the breaker");
assert!(status.success(), "breaker failed: {status:?}");
std::fs::read_to_string(result).expect("breaker result")
}
Loading
Loading