A cross-platform service manager
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.

serviceman.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. //go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
  2. // main runs the things and does the stuff
  3. package main
  4. import (
  5. "encoding/json"
  6. "flag"
  7. "fmt"
  8. "io/ioutil"
  9. "os"
  10. "os/exec"
  11. "os/user"
  12. "path/filepath"
  13. "strings"
  14. "time"
  15. "unicode/utf8"
  16. "git.rootprojects.org/root/go-serviceman/manager"
  17. "git.rootprojects.org/root/go-serviceman/runner"
  18. "git.rootprojects.org/root/go-serviceman/service"
  19. )
  20. var GitRev = "000000000"
  21. var GitVersion = "v0.5.3-pre+dirty"
  22. var GitTimestamp = time.Now().Format(time.RFC3339)
  23. func usage() {
  24. fmt.Println("Usage:")
  25. fmt.Println("\tserviceman <command> --help")
  26. fmt.Println("\tserviceman add ./foo-app -- --foo-arg")
  27. fmt.Println("\tserviceman run --config ./foo-app.json")
  28. fmt.Println("\tserviceman list --all")
  29. fmt.Println("\tserviceman start <name>")
  30. fmt.Println("\tserviceman stop <name>")
  31. }
  32. func main() {
  33. if len(os.Args) < 2 {
  34. fmt.Fprintf(os.Stderr, "Too few arguments: %s\n", strings.Join(os.Args, " "))
  35. usage()
  36. os.Exit(1)
  37. }
  38. top := os.Args[1]
  39. os.Args = append(os.Args[:1], os.Args[2:]...)
  40. switch top {
  41. case "version":
  42. fmt.Println(GitVersion, GitTimestamp, GitRev)
  43. case "run":
  44. run()
  45. case "add":
  46. add()
  47. case "start":
  48. start()
  49. case "stop":
  50. stop()
  51. case "list":
  52. list()
  53. default:
  54. fmt.Fprintf(os.Stderr, "Unknown argument %s\n", top)
  55. usage()
  56. os.Exit(1)
  57. }
  58. }
  59. func add() {
  60. conf := &service.Service{
  61. Restart: true,
  62. }
  63. force := false
  64. forUser := false
  65. forSystem := false
  66. dryrun := false
  67. pathEnv := ""
  68. flag.StringVar(&conf.Title, "title", "", "a human-friendly name for the service")
  69. flag.StringVar(&conf.Desc, "desc", "", "a human-friendly description of the service (ex: Foo App)")
  70. flag.StringVar(&conf.Name, "name", "", "a computer-friendly name for the service (ex: foo-app)")
  71. flag.StringVar(&conf.URL, "url", "", "the documentation on home page of the service")
  72. flag.StringVar(&conf.Workdir, "workdir", "", "the directory in which the service should be started (if supported)")
  73. flag.StringVar(&conf.ReverseDNS, "rdns", "", "a plist-friendly Reverse DNS name for launchctl (ex: com.example.foo-app)")
  74. flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
  75. flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
  76. flag.BoolVar(&force, "force", false, "if the interpreter or executable doesn't exist, or things don't make sense, try anyway")
  77. flag.StringVar(&pathEnv, "path", "", "set the path for the resulting systemd service")
  78. flag.StringVar(&conf.User, "username", "", "run the service as this user")
  79. flag.StringVar(&conf.Group, "groupname", "", "run the service as this group")
  80. flag.BoolVar(&conf.PrivilegedPorts, "cap-net-bind", false, "this service should have access to privileged ports")
  81. flag.BoolVar(&dryrun, "dryrun", false, "output the service file without modifying anything on disk")
  82. flag.Parse()
  83. flagargs := flag.Args()
  84. // You must have something to run, duh
  85. n := len(flagargs)
  86. if 0 == n {
  87. fmt.Println("Usage: serviceman add ./foo-app --foo-arg")
  88. os.Exit(2)
  89. return
  90. }
  91. if forUser && forSystem {
  92. fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
  93. os.Exit(1)
  94. return
  95. }
  96. // There are three groups of flags
  97. // serviceman --flag1 arg1 non-flag-arg --child1 -- --raw1 -- --raw2
  98. // serviceman --flag1 arg1 // these belong to serviceman
  99. // non-flag-arg --child1 // these will be interpretted
  100. // -- // separator
  101. // --raw1 -- --raw2 // after the separater (including additional separators) will be ignored
  102. rawargs := []string{}
  103. for i := range flagargs {
  104. if "--" == flagargs[i] {
  105. if len(flagargs) > i+1 {
  106. rawargs = flagargs[i+1:]
  107. }
  108. flagargs = flagargs[:i]
  109. break
  110. }
  111. }
  112. // Assumptions
  113. ass := []string{}
  114. if forUser {
  115. conf.System = false
  116. } else if forSystem {
  117. conf.System = true
  118. } else {
  119. conf.System = manager.IsPrivileged()
  120. if conf.System {
  121. ass = append(ass, "# Because you're a privileged user")
  122. ass = append(ass, " --system")
  123. ass = append(ass, "")
  124. } else {
  125. ass = append(ass, "# Because you're a unprivileged user")
  126. ass = append(ass, " --user")
  127. ass = append(ass, "")
  128. }
  129. }
  130. if "" == conf.Workdir {
  131. dir, _ := os.Getwd()
  132. conf.Workdir = dir
  133. ass = append(ass, "# Because this is your current working directory")
  134. ass = append(ass, fmt.Sprintf(" --workdir %s", conf.Workdir))
  135. ass = append(ass, "")
  136. }
  137. if "" == conf.Name {
  138. name, _ := os.Getwd()
  139. base := filepath.Base(name)
  140. ext := filepath.Ext(base)
  141. n := (len(base) - len(ext))
  142. name = base[:n]
  143. if "" == name {
  144. name = base
  145. }
  146. conf.Name = name
  147. ass = append(ass, "# Because this is the name of your current working directory")
  148. ass = append(ass, fmt.Sprintf(" --name %s", conf.Name))
  149. ass = append(ass, "")
  150. }
  151. if "" != pathEnv {
  152. conf.Envs = make(map[string]string)
  153. conf.Envs["PATH"] = pathEnv
  154. }
  155. exepath, err := findExec(flagargs[0], force)
  156. if nil != err {
  157. fmt.Fprintf(os.Stderr, "%s\n", err)
  158. os.Exit(3)
  159. return
  160. }
  161. flagargs[0] = exepath
  162. exeargs, err := testScript(flagargs[0], force)
  163. if nil != err {
  164. fmt.Fprintf(os.Stderr, "%s\n", err)
  165. os.Exit(3)
  166. return
  167. }
  168. flagargs = append(exeargs, flagargs...)
  169. // TODO
  170. for i := range flagargs {
  171. arg := flagargs[i]
  172. arg = filepath.ToSlash(arg)
  173. // Paths considered to be anything starting with ./, .\, /, \, C:
  174. if "." == arg || strings.Contains(arg, "/") {
  175. //if "." == arg || (len(arg) >= 2 && "./" == arg[:2] || '/' == arg[0] || "C:" == strings.ToUpper(arg[:1])) {
  176. var err error
  177. arg, err = filepath.Abs(arg)
  178. if nil == err {
  179. _, err = os.Stat(arg)
  180. }
  181. if nil != err {
  182. fmt.Printf("%q appears to be a file path, but %q could not be read\n", flagargs[i], arg)
  183. if !force {
  184. os.Exit(7)
  185. return
  186. }
  187. continue
  188. }
  189. if '\\' != os.PathSeparator {
  190. // Convert paths back to .\ for Windows
  191. arg = filepath.FromSlash(arg)
  192. }
  193. // Lookin' good
  194. flagargs[i] = arg
  195. }
  196. }
  197. // We won't bother with Interpreter here
  198. // (it's really just for documentation),
  199. // but we will add any and all unchecked args to the full slice
  200. conf.Exec = flagargs[0]
  201. conf.Argv = append(flagargs[1:], rawargs...)
  202. // TODO update docs: go to the work directory
  203. // TODO test with "npm start"
  204. conf.NormalizeWithoutPath()
  205. //fmt.Printf("\n%#v\n\n", conf)
  206. if conf.System && !manager.IsPrivileged() {
  207. fmt.Fprintf(os.Stderr, "Warning: You may need to use 'sudo' to add %q as a privileged system service.\n", conf.Name)
  208. }
  209. if len(ass) > 0 {
  210. fmt.Printf("OPTIONS: Making some assumptions...\n\n")
  211. for i := range ass {
  212. fmt.Println("\t" + ass[i])
  213. }
  214. }
  215. // Find who this is running as
  216. // And pretty print the command to run
  217. runAs := conf.User
  218. var wasflag bool
  219. fmt.Printf("COMMAND: Service %q will be run like this (more or less):\n\n", conf.Title)
  220. if conf.System {
  221. if "" == runAs {
  222. runAs = "root"
  223. }
  224. fmt.Printf("\t# Starts on system boot, as %q\n", runAs)
  225. } else {
  226. u, _ := user.Current()
  227. runAs = u.Name
  228. if "" == runAs {
  229. runAs = u.Username
  230. }
  231. fmt.Printf("\t# Starts as %q, when %q logs in\n", runAs, u.Username)
  232. }
  233. //fmt.Printf("\tpushd %s\n", conf.Workdir)
  234. fmt.Printf("\t%s\n", conf.Exec)
  235. for i := range conf.Argv {
  236. arg := conf.Argv[i]
  237. if '-' == arg[0] {
  238. if wasflag {
  239. fmt.Println()
  240. }
  241. wasflag = true
  242. fmt.Printf("\t\t%s", arg)
  243. } else {
  244. if wasflag {
  245. fmt.Printf(" %s\n", arg)
  246. } else {
  247. fmt.Printf("\t\t%s\n", arg)
  248. }
  249. wasflag = false
  250. }
  251. }
  252. if wasflag {
  253. fmt.Println()
  254. }
  255. fmt.Println()
  256. // TODO output config without installing
  257. if dryrun {
  258. b, err := manager.Render(conf)
  259. if nil != err {
  260. fmt.Fprintf(os.Stderr, "Error rendering: %s\n", err)
  261. os.Exit(10)
  262. }
  263. fmt.Println(string(b))
  264. return
  265. }
  266. fmt.Printf("LAUNCHER: ")
  267. servicetype, err := manager.Install(conf)
  268. if nil != err {
  269. fmt.Fprintf(os.Stderr, "%s\n", err)
  270. os.Exit(500)
  271. return
  272. }
  273. fmt.Printf("LOGS: ")
  274. printLogMessage(conf)
  275. fmt.Println()
  276. servicemode := "USER MODE"
  277. if conf.System {
  278. servicemode = "SYSTEM"
  279. }
  280. fmt.Printf(
  281. "SUCCESS:\n\n\t%q started as a %s %s service, running as %q\n",
  282. conf.Name,
  283. servicetype,
  284. servicemode,
  285. runAs,
  286. )
  287. fmt.Println()
  288. }
  289. func list() {
  290. var verbose bool
  291. forUser := false
  292. forSystem := false
  293. flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
  294. flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
  295. flag.BoolVar(&verbose, "all", false, "show all services (even those not managed by serviceman)")
  296. flag.Parse()
  297. if forUser && forSystem {
  298. fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
  299. os.Exit(1)
  300. return
  301. }
  302. conf := &service.Service{}
  303. if forUser {
  304. conf.System = false
  305. } else if forSystem {
  306. conf.System = true
  307. } else {
  308. conf.System = manager.IsPrivileged()
  309. }
  310. // Pretty much just for HomeDir
  311. conf.NormalizeWithoutPath()
  312. managed, others, errs := manager.List(conf)
  313. for i := range errs {
  314. fmt.Fprintf(os.Stderr, "possible error: %s\n", errs[i])
  315. }
  316. if len(errs) > 0 {
  317. fmt.Fprintf(os.Stderr, "\n")
  318. }
  319. fmt.Printf("serviceman-managed services:\n\n")
  320. for i := range managed {
  321. fmt.Println("\t" + managed[i])
  322. }
  323. if 0 == len(managed) {
  324. fmt.Println("\t(none)")
  325. }
  326. fmt.Println("")
  327. if verbose {
  328. fmt.Printf("other services:\n\n")
  329. for i := range others {
  330. fmt.Println("\t" + others[i])
  331. }
  332. if 0 == len(others) {
  333. fmt.Println("\t(none)")
  334. }
  335. fmt.Println("")
  336. }
  337. }
  338. func findExec(exe string, force bool) (string, error) {
  339. // ex: node => /usr/local/bin/node
  340. // ex: ./demo.js => /Users/aj/project/demo.js
  341. exepath, err := exec.LookPath(exe)
  342. if nil != err {
  343. var msg string
  344. if strings.Contains(filepath.ToSlash(exe), "/") {
  345. if _, err := os.Stat(exe); err != nil {
  346. msg = fmt.Sprintf("Error: '%s' could not be found in PATH or working directory.\n", exe)
  347. } else {
  348. msg = fmt.Sprintf("Error: '%s' is not an executable.\nYou may be able to fix that. Try running this:\n\tchmod a+x %s\n", exe, exe)
  349. }
  350. } else {
  351. if _, err := os.Stat(exe); err != nil {
  352. msg = fmt.Sprintf("Error: '%s' could not be found in PATH", exe)
  353. } else {
  354. msg = fmt.Sprintf("Error: '%s' could not be found in PATH, did you mean './%s'?\n", exe, exe)
  355. }
  356. }
  357. if !force {
  358. return "", fmt.Errorf(msg)
  359. }
  360. fmt.Fprintf(os.Stderr, "%s\n", msg)
  361. return exe, nil
  362. }
  363. // ex: \Users\aj\project\demo.js => /Users/aj/project/demo.js
  364. // Can't have an error here when lookpath succeeded
  365. exepath, _ = filepath.Abs(filepath.ToSlash(exepath))
  366. return exepath, nil
  367. }
  368. func testScript(exepath string, force bool) ([]string, error) {
  369. f, err := os.Open(exepath)
  370. b := make([]byte, 256)
  371. if nil == err {
  372. _, err = f.Read(b)
  373. }
  374. if nil != err || len(b) < len("#!/x") {
  375. msg := fmt.Sprintf("Error when testing if '%s' is a binary or script: could not read file: %s\n", exepath, err)
  376. if !force {
  377. return nil, fmt.Errorf(msg)
  378. }
  379. fmt.Fprintf(os.Stderr, "%s\n", msg)
  380. return nil, nil
  381. }
  382. // Nott sure if this is more readable and idiomatic as if else or switch
  383. // However, the order matters
  384. switch {
  385. case utf8.Valid(b):
  386. // Looks like an executable script
  387. if "#!/" == string(b[:3]) {
  388. break
  389. }
  390. msg := fmt.Sprintf("Error: %q looks like a script, but we don't know the interpreter.\nYou can probably fix this by...\n"+
  391. "\tExplicitly naming the interpreter (ex: 'python my-script.py' instead of just 'my-script.py')\n"+
  392. "\tPlacing a hashbang at the top of the script (ex: '#!/usr/bin/env python')", exepath)
  393. if !force {
  394. return nil, fmt.Errorf(msg)
  395. }
  396. return nil, nil
  397. case "#!/" != string(b[:3]):
  398. // Looks like a normal binary
  399. return nil, nil
  400. default:
  401. // Looks like a corrupt script file
  402. msg := "Error: It looks like you've specified a corrupt script file."
  403. if !force {
  404. return nil, fmt.Errorf(msg)
  405. }
  406. return nil, nil
  407. }
  408. // Deal with #!/whatever
  409. // Get that first line
  410. // "#!/usr/bin/env node" => ["/usr/bin/env", "node"]
  411. // "#!/usr/bin/node --harmony => ["/usr/bin/node", "--harmony"]
  412. s := string(b[2:]) // strip leading #!
  413. s = strings.Split(strings.Replace(s, "\r\n", "\n", -1), "\n")[0]
  414. allargs := strings.Split(strings.TrimSpace(s), " ")
  415. args := []string{}
  416. for i := range allargs {
  417. arg := strings.TrimSpace(allargs[i])
  418. if "" != arg {
  419. args = append(args, arg)
  420. }
  421. }
  422. if strings.HasSuffix(args[0], "/env") && len(args) > 1 {
  423. // TODO warn that "env" is probably not an executable if 1 = len(args)?
  424. args = args[1:]
  425. }
  426. exepath, err = findExec(args[0], force)
  427. if nil != err {
  428. return nil, err
  429. }
  430. args[0] = exepath
  431. return args, nil
  432. }
  433. func start() {
  434. forUser := false
  435. forSystem := false
  436. flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
  437. flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
  438. flag.Parse()
  439. args := flag.Args()
  440. if 1 != len(args) {
  441. fmt.Println("Usage: serviceman start <name>")
  442. os.Exit(1)
  443. }
  444. if forUser && forSystem {
  445. fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
  446. os.Exit(1)
  447. return
  448. }
  449. conf := &service.Service{
  450. Name: args[0],
  451. Restart: false,
  452. }
  453. if forUser {
  454. conf.System = false
  455. } else if forSystem {
  456. conf.System = true
  457. } else {
  458. conf.System = manager.IsPrivileged()
  459. }
  460. conf.NormalizeWithoutPath()
  461. err := manager.Start(conf)
  462. if nil != err {
  463. fmt.Fprintf(os.Stderr, "%s\n", err)
  464. os.Exit(500)
  465. return
  466. }
  467. }
  468. func stop() {
  469. forUser := false
  470. forSystem := false
  471. flag.BoolVar(&forSystem, "system", false, "attempt to add system service as an unprivileged/unelevated user")
  472. flag.BoolVar(&forUser, "user", false, "add user space / user mode service even when admin/root/sudo/elevated")
  473. flag.Parse()
  474. args := flag.Args()
  475. if 1 != len(args) {
  476. fmt.Println("Usage: serviceman stop <name>")
  477. os.Exit(1)
  478. }
  479. if forUser && forSystem {
  480. fmt.Println("Pfff! You can't --user AND --system! What are you trying to pull?")
  481. os.Exit(1)
  482. return
  483. }
  484. conf := &service.Service{
  485. Name: args[0],
  486. Restart: false,
  487. }
  488. if forUser {
  489. conf.System = false
  490. } else if forSystem {
  491. conf.System = true
  492. } else {
  493. conf.System = manager.IsPrivileged()
  494. }
  495. conf.NormalizeWithoutPath()
  496. if err := manager.Stop(conf); nil != err {
  497. fmt.Println(err)
  498. os.Exit(127)
  499. }
  500. }
  501. func run() {
  502. var confpath string
  503. var daemonize bool
  504. flag.StringVar(&confpath, "config", "", "path to a config file to run")
  505. flag.BoolVar(&daemonize, "daemon", false, "spawn a child process that lives in the background, and exit")
  506. flag.Parse()
  507. if "" == confpath {
  508. fmt.Fprintf(os.Stderr, "%s\n", strings.Join(flag.Args(), " "))
  509. fmt.Fprintf(os.Stderr, "--config /path/to/config.json is required\n")
  510. usage()
  511. os.Exit(1)
  512. }
  513. b, err := ioutil.ReadFile(confpath)
  514. if nil != err {
  515. fmt.Fprintf(os.Stderr, "Couldn't read config file: %s\n", err)
  516. os.Exit(400)
  517. }
  518. s := &service.Service{}
  519. err = json.Unmarshal(b, s)
  520. if nil != err {
  521. fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err)
  522. os.Exit(400)
  523. }
  524. m := map[string]interface{}{}
  525. err = json.Unmarshal(b, &m)
  526. if nil != err {
  527. fmt.Fprintf(os.Stderr, "Couldn't JSON parse config file: %s\n", err)
  528. os.Exit(400)
  529. }
  530. // default Restart to true
  531. if _, ok := m["restart"]; !ok {
  532. s.Restart = true
  533. }
  534. if "" == s.Exec {
  535. fmt.Fprintf(os.Stderr, "Missing exec\n")
  536. os.Exit(400)
  537. }
  538. force := false
  539. s.Normalize(force)
  540. fmt.Printf("All output will be directed to the logs at:\n\t%s\n", s.Logdir)
  541. err = os.MkdirAll(s.Logdir, 0755)
  542. if nil != err {
  543. fmt.Fprintf(os.Stderr, "%s\n", err)
  544. return
  545. }
  546. if !daemonize {
  547. //fmt.Fprintf(os.Stdout, "Running %s %s %s\n", s.Interpreter, s.Exec, strings.Join(s.Argv, " "))
  548. if err := runner.Start(s); nil != err {
  549. fmt.Println("Error:", err)
  550. }
  551. return
  552. }
  553. manager.Run(os.Args[0], "run", "--config", confpath)
  554. }