proxmox-backup/proxmox-rest-server/examples/minimal-rest-server.rs
Fabian Ebner e9b9f33aee rest server: daemon: update PID file before sending MAINPID notification
There is a race upon reload, where it can happen that:
1. systemd forks off /bin/kill -HUP $MAINPID
2. Current instance forks off new one and notifies systemd with the
   new MAINPID.
3. systemd sets new MAINPID.
4. systemd receives SIGCHLD for the kill process (which is the current
   control process for the service) and reads the PID of the old
   instance from the PID file, resetting MAINPID to the PID of the old
   instance.
5. Old instance exits.
6. systemd receives SIGCHLD for the old instance, reads the PID of the
   old instance from the PID file once more. systemd sees that the
   MAINPID matches the child PID and considers the service exited.
7. systemd receivese notification from the new PID and is confused.
   The service won't get active, because the notification wasn't
   handled.

To fix it, update the PID file before sending the MAINPID
notification, similar to what a comment in systemd's
src/core/service.c suggests:
> /* Forking services may occasionally move to a new PID.
>  * As long as they update the PID file before exiting the old
>  * PID, they're fine. */
but for our Type=notify "before sending the notification" rather than
"before exiting", because otherwise, the mix-up in 4. could still
happen (although it might not actually be problematic without the
mix-up in 6., it still seems better to avoid).

Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
2022-05-12 11:53:54 +02:00

235 lines
5.6 KiB
Rust

use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
use anyhow::{bail, format_err, Error};
use http::request::Parts;
use http::HeaderMap;
use hyper::{Body, Method, Response};
use lazy_static::lazy_static;
use proxmox_router::{
list_subdirs_api_method, Router, RpcEnvironmentType, SubdirMap, UserInformation,
};
use proxmox_schema::api;
use proxmox_rest_server::{ApiConfig, AuthError, RestEnvironment, RestServer, ServerAdapter};
// Create a Dummy User information system
struct DummyUserInfo;
impl UserInformation for DummyUserInfo {
fn is_superuser(&self, _userid: &str) -> bool {
// Always return true here, so we have access to everthing
true
}
fn is_group_member(&self, _userid: &str, group: &str) -> bool {
group == "Group"
}
fn lookup_privs(&self, _userid: &str, _path: &[&str]) -> u64 {
u64::MAX
}
}
struct MinimalServer;
// implement the server adapter
impl ServerAdapter for MinimalServer {
// normally this would check and authenticate the user
fn check_auth(
&self,
_headers: &HeaderMap,
_method: &Method,
) -> Pin<
Box<
dyn Future<Output = Result<(String, Box<dyn UserInformation + Sync + Send>), AuthError>>
+ Send,
>,
> {
Box::pin(async move {
// get some global/cached userinfo
let userinfo: Box<dyn UserInformation + Sync + Send> = Box::new(DummyUserInfo);
// Do some user checks, e.g. cookie/csrf
Ok(("User".to_string(), userinfo))
})
}
// this should return the index page of the webserver
// iow. what the user browses to
fn get_index(
&self,
_env: RestEnvironment,
_parts: Parts,
) -> Pin<Box<dyn Future<Output = Response<Body>> + Send>> {
Box::pin(async move {
// build an index page
http::Response::builder()
.body("hello world".into())
.unwrap()
})
}
}
// a few examples on how to do api calls with the Router
#[api]
/// A simple ping method. returns "pong"
fn ping() -> Result<String, Error> {
Ok("pong".to_string())
}
lazy_static! {
static ref ITEM_MAP: Mutex<HashMap<String, String>> = Mutex::new(HashMap::new());
}
#[api]
/// Lists all current items
fn list_items() -> Result<Vec<String>, Error> {
Ok(ITEM_MAP.lock().unwrap().keys().cloned().collect())
}
#[api(
input: {
properties: {
name: {
type: String,
description: "The name",
},
value: {
type: String,
description: "The value",
},
},
},
)]
/// creates a new item
fn create_item(name: String, value: String) -> Result<(), Error> {
let mut map = ITEM_MAP.lock().unwrap();
if map.contains_key(&name) {
bail!("{} already exists", name);
}
map.insert(name, value);
Ok(())
}
#[api(
input: {
properties: {
name: {
type: String,
description: "The name",
},
},
},
)]
/// returns the value of an item
fn get_item(name: String) -> Result<String, Error> {
ITEM_MAP
.lock()
.unwrap()
.get(&name)
.map(|s| s.to_string())
.ok_or_else(|| format_err!("no such item '{}'", name))
}
#[api(
input: {
properties: {
name: {
type: String,
description: "The name",
},
value: {
type: String,
description: "The value",
},
},
},
)]
/// updates an item
fn update_item(name: String, value: String) -> Result<(), Error> {
if let Some(val) = ITEM_MAP.lock().unwrap().get_mut(&name) {
*val = value;
} else {
bail!("no such item '{}'", name);
}
Ok(())
}
#[api(
input: {
properties: {
name: {
type: String,
description: "The name",
},
},
},
)]
/// deletes an item
fn delete_item(name: String) -> Result<(), Error> {
if ITEM_MAP.lock().unwrap().remove(&name).is_none() {
bail!("no such item '{}'", name);
}
Ok(())
}
const ITEM_ROUTER: Router = Router::new()
.get(&API_METHOD_GET_ITEM)
.put(&API_METHOD_UPDATE_ITEM)
.delete(&API_METHOD_DELETE_ITEM);
const SUBDIRS: SubdirMap = &[
(
"items",
&Router::new()
.get(&API_METHOD_LIST_ITEMS)
.post(&API_METHOD_CREATE_ITEM)
.match_all("name", &ITEM_ROUTER),
),
("ping", &Router::new().get(&API_METHOD_PING)),
];
const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
async fn run() -> Result<(), Error> {
// we first have to configure the api environment (basedir etc.)
let config = ApiConfig::new(
"/var/tmp/",
&ROUTER,
RpcEnvironmentType::PUBLIC,
MinimalServer,
)?;
let rest_server = RestServer::new(config);
// then we have to create a daemon that listens, accepts and serves
// the api to clients
proxmox_rest_server::daemon::create_daemon(
([127, 0, 0, 1], 65000).into(),
move |listener| {
let incoming = hyper::server::conn::AddrIncoming::from_listener(listener)?;
Ok(async move {
hyper::Server::builder(incoming).serve(rest_server).await?;
Ok(())
})
},
None,
)
.await?;
Ok(())
}
fn main() -> Result<(), Error> {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(async { run().await })
}