Windows support for the synchronous shim
Signed-off-by: James Sturtevant <jsturtevant@gmail.com> Signed-off-by: James Sturtevant <jstur@microsoft.com>
This commit is contained in:
		
							parent
							
								
									5c96c0fe63
								
							
						
					
					
						commit
						bceaf4aca3
					
				|  | @ -0,0 +1,99 @@ | |||
| name: CI-windows | ||||
| on: | ||||
|   pull_request: | ||||
|   push: | ||||
|   schedule: | ||||
|     - cron: '0 0 * * *' # Every day at midnight | ||||
| 
 | ||||
| jobs: | ||||
|   checks: | ||||
|     name: Checks | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     timeout-minutes: 20 | ||||
| 
 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [windows-latest] | ||||
| 
 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: arduino/setup-protoc@v1 | ||||
|         with: | ||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|       - run: cargo check --examples --tests -p containerd-shim -p containerd-shim-protos | ||||
|        | ||||
|       - run: rustup toolchain install nightly --component rustfmt | ||||
|       - run: cargo +nightly fmt -p containerd-shim -p containerd-shim-protos -- --check --files-with-diff | ||||
|        | ||||
|       - run: cargo clippy -p containerd-shim -p containerd-shim-protos -- -D warnings | ||||
|       - run: cargo doc --no-deps -p containerd-shim -p containerd-shim-protos | ||||
|         env: | ||||
|           RUSTDOCFLAGS: -Dwarnings | ||||
| 
 | ||||
|   tests: | ||||
|     name: Tests | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     timeout-minutes: 15 | ||||
| 
 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [windows-latest] | ||||
| 
 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - uses: arduino/setup-protoc@v1 | ||||
|         with: | ||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|       - name: Tests | ||||
|         run: | | ||||
|           cargo test -p containerd-shim -p containerd-shim-protos | ||||
| 
 | ||||
|   integration: | ||||
|     name: Integration | ||||
|     runs-on: ${{ matrix.os }} | ||||
|     timeout-minutes: 40 | ||||
| 
 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [windows-latest] | ||||
|         containerd: [1.7.0] | ||||
| 
 | ||||
|     steps: | ||||
|       - name: Checkout extensions | ||||
|         uses: actions/checkout@v3 | ||||
| 
 | ||||
|       - name: Install containerd | ||||
|         run: | | ||||
|           $ErrorActionPreference = "Stop" | ||||
| 
 | ||||
|           # Install containerd https://github.com/containerd/containerd/blob/v1.7.0/docs/getting-started.md#installing-containerd-on-windows | ||||
|           # Download and extract desired containerd Windows binaries | ||||
|           curl.exe -L https://github.com/containerd/containerd/releases/download/v${{ matrix.containerd }}/containerd-${{ matrix.containerd }}-windows-amd64.tar.gz -o containerd-windows-amd64.tar.gz | ||||
|           tar.exe xvf .\containerd-windows-amd64.tar.gz | ||||
| 
 | ||||
|           # Copy and configure | ||||
|           mkdir "$Env:ProgramFiles\containerd" | ||||
|           Copy-Item -Path ".\bin\*" -Destination "$Env:ProgramFiles\containerd" -Recurse -Force | ||||
|           cd $Env:ProgramFiles\containerd\ | ||||
|           .\containerd.exe config default | Out-File config.toml -Encoding ascii | ||||
| 
 | ||||
|           # Review the configuration. Depending on setup you may want to adjust: | ||||
|           # - the sandbox_image (Kubernetes pause image) | ||||
|           # - cni bin_dir and conf_dir locations | ||||
|           Get-Content config.toml | ||||
| 
 | ||||
|           # Register and start service | ||||
|           .\containerd.exe --register-service | ||||
|           Start-Service containerd | ||||
|         working-directory: ${{ runner.temp }} | ||||
|       - name: Run integration test | ||||
|         run: | | ||||
|           $ErrorActionPreference = "Stop" | ||||
| 
 | ||||
|           get-service containerd | ||||
|           $env:TTRPC_ADDRESS="\\.\pipe\containerd-containerd.ttrpc"   | ||||
| 
 | ||||
|           # run the example  | ||||
|           cargo run --example skeleton -- -namespace default -id 1234 -address "\\.\pipe\containerd-containerd"  -publish-binary ./bin/containerd start  | ||||
|           ps skeleton  | ||||
|           cargo run --example shim-proto-connect \\.\pipe\containerd-shim-17630016127144989388-pipe | ||||
|  | @ -9,3 +9,5 @@ Cargo.lock | |||
| # These are backup files generated by rustfmt | ||||
| **/*.rs.bk | ||||
| log | ||||
| 
 | ||||
| .vscode | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ homepage.workspace = true | |||
| 
 | ||||
| [dependencies] | ||||
| protobuf = "3.1" | ||||
| ttrpc = "0.7" | ||||
| ttrpc = { git = "https://github.com/jsturtevant/ttrpc-rust", rev = "d96bea3a41b6c6b85c1d8da53e5085fb0789a685" } # unmerged pr https://github.com/containerd/ttrpc-rust/pull/182  | ||||
| async-trait = { version = "0.1.48", optional = true } | ||||
| 
 | ||||
| [build-dependencies] | ||||
|  |  | |||
|  | @ -17,13 +17,16 @@ async = ["tokio", "containerd-shim-protos/async", "async-trait", "futures", "sig | |||
| name = "skeleton_async" | ||||
| required-features = ["async"] | ||||
| 
 | ||||
| [[example]] | ||||
| name = "windows-log-reader" | ||||
| path = "examples/windows_log_reader.rs" | ||||
| 
 | ||||
| [dependencies] | ||||
| go-flag = "0.1.0" | ||||
| thiserror = "1.0" | ||||
| log = { version = "0.4", features = ["std"] } | ||||
| libc = "0.2.95" | ||||
| nix = "0.26" | ||||
| command-fds = "0.2.1" | ||||
| lazy_static = "1.4.0" | ||||
| time = { version = "0.3.7", features = ["serde", "std"] } | ||||
| serde_json = "1.0.78" | ||||
|  | @ -45,5 +48,13 @@ signal-hook-tokio = { version = "0.3.1", optional = true, features = ["futures-v | |||
| [target.'cfg(target_os = "linux")'.dependencies] | ||||
| cgroups-rs = "0.2.9" | ||||
| 
 | ||||
| [target.'cfg(unix)'.dependencies] | ||||
| command-fds = "0.2.1" | ||||
| 
 | ||||
| [target.'cfg(windows)'.dependencies] | ||||
| windows-sys = {version = "0.48.0", features = ["Win32_Foundation","Win32_System_WindowsProgramming","Win32_System_Console", "Win32_System_Pipes","Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Threading"]} | ||||
| mio = { version = "0.8", features = ["os-ext", "os-poll"] } | ||||
| os_pipe = "1.1.3" | ||||
| 
 | ||||
| [dev-dependencies] | ||||
| tempfile = "3.0" | ||||
|  |  | |||
|  | @ -135,7 +135,30 @@ $ cat log | |||
| [INFO] reaper thread stopped | ||||
| ``` | ||||
| 
 | ||||
| ### Running on Windows | ||||
| ```powershell | ||||
| # Run containerd in background | ||||
| $env:TTRPC_ADDRESS="\\.\pipe\containerd-containerd.ttrpc" | ||||
| 
 | ||||
| $ cargo run --example skeleton -- -namespace default -id 1234 -address "\\.\pipe\containerd-containerd" start | ||||
| \\.\pipe\containerd-shim-17630016127144989388-pipe | ||||
| 
 | ||||
| # (Optional) Run the log collector in a separate command window | ||||
| # note: log reader won't work if containerd is connected to the named pipe, this works when running manually to help debug locally | ||||
| $ cargo run --example windows-log-reader \\.\pipe\containerd-shim-default-1234-log | ||||
| Reading logs from: \\.\pipe\containerd-shim-default-1234-log | ||||
| <logs will appear after next command> | ||||
| 
 | ||||
| $ cargo run --example shim-proto-connect \\.\pipe\containerd-shim-17630016127144989388-pipe | ||||
| Connecting to \\.\pipe\containerd-shim-17630016127144989388-pipe... | ||||
| Sending `Connect` request... | ||||
| Connect response: version: "example" | ||||
| Sending `Shutdown` request... | ||||
| Shutdown response: "" | ||||
| ``` | ||||
| 
 | ||||
| ## Supported Platforms | ||||
| Currently, following OSs and hardware architectures are supported, and more efforts are needed to enable and validate other OSs and architectures. | ||||
| - Linux | ||||
| - Mac OS | ||||
| - Windows | ||||
|  |  | |||
|  | @ -0,0 +1,71 @@ | |||
| /* | ||||
|    Copyright The containerd Authors. | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
| #[cfg(windows)] | ||||
| use std::error::Error; | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| fn main() -> Result<(), Box<dyn Error>> { | ||||
|     use std::{ | ||||
|         env, | ||||
|         fs::OpenOptions, | ||||
|         os::windows::{ | ||||
|             fs::OpenOptionsExt, | ||||
|             io::{FromRawHandle, IntoRawHandle}, | ||||
|         }, | ||||
|         time::Duration, | ||||
|     }; | ||||
| 
 | ||||
|     use mio::{windows::NamedPipe, Events, Interest, Poll, Token}; | ||||
|     use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; | ||||
| 
 | ||||
|     let args: Vec<String> = env::args().collect(); | ||||
| 
 | ||||
|     let address = args | ||||
|         .get(1) | ||||
|         .ok_or("First argument must be shims address to read logs (\\\\.\\pipe\\containerd-shim-{ns}-{id}-log) ") | ||||
|         .unwrap(); | ||||
| 
 | ||||
|     println!("Reading logs from: {}", &address); | ||||
| 
 | ||||
|     let mut opts = OpenOptions::new(); | ||||
|     opts.read(true) | ||||
|         .write(true) | ||||
|         .custom_flags(FILE_FLAG_OVERLAPPED); | ||||
|     let file = opts.open(address).unwrap(); | ||||
|     let mut client = unsafe { NamedPipe::from_raw_handle(file.into_raw_handle()) }; | ||||
| 
 | ||||
|     let mut stdio = std::io::stdout(); | ||||
|     let mut poll = Poll::new().unwrap(); | ||||
|     poll.registry() | ||||
|         .register(&mut client, Token(1), Interest::READABLE) | ||||
|         .unwrap(); | ||||
|     let mut events = Events::with_capacity(128); | ||||
|     loop { | ||||
|         poll.poll(&mut events, Some(Duration::from_millis(10))) | ||||
|             .unwrap(); | ||||
|         match std::io::copy(&mut client, &mut stdio) { | ||||
|             Ok(_) => break, | ||||
|             Err(_) => continue, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn main() { | ||||
|     println!("This example is only for Windows"); | ||||
| } | ||||
|  | @ -50,9 +50,11 @@ pub enum Error { | |||
|     Setup(#[from] log::SetLoggerError), | ||||
| 
 | ||||
|     /// Unable to pass fd to child process (we rely on `command_fds` crate for this).
 | ||||
|     #[cfg(unix)] | ||||
|     #[error("Failed to pass socket fd to child: {0}")] | ||||
|     FdMap(#[from] command_fds::FdMappingCollision), | ||||
| 
 | ||||
|     #[cfg(unix)] | ||||
|     #[error("Nix error: {0}")] | ||||
|     Nix(#[from] nix::Error), | ||||
| 
 | ||||
|  | @ -65,6 +67,7 @@ pub enum Error { | |||
|     #[error("Failed pre condition: {0}")] | ||||
|     FailedPreconditionError(String), | ||||
| 
 | ||||
|     #[cfg(unix)] | ||||
|     #[error("{context} error: {err}")] | ||||
|     MountError { | ||||
|         context: String, | ||||
|  |  | |||
|  | @ -32,20 +32,24 @@ | |||
| //! ```
 | ||||
| //!
 | ||||
| 
 | ||||
| use std::{collections::hash_map::DefaultHasher, fs::File, hash::Hasher, path::PathBuf}; | ||||
| #[cfg(windows)] | ||||
| use std::{fs::OpenOptions, os::windows::prelude::OpenOptionsExt}; | ||||
| #[cfg(unix)] | ||||
| use std::{ | ||||
|     collections::hash_map::DefaultHasher, | ||||
|     fs::File, | ||||
|     hash::Hasher, | ||||
|     os::unix::{io::RawFd, net::UnixListener}, | ||||
|     path::{Path, PathBuf}, | ||||
|     path::Path, | ||||
| }; | ||||
| 
 | ||||
| pub use containerd_shim_protos as protos; | ||||
| #[cfg(unix)] | ||||
| use nix::ioctl_write_ptr_bad; | ||||
| pub use protos::{ | ||||
|     shim::shim::DeleteResponse, | ||||
|     ttrpc::{context::Context, Result as TtrpcResult}, | ||||
| }; | ||||
| #[cfg(windows)] | ||||
| use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; | ||||
| 
 | ||||
| #[cfg(feature = "async")] | ||||
| pub use crate::asynchronous::*; | ||||
|  | @ -67,6 +71,7 @@ pub mod mount; | |||
| mod reap; | ||||
| #[cfg(not(feature = "async"))] | ||||
| pub mod synchronous; | ||||
| mod sys; | ||||
| pub mod util; | ||||
| 
 | ||||
| /// Generated request/response structures.
 | ||||
|  | @ -112,6 +117,7 @@ cfg_async! { | |||
|     pub use protos::ttrpc::r#async::TtrpcContext; | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| ioctl_write_ptr_bad!(ioctl_set_winsz, libc::TIOCSWINSZ, libc::winsize); | ||||
| 
 | ||||
| const TTRPC_ADDRESS: &str = "TTRPC_ADDRESS"; | ||||
|  | @ -149,6 +155,7 @@ pub struct StartOpts { | |||
| /// The shim process communicates with the containerd server through a communication channel
 | ||||
| /// created by containerd. One endpoint of the communication channel is passed to shim process
 | ||||
| /// through a file descriptor during forking, which is the fourth(3) file descriptor.
 | ||||
| #[cfg(unix)] | ||||
| const SOCKET_FD: RawFd = 3; | ||||
| 
 | ||||
| #[cfg(target_os = "linux")] | ||||
|  | @ -157,6 +164,9 @@ pub const SOCKET_ROOT: &str = "/run/containerd"; | |||
| #[cfg(target_os = "macos")] | ||||
| pub const SOCKET_ROOT: &str = "/var/run/containerd"; | ||||
| 
 | ||||
| #[cfg(target_os = "windows")] | ||||
| pub const SOCKET_ROOT: &str = r"\\.\pipe\containerd-containerd"; | ||||
| 
 | ||||
| /// Make socket path from containerd socket path, namespace and id.
 | ||||
| pub fn socket_address(socket_path: &str, namespace: &str, id: &str) -> String { | ||||
|     let path = PathBuf::from(socket_path) | ||||
|  | @ -171,9 +181,20 @@ pub fn socket_address(socket_path: &str, namespace: &str, id: &str) -> String { | |||
|         hasher.finish() | ||||
|     }; | ||||
| 
 | ||||
|     format_address(hash) | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| fn format_address(hash: u64) -> String { | ||||
|     format!(r"\\.\pipe\containerd-shim-{}-pipe", hash) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn format_address(hash: u64) -> String { | ||||
|     format!("unix://{}/{:x}.sock", SOCKET_ROOT, hash) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn parse_sockaddr(addr: &str) -> &str { | ||||
|     if let Some(addr) = addr.strip_prefix("unix://") { | ||||
|         return addr; | ||||
|  | @ -186,6 +207,26 @@ fn parse_sockaddr(addr: &str) -> &str { | |||
|     addr | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| fn start_listener(address: &str) -> std::io::Result<()> { | ||||
|     let mut opts = OpenOptions::new(); | ||||
|     opts.read(true) | ||||
|         .write(true) | ||||
|         .custom_flags(FILE_FLAG_OVERLAPPED); | ||||
|     if let Ok(f) = opts.open(address) { | ||||
|         info!("found existing named pipe: {}", address); | ||||
|         drop(f); | ||||
|         return Err(std::io::Error::new( | ||||
|             std::io::ErrorKind::AddrInUse, | ||||
|             "address already exists", | ||||
|         )); | ||||
|     } | ||||
| 
 | ||||
|     // windows starts the listener on the second invocation of the shim
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn start_listener(address: &str) -> std::io::Result<UnixListener> { | ||||
|     let path = parse_sockaddr(address); | ||||
|     // Try to create the needed directory hierarchy.
 | ||||
|  | @ -204,6 +245,7 @@ mod tests { | |||
|     use crate::start_listener; | ||||
| 
 | ||||
|     #[test] | ||||
|     #[cfg(unix)] | ||||
|     fn test_start_listener() { | ||||
|         let tmpdir = tempfile::tempdir().unwrap(); | ||||
|         let path = tmpdir.path().to_str().unwrap().to_owned(); | ||||
|  | @ -226,4 +268,16 @@ mod tests { | |||
|         let context = std::fs::read_to_string(&txt_file).unwrap(); | ||||
|         assert_eq!(context, "test"); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     #[cfg(windows)] | ||||
|     fn test_start_listener_windows() { | ||||
|         use mio::windows::NamedPipe; | ||||
| 
 | ||||
|         let named_pipe = "\\\\.\\pipe\\test-pipe-duplicate".to_string(); | ||||
| 
 | ||||
|         start_listener(&named_pipe).unwrap(); | ||||
|         let _pipe_server = NamedPipe::new(named_pipe.clone()).unwrap(); | ||||
|         start_listener(&named_pipe).expect_err("address already exists"); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -26,16 +26,20 @@ use std::{ | |||
| use log::{Metadata, Record}; | ||||
| 
 | ||||
| use crate::error::Error; | ||||
| #[cfg(windows)] | ||||
| use crate::sys::windows::NamedPipeLogger; | ||||
| 
 | ||||
| pub struct FifoLogger { | ||||
|     file: Mutex<File>, | ||||
| } | ||||
| 
 | ||||
| impl FifoLogger { | ||||
|     #[allow(dead_code)] | ||||
|     pub fn new() -> Result<FifoLogger, io::Error> { | ||||
|         Self::with_path("log") | ||||
|     } | ||||
| 
 | ||||
|     #[allow(dead_code)] | ||||
|     pub fn with_path<P: AsRef<Path>>(path: P) -> Result<FifoLogger, io::Error> { | ||||
|         let f = OpenOptions::new() | ||||
|             .write(true) | ||||
|  | @ -74,34 +78,61 @@ impl log::Log for FifoLogger { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| pub fn init(debug: bool) -> Result<(), Error> { | ||||
|     let logger = FifoLogger::new().map_err(io_error!(e, "failed to init logger"))?; | ||||
| 
 | ||||
|     let boxed_logger = Box::new(logger); | ||||
|     configure_logger(boxed_logger, debug) | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| // Containerd on windows expects the log to be a named pipe in the format of \\.\pipe\containerd-<namespace>-<id>-log
 | ||||
| // There is an assumption that there is always only one client connected which is containerd.
 | ||||
| // If there is a restart of containerd then logs during that time period will be lost.
 | ||||
| //
 | ||||
| // https://github.com/containerd/containerd/blob/v1.7.0/runtime/v2/shim_windows.go#L77
 | ||||
| // https://github.com/microsoft/hcsshim/blob/5871d0c4436f131c377655a3eb09fc9b5065f11d/cmd/containerd-shim-runhcs-v1/serve.go#L132-L137
 | ||||
| pub fn init(debug: bool, namespace: &String, id: &String) -> Result<(), Error> { | ||||
|     let logger = | ||||
|         NamedPipeLogger::new(namespace, id).map_err(io_error!(e, "failed to init logger"))?; | ||||
| 
 | ||||
|     let boxed_logger = Box::new(logger); | ||||
|     configure_logger(boxed_logger, debug) | ||||
| } | ||||
| 
 | ||||
| fn configure_logger(logger: Box<dyn log::Log>, debug: bool) -> Result<(), Error> { | ||||
|     let level = if debug { | ||||
|         log::LevelFilter::Debug | ||||
|     } else { | ||||
|         log::LevelFilter::Info | ||||
|     }; | ||||
| 
 | ||||
|     log::set_boxed_logger(Box::new(logger))?; | ||||
|     log::set_boxed_logger(logger)?; | ||||
|     log::set_max_level(level); | ||||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use log::{Log, Record}; | ||||
|     use nix::{sys::stat, unistd}; | ||||
| 
 | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_fifo_log() { | ||||
|         #[cfg(unix)] | ||||
|         use nix::{sys::stat, unistd}; | ||||
| 
 | ||||
|         let tmpdir = tempfile::tempdir().unwrap(); | ||||
|         let path = tmpdir.path().to_str().unwrap().to_owned() + "/log"; | ||||
| 
 | ||||
|         #[cfg(unix)] | ||||
|         unistd::mkfifo(Path::new(&path), stat::Mode::S_IRWXU).unwrap(); | ||||
| 
 | ||||
|         #[cfg(windows)] | ||||
|         File::create(path.clone()).unwrap(); | ||||
| 
 | ||||
|         let path1 = path.clone(); | ||||
|         let thread = std::thread::spawn(move || { | ||||
|             let _fifo = OpenOptions::new() | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| #![cfg(not(windows))] | ||||
| /* | ||||
|    Copyright The containerd Authors. | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,33 +32,37 @@ | |||
| //! ```
 | ||||
| //!
 | ||||
| 
 | ||||
| macro_rules! cfg_unix { | ||||
|     ($($item:item)*) => { | ||||
|         $( | ||||
|             #[cfg(unix)] | ||||
|             $item | ||||
|         )* | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| macro_rules! cfg_windows { | ||||
|     ($($item:item)*) => { | ||||
|         $( | ||||
|             #[cfg(windows)] | ||||
|             $item | ||||
|         )* | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| use std::{ | ||||
|     convert::TryFrom, | ||||
|     env, fs, | ||||
|     env, | ||||
|     io::Write, | ||||
|     os::unix::{fs::FileTypeExt, io::AsRawFd}, | ||||
|     path::Path, | ||||
|     process::{self, Command, Stdio}, | ||||
|     sync::{Arc, Condvar, Mutex}, | ||||
| }; | ||||
| 
 | ||||
| use command_fds::{CommandFdExt, FdMapping}; | ||||
| use libc::{SIGCHLD, SIGINT, SIGPIPE, SIGTERM}; | ||||
| pub use log::{debug, error, info, warn}; | ||||
| use nix::{ | ||||
|     errno::Errno, | ||||
|     sys::{ | ||||
|         signal::Signal, | ||||
|         wait::{self, WaitPidFlag, WaitStatus}, | ||||
|     }, | ||||
|     unistd::Pid, | ||||
| }; | ||||
| use signal_hook::iterator::Signals; | ||||
| use util::{read_address, write_address}; | ||||
| 
 | ||||
| use crate::{ | ||||
|     api::DeleteResponse, | ||||
|     args, logger, parse_sockaddr, | ||||
|     args, logger, | ||||
|     protos::{ | ||||
|         protobuf::Message, | ||||
|         shim::shim_ttrpc::{create_task, Task}, | ||||
|  | @ -66,9 +70,45 @@ use crate::{ | |||
|     }, | ||||
|     reap, socket_address, start_listener, | ||||
|     synchronous::publisher::RemotePublisher, | ||||
|     Config, Error, Result, StartOpts, SOCKET_FD, TTRPC_ADDRESS, | ||||
|     Config, Error, Result, StartOpts, TTRPC_ADDRESS, | ||||
| }; | ||||
| 
 | ||||
| cfg_unix! { | ||||
|     use std::os::unix::net::UnixListener; | ||||
|     use crate::{SOCKET_FD, parse_sockaddr}; | ||||
|     use command_fds::{CommandFdExt, FdMapping}; | ||||
|     use libc::{SIGCHLD, SIGINT, SIGPIPE, SIGTERM}; | ||||
|     use nix::{ | ||||
|         errno::Errno, | ||||
|         sys::{ | ||||
|             signal::Signal, | ||||
|             wait::{self, WaitPidFlag, WaitStatus}, | ||||
|         }, | ||||
|         unistd::Pid, | ||||
|     }; | ||||
|     use signal_hook::iterator::Signals; | ||||
|     use std::os::unix::fs::FileTypeExt; | ||||
|     use std::{convert::TryFrom, fs, path::Path}; | ||||
|     use std::os::fd::AsRawFd; | ||||
| } | ||||
| 
 | ||||
| cfg_windows! { | ||||
|     use std::{io, ptr}; | ||||
|     use windows_sys::Win32::{ | ||||
|         Foundation::{CloseHandle, HANDLE}, | ||||
|         System::{ | ||||
|             Console::SetConsoleCtrlHandler, | ||||
|             Threading::{CreateSemaphoreA, ReleaseSemaphore, }, | ||||
|         }, | ||||
|     }; | ||||
| 
 | ||||
|     static mut SEMAPHORE: HANDLE = 0 as HANDLE; | ||||
|     const MAX_SEM_COUNT: i32 = 255; | ||||
| 
 | ||||
|     use windows_sys::Win32::System::Threading::WaitForSingleObject; | ||||
|     use windows_sys::Win32::System::Threading::INFINITE; | ||||
| } | ||||
| 
 | ||||
| pub mod monitor; | ||||
| pub mod publisher; | ||||
| pub mod util; | ||||
|  | @ -158,9 +198,13 @@ where | |||
|     // Create shim instance
 | ||||
|     let mut config = opts.unwrap_or_default(); | ||||
| 
 | ||||
|     #[cfg(unix)] | ||||
|     // Setup signals
 | ||||
|     let signals = setup_signals(&config); | ||||
| 
 | ||||
|     #[cfg(windows)] | ||||
|     setup_signals(); | ||||
| 
 | ||||
|     if !config.no_sub_reaper { | ||||
|         reap::set_subreaper()?; | ||||
|     } | ||||
|  | @ -188,7 +232,10 @@ where | |||
|             Ok(()) | ||||
|         } | ||||
|         "delete" => { | ||||
|             #[cfg(unix)] | ||||
|             std::thread::spawn(move || handle_signals(signals)); | ||||
|             #[cfg(windows)] | ||||
|             std::thread::spawn(handle_signals); | ||||
|             let response = shim.delete_shim()?; | ||||
|             let stdout = std::io::stdout(); | ||||
|             let mut locked = stdout.lock(); | ||||
|  | @ -197,18 +244,28 @@ where | |||
|             Ok(()) | ||||
|         } | ||||
|         _ => { | ||||
|             #[cfg(windows)] | ||||
|             util::setup_debugger_event(); | ||||
| 
 | ||||
|             if !config.no_setup_logger { | ||||
|                 #[cfg(unix)] | ||||
|                 logger::init(flags.debug)?; | ||||
|                 #[cfg(windows)] | ||||
|                 logger::init(flags.debug, &flags.namespace, &flags.id)?; | ||||
|             } | ||||
| 
 | ||||
|             let publisher = publisher::RemotePublisher::new(&ttrpc_address)?; | ||||
|             let task = shim.create_task_service(publisher); | ||||
|             let task_service = create_task(Arc::new(Box::new(task))); | ||||
|             let mut server = Server::new().register_service(task_service); | ||||
|             server = server.add_listener(SOCKET_FD)?; | ||||
|             let mut server = create_server(flags)?; | ||||
|             server = server.register_service(task_service); | ||||
|             server.start()?; | ||||
| 
 | ||||
|             #[cfg(windows)] | ||||
|             signal_server_started(); | ||||
| 
 | ||||
|             info!("Shim successfully started, waiting for exit signal..."); | ||||
|             #[cfg(unix)] | ||||
|             std::thread::spawn(move || handle_signals(signals)); | ||||
|             shim.wait(); | ||||
| 
 | ||||
|  | @ -224,6 +281,22 @@ where | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| fn create_server(flags: args::Flags) -> Result<Server> { | ||||
|     let mut server = Server::new(); | ||||
|     let address = socket_address(&flags.address, &flags.namespace, &flags.id); | ||||
|     server = server.bind(address.as_str())?; | ||||
|     Ok(server) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn create_server(_flags: args::Flags) -> Result<Server> { | ||||
|     let mut server = Server::new(); | ||||
|     server = server.add_listener(SOCKET_FD)?; | ||||
|     Ok(server) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn setup_signals(config: &Config) -> Signals { | ||||
|     let signals = Signals::new([SIGTERM, SIGINT, SIGPIPE]).expect("new signal failed"); | ||||
|     if !config.no_reaper { | ||||
|  | @ -232,6 +305,30 @@ fn setup_signals(config: &Config) -> Signals { | |||
|     signals | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| fn setup_signals() { | ||||
|     unsafe { | ||||
|         SEMAPHORE = CreateSemaphoreA(ptr::null_mut(), 0, MAX_SEM_COUNT, ptr::null()); | ||||
|         if SEMAPHORE == 0 { | ||||
|             panic!("Failed to create semaphore: {}", io::Error::last_os_error()); | ||||
|         } | ||||
| 
 | ||||
|         if SetConsoleCtrlHandler(Some(signal_handler), 1) == 0 { | ||||
|             let e = io::Error::last_os_error(); | ||||
|             CloseHandle(SEMAPHORE); | ||||
|             SEMAPHORE = 0 as HANDLE; | ||||
|             panic!("Failed to set console handler: {}", e); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| unsafe extern "system" fn signal_handler(_: u32) -> i32 { | ||||
|     ReleaseSemaphore(SEMAPHORE, 1, ptr::null_mut()); | ||||
|     1 | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn handle_signals(mut signals: Signals) { | ||||
|     loop { | ||||
|         for sig in signals.wait() { | ||||
|  | @ -274,6 +371,16 @@ fn handle_signals(mut signals: Signals) { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| // must start on thread as waitforSingleObject puts the current thread to sleep
 | ||||
| fn handle_signals() { | ||||
|     unsafe { | ||||
|         WaitForSingleObject(SEMAPHORE, INFINITE); | ||||
|         //Windows doesn't have similiar signal like SIGCHLD
 | ||||
|         // We could implement something if required but for now
 | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| fn wait_socket_working(address: &str, interval_in_ms: u64, count: u32) -> Result<()> { | ||||
|     for _i in 0..count { | ||||
|         match Client::connect(address) { | ||||
|  | @ -292,6 +399,7 @@ fn remove_socket_silently(address: &str) { | |||
|     remove_socket(address).unwrap_or_else(|e| warn!("failed to remove file {} {:?}", address, e)) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn remove_socket(address: &str) -> Result<()> { | ||||
|     let path = parse_sockaddr(address); | ||||
|     if let Ok(md) = Path::new(path).metadata() { | ||||
|  | @ -302,6 +410,26 @@ fn remove_socket(address: &str) -> Result<()> { | |||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| fn remove_socket(address: &str) -> Result<()> { | ||||
|     use std::{ | ||||
|         fs::OpenOptions, | ||||
|         os::windows::prelude::{AsRawHandle, OpenOptionsExt}, | ||||
|     }; | ||||
| 
 | ||||
|     use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OVERLAPPED; | ||||
| 
 | ||||
|     let mut opts = OpenOptions::new(); | ||||
|     opts.read(true) | ||||
|         .write(true) | ||||
|         .custom_flags(FILE_FLAG_OVERLAPPED); | ||||
|     if let Ok(f) = opts.open(address) { | ||||
|         info!("attempting to remove existing named pipe: {}", address); | ||||
|         unsafe { CloseHandle(f.as_raw_handle() as isize) }; | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| /// Spawn is a helper func to launch shim process.
 | ||||
| /// Typically this expected to be called from `StartShim`.
 | ||||
| pub fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) -> Result<(u32, String)> { | ||||
|  | @ -311,6 +439,7 @@ pub fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) -> Result | |||
| 
 | ||||
|     // Create socket and prepare listener.
 | ||||
|     // We'll use `add_listener` when creating TTRPC server.
 | ||||
|     #[allow(clippy::let_unit_value)] | ||||
|     let listener = match start_listener(&address) { | ||||
|         Ok(l) => l, | ||||
|         Err(e) => { | ||||
|  | @ -320,6 +449,8 @@ pub fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) -> Result | |||
|                     err: e, | ||||
|                 }); | ||||
|             }; | ||||
|             // If the Address is already in use then make sure it is up and running and return the address
 | ||||
|             // This allows for running a single shim per container scenarios
 | ||||
|             if let Ok(()) = wait_socket_working(&address, 5, 200) { | ||||
|                 write_address(&address)?; | ||||
|                 return Ok((0, address)); | ||||
|  | @ -329,6 +460,18 @@ pub fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) -> Result | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     run_shim(cmd, cwd, opts, vars, listener, address) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| fn run_shim( | ||||
|     cmd: std::path::PathBuf, | ||||
|     cwd: std::path::PathBuf, | ||||
|     opts: StartOpts, | ||||
|     vars: Vec<(&str, &str)>, | ||||
|     listener: UnixListener, | ||||
|     address: String, | ||||
| ) -> Result<(u32, String)> { | ||||
|     let mut command = Command::new(cmd); | ||||
| 
 | ||||
|     command | ||||
|  | @ -363,6 +506,97 @@ pub fn spawn(opts: StartOpts, grouping: &str, vars: Vec<(&str, &str)>) -> Result | |||
|         }) | ||||
| } | ||||
| 
 | ||||
| // Activation pattern for Windows comes from the hcsshim: https://github.com/microsoft/hcsshim/blob/v0.10.0-rc.7/cmd/containerd-shim-runhcs-v1/serve.go#L57-L70
 | ||||
| // another way to do it would to create named pipe and pass it to the child process through handle inheritence but that would require duplicating
 | ||||
| // the logic in Rust's 'command' for process creation.  There is an  issue in Rust to make it simplier to specify handle inheritence and this could
 | ||||
| // be revisited once https://github.com/rust-lang/rust/issues/54760 is implemented.
 | ||||
| #[cfg(windows)] | ||||
| fn run_shim( | ||||
|     cmd: std::path::PathBuf, | ||||
|     cwd: std::path::PathBuf, | ||||
|     opts: StartOpts, | ||||
|     vars: Vec<(&str, &str)>, | ||||
|     _listener: (), | ||||
|     address: String, | ||||
| ) -> Result<(u32, String)> { | ||||
|     let mut command = Command::new(cmd); | ||||
| 
 | ||||
|     let (mut reader, writer) = os_pipe::pipe().map_err(io_error!(e, "create pipe"))?; | ||||
|     let stdio_writer = writer.try_clone().unwrap(); | ||||
| 
 | ||||
|     command | ||||
|         .current_dir(cwd) | ||||
|         .stdout(stdio_writer) | ||||
|         .stdin(Stdio::null()) | ||||
|         .stderr(Stdio::null()) | ||||
|         .args([ | ||||
|             "-namespace", | ||||
|             &opts.namespace, | ||||
|             "-id", | ||||
|             &opts.id, | ||||
|             "-address", | ||||
|             &opts.address, | ||||
|         ]); | ||||
| 
 | ||||
|     if opts.debug { | ||||
|         command.arg("-debug"); | ||||
|     } | ||||
|     command.envs(vars); | ||||
| 
 | ||||
|     disable_handle_inheritance(); | ||||
|     command | ||||
|         .spawn() | ||||
|         .map_err(io_error!(e, "spawn shim")) | ||||
|         .map(|child| { | ||||
|             // IMPORTANT: we must drop the writer and command to close up handles before we copy the reader to stderr
 | ||||
|             // AND the shim Start method must NOT write to stdout/stderr
 | ||||
|             drop(writer); | ||||
|             drop(command); | ||||
|             io::copy(&mut reader, &mut io::stderr()).unwrap(); | ||||
|             (child.id(), address) | ||||
|         }) | ||||
| } | ||||
| 
 | ||||
| // On Windows Rust currently sets the `HANDLE_FLAG_INHERIT` flag to true when using Command::spawn.
 | ||||
| // When a child process is spawned by another process (containerd) the child process inherits the parent's stdin, stdout, and stderr handles.
 | ||||
| // Due to the HANDLE_FLAG_INHERIT flag being set to true this will cause containerd to hand until the child process closes the handles.
 | ||||
| // As a workaround we can Disables inheritance on the io pipe handles.
 | ||||
| // This workaround comes from https://github.com/rust-lang/rust/issues/54760#issuecomment-1045940560
 | ||||
| #[cfg(windows)] | ||||
| fn disable_handle_inheritance() { | ||||
|     use windows_sys::Win32::{ | ||||
|         Foundation::{SetHandleInformation, HANDLE_FLAG_INHERIT}, | ||||
|         System::Console::{GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE}, | ||||
|     }; | ||||
| 
 | ||||
|     unsafe { | ||||
|         let std_err = GetStdHandle(STD_ERROR_HANDLE); | ||||
|         let std_in = GetStdHandle(STD_INPUT_HANDLE); | ||||
|         let std_out = GetStdHandle(STD_OUTPUT_HANDLE); | ||||
| 
 | ||||
|         for handle in [std_err, std_in, std_out] { | ||||
|             SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0); | ||||
|             //info!(" handle for... {:?}", handle);
 | ||||
|             //CloseHandle(handle);
 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // This closes the stdout handle which was mapped to the stderr on the first invocation of the shim.
 | ||||
| // This releases first process which will give containerd the address of the namedpipe.
 | ||||
| #[cfg(windows)] | ||||
| fn signal_server_started() { | ||||
|     use windows_sys::Win32::System::Console::{GetStdHandle, STD_OUTPUT_HANDLE}; | ||||
| 
 | ||||
|     unsafe { | ||||
|         let std_out = GetStdHandle(STD_OUTPUT_HANDLE); | ||||
| 
 | ||||
|         for handle in [std_out] { | ||||
|             CloseHandle(handle); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use std::thread; | ||||
|  |  | |||
|  | @ -25,9 +25,11 @@ use client::{ | |||
| }; | ||||
| use containerd_shim_protos as client; | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| use crate::util::connect; | ||||
| use crate::{ | ||||
|     error::Result, | ||||
|     util::{connect, convert_to_any, timestamp}, | ||||
|     util::{convert_to_any, timestamp}, | ||||
| }; | ||||
| 
 | ||||
| /// Remote publisher connects to containerd's TTRPC endpoint to publish events from shim.
 | ||||
|  | @ -47,10 +49,19 @@ impl RemotePublisher { | |||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(unix)] | ||||
|     fn connect(address: impl AsRef<str>) -> Result<Client> { | ||||
|         let fd = connect(address)?; | ||||
|         // Client::new() takes ownership of the RawFd.
 | ||||
|         Ok(Client::new(fd)) | ||||
|         Ok(Client::new_from_fd(fd)?) | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(windows)] | ||||
|     fn connect(address: impl AsRef<str>) -> Result<Client> { | ||||
|         match Client::connect(address.as_ref()) { | ||||
|             Ok(client) => Ok(client), | ||||
|             Err(e) => Err(e.into()), | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /// Publish a new event.
 | ||||
|  | @ -90,10 +101,7 @@ impl Events for RemotePublisher { | |||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use std::{ | ||||
|         os::unix::{io::AsRawFd, net::UnixListener}, | ||||
|         sync::{Arc, Barrier}, | ||||
|     }; | ||||
|     use std::sync::{Arc, Barrier}; | ||||
| 
 | ||||
|     use client::{ | ||||
|         api::{Empty, ForwardRequest}, | ||||
|  | @ -102,6 +110,8 @@ mod tests { | |||
|     use ttrpc::Server; | ||||
| 
 | ||||
|     use super::*; | ||||
|     #[cfg(windows)] | ||||
|     use crate::synchronous::wait_socket_working; | ||||
| 
 | ||||
|     struct FakeServer {} | ||||
| 
 | ||||
|  | @ -115,8 +125,12 @@ mod tests { | |||
| 
 | ||||
|     #[test] | ||||
|     fn test_connect() { | ||||
|         #[cfg(unix)] | ||||
|         let tmpdir = tempfile::tempdir().unwrap(); | ||||
|         #[cfg(unix)] | ||||
|         let path = format!("{}/socket", tmpdir.as_ref().to_str().unwrap()); | ||||
|         #[cfg(windows)] | ||||
|         let path = "\\\\.\\pipe\\test-pipe".to_string(); | ||||
|         let path1 = path.clone(); | ||||
| 
 | ||||
|         assert!(RemotePublisher::connect("a".repeat(16384)).is_err()); | ||||
|  | @ -125,17 +139,14 @@ mod tests { | |||
|         let barrier = Arc::new(Barrier::new(2)); | ||||
|         let barrier2 = barrier.clone(); | ||||
|         let thread = std::thread::spawn(move || { | ||||
|             let listener = UnixListener::bind(&path1).unwrap(); | ||||
|             listener.set_nonblocking(true).unwrap(); | ||||
|             let t = Arc::new(Box::new(FakeServer {}) as Box<dyn Events + Send + Sync>); | ||||
|             let service = client::create_events(t); | ||||
|             let mut server = Server::new() | ||||
|                 .add_listener(listener.as_raw_fd()) | ||||
|                 .unwrap() | ||||
|                 .register_service(service); | ||||
|             std::mem::forget(listener); | ||||
|             let mut server = create_server(&path1); | ||||
| 
 | ||||
|             server.start().unwrap(); | ||||
| 
 | ||||
|             #[cfg(windows)] | ||||
|             // make sure pipe is ready on windows
 | ||||
|             wait_socket_working(&path1, 5, 5).unwrap(); | ||||
| 
 | ||||
|             barrier2.wait(); | ||||
| 
 | ||||
|             barrier2.wait(); | ||||
|  | @ -153,4 +164,30 @@ mod tests { | |||
| 
 | ||||
|         thread.join().unwrap(); | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(unix)] | ||||
|     fn create_server(server_address: &String) -> Server { | ||||
|         use std::os::unix::{io::AsRawFd, net::UnixListener}; | ||||
|         let listener = UnixListener::bind(server_address).unwrap(); | ||||
|         listener.set_nonblocking(true).unwrap(); | ||||
|         let t = Arc::new(Box::new(FakeServer {}) as Box<dyn Events + Send + Sync>); | ||||
|         let service = client::create_events(t); | ||||
|         let server = Server::new() | ||||
|             .add_listener(listener.as_raw_fd()) | ||||
|             .unwrap() | ||||
|             .register_service(service); | ||||
|         std::mem::forget(listener); | ||||
|         server | ||||
|     } | ||||
| 
 | ||||
|     #[cfg(windows)] | ||||
|     fn create_server(server_address: &String) -> Server { | ||||
|         let t = Arc::new(Box::new(FakeServer {}) as Box<dyn Events + Send + Sync>); | ||||
|         let service = client::create_events(t); | ||||
| 
 | ||||
|         Server::new() | ||||
|             .bind(server_address) | ||||
|             .unwrap() | ||||
|             .register_service(service) | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -21,8 +21,10 @@ use std::{ | |||
| }; | ||||
| 
 | ||||
| use containerd_shim_protos::shim::oci::Options; | ||||
| #[cfg(unix)] | ||||
| use libc::mode_t; | ||||
| use log::warn; | ||||
| #[cfg(unix)] | ||||
| use nix::sys::stat::Mode; | ||||
| use oci_spec::runtime::Spec; | ||||
| 
 | ||||
|  | @ -117,6 +119,7 @@ pub fn read_spec_from_file(bundle: &str) -> crate::Result<Spec> { | |||
|     Spec::load(path).map_err(other_error!(e, "read spec file")) | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| pub fn mkdir(path: impl AsRef<Path>, mode: mode_t) -> crate::Result<()> { | ||||
|     let path_buf = path.as_ref().to_path_buf(); | ||||
|     if !path_buf.as_path().exists() { | ||||
|  | @ -143,3 +146,57 @@ impl Drop for HelperRemoveFile { | |||
|             .unwrap_or_else(|e| warn!("remove dir {} error: {}", &self.path, e)); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(target_os = "windows")] | ||||
| // helper to configure pause thread until signaled. Useful in attaching a debugger
 | ||||
| // https://github.com/microsoft/hcsshim/blob/v0.10.0-rc.7/cmd/containerd-shim-runhcs-v1/serve.go#L313-L315
 | ||||
| // use with https://github.com/moby/docker-signal
 | ||||
| pub(crate) fn setup_debugger_event() { | ||||
|     use std::{env, io, process}; | ||||
| 
 | ||||
|     use log::{debug, error}; | ||||
|     use windows_sys::Win32::System::Threading::{WaitForSingleObject, INFINITE}; | ||||
| 
 | ||||
|     let debugger = env::var("SHIM_DEBUGGER").unwrap_or_else(|_| "".to_string()); | ||||
|     if debugger.is_empty() { | ||||
|         return; | ||||
|     } | ||||
|     let event_name = format!("Global\\debugger-{}", process::id()); | ||||
|     debug!("Halting until signalled: {}", event_name); | ||||
|     let e = match create_event(event_name) { | ||||
|         Ok(e) => e, | ||||
|         Err(e) => { | ||||
|             error!("failed to create event for debugger: {}", e); | ||||
|             return; | ||||
|         } | ||||
|     }; | ||||
|     match unsafe { WaitForSingleObject(e, INFINITE) } { | ||||
|         0 => {} | ||||
|         _ => { | ||||
|             error!( | ||||
|                 "failed to wait for debugger event: {}", | ||||
|                 io::Error::last_os_error() | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|     debug!("signal received, continuing"); | ||||
| } | ||||
| 
 | ||||
| #[cfg(target_os = "windows")] | ||||
| fn create_event(name: String) -> crate::Result<isize> { | ||||
|     use std::{ffi::OsStr, io, os::windows::prelude::OsStrExt}; | ||||
| 
 | ||||
|     use windows_sys::Win32::System::Threading::CreateEventW; | ||||
| 
 | ||||
|     let name = OsStr::new(name.as_str()) | ||||
|         .encode_wide() | ||||
|         .chain(Some(0)) // add NULL termination
 | ||||
|         .collect::<Vec<_>>(); | ||||
| 
 | ||||
|     let result = unsafe { CreateEventW(std::ptr::null_mut(), 0, 0, name.as_ptr()) }; | ||||
|     match result { | ||||
|         0 => Err(Error::Other(io::Error::last_os_error().to_string())), | ||||
|         _ => Ok(result), | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| /* | ||||
|    Copyright The containerd Authors. | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| #[cfg(windows)] | ||||
| pub(crate) mod windows; | ||||
| #[cfg(windows)] | ||||
| pub use crate::sys::windows::NamedPipeLogger; | ||||
|  | @ -0,0 +1,17 @@ | |||
| /* | ||||
|    Copyright The containerd Authors. | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
| pub(crate) mod named_pipe_logger; | ||||
| pub use named_pipe_logger::NamedPipeLogger; | ||||
|  | @ -0,0 +1,236 @@ | |||
| /* | ||||
|    Copyright The containerd Authors. | ||||
| 
 | ||||
|    Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|    you may not use this file except in compliance with the License. | ||||
|    You may obtain a copy of the License at | ||||
| 
 | ||||
|        http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
|    Unless required by applicable law or agreed to in writing, software | ||||
|    distributed under the License is distributed on an "AS IS" BASIS, | ||||
|    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|    See the License for the specific language governing permissions and | ||||
|    limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| use std::{ | ||||
|     io::{self, Write}, | ||||
|     sync::{Arc, Mutex}, | ||||
|     thread, | ||||
| }; | ||||
| 
 | ||||
| use log::{Metadata, Record}; | ||||
| use mio::{windows::NamedPipe, Events, Interest, Poll, Token}; | ||||
| 
 | ||||
| pub struct NamedPipeLogger { | ||||
|     current_connection: Arc<Mutex<NamedPipe>>, | ||||
| } | ||||
| 
 | ||||
| impl NamedPipeLogger { | ||||
|     pub fn new(namespace: &String, id: &String) -> Result<NamedPipeLogger, io::Error> { | ||||
|         let pipe_name = format!("\\\\.\\pipe\\containerd-shim-{}-{}-log", namespace, id); | ||||
|         let mut pipe_server = NamedPipe::new(pipe_name).unwrap(); | ||||
|         let mut poll = Poll::new().unwrap(); | ||||
|         poll.registry() | ||||
|             .register( | ||||
|                 &mut pipe_server, | ||||
|                 Token(0), | ||||
|                 Interest::READABLE | Interest::WRITABLE, | ||||
|             ) | ||||
|             .unwrap(); | ||||
| 
 | ||||
|         let current_connection = Arc::new(Mutex::new(pipe_server)); | ||||
|         let server_connection = current_connection.clone(); | ||||
|         let logger = NamedPipeLogger { current_connection }; | ||||
| 
 | ||||
|         thread::spawn(move || { | ||||
|             let mut events = Events::with_capacity(128); | ||||
|             loop { | ||||
|                 poll.poll(&mut events, None).unwrap(); | ||||
| 
 | ||||
|                 for event in events.iter() { | ||||
|                     if event.is_writable() { | ||||
|                         match server_connection.lock().unwrap().connect() { | ||||
|                             Ok(()) => {} | ||||
|                             Err(ref e) if e.kind() == io::ErrorKind::Interrupted => { | ||||
|                                 // this would block just keep processing
 | ||||
|                             } | ||||
|                             Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { | ||||
|                                 // this would block just keep processing
 | ||||
|                             } | ||||
|                             Err(e) => { | ||||
|                                 panic!("Error connecting to client: {}", e); | ||||
|                             } | ||||
|                         }; | ||||
|                     } | ||||
|                     if event.is_readable() { | ||||
|                         server_connection.lock().unwrap().disconnect().unwrap(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         Ok(logger) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl log::Log for NamedPipeLogger { | ||||
|     fn enabled(&self, metadata: &Metadata) -> bool { | ||||
|         metadata.level() <= log::max_level() | ||||
|     } | ||||
| 
 | ||||
|     fn log(&self, record: &Record) { | ||||
|         if self.enabled(record.metadata()) { | ||||
|             let message = format!("[{}] {}\n", record.level(), record.args()); | ||||
| 
 | ||||
|             match self | ||||
|                 .current_connection | ||||
|                 .lock() | ||||
|                 .unwrap() | ||||
|                 .write(message.as_bytes()) | ||||
|             { | ||||
|                 Ok(_) => {} | ||||
|                 Err(ref e) if e.kind() == io::ErrorKind::Interrupted => { | ||||
|                     // this would block just keep processing
 | ||||
|                 } | ||||
|                 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { | ||||
|                     // this would block just keep processing
 | ||||
|                 } | ||||
|                 Err(e) if e.raw_os_error() == Some(536) => { | ||||
|                     // no client connected
 | ||||
|                 } | ||||
|                 Err(e) if e.raw_os_error() == Some(232) => { | ||||
|                     // client was connected but is in process of shutting down
 | ||||
|                 } | ||||
|                 Err(e) => { | ||||
|                     panic!("Error writing to client: {}", e) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn flush(&self) { | ||||
|         _ = self.current_connection.lock().unwrap().flush(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|     use std::{ | ||||
|         fs::OpenOptions, | ||||
|         io::Read, | ||||
|         os::windows::{ | ||||
|             fs::OpenOptionsExt, | ||||
|             io::{FromRawHandle, IntoRawHandle}, | ||||
|             prelude::AsRawHandle, | ||||
|         }, | ||||
|         time::Duration, | ||||
|     }; | ||||
| 
 | ||||
|     use log::{Log, Record}; | ||||
|     use mio::{windows::NamedPipe, Events, Interest, Poll, Token}; | ||||
|     use windows_sys::Win32::{ | ||||
|         Foundation::ERROR_PIPE_NOT_CONNECTED, Storage::FileSystem::FILE_FLAG_OVERLAPPED, | ||||
|     }; | ||||
| 
 | ||||
|     use super::*; | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_namedpipe_log_can_write_before_client_connected() { | ||||
|         let ns = "test".to_string(); | ||||
|         let id = "notconnected".to_string(); | ||||
|         let logger = NamedPipeLogger::new(&ns, &id).unwrap(); | ||||
| 
 | ||||
|         // test can write before a reader is connected (should succeed but the messages will be dropped)
 | ||||
|         log::set_max_level(log::LevelFilter::Info); | ||||
|         let record = Record::builder() | ||||
|             .level(log::Level::Info) | ||||
|             .line(Some(1)) | ||||
|             .file(Some("sample file")) | ||||
|             .args(format_args!("hello")) | ||||
|             .build(); | ||||
|         logger.log(&record); | ||||
|         logger.flush(); | ||||
|     } | ||||
| 
 | ||||
|     #[test] | ||||
|     fn test_namedpipe_log() { | ||||
|         use std::fs::File; | ||||
| 
 | ||||
|         let ns = "test".to_string(); | ||||
|         let id = "clients".to_string(); | ||||
|         let pipe_name = format!("\\\\.\\pipe\\containerd-shim-{}-{}-log", ns, id); | ||||
| 
 | ||||
|         let logger = NamedPipeLogger::new(&ns, &id).unwrap(); | ||||
|         let mut client = create_client(pipe_name.as_str()); | ||||
| 
 | ||||
|         log::set_max_level(log::LevelFilter::Info); | ||||
|         let record = Record::builder() | ||||
|             .level(log::Level::Info) | ||||
|             .line(Some(1)) | ||||
|             .file(Some("sample file")) | ||||
|             .args(format_args!("hello")) | ||||
|             .build(); | ||||
|         logger.log(&record); | ||||
|         logger.flush(); | ||||
| 
 | ||||
|         let buf = read_message(&mut client); | ||||
|         assert_eq!("[INFO] hello", std::str::from_utf8(&buf).unwrap()); | ||||
| 
 | ||||
|         // test that we can reconnect after a reader disconnects
 | ||||
|         // we need to get the raw handle and drop that as well to force full disconnect
 | ||||
|         // and give a few milliseconds for the disconnect to happen
 | ||||
|         println!("dropping client"); | ||||
|         let handle = client.as_raw_handle(); | ||||
|         drop(client); | ||||
|         let f = unsafe { File::from_raw_handle(handle) }; | ||||
|         drop(f); | ||||
|         std::thread::sleep(Duration::from_millis(100)); | ||||
| 
 | ||||
|         let mut client2 = create_client(pipe_name.as_str()); | ||||
|         logger.log(&record); | ||||
|         logger.flush(); | ||||
| 
 | ||||
|         read_message(&mut client2); | ||||
|     } | ||||
| 
 | ||||
|     fn read_message(client: &mut NamedPipe) -> [u8; 12] { | ||||
|         let mut poll = Poll::new().unwrap(); | ||||
|         poll.registry() | ||||
|             .register(client, Token(1), Interest::READABLE) | ||||
|             .unwrap(); | ||||
|         let mut events = Events::with_capacity(128); | ||||
|         let mut buf = [0; 12]; | ||||
|         loop { | ||||
|             poll.poll(&mut events, Some(Duration::from_millis(10))) | ||||
|                 .unwrap(); | ||||
|             match client.read(&mut buf) { | ||||
|                 Ok(0) => { | ||||
|                     panic!("Read no bytes from pipe") | ||||
|                 } | ||||
|                 Ok(_) => { | ||||
|                     break; | ||||
|                 } | ||||
|                 Err(e) if e.raw_os_error() == Some(ERROR_PIPE_NOT_CONNECTED as i32) => { | ||||
|                     panic!("not connected to the pipe"); | ||||
|                 } | ||||
|                 Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => { | ||||
|                     continue; | ||||
|                 } | ||||
|                 Err(e) => panic!("Error reading from pipe: {}", e), | ||||
|             } | ||||
|         } | ||||
|         buf | ||||
|     } | ||||
| 
 | ||||
|     fn create_client(pipe_name: &str) -> mio::windows::NamedPipe { | ||||
|         let mut opts = OpenOptions::new(); | ||||
|         opts.read(true) | ||||
|             .write(true) | ||||
|             .custom_flags(FILE_FLAG_OVERLAPPED); | ||||
|         let file = opts.open(pipe_name).unwrap(); | ||||
| 
 | ||||
|         unsafe { NamedPipe::from_raw_handle(file.into_raw_handle()) } | ||||
|     } | ||||
| } | ||||
|  | @ -14,10 +14,9 @@ | |||
|    limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| use std::{ | ||||
|     os::unix::io::RawFd, | ||||
|     time::{SystemTime, UNIX_EPOCH}, | ||||
| }; | ||||
| #[cfg(unix)] | ||||
| use std::os::unix::io::RawFd; | ||||
| use std::time::{SystemTime, UNIX_EPOCH}; | ||||
| 
 | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use time::OffsetDateTime; | ||||
|  | @ -100,6 +99,7 @@ impl From<JsonOptions> for Options { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| #[cfg(unix)] | ||||
| pub fn connect(address: impl AsRef<str>) -> Result<RawFd> { | ||||
|     use nix::{sys::socket::*, unistd::close}; | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue