subservice.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. package subservice
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "log"
  6. "net/http"
  7. "net/url"
  8. "os"
  9. "os/exec"
  10. "path/filepath"
  11. "runtime"
  12. "sort"
  13. "strconv"
  14. "strings"
  15. "time"
  16. "imuslab.com/arozos/mod/info/logger"
  17. modules "imuslab.com/arozos/mod/modules"
  18. "imuslab.com/arozos/mod/network/reverseproxy"
  19. "imuslab.com/arozos/mod/network/websocketproxy"
  20. user "imuslab.com/arozos/mod/user"
  21. )
  22. /*
  23. ArOZ Online System - Dynamic Subsystem loading services
  24. author: tobychui
  25. This module load in ArOZ Online Subservice using authorized reverse proxy channel.
  26. Please see the demo subservice module for more information on implementing a subservice module.
  27. */
  28. type SubService struct {
  29. Port int //Port that this subservice use
  30. ServiceDir string //The directory where the service is located
  31. Path string //Path that this subservice is located
  32. RpEndpoint string //Reverse Proxy Endpoint
  33. ProxyHandler *reverseproxy.ReverseProxy //Reverse Proxy Object
  34. Info modules.ModuleInfo //Module information for this subservice
  35. Process *exec.Cmd //The CMD runtime object of the process
  36. }
  37. type SubServiceRouter struct {
  38. ReservePaths []string
  39. RunningSubService []SubService
  40. BasePort int
  41. listenPort int
  42. userHandler *user.UserHandler
  43. moduleHandler *modules.ModuleHandler
  44. logger *logger.Logger
  45. }
  46. func NewSubServiceRouter(ReservePaths []string, basePort int, userHandler *user.UserHandler, moduleHandler *modules.ModuleHandler, parentPort int) *SubServiceRouter {
  47. //Create a service logger
  48. thisLogger, _ := logger.NewLogger("subserv", "system/logs/subservice", true)
  49. return &SubServiceRouter{
  50. ReservePaths: ReservePaths,
  51. RunningSubService: []SubService{},
  52. BasePort: basePort,
  53. listenPort: parentPort,
  54. userHandler: userHandler,
  55. moduleHandler: moduleHandler,
  56. logger: thisLogger,
  57. }
  58. }
  59. // Load and start all the subservices inside this rootpath
  60. func (sr *SubServiceRouter) LoadSubservicesFromRootPath(rootpath string) {
  61. scanningPath := filepath.ToSlash(filepath.Clean(rootpath)) + "/*"
  62. subservices, _ := filepath.Glob(scanningPath)
  63. for _, servicePath := range subservices {
  64. if !fileExists(servicePath + "/.disabled") {
  65. //Only enable module with no suspended config file
  66. err := sr.Launch(servicePath, true)
  67. if err != nil {
  68. sr.logger.PrintAndLog("Subservice", "Failed to start subservice: "+filepath.Base(servicePath)+" "+err.Error(), err)
  69. //log.Println(err)
  70. }
  71. }
  72. }
  73. }
  74. func (sr *SubServiceRouter) Launch(servicePath string, startupMode bool) error {
  75. //Get the executable name from its path
  76. servicePath = filepath.ToSlash(servicePath)
  77. binaryname := filepath.Base(servicePath)
  78. serviceRoot := filepath.Base(servicePath)
  79. binaryExecPath := filepath.ToSlash(binaryname)
  80. if runtime.GOOS == "windows" {
  81. binaryExecPath = binaryExecPath + ".exe"
  82. } else {
  83. binaryExecPath = binaryExecPath + "_" + runtime.GOOS + "_" + runtime.GOARCH
  84. }
  85. //Check if startscript exists. If no, try to launch the binaries
  86. if fileExists(servicePath + "/.startscript") {
  87. //Launch from start.bat or start.sh
  88. if !(fileExists(servicePath+"/start.sh") || fileExists(servicePath+"/start.bat")) {
  89. //log.Println("Failed to load subservice: " + serviceRoot + ", .startscript flag is TRUE but no start script found")
  90. return errors.New(".startscript flag is TRUE but no start script found")
  91. }
  92. startScriptName := "start.sh"
  93. if runtime.GOOS == "windows" {
  94. startScriptName = "start.bat"
  95. }
  96. binaryExecPath = startScriptName
  97. } else {
  98. //No startscript defined. Start from binary files if exists
  99. if runtime.GOOS == "windows" && !fileExists(servicePath+"/"+binaryExecPath) {
  100. if startupMode {
  101. //log.Println("Failed to load subservice: " + serviceRoot)
  102. return errors.New("Subservice executable not exists " + servicePath + "/" + binaryExecPath + ". Skipping this service")
  103. } else {
  104. return errors.New("Subservice executable " + servicePath + "/" + binaryExecPath + ". Skipping this service")
  105. }
  106. } else if runtime.GOOS == "linux" {
  107. //Check if service installed using which
  108. cmd := exec.Command("which", serviceRoot)
  109. searchResults, _ := cmd.CombinedOutput()
  110. if len(strings.TrimSpace(string(searchResults))) == 0 {
  111. //This is not installed. Check if it exists as a binary (aka ./myservice)
  112. if !fileExists(servicePath + "/" + binaryExecPath) {
  113. if startupMode {
  114. //log.Println("Package not installed. " + serviceRoot)
  115. return errors.New("Package not installed")
  116. } else {
  117. return errors.New("Package not installed")
  118. }
  119. }
  120. }
  121. } else if runtime.GOOS == "darwin" {
  122. //Skip the whereis approach that linux use
  123. if !fileExists(servicePath + "/" + binaryExecPath) {
  124. return errors.New("Subservice executable not exists " + servicePath + "/" + binaryExecPath + ". Skipping this service")
  125. }
  126. }
  127. }
  128. //Check if the suspend file exists. If yes, clear it
  129. if fileExists(servicePath + "/.disabled") {
  130. os.Remove(servicePath + "/.disabled")
  131. }
  132. //Check if there are config files that replace the -info tag. If yes, use it instead.
  133. out := []byte{}
  134. if fileExists(servicePath + "/moduleInfo.json") {
  135. launchConfig, err := os.ReadFile(servicePath + "/moduleInfo.json")
  136. if err != nil {
  137. if startupMode {
  138. log.Fatal("Failed to read moduleInfo.json: "+binaryname, err)
  139. } else {
  140. return errors.New("Failed to read moduleInfo.json: " + binaryname)
  141. }
  142. }
  143. out = launchConfig
  144. } else {
  145. infocmd := exec.Command(servicePath+"/"+binaryExecPath, "-info")
  146. launchConfig, err := infocmd.CombinedOutput()
  147. if err != nil {
  148. sr.logger.PrintAndLog("Subservice", "Missing module startup info for "+servicePath, errors.New("Startup flag -info return no JSON string and moduleInfo.json does not exists for "+servicePath))
  149. if startupMode {
  150. log.Fatal("Unable to start service: "+binaryname, err)
  151. } else {
  152. return errors.New("Unable to start service: " + binaryname)
  153. }
  154. }
  155. out = launchConfig
  156. }
  157. //Clean the module info and append it into the module list
  158. serviceLaunchInfo := strings.TrimSpace(string(out))
  159. thisModuleInfo := modules.ModuleInfo{}
  160. err := json.Unmarshal([]byte(serviceLaunchInfo), &thisModuleInfo)
  161. if err != nil {
  162. if startupMode {
  163. log.Fatal("Failed to load subservice: "+serviceRoot+"\n", err.Error())
  164. } else {
  165. return errors.New("Failed to load subservice: " + serviceRoot)
  166. }
  167. }
  168. var thisSubService SubService
  169. if fileExists(servicePath + "/.noproxy") {
  170. //Adaptive mode. This is designed for modules that do not designed with ArOZ Online in mind.
  171. //Ignore proxy setup and startup the application
  172. absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
  173. if fileExists(servicePath + "/.startscript") {
  174. initPath := servicePath + "/start.sh"
  175. if runtime.GOOS == "windows" {
  176. initPath = servicePath + "/start.bat"
  177. }
  178. if !fileExists(initPath) {
  179. if startupMode {
  180. log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
  181. } else {
  182. return errors.New("start.sh not found. Unable to startup service " + serviceRoot)
  183. }
  184. }
  185. absolutePath, _ = filepath.Abs(initPath)
  186. }
  187. cmd := exec.Command(absolutePath)
  188. cmd.Stdout = os.Stdout
  189. cmd.Stderr = os.Stderr
  190. cmd.Dir = filepath.ToSlash(servicePath + "/")
  191. //Spawn a new go routine to run this subservice
  192. go func(cmdObject *exec.Cmd) {
  193. if err := cmd.Start(); err != nil {
  194. panic(err)
  195. }
  196. }(cmd)
  197. //Create the servie object
  198. thisSubService = SubService{
  199. Path: binaryExecPath,
  200. Info: thisModuleInfo,
  201. ServiceDir: serviceRoot,
  202. Process: cmd,
  203. }
  204. sr.logger.PrintAndLog("Subservice", "Starting service "+serviceRoot+" in compatibility mode", nil)
  205. } else {
  206. //Create a proxy for this service
  207. //Get proxy endpoint from startDir dir
  208. rProxyEndpoint := filepath.Dir(thisModuleInfo.StartDir)
  209. //Check if this path is reversed
  210. if stringInSlice(rProxyEndpoint, sr.ReservePaths) || rProxyEndpoint == "" {
  211. if startupMode {
  212. log.Fatal(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
  213. } else {
  214. return errors.New(serviceRoot + " service try to request system reserve path as Reverse Proxy endpoint.")
  215. }
  216. }
  217. //Assign a port for this subservice
  218. thisServicePort := sr.GetNextUsablePort()
  219. //Run the subservice with the given port
  220. absolutePath, _ := filepath.Abs(servicePath + "/" + binaryExecPath)
  221. if fileExists(servicePath + "/.startscript") {
  222. initPath := servicePath + "/start.sh"
  223. if runtime.GOOS == "windows" {
  224. initPath = servicePath + "/start.bat"
  225. }
  226. if !fileExists(initPath) {
  227. if startupMode {
  228. log.Fatal("start.sh not found. Unable to startup service " + serviceRoot)
  229. } else {
  230. return errors.New(serviceRoot + "start.sh not found. Unable to startup service " + serviceRoot)
  231. }
  232. }
  233. absolutePath, _ = filepath.Abs(initPath)
  234. }
  235. servicePort := ":" + intToString(thisServicePort)
  236. if fileExists(filepath.Join(servicePath, "/.intport")) {
  237. servicePort = intToString(thisServicePort)
  238. }
  239. cmd := exec.Command(absolutePath, "-port", servicePort, "-rpt", "http://localhost:"+intToString(sr.listenPort)+"/api/ajgi/interface")
  240. cmd.Stdout = os.Stdout
  241. cmd.Stderr = os.Stderr
  242. cmd.Dir = filepath.ToSlash(servicePath + "/")
  243. //log.Println(cmd.Dir,binaryExecPath)
  244. //Spawn a new go routine to run this subservice
  245. go func(cmd *exec.Cmd) {
  246. if err := cmd.Start(); err != nil {
  247. panic(err)
  248. }
  249. }(cmd)
  250. //Create a subservice object for this subservice
  251. thisSubService = SubService{
  252. Port: thisServicePort,
  253. Path: binaryExecPath,
  254. ServiceDir: serviceRoot,
  255. RpEndpoint: rProxyEndpoint,
  256. Info: thisModuleInfo,
  257. Process: cmd,
  258. }
  259. sr.logger.PrintAndLog("Subservice", "Subservice Registered: "+thisModuleInfo.Name, nil)
  260. //Create a new proxy object
  261. path, _ := url.Parse("http://localhost:" + intToString(thisServicePort))
  262. proxy := reverseproxy.NewReverseProxy(path)
  263. thisSubService.ProxyHandler = proxy
  264. }
  265. //Append this subservice into the list
  266. sr.RunningSubService = append(sr.RunningSubService, thisSubService)
  267. //Append this module into the loaded module list
  268. sr.moduleHandler.LoadedModule = append(sr.moduleHandler.LoadedModule, &thisModuleInfo)
  269. return nil
  270. }
  271. func (sr *SubServiceRouter) HandleListing(w http.ResponseWriter, r *http.Request) {
  272. //List all subservice running in the background
  273. type visableInfo struct {
  274. Port int
  275. ServiceDir string
  276. Path string
  277. RpEndpoint string
  278. ProcessID int
  279. Info modules.ModuleInfo
  280. }
  281. type disabledServiceInfo struct {
  282. ServiceDir string
  283. Path string
  284. }
  285. enabled := []visableInfo{}
  286. disabled := []disabledServiceInfo{}
  287. for _, thisSubservice := range sr.RunningSubService {
  288. enabled = append(enabled, visableInfo{
  289. Port: thisSubservice.Port,
  290. Path: thisSubservice.Path,
  291. ServiceDir: thisSubservice.ServiceDir,
  292. RpEndpoint: thisSubservice.RpEndpoint,
  293. ProcessID: thisSubservice.Process.Process.Pid,
  294. Info: thisSubservice.Info,
  295. })
  296. }
  297. disabledModules, _ := filepath.Glob("subservice/*/.disabled")
  298. for _, modFile := range disabledModules {
  299. thisdsi := new(disabledServiceInfo)
  300. thisdsi.ServiceDir = filepath.Base(filepath.Dir(modFile))
  301. thisdsi.Path = filepath.Base(filepath.Dir(modFile))
  302. if runtime.GOOS == "windows" {
  303. thisdsi.Path = thisdsi.Path + ".exe"
  304. }
  305. disabled = append(disabled, *thisdsi)
  306. }
  307. jsonString, err := json.Marshal(struct {
  308. Enabled []visableInfo
  309. Disabled []disabledServiceInfo
  310. }{
  311. Enabled: enabled,
  312. Disabled: disabled,
  313. })
  314. if err != nil {
  315. sr.logger.PrintAndLog("Subservice", "Unable to list subservice folder", err)
  316. }
  317. sendJSONResponse(w, string(jsonString))
  318. }
  319. // Kill the subservice that is currently running
  320. func (sr *SubServiceRouter) HandleKillSubService(w http.ResponseWriter, r *http.Request) {
  321. userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
  322. //Require admin permission
  323. if !userinfo.IsAdmin() {
  324. sendErrorResponse(w, "Permission denied")
  325. return
  326. }
  327. //OK. Get paramters
  328. serviceDir, _ := mv(r, "serviceDir", true)
  329. //moduleName, _ := mv(r, "moduleName", true)
  330. err := sr.KillSubService(serviceDir)
  331. if err != nil {
  332. sendErrorResponse(w, err.Error())
  333. } else {
  334. sendOK(w)
  335. }
  336. }
  337. func (sr *SubServiceRouter) HandleStartSubService(w http.ResponseWriter, r *http.Request) {
  338. userinfo, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
  339. //Require admin permission
  340. if !userinfo.IsAdmin() {
  341. sendErrorResponse(w, "Permission denied")
  342. return
  343. }
  344. //OK. Get which dir to start
  345. serviceDir, _ := mv(r, "serviceDir", true)
  346. err := sr.StartSubService(serviceDir)
  347. if err != nil {
  348. sendErrorResponse(w, err.Error())
  349. } else {
  350. sendOK(w)
  351. }
  352. }
  353. // Check if the user has permission to access such proxy module
  354. func (sr *SubServiceRouter) CheckUserPermissionOnSubservice(ss *SubService, u *user.User) bool {
  355. moduleName := ss.Info.Name
  356. return u.GetModuleAccessPermission(moduleName)
  357. }
  358. // Check if the target is reverse proxy. If yes, return the proxy handler and the rewritten url in string
  359. func (sr *SubServiceRouter) CheckIfReverseProxyPath(r *http.Request) (bool, *reverseproxy.ReverseProxy, string, *SubService) {
  360. requestURL := r.URL.Path
  361. for _, subservice := range sr.RunningSubService {
  362. thisServiceProxyEP := subservice.RpEndpoint
  363. if thisServiceProxyEP != "" {
  364. if len(requestURL) > len(thisServiceProxyEP)+1 && requestURL[1:len(thisServiceProxyEP)+1] == thisServiceProxyEP {
  365. //This is a proxy path. Generate the rewrite URL
  366. //Get all GET paramters from URL
  367. values := r.URL.Query()
  368. counter := 0
  369. parsedGetTail := ""
  370. for k, v := range values {
  371. if counter == 0 {
  372. parsedGetTail = "?" + k + "=" + url.QueryEscape(v[0])
  373. } else {
  374. parsedGetTail = parsedGetTail + "&" + k + "=" + url.QueryEscape(v[0])
  375. }
  376. counter++
  377. }
  378. return true, subservice.ProxyHandler, requestURL[len(thisServiceProxyEP)+1:] + parsedGetTail, &subservice
  379. }
  380. }
  381. }
  382. return false, nil, "", &SubService{}
  383. }
  384. func (sr *SubServiceRouter) Close() {
  385. //Handle shutdown of subprocesses. Kill all of them
  386. for _, subservice := range sr.RunningSubService {
  387. cmd := subservice.Process
  388. if cmd != nil {
  389. if runtime.GOOS == "windows" {
  390. //Force kill with the power of CMD
  391. kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
  392. //kill.Stderr = os.Stderr
  393. //kill.Stdout = os.Stdout
  394. kill.Run()
  395. } else {
  396. //Send sigkill to process
  397. cmd.Process.Kill()
  398. }
  399. }
  400. }
  401. }
  402. func (sr *SubServiceRouter) KillSubService(serviceDir string) error {
  403. //Remove them from the system
  404. ssi := -1
  405. moduleName := ""
  406. for i, ss := range sr.RunningSubService {
  407. if ss.ServiceDir == serviceDir {
  408. ssi = i
  409. moduleName = ss.Info.Name
  410. //Kill the module cmd
  411. cmd := ss.Process
  412. if cmd != nil {
  413. if runtime.GOOS == "windows" {
  414. //Force kill with the power of CMD
  415. kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
  416. kill.Run()
  417. } else {
  418. err := cmd.Process.Kill()
  419. if err != nil {
  420. return err
  421. }
  422. }
  423. }
  424. //Write a suspended file into the module
  425. os.WriteFile("subservice/"+ss.ServiceDir+"/.disabled", []byte(""), 0755)
  426. }
  427. }
  428. //Pop this service from running Subservice
  429. if ssi != -1 {
  430. i := ssi
  431. copy(sr.RunningSubService[i:], sr.RunningSubService[i+1:])
  432. sr.RunningSubService = sr.RunningSubService[:len(sr.RunningSubService)-1]
  433. }
  434. //Pop the related module from the loadedModule list
  435. mi := -1
  436. for i, m := range sr.moduleHandler.LoadedModule {
  437. if m.Name == moduleName {
  438. mi = i
  439. }
  440. }
  441. if mi != -1 {
  442. i := mi
  443. copy(sr.moduleHandler.LoadedModule[i:], sr.moduleHandler.LoadedModule[i+1:])
  444. sr.moduleHandler.LoadedModule = sr.moduleHandler.LoadedModule[:len(sr.moduleHandler.LoadedModule)-1]
  445. }
  446. return nil
  447. }
  448. func (sr *SubServiceRouter) StartSubService(serviceDir string) error {
  449. if fileExists("subservice/" + serviceDir) {
  450. err := sr.Launch("subservice/"+serviceDir, false)
  451. if err != nil {
  452. return err
  453. }
  454. } else {
  455. return errors.New("Subservice directory not exists.")
  456. }
  457. //Sort the list
  458. sort.Slice(sr.moduleHandler.LoadedModule, func(i, j int) bool {
  459. return sr.moduleHandler.LoadedModule[i].Name < sr.moduleHandler.LoadedModule[j].Name
  460. })
  461. sort.Slice(sr.RunningSubService, func(i, j int) bool {
  462. return sr.RunningSubService[i].Info.Name < sr.RunningSubService[j].Info.Name
  463. })
  464. return nil
  465. }
  466. // Get a list of subservice roots in realpath
  467. func (sr *SubServiceRouter) GetSubserviceRoot() []string {
  468. subserviceRoots := []string{}
  469. for _, subService := range sr.RunningSubService {
  470. subserviceRoots = append(subserviceRoots, subService.Path)
  471. }
  472. return subserviceRoots
  473. }
  474. // Scan and get the next avaible port for subservice from its basePort
  475. func (sr *SubServiceRouter) GetNextUsablePort() int {
  476. basePort := sr.BasePort
  477. for sr.CheckIfPortInUse(basePort) {
  478. basePort++
  479. }
  480. return basePort
  481. }
  482. func (sr *SubServiceRouter) CheckIfPortInUse(port int) bool {
  483. for _, service := range sr.RunningSubService {
  484. if service.Port == port {
  485. return true
  486. }
  487. }
  488. return false
  489. }
  490. func (sr *SubServiceRouter) HandleRoutingRequest(w http.ResponseWriter, r *http.Request, proxy *reverseproxy.ReverseProxy, subserviceObject *SubService, rewriteURL string) {
  491. u, _ := sr.userHandler.GetUserInfoFromRequest(w, r)
  492. if !sr.CheckUserPermissionOnSubservice(subserviceObject, u) {
  493. //Permission denied
  494. http.NotFound(w, r)
  495. return
  496. }
  497. //Perform reverse proxy serving
  498. r.URL, _ = url.Parse(rewriteURL)
  499. token, _ := sr.userHandler.GetAuthAgent().NewTokenFromRequest(w, r)
  500. r.Header.Set("aouser", u.Username)
  501. r.Header.Set("aotoken", token)
  502. r.Header.Set("X-Forwarded-Host", r.Host)
  503. if r.Header["Upgrade"] != nil && r.Header["Upgrade"][0] == "websocket" {
  504. //Handle WebSocket request. Forward the custom Upgrade header and rewrite origin
  505. r.Header.Set("A-Upgrade", "websocket")
  506. u, _ := url.Parse("ws://localhost:" + strconv.Itoa(subserviceObject.Port) + r.URL.String())
  507. wspHandler := websocketproxy.NewProxy(u)
  508. wspHandler.ServeHTTP(w, r)
  509. return
  510. }
  511. r.Host = r.URL.Host
  512. err := proxy.ServeHTTP(w, r)
  513. if err != nil {
  514. //Check if it is cancelling events.
  515. if !strings.Contains(err.Error(), "cancel") {
  516. sr.logger.PrintAndLog("Subservice", subserviceObject.Info.Name+" IS NOT RESPONDING!", err)
  517. sr.RestartSubService(subserviceObject)
  518. }
  519. }
  520. }
  521. // Handle fail start over when the remote target is not responding
  522. func (sr *SubServiceRouter) RestartSubService(ss *SubService) {
  523. go func(ss *SubService) {
  524. //Kill the original subservice
  525. sr.KillSubService(ss.ServiceDir)
  526. sr.logger.PrintAndLog("Subservice", "RESTARTING SUBSERVICE "+ss.Info.Name+" IN 10 SECOUNDS", nil)
  527. time.Sleep(10000 * time.Millisecond)
  528. sr.StartSubService(ss.ServiceDir)
  529. sr.logger.PrintAndLog("Subservice", "SUBSERVICE "+ss.Info.Name+" RESTARTED", nil)
  530. }(ss)
  531. }