325 lines
7.1 KiB
Go
325 lines
7.1 KiB
Go
package manager
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"git.rootprojects.org/root/go-serviceman/runner"
|
|
"git.rootprojects.org/root/go-serviceman/service"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
var (
|
|
srvLen int
|
|
srvExt = ".json"
|
|
srvSysPath = "/opt/serviceman/etc"
|
|
srvUserPath = ".local/opt/serviceman/etc"
|
|
)
|
|
|
|
func init() {
|
|
srvLen = len(srvExt)
|
|
}
|
|
|
|
// TODO nab some goodness from https://github.com/takama/daemon
|
|
|
|
// TODO system service requires elevated privileges
|
|
// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/
|
|
func install(c *service.Service) (string, error) {
|
|
/*
|
|
// LEAVE THIS DOCUMENTATION HERE
|
|
reg.exe
|
|
/V <value name> - "Telebit"
|
|
/T <data type> - "REG_SZ" - String
|
|
/D <value data>
|
|
/C - case sensitive
|
|
/F <search data??> - not sure...
|
|
|
|
// Special Note:
|
|
"/c" is similar to -- (*nix), and required within the data string
|
|
So instead of setting "do.exe --do-arg1 --do-arg2"
|
|
you must set "do.exe /c --do-arg1 --do-arg2"
|
|
|
|
vars.telebitNode += '.exe';
|
|
var cmd = 'reg.exe add "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run"'
|
|
+ ' /V "Telebit" /t REG_SZ /D '
|
|
+ '"' + things.argv[0] + ' /c ' // something like C:\Program Files (x64)\nodejs\node.exe
|
|
+ [ path.join(__dirname, 'bin/telebitd.js')
|
|
, 'daemon'
|
|
, '--config'
|
|
, path.join(os.homedir(), '.config/telebit/telebitd.yml')
|
|
].join(' ')
|
|
+ '" /F'
|
|
;
|
|
*/
|
|
autorunKey := `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
|
|
k, _, err := registry.CreateKey(
|
|
registry.CURRENT_USER,
|
|
autorunKey,
|
|
registry.SET_VALUE,
|
|
)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer k.Close()
|
|
|
|
// Try to stop before trying to copy the file
|
|
_ = runner.Stop(c)
|
|
|
|
args, err := installServiceman(c)
|
|
if nil != err {
|
|
return "", err
|
|
}
|
|
|
|
/*
|
|
setArgs := ""
|
|
args := c.Argv
|
|
exec := c.Exec
|
|
bin := c.Interpreter
|
|
if "" != bin {
|
|
// If this is something like node or python,
|
|
// the interpeter must be called as "the main thing"
|
|
// and "the app" must be an argument
|
|
args = append([]string{exec}, args...)
|
|
} else {
|
|
// Otherwise, if "the app" is a true binary,
|
|
// it can be "the main thing"
|
|
bin = exec
|
|
}
|
|
|
|
// The final string ends up looking something like one of these:
|
|
// `"C:\Users\aj\.local\opt\appname\appname.js" -p 8080`
|
|
// `"C:\Program Files (x64)\nodejs\node.exe" C:\Users\aj\.local\opt\appname\appname.js -p 8080`
|
|
regSZ := bin + setArgs + strings.Join(c.Argv, " ")
|
|
*/
|
|
|
|
regSZ := fmt.Sprintf(`"%s" %s`, args[0], strings.Join(args[1:], " "))
|
|
if len(regSZ) > 260 {
|
|
return "", fmt.Errorf("data value is too long for registry entry")
|
|
}
|
|
// In order for a windows gui program to not show a console,
|
|
// it has to not output any messages?
|
|
//fmt.Println("Set Registry Key:")
|
|
//fmt.Println(autorunKey, c.Title, regSZ)
|
|
k.SetStringValue(c.Title, regSZ)
|
|
|
|
err = start(c)
|
|
return "serviceman", err
|
|
}
|
|
|
|
func Render(c *service.Service) ([]byte, error) {
|
|
b, err := json.Marshal(c)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func start(conf *service.Service) error {
|
|
args := getRunnerArgs(conf)
|
|
args = append(args, "--daemon")
|
|
return Run(args[0], args[1:]...)
|
|
}
|
|
|
|
func stop(conf *service.Service) error {
|
|
return runner.Stop(conf)
|
|
}
|
|
|
|
func list(c *service.Service) ([]string, []string, []error) {
|
|
var errs []error
|
|
|
|
regs, err := listRegistry(c)
|
|
if nil != err {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
cfgs, errors := listConfigs(c)
|
|
if 0 != len(errors) {
|
|
errs = append(errs, errors...)
|
|
}
|
|
|
|
managed := []string{}
|
|
for i := range cfgs {
|
|
managed = append(managed, cfgs[i].Name)
|
|
}
|
|
|
|
others := []string{}
|
|
for i := range regs {
|
|
reg := regs[i]
|
|
if 0 == len(cfgs) {
|
|
others = append(others, reg)
|
|
continue
|
|
}
|
|
|
|
var found bool
|
|
for j := range cfgs {
|
|
cfg := cfgs[j]
|
|
// Registry Value Names are case-insensitive
|
|
if strings.ToLower(reg) == strings.ToLower(cfg.Title) {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
others = append(others, reg)
|
|
}
|
|
}
|
|
|
|
return managed, others, errs
|
|
}
|
|
|
|
func getRunnerArgs(c *service.Service) []string {
|
|
self := os.Args[0]
|
|
debug := ""
|
|
if strings.Contains(self, "debug.exe") {
|
|
debug = "debug."
|
|
}
|
|
|
|
smdir := `\opt\serviceman`
|
|
// TODO support service level services (which probably wouldn't need serviceman)
|
|
smdir = filepath.Join(c.Home, ".local", smdir)
|
|
// for now we'll scope the runner to the name of the application
|
|
smbin := filepath.Join(smdir, `bin\serviceman.`+debug+c.Name+`.exe`)
|
|
|
|
confpath := filepath.Join(smdir, `etc`)
|
|
conffile := filepath.Join(confpath, c.Name+`.json`)
|
|
|
|
return []string{
|
|
smbin,
|
|
"run",
|
|
"--config",
|
|
conffile,
|
|
}
|
|
}
|
|
|
|
type winConf struct {
|
|
Filename string `json:"-"`
|
|
Name string `json:"name"`
|
|
Title string `json:"title"`
|
|
}
|
|
|
|
func listConfigs(c *service.Service) ([]winConf, []error) {
|
|
var errs []error
|
|
|
|
smdir := `\opt\serviceman`
|
|
if !c.System {
|
|
smdir = filepath.Join(c.Home, ".local", smdir)
|
|
}
|
|
confpath := filepath.Join(smdir, `etc`)
|
|
|
|
infos, err := ioutil.ReadDir(confpath)
|
|
if nil != err {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
errs = append(errs, &ManageError{
|
|
Name: confpath,
|
|
Hint: "Read directory",
|
|
Parent: err,
|
|
})
|
|
return nil, errs
|
|
}
|
|
|
|
// TODO report active status
|
|
srvs := []winConf{}
|
|
for i := range infos {
|
|
filename := strings.ToLower(infos[i].Name())
|
|
if len(filename) <= srvLen || !strings.HasSuffix(filename, srvExt) {
|
|
continue
|
|
}
|
|
|
|
name := filename[:len(filename)-srvLen]
|
|
b, err := ioutil.ReadFile(filepath.Join(confpath, filename))
|
|
if nil != err {
|
|
errs = append(errs, &ManageError{
|
|
Name: name,
|
|
Hint: "Read file",
|
|
Parent: err,
|
|
})
|
|
continue
|
|
}
|
|
cfg := winConf{Filename: filename}
|
|
err = json.Unmarshal(b, &cfg)
|
|
if nil != err {
|
|
errs = append(errs, &ManageError{
|
|
Name: name,
|
|
Hint: "Parse JSON",
|
|
Parent: err,
|
|
})
|
|
continue
|
|
}
|
|
|
|
srvs = append(srvs, cfg)
|
|
}
|
|
|
|
return srvs, errs
|
|
}
|
|
|
|
func listRegistry(c *service.Service) ([]string, error) {
|
|
autorunKey := `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`
|
|
k, _, err := registry.CreateKey(
|
|
registry.CURRENT_USER,
|
|
autorunKey,
|
|
registry.QUERY_VALUE,
|
|
)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer k.Close()
|
|
|
|
return k.ReadValueNames(-1)
|
|
}
|
|
|
|
// copies self to install path and returns config path
|
|
func installServiceman(c *service.Service) ([]string, error) {
|
|
// TODO check version and upgrade or dismiss
|
|
self := os.Args[0]
|
|
|
|
args := getRunnerArgs(c)
|
|
smbin := args[0]
|
|
conffile := args[len(args)-1]
|
|
|
|
if smbin != self {
|
|
err := os.MkdirAll(filepath.Dir(smbin), 0755)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
// Note: self may be the short name, in which case
|
|
// we should just use whatever is closest in the path
|
|
// exec.LookPath will handle this correctly
|
|
self, err = exec.LookPath(self)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
bin, err := ioutil.ReadFile(self)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
err = ioutil.WriteFile(smbin, bin, 0755)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
b, err := Render(c)
|
|
if nil != err {
|
|
// this should be impossible, so we'll just panic
|
|
panic(err)
|
|
}
|
|
err = os.MkdirAll(filepath.Dir(conffile), 0755)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
err = ioutil.WriteFile(conffile, b, 0640)
|
|
if nil != err {
|
|
return nil, err
|
|
}
|
|
|
|
return args, nil
|
|
}
|