A library for writing host-specific, single-binary configuration management and deployment tools
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

231 lines
6.1 KiB

7 years ago
7 years ago
5 years ago
7 years ago
7 years ago
7 years ago
7 years ago
5 years ago
7 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
5 years ago
7 years ago
7 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
5 years ago
7 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
5 years ago
7 years ago
7 years ago
7 years ago
5 years ago
7 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
5 years ago
7 years ago
7 years ago
5 years ago
7 years ago
  1. use std::error::Error;
  2. use std::fmt;
  3. use std::fs;
  4. use std::io;
  5. use std::ops::Deref;
  6. use std::process::Output;
  7. use std::thread::sleep;
  8. use std::time::Duration;
  9. use command_runner::{CommandRunner, SetuidCommandRunner};
  10. use resources::Resource;
  11. use symbols::file::File as FileSymbol;
  12. use symbols::{Action, OwnedSymbolAction, Symbol, SymbolAction, SymbolRunner};
  13. #[derive(Debug)]
  14. pub enum NodeJsSystemdUserServiceError<E: Error> {
  15. ActivationFailed(io::Result<Output>),
  16. ExecError(E),
  17. GenericError,
  18. }
  19. impl From<io::Error> for NodeJsSystemdUserServiceError<io::Error> {
  20. fn from(err: io::Error) -> NodeJsSystemdUserServiceError<io::Error> {
  21. NodeJsSystemdUserServiceError::ExecError(err)
  22. }
  23. }
  24. impl<E: Error> Error for NodeJsSystemdUserServiceError<E> {
  25. fn description(&self) -> &str {
  26. match self {
  27. NodeJsSystemdUserServiceError::ExecError(ref e) => e.description(),
  28. NodeJsSystemdUserServiceError::GenericError => "Generic error",
  29. NodeJsSystemdUserServiceError::ActivationFailed(_) => "Activation of service failed",
  30. }
  31. }
  32. fn cause(&self) -> Option<&dyn Error> {
  33. match self {
  34. NodeJsSystemdUserServiceError::ExecError(ref e) => Some(e),
  35. _ => None,
  36. }
  37. }
  38. }
  39. impl<E: Error> fmt::Display for NodeJsSystemdUserServiceError<E> {
  40. fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
  41. try!(write!(f, "{}", self.description()));
  42. if let NodeJsSystemdUserServiceError::ActivationFailed(Ok(ref log)) = self {
  43. try!(write!(f, ": {:?}", log));
  44. };
  45. Ok(())
  46. }
  47. }
  48. pub struct NodeJsSystemdUserService<'a, C, R>
  49. where
  50. C: Deref<Target = str>,
  51. R: CommandRunner,
  52. {
  53. service_name: &'a str,
  54. user_name: &'a str,
  55. command_runner: R,
  56. file: FileSymbol<C, String>,
  57. }
  58. impl<'a, R> NodeJsSystemdUserService<'a, String, SetuidCommandRunner<'a, R>>
  59. where
  60. R: CommandRunner,
  61. {
  62. pub fn new(
  63. home: &'a str,
  64. user_name: &'a str,
  65. service_name: &'a str,
  66. path: &'a str,
  67. command_runner: &'a R,
  68. ) -> Self {
  69. let file_path = format!(
  70. "{}/.config/systemd/user/{}.service",
  71. home.trim_end(),
  72. service_name
  73. );
  74. let port = format!("/var/tmp/{}-{}.socket", user_name, service_name);
  75. let content = format!(
  76. "[Service]
  77. Environment=NODE_ENV=production
  78. Environment=PORT={1}
  79. ExecStartPre=/bin/rm -f {1}
  80. ExecStart=/usr/bin/nodejs {0}
  81. ExecStartPost=/bin/sh -c 'sleep 1 && chmod 666 {1}'
  82. # FIXME: This only works if the nodejs path is a directory
  83. WorkingDirectory={0}
  84. Restart=always
  85. #RuntimeDirectory=service
  86. #RuntimeDirectoryMode=766
  87. [Install]
  88. WantedBy=default.target
  89. ",
  90. path, port
  91. );
  92. NodeJsSystemdUserService {
  93. service_name,
  94. user_name,
  95. command_runner: SetuidCommandRunner::new(user_name, command_runner),
  96. file: FileSymbol::new(file_path, content),
  97. }
  98. }
  99. }
  100. impl<'a, C, R> NodeJsSystemdUserService<'a, C, R>
  101. where
  102. C: Deref<Target = str>,
  103. R: CommandRunner,
  104. {
  105. fn systemctl_wait_for_dbus(&self, args: &[&str]) -> Result<String, Box<dyn Error>> {
  106. let mut tries = 5;
  107. loop {
  108. let result = try!(self.command_runner.run_with_args("systemctl", args));
  109. if !result.status.success() {
  110. let raw_stderr = try!(String::from_utf8(result.stderr));
  111. let stderr = raw_stderr.trim_end();
  112. if stderr != "Failed to connect to bus: No such file or directory" {
  113. return Err(stderr.into());
  114. }
  115. } else {
  116. return Ok(
  117. try!(String::from_utf8(result.stdout))
  118. .trim_end()
  119. .to_string(),
  120. );
  121. }
  122. tries -= 1;
  123. if tries == 0 {
  124. return Err("Gave up waiting for dbus to appear".to_string().into());
  125. }
  126. sleep(Duration::from_millis(500));
  127. }
  128. }
  129. fn check_if_service(&self) -> Result<bool, Box<dyn Error>> {
  130. loop {
  131. let active_state = try!(self.systemctl_wait_for_dbus(&[
  132. "--user",
  133. "show",
  134. "--property",
  135. "ActiveState",
  136. self.service_name
  137. ]));
  138. match active_state.as_ref() {
  139. "ActiveState=activating" => sleep(Duration::from_millis(500)),
  140. "ActiveState=active" => return Ok(true),
  141. "ActiveState=failed" => {
  142. return Err(Box::new(NodeJsSystemdUserServiceError::ActivationFailed(
  143. self.command_runner.run_with_args(
  144. "journalctl",
  145. &["--user", &format!("--user-unit={}", self.service_name)],
  146. ),
  147. )
  148. as NodeJsSystemdUserServiceError<io::Error>))
  149. }
  150. _ => return Ok(false),
  151. }
  152. }
  153. }
  154. }
  155. impl<'a, C, R> Symbol for NodeJsSystemdUserService<'a, C, R>
  156. where
  157. C: Deref<Target = str>,
  158. R: CommandRunner,
  159. {
  160. fn target_reached(&self) -> Result<bool, Box<dyn Error>> {
  161. if !(try!(self.file.target_reached())) {
  162. return Ok(false);
  163. }
  164. self.check_if_service()
  165. }
  166. fn execute(&self) -> Result<(), Box<dyn Error>> {
  167. try!(self.file.execute());
  168. try!(self.systemctl_wait_for_dbus(&["--user", "enable", self.service_name]));
  169. try!(self.systemctl_wait_for_dbus(&["--user", "restart", self.service_name]));
  170. if !(try!(self.check_if_service())) {
  171. return Err(Box::new(
  172. NodeJsSystemdUserServiceError::GenericError as NodeJsSystemdUserServiceError<io::Error>,
  173. ));
  174. }
  175. let file_name = format!("/var/tmp/{}-{}.socket", self.user_name, self.service_name);
  176. fs::metadata(&file_name)
  177. .map(|_| ())
  178. .map_err(|e| Box::new(e) as Box<dyn Error>)
  179. }
  180. fn get_prerequisites(&self) -> Vec<Resource> {
  181. let mut r = vec![Resource::new(
  182. "file",
  183. format!("/var/lib/systemd/linger/{}", self.user_name),
  184. )];
  185. r.extend(self.file.get_prerequisites().into_iter());
  186. r
  187. }
  188. fn as_action<'b>(&'b self, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b> {
  189. Box::new(SymbolAction::new(runner, self))
  190. }
  191. fn into_action<'b>(self: Box<Self>, runner: &'b dyn SymbolRunner) -> Box<dyn Action + 'b>
  192. where
  193. Self: 'b,
  194. {
  195. Box::new(OwnedSymbolAction::new(runner, *self))
  196. }
  197. }
  198. impl<'a, C, R> fmt::Display for NodeJsSystemdUserService<'a, C, R>
  199. where
  200. C: Deref<Target = str>,
  201. R: CommandRunner,
  202. {
  203. fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
  204. write!(
  205. f,
  206. "Systemd Node.js user service unit for {}",
  207. self.service_name
  208. )
  209. }
  210. }