diff --git a/.gitignore b/.gitignore index dbc89fc..84ff40b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /cmd/watchdog/installer/static /watchdog /cmd/watchdog/watchdog +watchdog.exe xversion.go *.json diff --git a/cmd/watchdog/installer/doc.go b/cmd/watchdog/installer/doc.go new file mode 100644 index 0000000..ec2b059 --- /dev/null +++ b/cmd/watchdog/installer/doc.go @@ -0,0 +1,7 @@ +// Package installer can be used cross-platform to install apps +// as either userspace or system services for fairly simple applications. +// This is not intended for complex installers. +// +// I'm prototyping this out to be useful for more than just watchdog +// hence there are a few unnecessary things for the sake of the trying it out +package installer diff --git a/cmd/watchdog/installer/filesystem.go b/cmd/watchdog/installer/filesystem.go index c41f2b9..d0395b5 100644 --- a/cmd/watchdog/installer/filesystem.go +++ b/cmd/watchdog/installer/filesystem.go @@ -1,19 +1,19 @@ package installer -// "A little copying is better than a little dependency" -// These are here so that we don't need a dependency on http.FileSystem and http.File - import ( "io" "os" ) -// Same as http.FileSystem +// "A little copying is better than a little dependency" +// These are here so that we don't need a dependency on http.FileSystem and http.File + +// FileSystem is the same as http.FileSystem type FileSystem interface { Open(name string) (File, error) } -// Same as http.File +// File is the same as http.File type File interface { io.Closer io.Reader diff --git a/cmd/watchdog/installer/install.go b/cmd/watchdog/installer/install.go index 5070f5f..4f91a45 100644 --- a/cmd/watchdog/installer/install.go +++ b/cmd/watchdog/installer/install.go @@ -1,7 +1,5 @@ //go:generate go run -mod=vendor github.com/UnnoTed/fileb0x b0x.toml -// I'm prototyping this out to be useful for more than just watchdog -// hence there are a few unnecessary things for the sake of the trying it out package installer import ( @@ -10,6 +8,47 @@ import ( "strings" ) +// Config should describe the service well-enough for it to +// run on Mac, Linux, and Windows. +// +// &Config{ +// // A human-friendy name +// Title: "Foobar App", +// // A computer-friendly name +// Name: "foobar-app", +// // A name for OS X plist +// ReverseDNS: "com.example.foobar-app", +// // A human-friendly description +// Desc: "Foobar App", +// // The app /service homepage +// URL: "https://example.com/foobar-app/", +// // The full path of the interpreter, if any (ruby, python, node, etc) +// Interpreter: "/opt/node/bin/node", +// // The name of the executable (or script) +// Exec: "foobar-app.js", +// // An array of arguments +// Argv: []string{"-c", "/path/to/config.json"}, +// // A map of Environment variables that should be set +// Envs: map[string]string{ +// PORT: "8080", +// ENV: "development", +// }, +// // The user (Linux & Mac only). +// // This does not apply to userspace services. +// // There may be special considerations +// User: "www-data", +// // If different from User +// Group: "", +// // Whether to install as a system or user service +// System: false, +// // Whether or not the service may need privileged ports +// PrivilegedPorts: false, +// } +// +// Note that some fields are exported for templating, +// but not intended to be set by you. +// These are documented as omitted from JSON. +// Try to stick to what's outlined above. type Config struct { Title string `json:"title"` Name string `json:"name"` @@ -23,7 +62,7 @@ type Config struct { User string `json:"user"` Group string `json:"group"` home string `json:"-"` - Local string `json:"local"` + Local string `json:"-"` LogDir string `json:"-"` System bool `json:"system"` Restart bool `json:"restart"` @@ -32,6 +71,8 @@ type Config struct { MultiuserProtection bool `json:"multiuser_protection"` } +// Install will do a best-effort attempt to install a start-on-startup +// user or system service via systemd, launchd, or reg.exe func Install(c *Config) error { if "" == c.Exec { c.Exec = c.Name diff --git a/cmd/watchdog/installer/install_windows.go b/cmd/watchdog/installer/install_windows.go index 2246e90..0ea74e7 100644 --- a/cmd/watchdog/installer/install_windows.go +++ b/cmd/watchdog/installer/install_windows.go @@ -2,16 +2,81 @@ package installer import ( "fmt" - //"golang.org/x/sys/windows" + "log" + "path/filepath" + "strings" + + "golang.org/x/sys/windows/registry" ) -// See -// https://github.com/golang/go/issues/28804 -// https://stackoverflow.com/questions/31558066/how-to-ask-for-administer-privileges-on-windows-with-go/31561120 -// https://stackoverflow.com/questions/27366298/check-if-application-is-running-as-administrator-in-golang -// https://www.reddit.com/r/golang/comments/53dthc/way_to_detect_if_the_programs_running_with/ -// https://play.golang.org/p/bBtRZrk4_p +// TODO system service requires elevated privileges +// See https://coolaj86.com/articles/golang-and-windows-and-admins-oh-my/ func install(c *Config) error { //token := windows.Token(0) - return fmt.Errorf("not yet implemented") + /* + // LEAVE THIS DOCUMENTATION HERE + reg.exe + /V - "Telebit" + /T - "REG_SZ" - String + /D + /C - case sensitive + /F - 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() + + setArgs := "" + args := c.Argv + exec := filepath.Join(c.home, ".local", "opt", c.Name, 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 + } + if 0 != len(args) { + // On Windows the /c acts kinda like -- does on *nix, + // at least for commands in the registry that have arguments + setArgs = ` /c ` + } + + // The final string ends up looking something like one of these: + // "C:\Users\aj\.local\opt\appname\appname.js /c -p 8080" + // "C:\Program Files (x64)\nodejs\node.exe /c C:\Users\aj\.local\opt\appname\appname.js -p 8080" + regSZ := bin + setArgs + strings.Join(c.Argv, " ") + fmt.Println("Set Registry Key:") + fmt.Println(autorunKey, c.Title, regSZ) + k.SetStringValue(c.Title, regSZ) + + return nil } diff --git a/cmd/watchdog/installer/whoami_windows.go b/cmd/watchdog/installer/whoami_windows.go index 5b48f06..f84c60c 100644 --- a/cmd/watchdog/installer/whoami_windows.go +++ b/cmd/watchdog/installer/whoami_windows.go @@ -2,7 +2,9 @@ package installer import "os/user" -func IsAdmin() { +// IsAdmin returns true if the user can be determined to be an admin +// and false otherwise (errs on the side of non-admin). +func IsAdmin() bool { u, err := user.Current() if nil != err { return false @@ -15,7 +17,11 @@ func IsAdmin() { return true } - ids := u.GroupIds() + ids, err := u.GroupIds() + if nil != err { + return false + } + for i := range ids { if "S-1-5-32-544" == ids[i] { return true diff --git a/go.mod b/go.mod index 60cb81e..988e9f0 100644 --- a/go.mod +++ b/go.mod @@ -6,4 +6,5 @@ require ( git.rootprojects.org/root/go-gitver v1.1.1 github.com/UnnoTed/fileb0x v1.1.3 golang.org/x/net v0.0.0-20180921000356-2f5d2388922f + golang.org/x/sys v0.0.0-20181019160139-8e24a49d80f8 )