├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── cmd ├── init │ └── main.go └── systemctl │ └── main.go ├── config └── config.go ├── cover.sh ├── system ├── daemon.go ├── daemon_test.go ├── errors.go ├── job.go ├── job_generate.go ├── job_test.go ├── log.go ├── log_test.go ├── set.go ├── state.go ├── status.go ├── target.go ├── target_test.go ├── transaction.go ├── unit.go └── unit_test.go ├── systemctl ├── cli │ ├── list-units.go │ ├── root.go │ ├── start.go │ ├── status.go │ └── stop.go ├── interfaces.go └── rpc.go ├── systemgo.yaml └── unit ├── automount └── sub.go ├── busname └── sub.go ├── definition.go ├── definition_test.go ├── device └── sub.go ├── errors.go ├── errors_test.go ├── interfaces.go ├── mount └── sub.go ├── package.go ├── path └── sub.go ├── scope └── sub.go ├── service ├── service.go └── service_test.go ├── slice └── sub.go ├── socket └── sub.go ├── state.go ├── state_test.go ├── status.go ├── status_test.go ├── swap └── sub.go ├── timer └── sub.go └── unit.go /.gitignore: -------------------------------------------------------------------------------- 1 | /test/testfile 2 | 3 | /bin 4 | /cover.out 5 | *_string.go 6 | mock_* 7 | 8 | *~ 9 | 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/BurntSushi/toml"] 2 | path = vendor/github.com/BurntSushi/toml 3 | url = https://github.com/BurntSushi/toml 4 | [submodule "vendor/github.com/Sirupsen/logrus"] 5 | path = vendor/github.com/Sirupsen/logrus 6 | url = https://github.com/Sirupsen/logrus 7 | [submodule "vendor/github.com/coreos/go-systemd"] 8 | path = vendor/github.com/coreos/go-systemd 9 | url = https://github.com/coreos/go-systemd 10 | [submodule "vendor/github.com/fsnotify/fsnotify"] 11 | path = vendor/github.com/fsnotify/fsnotify 12 | url = https://github.com/fsnotify/fsnotify 13 | [submodule "vendor/github.com/golang/mock"] 14 | path = vendor/github.com/golang/mock 15 | url = https://github.com/golang/mock 16 | [submodule "vendor/github.com/hashicorp/hcl"] 17 | path = vendor/github.com/hashicorp/hcl 18 | url = https://github.com/hashicorp/hcl 19 | [submodule "vendor/github.com/magiconair/properties"] 20 | path = vendor/github.com/magiconair/properties 21 | url = https://github.com/magiconair/properties 22 | [submodule "vendor/github.com/mitchellh/mapstructure"] 23 | path = vendor/github.com/mitchellh/mapstructure 24 | url = https://github.com/mitchellh/mapstructure 25 | [submodule "vendor/github.com/spf13/cast"] 26 | path = vendor/github.com/spf13/cast 27 | url = https://github.com/spf13/cast 28 | [submodule "vendor/github.com/spf13/cobra"] 29 | path = vendor/github.com/spf13/cobra 30 | url = https://github.com/bpowers/cobra 31 | branch = _helpless 32 | [submodule "vendor/github.com/spf13/jwalterweatherman"] 33 | path = vendor/github.com/spf13/jwalterweatherman 34 | url = https://github.com/spf13/jwalterweatherman 35 | [submodule "vendor/github.com/spf13/pflag"] 36 | path = vendor/github.com/spf13/pflag 37 | url = https://github.com/spf13/pflag 38 | [submodule "vendor/github.com/spf13/viper"] 39 | path = vendor/github.com/spf13/viper 40 | url = https://github.com/spf13/viper 41 | [submodule "vendor/gopkg.in/yaml.v2"] 42 | path = vendor/gopkg.in/yaml.v2 43 | url = https://gopkg.in/yaml.v2 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - tip 5 | script: 6 | - make travis 7 | env: 8 | global: 9 | - secure: o1obdZ+I0NoUpWSGLFMy8csuZ1pKczoR0SO2U6nHpWwomO6bbaCAhhe02HxuRpx+usHwUCzLBkHDk5tI3A6vWbqm6TLW/ZtWdE/0XqxQdg8KLMzSrFYHh0tFTi0Y2CEieGFN1N+WY2Y79XlHh8210Q5vW/4RdVKW+YctCzj4+zBCab4ArqM+WmnXXlQcaTMMnHEhYjai25XgpMnDVi6J0ydCRe1uLm0pp4cnJ9Hb+dRDscR/sA1sb5We6Yy+GfwgmR12/V+XsLL2ei9w9iMMQ1iT9MWuAOypaMtw/iWPFWeUjpk0xRNNHMWfZOdOHkhYpIFPz+SGF3+ghrpbae+gniL6KBo466SRcKr7mfW9CL4B7YGt2bhzVV4+3kpDVwid9fmDS+C9KpHiMYzktNzqTgVkM0NTOT9p9dgN46NolyufU2sj8jz989jzG7MbgLCI8G9XfmbGAXCLty6rS4+V4u3m+r8NdH7O6n7rqMHAjlx/3w8sokteBcGBfg+D8I5GqXk0GhtOv2xiXmG9dKzdK9A2SuHmViCE2WJXtfGyUAW1o30JjIK3nC+et+a43+MBKiTh2o/S77rUzpOzbxTXWuEyc6YkvEX2pzoAfPFHOWjrxTTltBkKgAKnV1PUUJUb1i0+wKWHkuUa0sCjPCNdo49Z7kqRZAMuJ8x/AjzBCOA= 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Romans Volosatovs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPO=github.com/plasma-umass/systemgo 2 | 3 | TEST=test 4 | UNIT=unit 5 | SYSTEM=system 6 | SYSTEMCTL=cmd/systemctl 7 | INIT=cmd/init 8 | 9 | BINDIR=bin 10 | 11 | COVER=cover.sh 12 | COVER_PROFILE=cover.out 13 | COVER_METHOD=atomic 14 | COVER_DEFAULT=html 15 | TRAVIS_MODE=travis-ci 16 | 17 | PKGS=`go list $(REPO)/... | grep -v "vendor"` 18 | 19 | PKG_SYSTEMGO=$(REPO) 20 | PKG_TEST=$(REPO)/$(TEST) 21 | PKG_UNIT=$(REPO)/$(UNIT) 22 | PKG_INIT=$(REPO)/$(INIT) 23 | PKG_SYSTEM=$(REPO)/$(SYSTEM) 24 | PKG_SYSTEMCTL=$(REPO)/$(SYSTEMCTL) 25 | 26 | 27 | ABS_REPO=$(GOPATH)/src/$(REPO) 28 | 29 | ABS_BINDIR=$(ABS_REPO)/bin 30 | 31 | ABS_COVER=$(ABS_REPO)/cover.sh 32 | ABS_COVER_PROFILE=$(ABS_REPO)/cover.out 33 | 34 | ABS_SYSTEMGO=$(ABS_REPO) 35 | ABS_TEST=$(ABS_REPO)/$(TEST) 36 | ABS_UNIT=$(ABS_REPO)/$(UNIT) 37 | ABS_INIT=$(ABS_REPO)/$(INIT) 38 | ABS_SYSTEM=$(ABS_REPO)/$(SYSTEM) 39 | ABS_SYSTEMCTL=$(ABS_REPO)/$(SYSTEMCTL) 40 | 41 | MOCK_PKGS=mock_unit mock_systemctl 42 | #system_interfaces=Supervisable,Dependency,Reloader 43 | unit_interfaces=Interface,Reloader,Starter,Stopper 44 | systemctl_interfaces=Daemon 45 | 46 | all: build test 47 | 48 | build: generate vet init systemctl 49 | 50 | depend: 51 | @echo "Checking build dependencies..." 52 | @go get -v golang.org/x/tools/cmd/stringer 53 | @go get -v github.com/coreos/go-systemd/unit 54 | @go get -v -d $(REPO)/... 55 | 56 | dependtest: dependmock 57 | @go get -v github.com/stretchr/testify 58 | 59 | dependmock: 60 | @echo "Checking mock testing dependencies..." 61 | @go get -v github.com/golang/mock/gomock 62 | @go get -v github.com/golang/mock/mockgen 63 | 64 | dependcover: dependtest 65 | @echo "Checking coverage testing dependencies..." 66 | @go get -v golang.org/x/tools/cmd/cover 67 | @go get -v github.com/wadey/gocovmerge 68 | 69 | dependcoverall: dependcover 70 | @echo "Checking coveralls.io testing dependencies..." 71 | @go get -v github.com/mattn/goveralls 72 | 73 | vet: generate 74 | @echo "Running 'go vet'..." 75 | @go vet $(PKGS) 76 | 77 | generate: depend 78 | @echo "Running 'go generate'..." 79 | @go generate -x $(REPO)/... 80 | 81 | $(MOCK_PKGS) test cover build: generate 82 | 83 | init systemctl: % : $(wildcard cmd/%/*.go) 84 | @echo "Building $@..." 85 | @go build -o $(ABS_BINDIR)/$@ $(REPO)/cmd/$@ 86 | @echo "$@ built and saved to $(ABS_BINDIR)/$@" 87 | 88 | install: build 89 | @echo "Installing..." 90 | @go get -v $(REPO)/... 91 | 92 | 93 | mock: dependmock $(MOCK_PKGS) 94 | 95 | $(MOCK_PKGS): mock_%: $(wildcard %/interfaces.go) 96 | @echo "Mocking $* interfaces..." 97 | @mkdir -p $(ABS_TEST)/$@ 98 | @mockgen -destination=$(ABS_TEST)/$@/$@.go -package=$@ $(REPO)/$* $($*_interfaces) 99 | @echo "$@ package built and saved to $(ABS_TEST)/$@" 100 | @go get $(PKG_TEST)/$@ 101 | 102 | test: dependtest mock 103 | @echo "Starting tests..." 104 | @go test -v $(PKGS) 105 | 106 | cover: dependcover 107 | @echo "Creating html coverage report..." 108 | @$(ABS_COVER) --html 109 | 110 | coveralls: dependcoverall 111 | @echo "Pushing coverage statistics to coveralls.io..." 112 | @$(ABS_COVER) --coveralls 113 | 114 | 115 | travis: dependcoverall build mock 116 | @echo "Starting travis build..." 117 | @./bin/init& 118 | @$(ABS_COVER) --coveralls $(TRAVIS_MODE) 119 | 120 | 121 | clean: cleancover cleanbin cleanstringers cleanmock 122 | 123 | cleanbin: 124 | @echo "Removing compiled binaries..." 125 | @-rm -rf $(ABS_BINDIR) 126 | cleanstringers: 127 | @echo "Removing generated stringers..." 128 | @-rm `find $(ABS_REPO) -name '*_string.go'` 129 | cleancover: 130 | @echo "Removing coverage profile..." 131 | @-rm -f $(ABS_COVER_PROFILE) 132 | cleanmock: 133 | @echo "Removing mock units..." 134 | @-rm -rf `find $(ABS_REPO) -name 'mock_*'` 135 | 136 | .PHONY: all generate test dependtest depend cover dependcover systemctl init install clean cleanbin cleanstringers cleancover cleanmock build travis mock_system mock_unit vet init cmd/init cmd/systemctl $(SYSTEMCTL) 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/plasma-umass/systemgo.svg?branch=master&bust=1)](https://travis-ci.org/plasma-umass/systemgo) 2 | [![Coverage Status](https://coveralls.io/repos/github/plasma-umass/systemgo/badge.svg?branch=master&bust=1)](https://coveralls.io/github/plasma-umass/systemgo?branch=master) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/plasma-umass/systemgo)](https://goreportcard.com/report/github.com/plasma-umass/systemgo) 4 | [![GoDoc](https://godoc.org/github.com/plasma-umass/systemgo?status.svg)](https://godoc.org/github.com/plasma-umass/systemgo) 5 | [![GSoC Project abstract](http://b.repl.ca/v1/GSoC_Project-abstract-orange.png)](https://summerofcode.withgoogle.com/projects/#6227933760847872) 6 | # Description 7 | An init system in Go, intended to run on [Browsix](https://github.com/plasma-umass/browsix) and other Unix-like OS(GNU/Linux incl.) 8 | # Features 9 | * Fast and concurrent 10 | * Handles dependencies well 11 | * [Systemd](https://github.com/Systemd/Systemd)-compatible 12 | 13 | # Progress 14 | - [x] Logging 15 | - [x] Dependency resolution 16 | - [x] Wants 17 | - [x] Requires 18 | - [x] After 19 | - [x] Before 20 | - [x] Systemctl 21 | 22 | # Supported Systemd functionality 23 | ## Commands 24 | - [x] start 25 | - [x] stop 26 | - [ ] reload 27 | - [x] restart 28 | - [x] status 29 | - [x] isolate 30 | - [x] list-units 31 | - [x] enable 32 | - [x] disable 33 | 34 | ## Unit types 35 | - [ ] Service 36 | - [x] Simple 37 | - [ ] Forking 38 | - [x] Oneshot 39 | - [ ] Mount 40 | - [x] Target 41 | - [ ] Socket 42 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - [ ] Let u.Define return <-chan error ? 2 | - [ ] Systemctl help, descriptions 3 | -------------------------------------------------------------------------------- /cmd/init/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "net/rpc" 8 | "os" 9 | "os/signal" 10 | "sync" 11 | "time" 12 | 13 | log "github.com/Sirupsen/logrus" 14 | 15 | "github.com/plasma-umass/systemgo/config" 16 | "github.com/plasma-umass/systemgo/system" 17 | "github.com/plasma-umass/systemgo/systemctl" 18 | ) 19 | 20 | // Initializes the system, sets the default paths, as specified in configuration and attempts to start the default target, falls back to "rescue.target", if it fails 21 | func main() { 22 | go Serve() 23 | 24 | // Initialize system 25 | log.Info("Systemgo starting...") 26 | 27 | sys.SetPaths(config.Paths...) 28 | 29 | // Start the default target 30 | if err := sys.Start(config.Target); err != nil { 31 | log.Errorf("Error starting default target %s: %s", config.Target, err) 32 | if err = sys.Start(config.RESCUE_TARGET); err != nil { 33 | log.Errorf("Error starting rescue target %s: %s", config.RESCUE_TARGET, err) 34 | } 35 | } 36 | 37 | if log.GetLevel() == log.DebugLevel { 38 | go printUnits() 39 | } 40 | 41 | exit := make(chan os.Signal) 42 | signal.Notify(exit, os.Interrupt, os.Kill) 43 | <-exit 44 | 45 | log.Infoln("Shutting down...") 46 | if err := sys.Isolate("shutdown.target"); err != nil { 47 | log.Fatalf("Error shutting down: %s", err) 48 | } 49 | 50 | wg := &sync.WaitGroup{} 51 | for _, u := range sys.Units() { 52 | if u.IsActive() { 53 | log.Infof("Waiting for %s to stop", u.Name()) 54 | wg.Add(1) 55 | 56 | go func(u *system.Unit) { 57 | defer wg.Done() 58 | 59 | var t time.Duration 60 | for range time.Tick(time.Second) { 61 | t += time.Second 62 | if !u.IsActive() || t == time.Minute { 63 | return 64 | } 65 | } 66 | }(u) 67 | } 68 | } 69 | } 70 | 71 | // Instance of a system 72 | var sys = system.New() 73 | 74 | // Listen for systemctl requests 75 | func Serve() { 76 | for { 77 | if err := listenHTTP(config.Port.String()); err != nil { 78 | log.Errorf("Error listening on %v: %s", config.Port, err) 79 | } 80 | log.Infof("Retrying in %v seconds", config.Retry) 81 | time.Sleep(config.Retry) 82 | } 83 | } 84 | 85 | // Handle systemctl requests using HTTP 86 | func listenHTTP(addr string) (err error) { 87 | daemonRPC := systemctl.NewServer(sys) 88 | rpc.Register(daemonRPC) 89 | rpc.HandleHTTP() 90 | 91 | e := log.WithField("port", config.Port) 92 | 93 | l, err := net.Listen("tcp", config.Port.String()) 94 | if err != nil { 95 | e.Fatalf("Listen error: %s", err) 96 | } 97 | 98 | log.Infof("Listening on http://localhost%s", config.Port) 99 | return http.Serve(l, nil) 100 | } 101 | 102 | func printUnits() { 103 | for range time.Tick(5 * time.Second) { 104 | for _, u := range sys.Units() { 105 | st, err := sys.StatusOf(u.Name()) 106 | if err != nil { 107 | panic(err) 108 | } 109 | fmt.Println("********************************************************************************") 110 | fmt.Println("\t\t", u.Name()) 111 | fmt.Println("********************************************************************************") 112 | fmt.Printf("->Status:\n%s\n", st) 113 | fmt.Println("--------------------------------------------------------------------------------") 114 | fmt.Printf("->Unit:\n%+v\n", u) 115 | fmt.Println("********************************************************************************") 116 | } 117 | fmt.Println("********************************************************************************") 118 | fmt.Println("********************************************************************************") 119 | fmt.Println("********************************************************************************") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /cmd/systemctl/main.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Romans Volosatovs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package main 22 | 23 | import "github.com/plasma-umass/systemgo/systemctl/cli" 24 | 25 | func main() { 26 | cli.Execute() 27 | } 28 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/plasma-umass/systemgo/system" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | const ( 14 | DEFAULT_PORT = 8008 15 | DEFAULT_TARGET = "default.target" 16 | RESCUE_TARGET = "rescue.target" 17 | ) 18 | 19 | var ( 20 | // Default target 21 | Target string 22 | 23 | // Paths to search for unit files 24 | Paths []string 25 | 26 | // Port for system daemon to listen on 27 | Port port 28 | 29 | // Retry specifies the period(in seconds) to wait before 30 | // restarting the http service if it fails 31 | Retry time.Duration 32 | 33 | // Wheter to show debugging statements 34 | Debug bool 35 | ) 36 | 37 | type port int 38 | 39 | func (p port) String() string { 40 | return fmt.Sprintf(":%v", int(p)) 41 | } 42 | 43 | func init() { 44 | viper.SetDefault("port", DEFAULT_PORT) 45 | viper.SetDefault("target", DEFAULT_TARGET) 46 | viper.SetDefault("paths", system.DEFAULT_PATHS) 47 | viper.SetDefault("retry", 1) 48 | viper.SetDefault("debug", false) 49 | 50 | viper.SetEnvPrefix("systemgo") 51 | viper.AutomaticEnv() 52 | 53 | viper.SetConfigName("systemgo") 54 | viper.SetConfigType("yaml") 55 | 56 | viper.AddConfigPath(".") 57 | if os.Getenv("XDG_CONFIG_HOME") != "" { 58 | viper.AddConfigPath("$XDG_CONFIG_HOME/systemgo") 59 | } 60 | viper.AddConfigPath("/etc/systemgo") 61 | 62 | if err := viper.ReadInConfig(); err != nil { 63 | if os.IsNotExist(err) { 64 | log.Warn("Config file not found, using defaults") 65 | } else { 66 | log.WithFields(log.Fields{ 67 | "file": viper.ConfigFileUsed(), 68 | "err": err, 69 | }).Errorf("Parse error, using defaults") 70 | } 71 | } else { 72 | log.WithFields(log.Fields{ 73 | "file": viper.ConfigFileUsed(), 74 | }).Infof("Found configuration file") 75 | } 76 | 77 | Target = viper.GetString("target") 78 | Paths = viper.GetStringSlice("paths") 79 | Port = port(viper.GetInt("port")) 80 | Retry = viper.GetDuration("retry") * time.Second 81 | Debug = viper.GetBool("debug") 82 | 83 | if Debug { 84 | log.SetLevel(log.DebugLevel) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cover.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Generate test coverage statistics for Go packages. 3 | # 4 | # Works around the fact that `go test -coverprofile` currently does not work 5 | # with multiple packages, see https://code.google.com/p/go/issues/detail?id=6909 6 | # 7 | # Usage: script/coverage [--html|--coveralls] 8 | # 9 | # --html Additionally create HTML report and open it in browser 10 | # --coveralls Push coverage statistics to coveralls.io 11 | # 12 | 13 | # Adopted from https://github.com/mlafeldt/chef-runner/raw/v0.7.0/script/coverage (Apache v2) 14 | # And https://github.com/golang/go/issues/6909 (mmindenhall comment) 15 | # Edited by Romans Volosatovs on 29.06.2016 16 | 17 | set -e 18 | 19 | workdir=".cover" 20 | profile="cover.out" 21 | mode="atomic" 22 | 23 | pkgs=`go list ./... | grep -vE 'vendor|mock|test|cmd'` 24 | pkg_seq=`tr ' ' ',' <<< ${pkgs}` 25 | pkg_seq=${pkg_seq%','} 26 | 27 | generate_cover_data() { 28 | mkdir -p "$workdir" 29 | 30 | # For each package with test files, run with full coverage (including other packages) 31 | go list -f '{{if gt (len .TestGoFiles) 0}}"go test -v -covermode='${mode}' -coverprofile='${workdir}'/{{.Name}}.coverprofile -coverpkg='${pkg_seq}' {{.ImportPath}}"{{end}}' ${pkgs} | xargs -I {} bash -c {} 32 | 33 | # Merge the generated cover profiles into a single file 34 | gocovmerge `ls $workdir/*.coverprofile` > $profile 35 | 36 | rm -rf "$workdir" 37 | } 38 | 39 | show_cover_report() { 40 | go tool cover -${1}="$profile" 41 | } 42 | 43 | push_to_coveralls() { 44 | if [ $# = 0 ]; then 45 | goveralls -coverprofile="$profile" 46 | else 47 | goveralls -coverprofile="$profile" -service=${1} 48 | fi 49 | } 50 | 51 | generate_cover_data 52 | show_cover_report func 53 | 54 | case "$1" in 55 | "") 56 | ;; 57 | --html) 58 | show_cover_report html ;; 59 | --coveralls) 60 | push_to_coveralls $2;; 61 | *) 62 | echo >&2 "error: invalid option: $1"; exit 1 ;; 63 | esac 64 | -------------------------------------------------------------------------------- /system/daemon.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "sync" 8 | "time" 9 | 10 | "github.com/plasma-umass/systemgo/unit" 11 | "github.com/plasma-umass/systemgo/unit/service" 12 | 13 | log "github.com/Sirupsen/logrus" 14 | ) 15 | 16 | // Default paths to search for unit paths - Daemon uses those, if none are specified 17 | var DEFAULT_PATHS = []string{"/etc/systemd/system/", "/run/systemd/system", "/lib/systemd/system"} 18 | 19 | var supported = map[string]bool{ 20 | ".service": true, 21 | ".target": true, 22 | ".mount": false, 23 | ".socket": false, 24 | } 25 | 26 | // SupportedSuffix returns a bool indicating if suffix represents a unit type, 27 | // which is supported by Systemgo 28 | func SupportedSuffix(suffix string) bool { 29 | return supported[suffix] 30 | } 31 | 32 | // Supported returns a bool indicating if filename represents a unit type, 33 | // which is supported by Systemgo 34 | func Supported(filename string) bool { 35 | return SupportedSuffix(filepath.Ext(filename)) 36 | } 37 | 38 | // Daemon supervises instances of Unit 39 | type Daemon struct { 40 | // System log 41 | Log *Log 42 | 43 | // Map of created units (name -> *Unit) 44 | units map[string]*Unit 45 | 46 | // Paths, where the unit file specifications get searched for 47 | paths []string 48 | 49 | // System state 50 | state State 51 | 52 | // System starting time 53 | since time.Time 54 | 55 | mutex sync.Mutex 56 | } 57 | 58 | // New returns an instance of a Daemon ready to use 59 | func New() (sys *Daemon) { 60 | return &Daemon{ 61 | units: make(map[string]*Unit), 62 | 63 | since: time.Now(), 64 | Log: NewLog(), 65 | paths: DEFAULT_PATHS, 66 | } 67 | } 68 | 69 | // Paths returns paths, which get searched for unit files by sys(first path gets searched first) 70 | func (sys *Daemon) Paths() (paths []string) { 71 | return sys.paths 72 | } 73 | 74 | // SetPaths sets paths, which get searched for unit files by sys(first path gets searched first) 75 | func (sys *Daemon) SetPaths(paths ...string) { 76 | sys.mutex.Lock() 77 | defer sys.mutex.Unlock() 78 | 79 | sys.paths = paths 80 | } 81 | 82 | // Since returns time, when sys was created 83 | func (sys *Daemon) Since() (t time.Time) { 84 | return sys.since 85 | } 86 | 87 | // IsEnabled returns enable state of the unit held in-memory under specified name. 88 | // If error is returned, it is going to be ErrNotFound 89 | // 90 | // TODO 91 | func (sys *Daemon) IsEnabled(name string) (st unit.Enable, err error) { 92 | //var u *Unit 93 | //if u, err = sys.Unit(name); err == nil && sys.Enabled[u] { 94 | //st = unit.Enabled 95 | //} 96 | return -1, ErrNotImplemented 97 | } 98 | 99 | // IsActive returns activation state of the unit held in-memory under specified name. 100 | // If error is returned, it is going to be ErrNotFound 101 | func (sys *Daemon) IsActive(name string) (st unit.Activation, err error) { 102 | var u *Unit 103 | if u, err = sys.Get(name); err == nil { 104 | st = u.Active() 105 | } 106 | return 107 | } 108 | 109 | // StatusOf returns status of the unit held in-memory under specified name. 110 | // If error is returned, it is going to be ErrNotFound 111 | func (sys *Daemon) StatusOf(name string) (st unit.Status, err error) { 112 | var u *Unit 113 | if u, err = sys.Get(name); err != nil { 114 | return 115 | } 116 | 117 | return u.Status(), nil 118 | } 119 | 120 | // Start gets names from internal hashmap, creates a new start transaction and runs it 121 | func (sys *Daemon) Start(names ...string) (err error) { 122 | log.WithField("names", names).Debugf("sys.Start") 123 | 124 | var tr *transaction 125 | if tr, err = sys.newTransaction(start, names); err != nil { 126 | return 127 | } 128 | return tr.Run() 129 | } 130 | 131 | // Stop gets names from internal hashmap, creates a new stop transaction and runs it 132 | func (sys *Daemon) Stop(names ...string) (err error) { 133 | log.WithField("names", names).Debugf("sys.Stop") 134 | 135 | var tr *transaction 136 | if tr, err = sys.newTransaction(stop, names); err != nil { 137 | return 138 | } 139 | return tr.Run() 140 | } 141 | 142 | // Isolate gets names from internal hashmap, creates a new start transaction, adds a stop job 143 | // for each unit currently active, but not in the transaction already and runs the transaction 144 | func (sys *Daemon) Isolate(names ...string) (err error) { 145 | log.WithField("names", names).Debugf("sys.Isolate") 146 | 147 | var tr *transaction 148 | if tr, err = sys.newTransaction(start, names); err != nil { 149 | return 150 | } 151 | 152 | for _, u := range sys.Units() { 153 | if _, ok := tr.unmerged[u]; ok { 154 | continue 155 | } 156 | 157 | if err = tr.add(stop, u, nil, true, true); err != nil { 158 | return 159 | } 160 | } 161 | return tr.Run() 162 | } 163 | 164 | // Restart gets names from internal hashmap, creates a new restart transaction and runs it 165 | func (sys *Daemon) Restart(names ...string) (err error) { 166 | log.WithField("names", names).Debugf("sys.Restart") 167 | 168 | var tr *transaction 169 | if tr, err = sys.newTransaction(restart, names); err != nil { 170 | return 171 | } 172 | return tr.Run() 173 | } 174 | 175 | // Reload gets names from internal hashmap, creates a new reload transaction and runs it 176 | func (sys *Daemon) Reload(names ...string) (err error) { 177 | log.WithField("names", names).Debugf("sys.Reload") 178 | 179 | var tr *transaction 180 | if tr, err = sys.newTransaction(reload, names); err != nil { 181 | return 182 | } 183 | return tr.Run() 184 | } 185 | 186 | func (sys *Daemon) newTransaction(typ jobType, names []string) (tr *transaction, err error) { 187 | sys.mutex.Lock() 188 | defer sys.mutex.Unlock() 189 | 190 | tr = newTransaction() 191 | 192 | for _, name := range names { 193 | var dep *Unit 194 | if dep, err = sys.Get(name); err != nil { 195 | return nil, err 196 | } 197 | 198 | if err = tr.add(typ, dep, nil, true, true); err != nil { 199 | return nil, err 200 | } 201 | } 202 | return 203 | } 204 | 205 | // Enable gets names from internal hasmap and calls Enable() on each unit returned 206 | func (sys *Daemon) Enable(names ...string) (err error) { 207 | log.WithField("names", names).Debugf("sys.Enable") 208 | 209 | return sys.getAndExecute(names, func(u *Unit, gerr error) error { 210 | if gerr != nil { 211 | return gerr 212 | } 213 | 214 | return u.Enable() 215 | }) 216 | } 217 | 218 | // Disable gets names from internal hasmap and calls Disable() on each unit returned 219 | func (sys *Daemon) Disable(names ...string) (err error) { 220 | log.WithField("names", names).Debugf("sys.Disable") 221 | 222 | return sys.getAndExecute(names, func(u *Unit, gerr error) error { 223 | if gerr != nil { 224 | return gerr 225 | } 226 | 227 | return u.Disable() 228 | }) 229 | } 230 | 231 | func (sys *Daemon) getAndExecute(names []string, fn func(*Unit, error) error) (err error) { 232 | for _, name := range names { 233 | if err = fn(sys.Get(name)); err != nil { 234 | return 235 | } 236 | } 237 | return 238 | } 239 | 240 | // Units returns a slice of all units created 241 | func (sys *Daemon) Units() (units []*Unit) { 242 | log.Debugf("sys.Units") 243 | 244 | unitSet := map[*Unit]struct{}{} 245 | for _, u := range sys.units { 246 | unitSet[u] = struct{}{} 247 | } 248 | 249 | units = make([]*Unit, 0, len(unitSet)) 250 | for u := range unitSet { 251 | units = append(units, u) 252 | } 253 | return 254 | } 255 | 256 | // Unit looks up unit name in the internal hasmap and returns the unit created associated with it 257 | // or nil and ErrNotFound, if it does not exist 258 | func (sys *Daemon) Unit(name string) (u *Unit, err error) { 259 | log.WithField("name", name).Debug("sys.Unit") 260 | 261 | var ok bool 262 | if u, ok = sys.units[name]; !ok { 263 | return nil, ErrNotFound 264 | } 265 | return 266 | } 267 | 268 | // Get looks up the unit name in the internal hasmap of loaded units and calls 269 | // sys.Load(name) if it can not be found. 270 | // If error is returned, it will be error from sys.Load(name) 271 | func (sys *Daemon) Get(name string) (u *Unit, err error) { 272 | log.WithField("name", name).Debug("sys.Get") 273 | 274 | if u, err = sys.Unit(name); err != nil || !u.IsLoaded() { 275 | return sys.load(name) 276 | } 277 | return 278 | } 279 | 280 | // Supervise creates a *Unit wrapping v and stores it in internal hashmap. 281 | // If a unit with name specified already exists - nil and ErrExists are returned 282 | func (sys *Daemon) Supervise(name string, v unit.Interface) (u *Unit, err error) { 283 | log.WithFields(log.Fields{ 284 | "name": name, 285 | "interface": v, 286 | }).Debugf("sys.Supervise") 287 | 288 | if u, err = sys.Unit(name); err == nil { 289 | return nil, ErrExists 290 | } 291 | 292 | return sys.newUnit(name, v), nil 293 | } 294 | 295 | func (sys *Daemon) newUnit(name string, v unit.Interface) (u *Unit) { 296 | log.WithFields(log.Fields{ 297 | "name": name, 298 | "interface": v, 299 | }).Debugf("sys.newUnit") 300 | 301 | u = NewUnit(v) 302 | u.name = name 303 | 304 | u.System = sys 305 | 306 | sys.units[name] = u 307 | if strings.HasSuffix(name, ".service") { 308 | sys.units[strings.TrimSuffix(name, ".service")] = u 309 | } 310 | 311 | return 312 | } 313 | 314 | // load searches for name in configured paths, parses it, and either overwrites the definition of already 315 | // created Unit or creates a new one 316 | func (sys *Daemon) load(name string) (u *Unit, err error) { 317 | log.WithField("name", name).Debugln("sys.Load") 318 | 319 | if !Supported(name) { 320 | return nil, ErrUnknownType 321 | } 322 | 323 | var paths []string 324 | if filepath.IsAbs(name) { 325 | paths = []string{name} 326 | } else { 327 | paths = make([]string, len(sys.paths)) 328 | for i, path := range sys.paths { 329 | paths[i] = filepath.Join(path, name) 330 | } 331 | } 332 | 333 | for _, path := range paths { 334 | var file *os.File 335 | if file, err = os.Open(path); err != nil { 336 | if os.IsNotExist(err) { 337 | continue 338 | } 339 | return nil, err 340 | } 341 | // Commented out because of gopherjs bug, 342 | // which breaks systemgo on Browsix 343 | // See https://goo.gl/AycBTv 344 | // 345 | //defer file.Close() 346 | 347 | // Check if a unit for name had already been created 348 | if u, err = sys.Unit(name); err != nil { 349 | // If not - create a new one 350 | var v unit.Interface 351 | switch filepath.Ext(name) { 352 | case ".target": 353 | v = &Target{System: sys} 354 | case ".service": 355 | v = &service.Unit{} 356 | default: 357 | panic("Trying to load an unsupported unit type") 358 | } 359 | 360 | u = sys.newUnit(name, v) 361 | } 362 | 363 | u.path = path 364 | sys.units[path] = u 365 | 366 | var info os.FileInfo 367 | if info, err = file.Stat(); err == nil && info.IsDir() { 368 | err = ErrIsDir 369 | } 370 | if err != nil { 371 | u.Log.Errorf("%s", err) 372 | file.Close() 373 | return u, err 374 | } 375 | 376 | if err = u.Interface.Define(file); err != nil { 377 | if me, ok := err.(unit.MultiError); ok { 378 | u.Log.Error("Definition is invalid:") 379 | for _, errmsg := range me.Errors() { 380 | u.Log.Error(errmsg) 381 | } 382 | } else { 383 | u.Log.Errorf("Error parsing definition: %s", err) 384 | } 385 | u.load = unit.Error 386 | file.Close() 387 | return u, err 388 | } 389 | 390 | u.load = unit.Loaded 391 | return u, file.Close() 392 | } 393 | 394 | return nil, ErrNotFound 395 | } 396 | 397 | // pathset returns a slice of paths to definitions of supported unit types found in path specified 398 | func pathset(path string) (definitions []string, err error) { 399 | var file *os.File 400 | if file, err = os.Open(path); err != nil { 401 | return nil, err 402 | } 403 | defer file.Close() 404 | 405 | var info os.FileInfo 406 | if info, err = file.Stat(); err != nil { 407 | return nil, err 408 | } else if !info.IsDir() { 409 | return nil, ErrNotDir 410 | } 411 | 412 | var names []string 413 | if names, err = file.Readdirnames(0); err != nil { 414 | return nil, err 415 | } 416 | 417 | definitions = make([]string, 0, len(names)) 418 | for _, name := range names { 419 | if Supported(name) { 420 | definitions = append(definitions, filepath.Join(path, name)) 421 | } 422 | } 423 | 424 | return 425 | } 426 | -------------------------------------------------------------------------------- /system/daemon_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | log "github.com/Sirupsen/logrus" 12 | "github.com/golang/mock/gomock" 13 | "github.com/plasma-umass/systemgo/test/mock_unit" 14 | "github.com/plasma-umass/systemgo/unit" 15 | "github.com/stretchr/testify/assert" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | type mockUnit struct { 20 | *mock_unit.MockInterface 21 | *mock_unit.MockStarter 22 | *mock_unit.MockStopper 23 | } 24 | 25 | func newMock(ctrl *gomock.Controller) (u *mockUnit) { 26 | return &mockUnit{ 27 | MockInterface: mock_unit.NewMockInterface(ctrl), 28 | MockStarter: mock_unit.NewMockStarter(ctrl), 29 | MockStopper: mock_unit.NewMockStopper(ctrl), 30 | } 31 | } 32 | 33 | func TestGet(t *testing.T) { 34 | sys := New() 35 | sys.SetPaths(os.TempDir()) 36 | 37 | name := "foo.service" 38 | 39 | fpath := filepath.Join(os.TempDir(), name) 40 | 41 | file, err := os.Create(fpath) 42 | require.NoError(t, err, "os.Create(fpath)") 43 | defer file.Close() 44 | 45 | ctrl := gomock.NewController(t) 46 | defer ctrl.Finish() 47 | 48 | m := newMock(ctrl) 49 | 50 | m.MockInterface.EXPECT().Define(gomock.Any()).Return(nil).Times(1) 51 | 52 | u, err := sys.Supervise(name, m) 53 | require.NoError(t, err) 54 | 55 | sys.units[fpath] = u 56 | 57 | for _, name := range []string{name, fpath} { 58 | ptr, err := sys.Get(name) 59 | require.NoError(t, err, name) 60 | assert.Equal(t, u, ptr, name) 61 | } 62 | } 63 | 64 | func TestSuported(t *testing.T) { 65 | for suffix, is := range supported { 66 | assert.Equal(t, is, Supported("foo"+suffix)) 67 | } 68 | 69 | assert.False(t, Supported("foo.wrong")) 70 | } 71 | 72 | func TestPathset(t *testing.T) { 73 | path, err := ioutil.TempDir("", "pathset-test") 74 | require.NoError(t, err, "ioutil.TempDir") 75 | defer os.RemoveAll(path) 76 | 77 | cases := []string{ 78 | "foo.service", 79 | "foo.mount", 80 | "foo.socket", 81 | "foo.target", 82 | "foo.wrong", 83 | } 84 | 85 | correct := 0 86 | for _, name := range cases { 87 | err = ioutil.WriteFile(filepath.Join(path, name), []byte{}, 0666) 88 | require.NoError(t, err, "ioutil.WriteFile") 89 | 90 | if Supported(name) { 91 | correct++ 92 | } 93 | } 94 | 95 | paths, err := pathset(path) 96 | require.NoError(t, err, "pathset") 97 | assert.Len(t, paths, correct, "paths") 98 | } 99 | 100 | func TestStart(t *testing.T) { 101 | ctrl := gomock.NewController(t) 102 | defer ctrl.Finish() 103 | 104 | mocks := map[string]*mockUnit{ 105 | "a": newMock(ctrl), 106 | "b": newMock(ctrl), 107 | "c": newMock(ctrl), 108 | } 109 | 110 | sequence := []string{ 111 | "a", "b", "c", 112 | } 113 | 114 | empty(mocks["a"], "wants", "before", "conflicts", "after", "requires") 115 | empty(mocks["b"], "wants", "before", "conflicts") 116 | empty(mocks["c"], "wants", "before", "conflicts") 117 | 118 | mocks["b"].MockInterface.EXPECT().After().Return([]string{"a"}).Times(1) 119 | mocks["b"].MockInterface.EXPECT().Requires().Return([]string{"a"}).Times(1) 120 | 121 | mocks["c"].MockInterface.EXPECT().After().Return([]string{"b", "a"}).Times(1) 122 | mocks["c"].MockInterface.EXPECT().Requires().Return([]string{"b", "a"}).Times(1) 123 | 124 | sys := New() 125 | 126 | for name, mock := range mocks { 127 | mock.MockInterface.EXPECT().Active().Return(unit.Inactive).AnyTimes() 128 | 129 | u, err := sys.Supervise(name, mock) 130 | require.NoError(t, err) 131 | 132 | u.load = unit.Loaded 133 | } 134 | 135 | calls := make([]*gomock.Call, len(sequence)) 136 | for i, name := range sequence { 137 | calls[i] = mocks[name].MockStarter.EXPECT().Start().Return(nil).Times(1) 138 | } 139 | gomock.InOrder(calls...) 140 | 141 | require.NoError(t, sys.Start("c"), "sys.Start("+"c"+")") 142 | 143 | names := make([]string, 0, len(mocks)) 144 | for name := range mocks { 145 | names = append(names, name) 146 | } 147 | waitForJobs(t, sys, names...) 148 | } 149 | 150 | func TestStop(t *testing.T) { 151 | ctrl := gomock.NewController(t) 152 | defer ctrl.Finish() 153 | 154 | m := newMock(ctrl) 155 | m.MockStopper.EXPECT().Stop().Return(nil).Times(1) 156 | m.MockInterface.EXPECT().Active().Return(unit.Active).AnyTimes() 157 | 158 | sys := New() 159 | 160 | u, err := sys.Supervise("TestStop", m) 161 | u.load = unit.Loaded 162 | require.NoError(t, err) 163 | 164 | require.NoError(t, sys.Stop("TestStop")) 165 | waitForJobs(t, sys, "TestStop") 166 | } 167 | 168 | func TestIsolate(t *testing.T) { 169 | ctrl := gomock.NewController(t) 170 | defer ctrl.Finish() 171 | 172 | sys := New() 173 | 174 | mocks := map[string]*mockUnit{ 175 | "a": newMock(ctrl), 176 | "b": newMock(ctrl), 177 | "c": newMock(ctrl), 178 | } 179 | 180 | mocks["a"].MockStopper.EXPECT().Stop().Return(nil).Times(1) 181 | mocks["b"].MockStopper.EXPECT().Stop().Return(nil).Times(1) 182 | 183 | empty(mocks["c"], "wants", "before", "conflicts", "after", "requires") 184 | 185 | for name, mock := range mocks { 186 | mock.MockInterface.EXPECT().Active().Return(unit.Active).AnyTimes() 187 | 188 | u, err := sys.Supervise(name, mock) 189 | require.NoError(t, err) 190 | 191 | u.load = unit.Loaded 192 | } 193 | 194 | require.NoError(t, sys.Isolate("c"), "sys.Isolate") 195 | 196 | names := make([]string, 0, len(mocks)) 197 | for name := range mocks { 198 | names = append(names, name) 199 | } 200 | waitForJobs(t, sys, "a", "b") 201 | } 202 | 203 | func waitForJobs(t *testing.T, sys *Daemon, names ...string) { 204 | wg := &sync.WaitGroup{} 205 | for _, name := range names { 206 | u, err := sys.Unit(name) 207 | require.NoError(t, err, "sys.Unit", name) 208 | 209 | wg.Add(1) 210 | go func(name string, u *Unit) { 211 | defer wg.Done() 212 | 213 | for u.job == nil { 214 | log.Warnf("%s job still nil", name) 215 | time.Sleep(100 * time.Millisecond) 216 | } 217 | 218 | log.Warnf("Waiting for %s job to finish", name) 219 | u.job.Wait() 220 | 221 | assert.True(t, u.job.Success()) 222 | }(name, u) 223 | } 224 | wg.Wait() 225 | } 226 | 227 | func TestEnable(t *testing.T) { 228 | ctrl := gomock.NewController(t) 229 | defer ctrl.Finish() 230 | 231 | sys := New() 232 | 233 | m := mock_unit.NewMockInterface(ctrl) 234 | m.EXPECT().WantedBy().Return([]string{"test.target"}).Times(2) 235 | m.EXPECT().RequiredBy().Return([]string{"test.target"}).Times(2) 236 | 237 | var err error 238 | 239 | for name, iface := range map[string]unit.Interface{ 240 | "test.target": nil, 241 | "test.service": m, 242 | } { 243 | e := log.WithField("name", name) 244 | 245 | var u *Unit 246 | if u, err = sys.Supervise(name, iface); err != nil { 247 | e.WithField("err", err).Fatal("sys.Supervise") 248 | } 249 | 250 | var f *os.File 251 | if f, err = ioutil.TempFile("", name); err != nil { 252 | e.WithField("err", err).Fatal("ioutil.TempDir") 253 | } 254 | 255 | u.path = f.Name() 256 | u.load = unit.Loaded 257 | } 258 | 259 | require.NoError(t, sys.Enable("test.service"), "sys.Enable") 260 | 261 | for _, suffix := range []string{"wants", "requires"} { 262 | path, err := os.Readlink(filepath.Join(sys.units["test.target"].path+"."+suffix, "test.service")) 263 | require.NoError(t, err, "os.Readlink") 264 | assert.Equal(t, path, sys.units["test.service"].path, "link path") 265 | } 266 | 267 | // TODO implement 268 | //st, err := sys.IsEnabled("test.service") 269 | //assert.NoError(t, err, "sys.IsEnabled") 270 | //assert.Equal(t, unit.Enabled, st, "sys.IsEnabled") 271 | 272 | require.NoError(t, sys.Disable("test.service"), "sys.Disable") 273 | for _, suffix := range []string{"wants", "requires"} { 274 | _, err := os.Open(filepath.Join(sys.units["test.target"].path+"."+suffix, "test.service")) 275 | assert.True(t, os.IsNotExist(err), "os.Open") 276 | } 277 | 278 | // TODO implement 279 | //st, err = sys.IsEnabled("test.service") 280 | //assert.NoError(t, err, "sys.IsEnabled") 281 | //assert.Equal(t, unit.Disabled, st, "sys.IsEnabled") 282 | } 283 | 284 | func empty(m *mockUnit, methods ...string) { 285 | for _, method := range methods { 286 | emptyOne(m, method).Times(1) 287 | } 288 | } 289 | 290 | func emptyOne(m *mockUnit, method string) (c *gomock.Call) { 291 | exp := m.MockInterface.EXPECT() 292 | switch method { 293 | case "requires": 294 | c = exp.Requires() 295 | case "wants": 296 | c = exp.Wants() 297 | case "before": 298 | c = exp.Before() 299 | case "after": 300 | c = exp.After() 301 | case "wantedBy": 302 | c = exp.WantedBy() 303 | case "requiredBy": 304 | c = exp.RequiredBy() 305 | case "conflicts": 306 | c = exp.Conflicts() 307 | } 308 | return c.Return([]string{}) 309 | } 310 | -------------------------------------------------------------------------------- /system/errors.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "errors" 4 | 5 | var ErrIsDir = errors.New("Is a directory") 6 | var ErrNotDir = errors.New("Is not a directory") 7 | var ErrNotFound = errors.New("Not found") 8 | var ErrDepFail = errors.New("Dependency failed to start. See unit log for details.") 9 | var ErrDepConflict = errors.New("Error stopping conflicting unit") 10 | var ErrNotLoaded = errors.New("Unit is not loaded.") 11 | var ErrNoReload = errors.New("Unit does not support reloading") 12 | var ErrUnknownType = errors.New("Unknown type") 13 | var ErrNotActive = errors.New("Unit is not active") 14 | var ErrExists = errors.New("Unit already exists") 15 | var ErrNotImplemented = errors.New("Not implemented yet") 16 | var ErrUnmergeable = errors.New("Unmergeable job types") 17 | -------------------------------------------------------------------------------- /system/job.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "sync" 5 | 6 | log "github.com/Sirupsen/logrus" 7 | ) 8 | 9 | const job_type_count = 4 10 | 11 | type job struct { 12 | typ jobType 13 | unit *Unit 14 | 15 | wants, requires, conflicts set 16 | wantedBy, requiredBy, conflictedBy set 17 | after, before set 18 | 19 | executed bool 20 | 21 | waitch chan struct{} 22 | err error 23 | 24 | mutex sync.Mutex 25 | } 26 | 27 | func newJob(typ jobType, u *Unit) (j *job) { 28 | log.WithFields(log.Fields{ 29 | "typ": typ, 30 | "u": u, 31 | }).Debugf("newJob") 32 | 33 | return &job{ 34 | typ: typ, 35 | unit: u, 36 | 37 | requires: set{}, 38 | wants: set{}, 39 | conflicts: set{}, 40 | 41 | requiredBy: set{}, 42 | wantedBy: set{}, 43 | conflictedBy: set{}, 44 | 45 | after: set{}, 46 | before: set{}, 47 | 48 | waitch: make(chan struct{}), 49 | } 50 | } 51 | 52 | //func (j *job) String() string { 53 | //return fmt.Sprintf("%s job for %s", j.typ, j.unit.Name()) 54 | //} 55 | 56 | func (j *job) IsRedundant() bool { 57 | switch j.typ { 58 | case stop: 59 | return j.unit.IsDeactivating() || j.unit.IsDead() 60 | case start: 61 | return j.unit.IsActivating() || j.unit.IsActive() 62 | case reload: 63 | return j.unit.IsReloading() 64 | default: 65 | return false 66 | } 67 | } 68 | 69 | func (j *job) IsRunning() bool { 70 | return !j.executed 71 | } 72 | 73 | func (j *job) Success() bool { 74 | return j.State() == success 75 | } 76 | 77 | func (j *job) Failed() bool { 78 | return j.State() == failed 79 | } 80 | 81 | func (j *job) Wait() (finished bool) { 82 | <-j.waitch 83 | return true 84 | } 85 | 86 | func (j *job) isOrphan() bool { 87 | return len(j.wantedBy) == 0 && len(j.requiredBy) == 0 && len(j.conflictedBy) == 0 88 | } 89 | 90 | func (j *job) State() (st jobState) { 91 | switch { 92 | case j.IsRunning(): 93 | return running 94 | case j.err == nil: 95 | return success 96 | default: 97 | return failed 98 | } 99 | } 100 | 101 | func (j *job) Run() (err error) { 102 | e := log.WithFields(log.Fields{ 103 | "unit": j.unit.Name(), 104 | "job": j.typ, 105 | }) 106 | e.Debugf("j.Run()") 107 | 108 | j.unit.job = j 109 | defer func() { 110 | j.err = err 111 | j.finish() 112 | }() 113 | 114 | wg := &sync.WaitGroup{} 115 | for dep := range j.requires { 116 | wg.Add(1) 117 | go func(dep *job) { 118 | e := e.WithField("dep", dep.unit.Name()) 119 | 120 | e.Debug("dep.Wait") 121 | dep.Wait() 122 | e.Debug("dep.Wait returned") 123 | 124 | if !dep.Success() { 125 | e.Debugf("->!dep.Success: %s", dep.State()) 126 | j.unit.Log.Errorf("%s failed to %s", dep.unit.Name(), dep.typ) 127 | err = ErrDepFail 128 | } 129 | wg.Done() 130 | }(dep) 131 | } 132 | wg.Wait() 133 | 134 | if err != nil { 135 | e.Debugf("failed: %s", err) 136 | return 137 | } 138 | 139 | switch j.typ { 140 | case start: 141 | return j.unit.start() 142 | case stop: 143 | return j.unit.stop() 144 | case restart: 145 | if err = j.unit.stop(); err != nil { 146 | return err 147 | } 148 | return j.unit.start() 149 | case reload: 150 | return j.unit.reload() 151 | default: 152 | panic(ErrUnknownType) 153 | } 154 | } 155 | 156 | func (j *job) finish() { 157 | j.executed = true 158 | close(j.waitch) 159 | } 160 | 161 | var mergeTable = map[jobType]map[jobType]jobType{ 162 | start: { 163 | start: start, 164 | //verify_active: start, 165 | reload: reload, //reload_or_start 166 | restart: restart, 167 | }, 168 | reload: { 169 | start: reload, //reload_or_start 170 | //verify_active: reload, 171 | restart: restart, 172 | }, 173 | restart: { 174 | start: restart, 175 | //verify_active: restart, 176 | reload: restart, 177 | }, 178 | } 179 | 180 | func (j *job) mergeWith(other *job) (err error) { 181 | t, ok := mergeTable[j.typ][other.typ] 182 | if !ok { 183 | return ErrUnmergeable 184 | } 185 | 186 | j.typ = t 187 | 188 | for jSet, oSet := range map[*set]*set{ 189 | &j.wantedBy: &other.wantedBy, 190 | &j.requiredBy: &other.requiredBy, 191 | &j.conflictedBy: &other.conflictedBy, 192 | 193 | &j.wants: &other.wants, 194 | &j.requires: &other.requires, 195 | &j.conflicts: &other.conflicts, 196 | } { 197 | for oJob := range *oSet { 198 | jSet.Put(oJob) 199 | } 200 | } 201 | 202 | return 203 | } 204 | -------------------------------------------------------------------------------- /system/job_generate.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | type jobState int 4 | 5 | //go:generate stringer -type=jobState job_generate.go 6 | const ( 7 | waiting jobState = iota 8 | running 9 | success 10 | failed 11 | ) 12 | 13 | type jobType int 14 | 15 | //go:generate stringer -type=jobType job_generate.go 16 | const ( 17 | start jobType = iota 18 | stop 19 | reload 20 | restart 21 | ) 22 | -------------------------------------------------------------------------------- /system/job_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestJobState(t *testing.T) { 11 | j := newJob(-1, nil) 12 | assert.Equal(t, running, j.State()) 13 | 14 | j.finish() 15 | assert.Equal(t, success, j.State()) 16 | assert.True(t, j.Success()) 17 | 18 | j.err = errors.New("") 19 | assert.Equal(t, failed, j.State()) 20 | } 21 | -------------------------------------------------------------------------------- /system/log.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | log "github.com/Sirupsen/logrus" 8 | ) 9 | 10 | // Maximum number of bytes kept if log buffer 11 | const BUFFER_SIZE = 10000 12 | 13 | type debugHook struct{} 14 | 15 | func (h *debugHook) Levels() []log.Level { 16 | return log.AllLevels 17 | } 18 | 19 | func (h *debugHook) Fire(e *log.Entry) error { 20 | log.WithFields(e.Data).Debug("\t", e.Message) 21 | return nil 22 | } 23 | 24 | // Log uses log.Logger to write data to embedded bytes.Buffer 25 | // Keeps up to 10000 bytes of data in-memory 26 | type Log struct { 27 | *log.Logger 28 | *bytes.Reader 29 | buffer *bytes.Buffer 30 | } 31 | 32 | // NewLog returns a new log 33 | func NewLog() (l *Log) { 34 | defer func() { 35 | l.Logger = &log.Logger{ 36 | Out: l, 37 | Formatter: &log.TextFormatter{ 38 | FullTimestamp: true, 39 | }, 40 | Level: log.InfoLevel, 41 | Hooks: log.LevelHooks{}, 42 | } 43 | l.Hooks.Add(&debugHook{}) 44 | }() 45 | return &Log{ 46 | buffer: bytes.NewBuffer(make([]byte, 0, BUFFER_SIZE)), 47 | } 48 | } 49 | 50 | func (l *Log) Len() (n int) { 51 | return l.buffer.Len() 52 | } 53 | 54 | func (l *Log) Cap() (n int) { 55 | return l.buffer.Cap() 56 | } 57 | 58 | func (l *Log) Read(b []byte) (n int, err error) { 59 | if l.Reader == nil { 60 | l.Reader = bytes.NewReader(l.buffer.Bytes()) 61 | } 62 | defer func() { 63 | if err == nil && l.Reader.Len() == 0 { 64 | err = io.EOF 65 | l.Reader = nil 66 | } 67 | }() 68 | return l.Reader.Read(b) 69 | } 70 | 71 | func (l *Log) Write(b []byte) (n int, err error) { 72 | if l.Len()+len(b) <= l.Cap() { 73 | return l.buffer.Write(b) 74 | } 75 | 76 | // Make sure that no 'partial' strings are left in buffer, as the buffer capacity is exceeded 77 | defer func() { 78 | if err == nil { 79 | _, err = l.buffer.ReadString('\n') 80 | } 81 | }() 82 | 83 | if len(b) >= l.Cap() { 84 | l.buffer.Reset() 85 | return l.buffer.Write(b[len(b)-l.Cap():]) 86 | } 87 | 88 | if _, err = l.buffer.Read(make([]byte, len(b)-l.Cap()+l.Len())); err != nil { 89 | return 0, err 90 | } 91 | 92 | return l.buffer.Write(b) 93 | } 94 | -------------------------------------------------------------------------------- /system/log_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | var lorem = []byte(` 13 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 14 | unc tortor magna, vestibulum a volutpat fermentum, aliquam quis enim. 15 | Quisque eget sapien nulla. Nulla et sem nec ante consequat auctor nec ut mauris. 16 | Praesent in nulla bibendum, sodales odio eget, posuere elit. 17 | Suspendisse tristique ligula non rutrum convallis. 18 | Maecenas eget urna ac nunc imperdiet tincidunt eu ut leo. 19 | Sed lacinia, ipsum et tincidunt viverra, metus ipsum pellentesque mi, quis laoreet nisl sapien quis nunc. 20 | Fusce aliquet metus sit amet libero euismod sollicitudin et ut arcu.`) 21 | 22 | func TestWrite(t *testing.T) { 23 | l := NewLog() 24 | 25 | l.Write(lorem) 26 | assert.Equal(t, len(lorem), l.Len(), "l.Len()") 27 | 28 | l.buffer.Reset() 29 | 30 | // []byte to write into buffer 31 | var b []byte 32 | 33 | // Times lorem fits in buffer 34 | var n int = BUFFER_SIZE / len(lorem) 35 | for i := 0; i <= n; i++ { 36 | b = append(b, lorem...) 37 | } 38 | 39 | n, err := l.Write(b) 40 | assert.NoError(t, err, "l.Write(b)") 41 | assert.Equal(t, n, BUFFER_SIZE, "l.Write(b)") 42 | assert.Equal(t, l.Cap(), BUFFER_SIZE, "l.Cap()") 43 | 44 | var char byte 45 | if (BUFFER_SIZE-len(b))%len(b) == 0 { 46 | char = b[0] 47 | } else { 48 | r := bytes.NewReader(b) 49 | for { 50 | // Search for first \n and char to next one after that 51 | c, err := r.ReadByte() 52 | require.NoError(t, err, "r.ReadByte()") 53 | 54 | if c == '\n' { 55 | char, _ = r.ReadByte() 56 | break 57 | } 58 | } 59 | } 60 | 61 | assert.Equal(t, char, l.buffer.Bytes()[0]) 62 | } 63 | 64 | func TestRead(t *testing.T) { 65 | l := NewLog() 66 | l.buffer = bytes.NewBuffer(lorem) 67 | 68 | b, err := ioutil.ReadAll(l) 69 | assert.NoError(t, err, "first ioutil.ReadAll(l)") 70 | 71 | bTest, err := ioutil.ReadAll(l) 72 | assert.NoError(t, err, "second ioutil.ReadAll(l)") 73 | 74 | assert.Equal(t, b, bTest, "ioutil.ReadAll(l) bytes read") 75 | } 76 | -------------------------------------------------------------------------------- /system/set.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | type set map[*job]struct{} 4 | 5 | func (s set) Contains(j *job) (ok bool) { 6 | _, ok = s[j] 7 | return 8 | } 9 | 10 | func (s set) Put(j *job) { 11 | s[j] = struct{}{} 12 | } 13 | -------------------------------------------------------------------------------- /system/state.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | // System state 4 | type State int 5 | 6 | //go:generate stringer -type=State state.go 7 | 8 | const ( 9 | Initializing State = iota 10 | Starting 11 | Running 12 | Degraded 13 | Maintenance 14 | Stopping 15 | ) 16 | -------------------------------------------------------------------------------- /system/status.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "time" 7 | ) 8 | 9 | // System status 10 | type Status struct { 11 | // State of the system 12 | State State `json:"State,string"` 13 | 14 | // Number of queued jobs in-total 15 | Jobs int `json:"Jobs"` 16 | 17 | // Number of failed units 18 | Failed int `json:"Failed"` 19 | 20 | // Init time 21 | Since time.Time `json:"Since"` 22 | 23 | // Log 24 | Log []byte `json:"Log, omitempty"` 25 | } 26 | 27 | func (s Status) String() (out string) { 28 | defer func() { 29 | if len(s.Log) > 0 { 30 | out += fmt.Sprintf("\nLog:\n%s\n", s.Log) 31 | } 32 | }() 33 | return fmt.Sprintf( 34 | `State: %s 35 | Jobs: %v queued 36 | Failed: %v units 37 | Since: %v`, 38 | s.State, s.Jobs, s.Failed, s.Since) 39 | } 40 | 41 | // Status returns status of the system 42 | // If error is returned it is going to be an error, 43 | // returned by the call to ioutil.ReadAll(sys.Log) 44 | func (sys *Daemon) Status() (st Status, err error) { 45 | st = Status{Since: sys.since} 46 | 47 | for _, u := range sys.Units() { 48 | switch { 49 | case u.job == nil: 50 | continue 51 | case u.job.IsRunning(): 52 | st.Jobs++ 53 | case u.job.Failed(): 54 | st.Failed++ 55 | } 56 | } 57 | 58 | if st.Failed > 0 { 59 | st.State = Degraded 60 | } else { 61 | st.State = Running 62 | } 63 | 64 | st.Log, err = ioutil.ReadAll(sys.Log) 65 | 66 | return 67 | } 68 | -------------------------------------------------------------------------------- /system/target.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/plasma-umass/systemgo/unit" 7 | ) 8 | 9 | const ( 10 | active = "active" 11 | dead = "dead" 12 | ) 13 | 14 | // Target unit type is used for grouping units 15 | type Target struct { 16 | unit.Definition 17 | System *Daemon 18 | } 19 | 20 | // Define attempts to fill the targ definition by parsing r 21 | func (targ *Target) Define(r io.Reader) (err error) { 22 | return unit.ParseDefinition(r, &targ.Definition) 23 | } 24 | 25 | // Active returns activation status of the unit 26 | func (targ *Target) Active() unit.Activation { 27 | encountered := map[unit.Activation]bool{} 28 | 29 | for _, name := range targ.Definition.Unit.Requires { 30 | dep, err := targ.System.Unit(name) 31 | if err != nil { 32 | return unit.Inactive 33 | } 34 | encountered[dep.Active()] = true 35 | } 36 | 37 | for _, state := range []unit.Activation{unit.Failed, unit.Activating, unit.Deactivating, unit.Reloading, unit.Inactive} { 38 | if encountered[state] { 39 | return state 40 | } 41 | } 42 | 43 | return unit.Active 44 | } 45 | 46 | func (targ *Target) Sub() string { 47 | if unit.IsActive(targ) { 48 | return active 49 | } 50 | return dead 51 | } 52 | -------------------------------------------------------------------------------- /system/target_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | "github.com/plasma-umass/systemgo/test/mock_unit" 9 | "github.com/plasma-umass/systemgo/unit" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestTargetActive(t *testing.T) { 14 | ctrl := gomock.NewController(t) 15 | defer ctrl.Finish() 16 | 17 | sys := New() 18 | targ := &Target{System: sys} 19 | 20 | for name, st := range map[string]unit.Activation{ 21 | "active": unit.Active, 22 | "inactive": unit.Inactive, 23 | "reloading": unit.Reloading, 24 | "failed": unit.Failed, 25 | "activating": unit.Activating, 26 | "deactivating": unit.Deactivating, 27 | } { 28 | u := mock_unit.NewMockInterface(ctrl) 29 | u.EXPECT().Active().Return(st).AnyTimes() 30 | sys.Supervise(name, u) 31 | } 32 | 33 | for deps, expected := range map[*[]string]unit.Activation{ 34 | {"non-existent"}: unit.Inactive, 35 | {"active"}: unit.Active, 36 | {"active", "inactive"}: unit.Inactive, 37 | {"active", "inactive", "failed"}: unit.Failed, 38 | {"active", "inactive", "activating"}: unit.Activating, 39 | {"active", "inactive", "deactivating"}: unit.Deactivating, 40 | {"active", "inactive", "reloading"}: unit.Reloading, 41 | } { 42 | targ.Definition.Unit.Requires = *deps 43 | assert.Equal(t, expected, targ.Active(), fmt.Sprintf("Deps: %v", *deps)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /system/transaction.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | log "github.com/Sirupsen/logrus" 8 | ) 9 | 10 | type transaction struct { 11 | unmerged map[*Unit]*prospectiveJobs 12 | merged map[*Unit]*job 13 | } 14 | 15 | type prospectiveJobs struct { 16 | anchored, optional [job_type_count]*job 17 | } 18 | 19 | func newTransaction() (tr *transaction) { 20 | log.Debugf("newTransaction") 21 | 22 | return &transaction{ 23 | unmerged: map[*Unit]*prospectiveJobs{}, 24 | merged: map[*Unit]*job{}, 25 | } 26 | } 27 | 28 | func (tr *transaction) Run() (err error) { 29 | log.WithField("transaction", tr).Debugf("tr.Run") 30 | 31 | if err = tr.merge(); err != nil { 32 | return 33 | } 34 | 35 | var ordering []*job 36 | if ordering, err = tr.order(); err != nil { 37 | return 38 | } 39 | 40 | for _, j := range ordering { 41 | if j.IsRedundant() { 42 | continue 43 | } 44 | 45 | log.Debugf("dispatching job for %s", j.unit.Name()) 46 | go j.Run() 47 | } 48 | return 49 | } 50 | 51 | // recursively adds jobs to transaction 52 | // tries to load dependencies not already present 53 | func (tr *transaction) add(typ jobType, u *Unit, parent *job, required, anchor bool) (err error) { 54 | log.WithFields(log.Fields{ 55 | "typ": typ, 56 | "u": u, 57 | "parent": parent, 58 | "required": required, 59 | "anchor": anchor, 60 | }).Debug("tr.add") 61 | // TODO: decide if these checks are necessary to do here, 62 | // as they are performed by the unit method calls already 63 | // 64 | //switch typ { 65 | //case reload: 66 | // if !u.IsReloader() { 67 | // return ErrNoReload 68 | // } 69 | //case start: 70 | // if !u.CanStart() {} 71 | //} 72 | var j *job 73 | var isNew bool 74 | 75 | if tr.unmerged[u] == nil { 76 | tr.unmerged[u] = &prospectiveJobs{} 77 | } 78 | 79 | if anchor { 80 | if j = tr.unmerged[u].anchored[typ]; j == nil { 81 | j = newJob(typ, u) 82 | log.Debugf("Created %s", j) 83 | 84 | tr.unmerged[u].anchored[typ] = j 85 | 86 | isNew = true 87 | } 88 | } else { 89 | if j = tr.unmerged[u].optional[typ]; j == nil { 90 | j = newJob(typ, u) 91 | log.Debugf("Created %s", j) 92 | 93 | tr.unmerged[u].optional[typ] = j 94 | 95 | isNew = true 96 | } 97 | } 98 | 99 | if parent != nil { 100 | if required { 101 | parent.requires.Put(j) 102 | j.requiredBy.Put(parent) 103 | } else { 104 | parent.wants.Put(j) 105 | j.wantedBy.Put(parent) 106 | } 107 | } 108 | 109 | if isNew && typ != stop { 110 | for _, name := range u.Conflicts() { 111 | dep, err := u.System.Get(name) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | if err = tr.add(stop, dep, j, true, anchor); err != nil { 117 | return err 118 | } 119 | } 120 | 121 | for _, name := range u.Requires() { 122 | dep, err := u.System.Get(name) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | if err = tr.add(start, dep, j, true, anchor); err != nil { 128 | return err 129 | } 130 | } 131 | 132 | for _, name := range u.Wants() { 133 | dep, err := u.System.Get(name) 134 | if err != nil { 135 | continue 136 | } 137 | 138 | tr.add(start, dep, j, false, false) 139 | } 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (tr *transaction) merge() (err error) { 146 | log.Debug("tr.merge") 147 | 148 | for u, prospective := range tr.unmerged { 149 | var merged *job 150 | 151 | for _, j := range prospective.anchored { 152 | if j == nil { 153 | continue 154 | } 155 | 156 | if merged == nil { 157 | merged = j 158 | } else { 159 | if err = merged.mergeWith(j); err != nil { 160 | return 161 | } 162 | } 163 | } 164 | 165 | for _, j := range prospective.optional { 166 | if j == nil { 167 | continue 168 | } 169 | 170 | if merged == nil { 171 | merged = j 172 | } else { 173 | if err = merged.mergeWith(j); err != nil { 174 | // TODO be smart when deleting unmergeable jobs 175 | tr.delete(j) 176 | } 177 | } 178 | 179 | prospective.optional[j.typ] = nil 180 | } 181 | 182 | tr.merged[u] = merged 183 | delete(tr.unmerged, u) 184 | } 185 | 186 | return nil 187 | } 188 | 189 | // TODO implement something along these lines 190 | //for _, j := range prospective { 191 | // for _, other := range prospective { 192 | // if j == other || canMerge(j.typ, other.typ) { 193 | // continue 194 | // } 195 | 196 | // switch { 197 | // case tr.anchored[j] && tr.anchored[other]: 198 | // return ErrDepConflict 199 | // case !tr.anchored[j] && !tr.anchored[other]: 200 | // // If there is an orphaned stop job - remove it 201 | // // See https://goo.gl/z8SSDy 202 | // switch { 203 | // case j.typ == stop && len(j.conflictedBy) == 0: 204 | // tr.delete(j) 205 | // case other.typ == stop && len(other.conflictedBy) == 0: 206 | // tr.delete(other) 207 | // default: 208 | // tr.delete(j) 209 | // } 210 | // case tr.anchored[j]: 211 | // tr.delete(other) 212 | // case tr.anchored[other]: 213 | // tr.delete(j) 214 | // } 215 | 216 | // } 217 | //} 218 | //break 219 | //} 220 | 221 | // deletes j from transaction 222 | // removes all references to j 223 | // recurses on orphaned and broken jobs 224 | func (tr *transaction) delete(j *job) { 225 | log.WithField("j", j).Debug("tr.delete") 226 | 227 | delete(tr.merged, j.unit) 228 | 229 | for deps, f := range map[*set]func(*job){ 230 | &j.wantedBy: func(depender *job) { 231 | delete(depender.wants, j) 232 | }, 233 | &j.requiredBy: func(depender *job) { 234 | delete(depender.requires, j) 235 | defer tr.delete(depender) 236 | }, 237 | &j.conflictedBy: func(depender *job) { 238 | delete(depender.conflicts, j) 239 | defer tr.delete(depender) 240 | }, 241 | 242 | &j.wants: func(dependency *job) { 243 | delete(dependency.wantedBy, j) 244 | if dependency.isOrphan() { 245 | defer tr.delete(dependency) 246 | } 247 | }, 248 | &j.requires: func(dependency *job) { 249 | delete(dependency.requiredBy, j) 250 | if dependency.isOrphan() { 251 | defer tr.delete(dependency) 252 | } 253 | }, 254 | &j.conflicts: func(dependency *job) { 255 | delete(dependency.conflictedBy, j) 256 | if dependency.isOrphan() { 257 | defer tr.delete(dependency) 258 | } 259 | }, 260 | } { 261 | for dep := range *deps { 262 | f(dep) 263 | } 264 | } 265 | } 266 | 267 | func canMerge(what, with jobType) (ok bool) { 268 | _, ok = mergeTable[what][with] 269 | return 270 | } 271 | 272 | func (tr *transaction) order() (ordering []*job, err error) { 273 | log.Debug("tr.order") 274 | 275 | g := newGraph() 276 | 277 | for u, j := range tr.merged { 278 | if j.typ == stop { 279 | // TODO Introduce stop job ordering(if needed) 280 | continue 281 | } 282 | 283 | log.Debugf("Checking after of %s...", j.unit.Name()) 284 | for _, depname := range u.After() { 285 | var dep *Unit 286 | if dep, err = u.System.Unit(depname); err != nil { 287 | continue 288 | } 289 | 290 | depJob, ok := tr.merged[dep] 291 | if ok { 292 | j.after.Put(depJob) 293 | depJob.before.Put(j) 294 | } 295 | } 296 | 297 | log.Debugf("Checking before of %s...", j.unit.Name()) 298 | for _, depname := range u.Before() { 299 | var dep *Unit 300 | if dep, err = u.System.Unit(depname); err != nil { 301 | continue 302 | } 303 | 304 | depJob, ok := tr.merged[dep] 305 | if ok { 306 | depJob.after.Put(j) 307 | j.before.Put(depJob) 308 | } 309 | } 310 | } 311 | 312 | g.ordering = make([]*job, 0, len(tr.merged)) 313 | for _, j := range tr.merged { 314 | if err = g.order(j); err != nil { 315 | return nil, fmt.Errorf("Dependency cycle determined:\njob for %s depends on %s", j.unit.Name(), err) 316 | } 317 | } 318 | 319 | return g.ordering, nil 320 | } 321 | 322 | type graph struct { 323 | visited, ordered set 324 | ordering []*job 325 | } 326 | 327 | func newGraph() (g *graph) { 328 | log.Debugf("newGraph") 329 | 330 | return &graph{ 331 | visited: set{}, 332 | ordered: set{}, 333 | } 334 | } 335 | 336 | var errBlank = errors.New("") 337 | 338 | func (g *graph) order(j *job) (err error) { 339 | log.WithField("j", j).Debugf("g.order") 340 | 341 | if g.ordered.Contains(j) { 342 | return nil 343 | } 344 | 345 | if g.visited.Contains(j) { 346 | return errBlank 347 | } 348 | 349 | g.visited.Put(j) 350 | 351 | for depJob := range j.after { 352 | if err = g.order(depJob); err != nil { 353 | if err == errBlank { 354 | return fmt.Errorf("%s\n", depJob.unit.Name()) 355 | } 356 | return fmt.Errorf("%v\n%s depends on %s", j.unit.Name(), j.unit.Name(), err) 357 | } 358 | } 359 | 360 | delete(g.visited, j) 361 | 362 | if !g.ordered.Contains(j) { 363 | g.ordering = append(g.ordering, j) 364 | g.ordered.Put(j) 365 | } 366 | 367 | return nil 368 | } 369 | -------------------------------------------------------------------------------- /system/unit.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | 10 | log "github.com/Sirupsen/logrus" 11 | "github.com/plasma-umass/systemgo/unit" 12 | ) 13 | 14 | var ErrIsStarting = errors.New("Unit is already starting") 15 | 16 | type Unit struct { 17 | unit.Interface 18 | 19 | // System the Unit came from 20 | System *Daemon 21 | 22 | // Unit log 23 | Log *Log 24 | 25 | name string 26 | path string 27 | load unit.Load 28 | 29 | job *job 30 | 31 | mutex sync.Mutex 32 | } 33 | 34 | // TODO introduce a better workaround 35 | const ( 36 | starting = "starting" 37 | stopping = "stopping" 38 | reloading = "reloading" 39 | ) 40 | 41 | // NewUnit returns an instance of new unit wrapping v 42 | func NewUnit(v unit.Interface) (u *Unit) { 43 | return &Unit{ 44 | Interface: v, 45 | Log: NewLog(), 46 | } 47 | } 48 | 49 | //func (u *Unit) String() string { 50 | //return u.Name() 51 | //} 52 | 53 | // Path returns path to the defintion unit was loaded from 54 | func (u *Unit) Path() string { 55 | return u.path 56 | } 57 | 58 | // Name returns the name of the unit(filename of the defintion) 59 | func (u *Unit) Name() string { 60 | return u.name 61 | } 62 | 63 | // Loaded returns load state of the unit 64 | func (u *Unit) Loaded() unit.Load { 65 | return u.load 66 | } 67 | 68 | func (u *Unit) IsDead() bool { 69 | return u.Active() == unit.Inactive 70 | } 71 | func (u *Unit) IsActive() bool { 72 | return u.Active() == unit.Active 73 | } 74 | func (u *Unit) IsActivating() bool { 75 | return u.Active() == unit.Activating 76 | } 77 | func (u *Unit) IsDeactivating() bool { 78 | return u.Active() == unit.Deactivating 79 | } 80 | func (u *Unit) IsReloading() bool { 81 | return u.Active() == unit.Reloading 82 | } 83 | 84 | func (u *Unit) IsLoaded() bool { 85 | return u.Loaded() == unit.Loaded 86 | } 87 | 88 | // IsReloader returns whether u.Interface is capable of reloading 89 | func (u *Unit) IsReloader() (ok bool) { 90 | _, ok = u.Interface.(unit.Reloader) 91 | return 92 | } 93 | 94 | func (u *Unit) Active() (st unit.Activation) { 95 | if u.jobRunning() { 96 | switch u.job.typ { 97 | case start: 98 | return unit.Activating 99 | case stop: 100 | return unit.Deactivating 101 | case reload: 102 | return unit.Reloading 103 | } 104 | } 105 | 106 | return u.Interface.Active() 107 | } 108 | 109 | func (u *Unit) Sub() string { 110 | if u.jobRunning() { 111 | switch u.job.typ { 112 | case start: 113 | return starting 114 | case stop: 115 | return stopping 116 | case reload: 117 | return reloading 118 | } 119 | } 120 | 121 | return u.Interface.Sub() 122 | } 123 | 124 | func (u *Unit) jobRunning() bool { 125 | return u.job != nil && u.job.IsRunning() 126 | } 127 | 128 | // Status returns status of the unit 129 | func (u *Unit) Status() unit.Status { 130 | st := unit.Status{ 131 | Load: unit.LoadStatus{ 132 | Path: u.Path(), 133 | Loaded: u.Loaded(), 134 | State: -1, // TODO 135 | }, 136 | Activation: unit.ActivationStatus{ 137 | State: u.Active(), 138 | Sub: u.Sub(), 139 | }, 140 | } 141 | 142 | var err error 143 | if st.Log, err = ioutil.ReadAll(u.Log); err != nil { 144 | u.Log.Errorf("Error reading log: %s", err) 145 | } 146 | 147 | // TODO deal with different unit types requiring different status 148 | // something like u.Interface.HasX() ? 149 | switch u.Interface.(type) { 150 | //case *unit.Service: 151 | //return unit.ServiceStatus{st} 152 | default: 153 | return st 154 | } 155 | } 156 | 157 | // Requires returns a slice of unit names as found in definition and absolute paths 158 | // of units symlinked in units '.wants' directory 159 | func (u *Unit) Requires() (names []string) { 160 | names = u.Interface.Requires() 161 | 162 | if paths, err := readDepDir(u.requiresDir()); err == nil { 163 | names = append(names, paths...) 164 | } 165 | 166 | return 167 | } 168 | 169 | // Wants returns a slice of unit names as found in definition and absolute paths 170 | // of units symlinked in units '.wants' directory 171 | func (u *Unit) Wants() (names []string) { 172 | names = u.Interface.Wants() 173 | 174 | if paths, err := readDepDir(u.wantsDir()); err == nil { 175 | names = append(names, paths...) 176 | } 177 | 178 | return 179 | } 180 | 181 | func (u *Unit) wantsDir() (path string) { 182 | return u.depDir("wants") 183 | } 184 | 185 | func (u *Unit) requiresDir() (path string) { 186 | return u.depDir("requires") 187 | } 188 | 189 | func (u *Unit) depDir(suffix string) (path string) { 190 | return u.Path() + "." + suffix 191 | } 192 | 193 | // Enable creates symlinks to u definition in dependency directories of each unit dependant on u 194 | func (u *Unit) Enable() (err error) { 195 | err = u.System.getAndExecute(u.RequiredBy(), func(dep *Unit, gerr error) error { 196 | if gerr != nil { 197 | return gerr 198 | } 199 | 200 | return dep.addRequiresDep(u) 201 | }) 202 | if err != nil { 203 | return 204 | } 205 | 206 | return u.System.getAndExecute(u.WantedBy(), func(dep *Unit, gerr error) error { 207 | if gerr != nil { 208 | return gerr 209 | } 210 | 211 | return dep.addWantsDep(u) 212 | }) 213 | } 214 | 215 | func (u *Unit) addWantsDep(dep *Unit) (err error) { 216 | return linkDep(u.wantsDir(), dep) 217 | } 218 | 219 | func (u *Unit) addRequiresDep(dep *Unit) (err error) { 220 | return linkDep(u.requiresDir(), dep) 221 | } 222 | 223 | func linkDep(dir string, dep *Unit) (err error) { 224 | if err = os.Mkdir(dir, 0755); err != nil && !os.IsExist(err) { 225 | return err 226 | } 227 | 228 | return os.Symlink(dep.Path(), filepath.Join(dir, dep.Name())) 229 | } 230 | 231 | // Disable removes symlinks(if they exist) created by Enable 232 | func (u *Unit) Disable() (err error) { 233 | err = u.System.getAndExecute(u.RequiredBy(), func(dep *Unit, gerr error) error { 234 | if gerr != nil { 235 | return gerr 236 | } 237 | 238 | return dep.removeRequiresDep(u) 239 | }) 240 | if err != nil { 241 | return 242 | } 243 | 244 | return u.System.getAndExecute(u.WantedBy(), func(dep *Unit, gerr error) error { 245 | if gerr != nil { 246 | return gerr 247 | } 248 | 249 | return dep.removeWantsDep(u) 250 | }) 251 | } 252 | 253 | func (u *Unit) removeWantsDep(dep *Unit) (err error) { 254 | return unlinkDep(u.wantsDir(), dep) 255 | } 256 | 257 | func (u *Unit) removeRequiresDep(dep *Unit) (err error) { 258 | return unlinkDep(u.requiresDir(), dep) 259 | } 260 | 261 | func unlinkDep(dir string, dep *Unit) (err error) { 262 | if err = os.Remove(filepath.Join(dir, dep.Name())); err != nil && !os.IsNotExist(err) { 263 | return 264 | } 265 | return nil 266 | } 267 | 268 | // Reload creates a new reload transaction and runs it 269 | func (u *Unit) Reload() (err error) { 270 | log.WithField("u", u).Debugf("u.Reload") 271 | 272 | tr := newTransaction() 273 | if err = tr.add(reload, u, nil, true, true); err != nil { 274 | return 275 | } 276 | return tr.Run() 277 | } 278 | 279 | func (u *Unit) reload() (err error) { 280 | log.WithField("u", u).Debugf("u.reload") 281 | 282 | reloader, ok := u.Interface.(unit.Reloader) 283 | if !ok { 284 | return ErrNoReload 285 | } 286 | return reloader.Reload() 287 | } 288 | 289 | // Start creates a new start transaction and runs it 290 | func (u *Unit) Start() (err error) { 291 | log.WithField("unit", u.Name()).Debugf("u.Start") 292 | 293 | tr := newTransaction() 294 | if err = tr.add(start, u, nil, true, true); err != nil { 295 | return 296 | } 297 | return tr.Run() 298 | } 299 | 300 | func (u *Unit) start() (err error) { 301 | e := log.WithField("unit", u.Name()) 302 | e.Debugf("u.start") 303 | 304 | if !u.IsLoaded() { 305 | e.Debug("not loaded") 306 | return ErrNotLoaded 307 | } 308 | 309 | u.Log.Println("Starting...") 310 | 311 | starter, ok := u.Interface.(unit.Starter) 312 | if !ok { 313 | e.Debugf("Interface is not unit.Starter") 314 | return nil 315 | } 316 | 317 | e.Debugf("Interface.Start") 318 | return starter.Start() 319 | } 320 | 321 | // Stop creates a new stop transaction and runs it 322 | func (u *Unit) Stop() (err error) { 323 | log.WithField("u", u).Debugf("u.Stop") 324 | 325 | tr := newTransaction() 326 | if err = tr.add(stop, u, nil, true, true); err != nil { 327 | return 328 | } 329 | return tr.Run() 330 | } 331 | 332 | func (u *Unit) stop() (err error) { 333 | log.WithField("u", u).Debugf("u.stop") 334 | 335 | if !u.IsLoaded() { 336 | return ErrNotLoaded 337 | } 338 | 339 | u.Log.Println("Stopping...") 340 | 341 | stopper, ok := u.Interface.(unit.Stopper) 342 | if !ok { 343 | return nil 344 | } 345 | 346 | return stopper.Stop() 347 | } 348 | 349 | func readDepDir(dir string) (paths []string, err error) { 350 | var links []string 351 | if links, err = pathset(dir); err != nil { 352 | return 353 | } 354 | 355 | paths = make([]string, 0, len(links)) 356 | for _, path := range links { 357 | if path, err = filepath.EvalSymlinks(path); err != nil { 358 | return 359 | } 360 | paths = append(paths, path) 361 | } 362 | return 363 | } 364 | -------------------------------------------------------------------------------- /system/unit_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/plasma-umass/systemgo/test/mock_unit" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | var deps = struct { 15 | defined, onDisk []string 16 | }{ 17 | []string{"foo.service", "bar.service"}, 18 | []string{"test.service", "test.target"}, 19 | } 20 | 21 | func init() { 22 | for i, name := range deps.onDisk { 23 | deps.onDisk[i] = filepath.Join(os.TempDir(), name) 24 | } 25 | } 26 | 27 | func TestDeps(t *testing.T) { 28 | ctrl := gomock.NewController(t) 29 | defer ctrl.Finish() 30 | 31 | m := mock_unit.NewMockInterface(ctrl) 32 | m.EXPECT().Wants().Return(deps.defined).Times(1) 33 | m.EXPECT().Requires().Return(deps.defined).Times(1) 34 | 35 | u := NewUnit(m) 36 | u.path = filepath.Join(os.TempDir(), "testDeps") 37 | 38 | for _, suffix := range []string{".requires", ".wants"} { 39 | dirpath := u.path + suffix 40 | require.NoError(t, os.Mkdir(dirpath, 0755)) 41 | defer os.RemoveAll(dirpath) 42 | 43 | for _, name := range append(deps.onDisk[:], "wrong.unittype", "foo.bar") { 44 | f, err := os.Create(name) 45 | defer os.Remove(name) 46 | 47 | require.NoError(t, err) 48 | require.NoError(t, f.Close()) 49 | require.NoError(t, os.Symlink(name, filepath.Join(dirpath, filepath.Base(name)))) 50 | } 51 | } 52 | 53 | expected := append(deps.defined[:], deps.onDisk[:]...) 54 | 55 | for _, deps := range [][]string{u.Wants(), u.Requires()} { 56 | for _, dep := range deps { 57 | assert.Contains(t, expected, dep) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /systemctl/cli/list-units.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Romans Volosatovs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cli 22 | 23 | import ( 24 | "fmt" 25 | "os" 26 | "text/tabwriter" 27 | 28 | log "github.com/Sirupsen/logrus" 29 | "github.com/plasma-umass/systemgo/systemctl" 30 | "github.com/plasma-umass/systemgo/unit" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | // list-unitsCmd represents the list-units command 35 | var listUnitsCmd = &cobra.Command{ 36 | Use: "list-units", 37 | Short: "list units", 38 | Long: `list units lists all units known to systemgo`, 39 | Run: func(cmd *cobra.Command, args []string) { 40 | var resp systemctl.Response 41 | if err := client.Call("Server.StatusAll", args, &resp); err != nil { 42 | log.Error(err) 43 | } 44 | 45 | if resp.Yield != nil { 46 | w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0) 47 | fmt.Fprintln(w, "unit\tload\tactive\tsub") 48 | for name, st := range resp.Yield.(map[string]unit.Status) { 49 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n", 50 | name, st.Load.Loaded, st.Activation.State, st.Activation.Sub) 51 | } 52 | 53 | if err := w.Flush(); err != nil { 54 | log.Error(err) 55 | } 56 | } 57 | }, 58 | } 59 | 60 | func init() { 61 | RootCmd.AddCommand(listUnitsCmd) 62 | } 63 | -------------------------------------------------------------------------------- /systemctl/cli/root.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Romans Volosatovs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cli 22 | 23 | import ( 24 | "fmt" 25 | "net/rpc" 26 | "os" 27 | 28 | log "github.com/Sirupsen/logrus" 29 | 30 | "github.com/plasma-umass/systemgo/config" 31 | "github.com/spf13/cobra" 32 | ) 33 | 34 | var client *rpc.Client 35 | 36 | var cfgFile string 37 | 38 | // RootCmd represents the base command when called without any subcommands 39 | var RootCmd = &cobra.Command{ 40 | Use: "systemctl", 41 | Short: "Query or send control commands to the systemgo manager", 42 | Long: `TODO: add description`, 43 | Run: listUnitsCmd.Run, 44 | } 45 | 46 | // Execute adds all child commands to the root command sets flags appropriately. 47 | // This is called by main.main(). It only needs to happen once to the rootCmd. 48 | func Execute() { 49 | if err := RootCmd.Execute(); err != nil { 50 | fmt.Println(err) 51 | os.Exit(-1) 52 | } 53 | } 54 | 55 | func init() { 56 | addr := fmt.Sprintf("localhost%s", config.Port) 57 | 58 | e := log.WithField("addr", addr) 59 | e.Debugf("Dialing...") 60 | 61 | var err error 62 | if client, err = rpc.DialHTTP("tcp", addr); err != nil { 63 | e.Fatalf("Dial failed: %s", err) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /systemctl/cli/start.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Romans Volosatovs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cli 22 | 23 | import ( 24 | log "github.com/Sirupsen/logrus" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | // startCmd represents the start command 30 | var startCmd = &cobra.Command{ 31 | Use: "start", 32 | Short: "Start (activate) one or more units", 33 | Long: `TODO: add description`, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | if err := client.Call("Server.Start", args, nil); err != nil { 36 | log.Error(err) 37 | } 38 | }, 39 | } 40 | 41 | func init() { 42 | RootCmd.AddCommand(startCmd) 43 | 44 | // Here you will define your flags and configuration settings. 45 | 46 | // Cobra supports Persistent Flags which will work for this command 47 | // and all subcommands, e.g.: 48 | // startCmd.PersistentFlags().String("foo", "", "A help for foo") 49 | 50 | // Cobra supports local flags which will only run when this command 51 | // is called directly, e.g.: 52 | // startCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 53 | 54 | } 55 | -------------------------------------------------------------------------------- /systemctl/cli/status.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Romans Volosatovs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cli 22 | 23 | import ( 24 | "fmt" 25 | 26 | "github.com/plasma-umass/systemgo/systemctl" 27 | "github.com/plasma-umass/systemgo/unit" 28 | "github.com/spf13/cobra" 29 | 30 | log "github.com/Sirupsen/logrus" 31 | ) 32 | 33 | // statusCmd represents the status command 34 | var statusCmd = &cobra.Command{ 35 | Use: "status", 36 | Short: "Show runtime status of one or more units", 37 | Long: `TODO: add description`, 38 | Run: func(cmd *cobra.Command, args []string) { 39 | var resp systemctl.Response 40 | if err := client.Call("Server.Status", args, &resp); err != nil { 41 | log.Error(err) 42 | } 43 | 44 | if resp.Yield != nil { 45 | for _, st := range resp.Yield.(map[string]unit.Status) { 46 | fmt.Println(st) 47 | } 48 | } 49 | }, 50 | } 51 | 52 | func init() { 53 | RootCmd.AddCommand(statusCmd) 54 | 55 | // Here you will define your flags and configuration settings. 56 | 57 | // Cobra supports Persistent Flags which will work for this command 58 | // and all subcommands, e.g.: 59 | // statusCmd.PersistentFlags().String("foo", "", "A help for foo") 60 | 61 | // Cobra supports local flags which will only run when this command 62 | // is called directly, e.g.: 63 | // statusCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 64 | 65 | } 66 | -------------------------------------------------------------------------------- /systemctl/cli/stop.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Romans Volosatovs 2 | // 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package cli 22 | 23 | import ( 24 | "log" 25 | 26 | "github.com/spf13/cobra" 27 | ) 28 | 29 | // stopCmd represents the stop command 30 | var stopCmd = &cobra.Command{ 31 | Use: "stop", 32 | Short: "Stop (deactivate) one or more units", 33 | Long: `TODO: add description`, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | if err := client.Call("Server.Stop", args, nil); err != nil { 36 | log.Fatalln(err.Error()) 37 | } 38 | }, 39 | } 40 | 41 | func init() { 42 | RootCmd.AddCommand(stopCmd) 43 | 44 | // Here you will define your flags and configuration settings. 45 | 46 | // Cobra supports Persistent Flags which will work for this command 47 | // and all subcommands, e.g.: 48 | // stopCmd.PersistentFlags().String("foo", "", "A help for foo") 49 | 50 | // Cobra supports local flags which will only run when this command 51 | // is called directly, e.g.: 52 | // stopCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 53 | 54 | } 55 | -------------------------------------------------------------------------------- /systemctl/interfaces.go: -------------------------------------------------------------------------------- 1 | package systemctl 2 | 3 | import ( 4 | "github.com/plasma-umass/systemgo/system" 5 | "github.com/plasma-umass/systemgo/unit" 6 | ) 7 | 8 | type Daemon interface { 9 | Start(...string) error 10 | Stop(...string) error 11 | Isolate(...string) error 12 | Restart(...string) error 13 | Reload(...string) error 14 | Enable(...string) error 15 | Disable(...string) error 16 | 17 | Units() []*system.Unit 18 | Status() (system.Status, error) 19 | StatusOf(string) (unit.Status, error) 20 | IsEnabled(string) (unit.Enable, error) 21 | IsActive(string) (unit.Activation, error) 22 | } 23 | -------------------------------------------------------------------------------- /systemctl/rpc.go: -------------------------------------------------------------------------------- 1 | package systemctl 2 | 3 | import ( 4 | "encoding/gob" 5 | "fmt" 6 | 7 | "github.com/plasma-umass/systemgo/unit" 8 | ) 9 | 10 | type Response struct { 11 | Yield interface{} 12 | } 13 | 14 | func init() { 15 | gob.Register(map[string]unit.Status{}) 16 | } 17 | 18 | func newResponse() (resp *Response) { 19 | return &Response{ 20 | Yield: map[string]fmt.Stringer{}, 21 | } 22 | } 23 | 24 | func NewServer(sys Daemon) (sv *Server) { 25 | return &Server{sys} 26 | } 27 | 28 | type Server struct { 29 | sys Daemon 30 | } 31 | 32 | func (sv *Server) Start(names []string, resp *Response) (err error) { 33 | return sv.sys.Start(names...) 34 | } 35 | 36 | func (sv *Server) Stop(names []string, resp *Response) (err error) { 37 | return sv.sys.Stop(names...) 38 | } 39 | 40 | func (sv *Server) Restart(names []string, resp *Response) (err error) { 41 | return sv.sys.Restart(names...) 42 | } 43 | 44 | func (sv *Server) Isolate(names []string, resp *Response) (err error) { 45 | return sv.sys.Isolate(names...) 46 | } 47 | 48 | func (sv *Server) Reload(names []string, resp *Response) (err error) { 49 | return sv.sys.Reload(names...) 50 | } 51 | 52 | func (sv *Server) Enable(names []string, resp *Response) (err error) { 53 | return sv.sys.Enable(names...) 54 | } 55 | 56 | func (sv *Server) Disable(names []string, resp *Response) (err error) { 57 | return sv.sys.Disable(names...) 58 | } 59 | 60 | func (sv *Server) Status(names []string, resp *Response) (err error) { 61 | *resp = *newResponse() 62 | 63 | statuses := map[string]unit.Status{} 64 | 65 | for _, name := range names { 66 | var st unit.Status 67 | if st, err = sv.sys.StatusOf(name); err != nil { 68 | continue 69 | } 70 | 71 | statuses[name] = st 72 | } 73 | 74 | resp.Yield = statuses 75 | return err 76 | } 77 | 78 | func (sv *Server) StatusAll(names []string, resp *Response) (err error) { 79 | units := sv.sys.Units() 80 | 81 | names = make([]string, 0, len(units)) 82 | for _, u := range units { 83 | names = append(names, u.Name()) 84 | } 85 | return sv.Status(names, resp) 86 | } 87 | -------------------------------------------------------------------------------- /systemgo.yaml: -------------------------------------------------------------------------------- 1 | target: default.target 2 | paths: 3 | - /etc/systemd/system 4 | - /run/systemd/system 5 | - /lib/systemd/system 6 | 7 | port: 8008 8 | retry: 5 9 | 10 | debug: true 11 | -------------------------------------------------------------------------------- /unit/automount/sub.go: -------------------------------------------------------------------------------- 1 | package automount 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Waiting 9 | Running 10 | Failed 11 | ) 12 | -------------------------------------------------------------------------------- /unit/busname/sub.go: -------------------------------------------------------------------------------- 1 | package busname 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Making 9 | Registered 10 | Listening 11 | Running 12 | Sigterm 13 | Sigkill 14 | Failed 15 | ) 16 | -------------------------------------------------------------------------------- /unit/definition.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/coreos/go-systemd/unit" 11 | ) 12 | 13 | // Definition of a unit matching the fields found in unit-file 14 | type Definition struct { 15 | Unit struct { 16 | Description string 17 | Documentation string 18 | Wants, Requires, Conflicts, Before, After []string 19 | } 20 | Install struct { 21 | WantedBy, RequiredBy []string 22 | } 23 | } 24 | 25 | // Description returns a string as found in Definition 26 | func (def Definition) Description() string { 27 | return def.Unit.Description 28 | } 29 | 30 | // Documentation returns a string as found in Definition 31 | func (def Definition) Documentation() string { 32 | return def.Unit.Documentation 33 | } 34 | 35 | // Wants returns a slice of unit names as found in Definition 36 | func (def Definition) Wants() []string { 37 | return def.Unit.Wants 38 | } 39 | 40 | // Requires returns a slice of unit names as found in Definition 41 | func (def Definition) Requires() []string { 42 | return def.Unit.Requires 43 | } 44 | 45 | // Conflicts returns a slice of unit names as found in Definition 46 | func (def Definition) Conflicts() []string { 47 | return def.Unit.Conflicts 48 | } 49 | 50 | // After returns a slice of unit names as found in Definition 51 | func (def Definition) After() []string { 52 | return def.Unit.After 53 | } 54 | 55 | // Before returns a slice of unit names as found in Definition 56 | func (def Definition) Before() []string { 57 | return def.Unit.Before 58 | } 59 | 60 | // RequiredBy returns a slice of unit names as found in Definition 61 | func (def Definition) RequiredBy() []string { 62 | return def.Install.RequiredBy 63 | } 64 | 65 | // WantedBy returns a slice of unit names as found in Definition 66 | func (def Definition) WantedBy() []string { 67 | return def.Install.WantedBy 68 | } 69 | 70 | // ParseDefinition parses the data in Systemd unit-file format and stores the result in value pointed by Definition 71 | func ParseDefinition(r io.Reader, v interface{}) (err error) { 72 | // Access the underlying value of the pointer 73 | def := reflect.ValueOf(v).Elem() 74 | 75 | if !def.IsValid() || !def.CanSet() { 76 | return ErrWrongVal 77 | } 78 | 79 | // Deserialized options 80 | var opts []*unit.UnitOption 81 | if opts, err = unit.Deserialize(r); err != nil { 82 | return 83 | } 84 | 85 | // Loop over deserialized options trying to match them to the ones as found in Definition 86 | for _, opt := range opts { 87 | if v := def.FieldByName(opt.Section); v.IsValid() && v.CanSet() { 88 | if v := v.FieldByName(opt.Name); v.IsValid() && v.CanSet() { 89 | // reflect.Kind of field in Definition 90 | switch v.Kind() { 91 | 92 | case reflect.String: 93 | v.SetString(opt.Value) 94 | 95 | case reflect.Bool: 96 | if opt.Value == "yes" { 97 | v.SetBool(true) 98 | } else if opt.Value != "no" { 99 | return ParseErr(opt.Name, errors.New(`Value should be "yes" or "no"`)) 100 | } 101 | 102 | case reflect.Slice: 103 | if _, ok := v.Interface().([]string); ok { // []string 104 | v.Set(reflect.ValueOf(strings.Fields(opt.Value))) 105 | 106 | } else if _, ok := v.Interface().([]int); ok { // []int 107 | ints := []int{} 108 | for _, val := range strings.Fields(opt.Value) { 109 | if converted, err := strconv.Atoi(val); err == nil { 110 | ints = append(ints, converted) 111 | } else { 112 | return ParseErr(opt.Name, err) 113 | } 114 | } 115 | v.Set(reflect.ValueOf(ints)) 116 | } 117 | 118 | default: 119 | return ParseErr(opt.Name, ErrUnknownType) 120 | } 121 | } else { 122 | return ParseErr(opt.Name, ErrNotExist) 123 | } 124 | } else { 125 | return ParseErr(opt.Name, ErrNotExist) 126 | } 127 | } 128 | return 129 | } 130 | -------------------------------------------------------------------------------- /unit/definition_test.go: -------------------------------------------------------------------------------- 1 | package unit_test 2 | 3 | import ( 4 | "io" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/plasma-umass/systemgo/unit" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var DEFAULT_INTS = []int{1, 2, 3} 14 | 15 | const DEFAULT_BOOL = true 16 | const DEFAULT_UNIT = `[Unit] 17 | Description=Description 18 | Documentation=Documentation 19 | 20 | Wants=Wants 21 | Requires=Requires 22 | Conflicts=Conflicts 23 | Before=Before 24 | After=After 25 | 26 | [Install] 27 | WantedBy=WantedBy 28 | RequiredBy=RequiredBy` 29 | 30 | func TestParseDefinition(t *testing.T) { 31 | cases := []struct { 32 | def interface{} 33 | correct bool 34 | contents io.Reader 35 | }{ 36 | {&unit.Definition{}, true, 37 | strings.NewReader(DEFAULT_UNIT), 38 | }, 39 | {&unit.Definition{}, false, 40 | strings.NewReader(DEFAULT_UNIT + ` 41 | Wrong=Field 42 | Test=should fail`), 43 | }, 44 | {&struct { 45 | unit.Definition 46 | Test struct { 47 | Ints []int 48 | Bool bool 49 | } 50 | }{}, true, 51 | strings.NewReader(DEFAULT_UNIT + ` 52 | [Test] 53 | Ints=1 2 3 54 | Bool=yes`), 55 | }, 56 | {&struct { 57 | unit.Definition 58 | Test struct { 59 | Ints []int 60 | Bool bool 61 | } 62 | }{}, false, 63 | strings.NewReader(DEFAULT_UNIT + ` 64 | [Test] 65 | Ints=1 2 3 66 | Bool=foo`), 67 | }, 68 | {&struct { 69 | unit.Definition 70 | Test struct { 71 | Ints []int 72 | Bool bool 73 | } 74 | }{}, false, 75 | strings.NewReader(DEFAULT_UNIT + ` 76 | [Test] 77 | Ints=a b 3 78 | Bool=foo`), 79 | }, 80 | } 81 | 82 | for _, c := range cases { 83 | err := unit.ParseDefinition(c.contents, c.def) 84 | if !c.correct { 85 | assert.Error(t, err, "ParseDefinition") 86 | continue 87 | } 88 | if !assert.NoError(t, err, "ParseDefinition") { 89 | continue 90 | } 91 | 92 | defVal := reflect.ValueOf(c.def).Elem() 93 | for i := 0; i < defVal.NumField(); i++ { 94 | 95 | section := defVal.Field(i) 96 | sectionType := section.Type() 97 | 98 | for j := 0; j < section.NumField(); j++ { 99 | option := struct { 100 | reflect.Value 101 | Name string 102 | }{ 103 | section.Field(j), 104 | sectionType.Field(j).Name, 105 | } 106 | 107 | switch option.Kind() { 108 | case reflect.String: 109 | assert.Equal(t, option.String(), option.Name, "string") 110 | 111 | m := methodByName(defVal, option.Name).(func() string) 112 | assert.Equal(t, m(), option.Name, "string getter") 113 | 114 | case reflect.Bool: 115 | assert.Equal(t, option.Bool(), DEFAULT_BOOL, "bool") 116 | 117 | // Workaround for the non-existent bool getter 118 | if defVal.MethodByName(option.Name).IsValid() { 119 | m := methodByName(defVal, option.Name).(func() bool) 120 | assert.Equal(t, m(), DEFAULT_BOOL, "bool getter") 121 | } 122 | case reflect.Slice: 123 | if slice, ok := interfaceOf(option.Value).([]string); ok { 124 | expect := []string{option.Name} 125 | assert.Equal(t, slice, expect, "[]string") 126 | 127 | m := methodByName(defVal, option.Name).(func() []string) 128 | assert.Equal(t, m(), expect, "[]string getter") 129 | 130 | } else if slice, ok := interfaceOf(option.Value).([]int); ok { 131 | assert.Equal(t, slice, DEFAULT_INTS, "[]int") 132 | 133 | // Workaround for the non-existent []int getter 134 | if defVal.MethodByName(option.Name).IsValid() { 135 | m := methodByName(defVal, option.Name).(func() []string) 136 | assert.Equal(t, m(), DEFAULT_INTS, "[]int getter") 137 | } 138 | } 139 | } 140 | } 141 | } 142 | } 143 | } 144 | 145 | func interfaceOf(val reflect.Value) interface{} { 146 | return val.Interface() 147 | } 148 | 149 | func methodByName(val reflect.Value, name string) interface{} { 150 | return interfaceOf(val.MethodByName(name)) 151 | } 152 | -------------------------------------------------------------------------------- /unit/device/sub.go: -------------------------------------------------------------------------------- 1 | package busname 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Tentative 9 | Plugged 10 | ) 11 | -------------------------------------------------------------------------------- /unit/errors.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | var ErrNotSet = errors.New("Field not specified") 9 | var ErrNotExist = errors.New("Does not exist") 10 | var ErrNotSupported = errors.New("Not supported") 11 | var ErrUnknownType = errors.New("Unknown type") 12 | var ErrPathNotAbs = errors.New("Path specified is not absolute") 13 | var ErrNotParsed = errors.New("Unit definition is not parsed properly") 14 | var ErrWrongVal = errors.New("Wrong value received") 15 | var ErrNotStarted = errors.New("Unit not started") 16 | 17 | type ParseError struct { 18 | Source string 19 | Err error 20 | } 21 | 22 | func ParseErr(source string, err error) ParseError { 23 | return ParseError{ 24 | Source: source, 25 | Err: err, 26 | } 27 | } 28 | 29 | func (err ParseError) Error() string { 30 | return fmt.Sprintf("%s: %s", err.Source, err.Err) 31 | } 32 | 33 | type MultiError []error 34 | 35 | func (m MultiError) Errors() (errs []string) { 36 | errs = make([]string, len(m)) 37 | for i, err := range m { 38 | errs[i] = err.Error() 39 | } 40 | return 41 | } 42 | 43 | func (m MultiError) Error() string { 44 | if len(m) == 0 { 45 | return "No errors" 46 | } 47 | return fmt.Sprintf("%d errors encountered, first: %s", len(m), m[0]) 48 | } 49 | -------------------------------------------------------------------------------- /unit/errors_test.go: -------------------------------------------------------------------------------- 1 | package unit_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/plasma-umass/systemgo/unit" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | var ErrTest = errors.New("test") 13 | var source = "test" 14 | var expected = fmt.Sprintf("%s: %s", source, ErrTest) 15 | 16 | func TestParseErr(t *testing.T) { 17 | pe := unit.ParseErr(source, ErrTest) 18 | 19 | assert.Equal(t, pe.Source, "test", "pe.Source") 20 | assert.Equal(t, pe.Err, ErrTest, "pe.Err") 21 | assert.EqualError(t, pe, expected) 22 | } 23 | 24 | var errCount = 5 25 | 26 | func TestMultiErr(t *testing.T) { 27 | me := unit.MultiError{} 28 | 29 | for i := 0; i < errCount; i++ { 30 | me = append(me, ErrTest) 31 | } 32 | 33 | assert.Len(t, me, errCount) 34 | 35 | for i, msg := range me.Errors() { 36 | assert.EqualError(t, me[i], msg) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /unit/interfaces.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import "io" 4 | 5 | type Interface interface { 6 | Definer 7 | Subber 8 | 9 | Description() string 10 | Documentation() string 11 | 12 | Dependency 13 | } 14 | 15 | type Definer interface { 16 | Define(io.Reader) error 17 | } 18 | 19 | // Subber is implemented by any value that has Sub and Active methods 20 | type Subber interface { 21 | Active() Activation 22 | Sub() string 23 | } 24 | 25 | // StartStopper is implemented by any value that has Start and Stop methods 26 | type StartStopper interface { 27 | Starter 28 | Stopper 29 | } 30 | 31 | // Starter is implemented by any value that has a Start method 32 | type Starter interface { 33 | Start() error 34 | } 35 | 36 | // Stopper is implemented by any value that has a Stop method 37 | type Stopper interface { 38 | Stop() error 39 | } 40 | 41 | // Reloader is implemented by any value capable of reloading itself(or its definition) 42 | type Reloader interface { 43 | Reload() error 44 | } 45 | 46 | type Dependency interface { 47 | Wants() []string 48 | Requires() []string 49 | 50 | Conflicts() []string 51 | 52 | RequiredBy() []string 53 | WantedBy() []string 54 | 55 | After() []string 56 | Before() []string 57 | } 58 | -------------------------------------------------------------------------------- /unit/mount/sub.go: -------------------------------------------------------------------------------- 1 | package mount 2 | 3 | // Status of mount units -- https://goo.gl/vg6p7Q 4 | type Sub int 5 | 6 | //go:generate stringer -type=Sub sub.go 7 | const ( 8 | Dead Sub = iota 9 | Mounting 10 | MountingDone 11 | Mounted 12 | Remounting 13 | Unmounting 14 | MountingSigterm 15 | MountingSigkill 16 | RemountingSigterm 17 | RemountingSigkill 18 | UnmountingSigterm 19 | UnmountingSigkill 20 | Failed 21 | ) 22 | -------------------------------------------------------------------------------- /unit/package.go: -------------------------------------------------------------------------------- 1 | // Package unit defines unit types, which system.Interface can supervise 2 | package unit 3 | -------------------------------------------------------------------------------- /unit/path/sub.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Waiting 9 | Running 10 | Failed 11 | ) 12 | -------------------------------------------------------------------------------- /unit/scope/sub.go: -------------------------------------------------------------------------------- 1 | package scope 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Running 9 | Abandoned 10 | StopSigterm 11 | StopSigkill 12 | Failed 13 | ) 14 | -------------------------------------------------------------------------------- /unit/service/service.go: -------------------------------------------------------------------------------- 1 | // Package service defines a service unit type 2 | package service 3 | 4 | import ( 5 | "io" 6 | "os/exec" 7 | "strings" 8 | 9 | "github.com/plasma-umass/systemgo/unit" 10 | 11 | log "github.com/Sirupsen/logrus" 12 | ) 13 | 14 | const DEFAULT_TYPE = "simple" 15 | 16 | const ( 17 | dead = "dead" 18 | startPre = "startPre" 19 | start = "start" 20 | startPost = "startPost" 21 | running = "running" 22 | exited = "exited" // not running anymore, but RemainAfterExit true for this unit 23 | reload = "reload" 24 | stop = "stop" 25 | stopSigabrt = "stopSigabrt" // watchdog timeout 26 | stopSigterm = "stopSigterm" 27 | stopSigkill = "stopSigkill" 28 | stopPost = "stopPost" 29 | finalSigterm = "finalSigterm" 30 | finalSigkill = "finalSigkill" 31 | failed = "failed" 32 | autoRestart = "autoRestart" 33 | ) 34 | 35 | var supported = map[string]bool{ 36 | "oneshot": true, 37 | "simple": true, 38 | "forking": false, 39 | "dbus": false, 40 | "notify": false, 41 | "idle": false, 42 | } 43 | 44 | // Service unit 45 | type Unit struct { 46 | Definition 47 | *exec.Cmd 48 | } 49 | 50 | // Service unit definition 51 | type Definition struct { 52 | unit.Definition 53 | Service struct { 54 | Type string 55 | ExecStart, ExecStop, ExecReload string 56 | //Restart string 57 | //RestartSec int 58 | RemainAfterExit bool 59 | WorkingDirectory string 60 | //PIDFile string 61 | } 62 | } 63 | 64 | func Supported(typ string) (is bool) { 65 | return supported[typ] 66 | } 67 | 68 | //func (sv *Unit) String() string { 69 | //return sv.Service.ExecStart 70 | //} 71 | 72 | // Define attempts to fill the sv definition by parsing r 73 | func (sv *Unit) Define(r io.Reader /*, errch chan<- error*/) (err error) { 74 | log.WithField("r", r).Debugf("sv.Define") 75 | 76 | def := Definition{} 77 | def.Service.Type = DEFAULT_TYPE 78 | 79 | if err = unit.ParseDefinition(r, &def); err != nil { 80 | return 81 | } 82 | 83 | merr := unit.MultiError{} 84 | 85 | // Check definition for errors 86 | switch { 87 | case def.Service.ExecStart == "": 88 | merr = append(merr, unit.ParseErr("ExecStart", unit.ErrNotSet)) 89 | 90 | case !Supported(def.Service.Type): 91 | merr = append(merr, unit.ParseErr("Type", unit.ParseErr(def.Service.Type, unit.ErrNotSupported))) 92 | } 93 | 94 | if len(merr) > 0 { 95 | return merr 96 | } 97 | 98 | sv.Definition = def 99 | 100 | cmd := strings.Fields(def.Service.ExecStart) 101 | sv.Cmd = exec.Command(cmd[0], cmd[1:]...) 102 | sv.Cmd.Dir = sv.Definition.Service.WorkingDirectory 103 | 104 | return nil 105 | } 106 | 107 | // Start executes the command specified in service definition 108 | func (sv *Unit) Start() (err error) { 109 | e := log.WithField("ExecStart", sv.Definition.Service.ExecStart) 110 | 111 | e.Debug("sv.Start") 112 | 113 | switch sv.Definition.Service.Type { 114 | case "simple": 115 | if err = sv.Cmd.Start(); err == nil { 116 | go sv.Cmd.Wait() 117 | } 118 | case "oneshot": 119 | err = sv.Cmd.Run() 120 | default: 121 | panic("Unknown service type") 122 | } 123 | 124 | e.WithField("err", err).Debug("started") 125 | return 126 | } 127 | 128 | // Stop stops execution of the command specified in service definition 129 | func (sv *Unit) Stop() (err error) { 130 | if cmd := strings.Fields(sv.Definition.Service.ExecStop); len(cmd) > 0 { 131 | return exec.Command(cmd[0], cmd[1:]...).Run() 132 | } 133 | if sv.Cmd.Process != nil { 134 | return sv.Cmd.Process.Kill() 135 | } 136 | return nil 137 | } 138 | 139 | // Sub reports the sub status of a service 140 | func (sv *Unit) Sub() string { 141 | log.WithField("sv", sv).Debugf("sv.Sub") 142 | 143 | switch { 144 | case sv.Cmd.Process == nil: 145 | // Service has not been started yet 146 | return dead 147 | 148 | case sv.Cmd.ProcessState == nil: 149 | // Wait has not returned yet 150 | return running 151 | 152 | case sv.ProcessState.Exited(), sv.ProcessState.Success(): 153 | if sv.Definition.Service.RemainAfterExit { 154 | return exited 155 | } 156 | return dead 157 | 158 | default: 159 | // Service process has finished, but did not return a 0 exit code 160 | return failed 161 | } 162 | } 163 | 164 | // Active reports activation status of a service 165 | func (sv *Unit) Active() unit.Activation { 166 | log.WithField("sv", sv).Debugf("sv.Active") 167 | 168 | // based of Systemd transtition table found in https://goo.gl/oEjikJ 169 | switch sv.Sub() { 170 | case dead: 171 | return unit.Inactive 172 | case failed: 173 | return unit.Failed 174 | case reload: 175 | return unit.Reloading 176 | case running, exited: 177 | return unit.Active 178 | case start, startPre, startPost, autoRestart: 179 | return unit.Activating 180 | case stop, stopSigabrt, stopPost, stopSigkill, stopSigterm, finalSigkill, finalSigterm: 181 | return unit.Deactivating 182 | default: 183 | panic("Unknown service sub state") 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /unit/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/plasma-umass/systemgo/unit" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestDefine(t *testing.T) { 14 | sv := Unit{} 15 | assert.NoError(t, sv.Define(strings.NewReader(`[Service] 16 | ExecStart=/bin/echo test`)), "sv.Define") 17 | assert.Equal(t, sv.Definition.Service.Type, DEFAULT_TYPE, "sv.Definition.Service.Type") 18 | 19 | var err error 20 | 21 | sv = Unit{} 22 | if err = sv.Define(strings.NewReader(`[Service]`)); assert.Error(t, err, "sv.Define with wrong definition") { 23 | if me, ok := err.(unit.MultiError); assert.True(t, ok, "error is MultiError") { 24 | if pe, ok := me[0].(unit.ParseError); assert.True(t, ok, "error is ParseError") { 25 | assert.Equal(t, "ExecStart", pe.Source) 26 | assert.Equal(t, unit.ErrNotSet, pe.Err) 27 | } 28 | } 29 | } 30 | } 31 | 32 | // Simple service type test 33 | func TestStartSimple(t *testing.T) { 34 | sv := Unit{} 35 | sv.Definition.Service.Type = "simple" 36 | 37 | assert.Panics(t, func() { sv.Start() }, "Start with nil *Cmd") 38 | 39 | sv.Cmd = exec.Command("sleep", "60") 40 | 41 | assert.NoError(t, sv.Start(), "sv.Start") 42 | assert.NotNil(t, sv.Cmd.Process) 43 | assert.Nil(t, sv.Cmd.ProcessState) 44 | } 45 | 46 | func TestStartOneshot(t *testing.T) { 47 | sv := Unit{} 48 | sv.Definition.Service.Type = "oneshot" 49 | 50 | assert.Panics(t, func() { sv.Start() }, "Start with nil *Cmd") 51 | 52 | sv.Cmd = exec.Command("echo", "test") 53 | 54 | assert.NoError(t, sv.Start(), "sv.Start") 55 | assert.NotNil(t, sv.Cmd.Process) 56 | if assert.NotNil(t, sv.Cmd.ProcessState) { 57 | assert.True(t, sv.Cmd.ProcessState.Success()) 58 | } 59 | 60 | } 61 | 62 | func TestActive(t *testing.T) { 63 | // Oneshot service 64 | sv := Unit{} 65 | sv.Cmd = exec.Command("echo", "test") 66 | 67 | sv.Definition.Service.Type = "oneshot" 68 | sv.Definition.Service.RemainAfterExit = true 69 | if assert.NoError(t, sv.Cmd.Run(), "oneshot Cmd.Run()") { 70 | assert.Equal(t, unit.Active, sv.Active(), fmt.Sprintf("oneshot service - %s", sv.Active())) 71 | } 72 | 73 | // Simple service 74 | sv = Unit{} 75 | sv.Cmd = exec.Command("sleep", "60") 76 | sv.Definition.Service.Type = "simple" 77 | if assert.NoError(t, sv.Cmd.Start(), "simple Cmd.Run()") { 78 | assert.Equal(t, unit.Active, sv.Active(), fmt.Sprintf("simple service - %s", sv.Active())) 79 | } 80 | // TODO 81 | //assert.NoError(t, sv.Cmd.Process.Kill()) 82 | //assert.Equal(t, unit.Failed, sv.Active(), fmt.Sprintf("Active(): %s, Sub(): %s", sv.Active(), sv.sub())) 83 | 84 | //sv = &Unit{} 85 | //assert.Equal(t, unit.Activating, sv.Active()) 86 | 87 | //sv = &Unit{} 88 | //assert.Equal(t, unit.Reloading, sv.Active()) 89 | 90 | //sv = Unit{} 91 | //assert.Equal(t, unit.Inactive, sv.Active()) 92 | 93 | } 94 | 95 | func TestSuported(t *testing.T) { 96 | for typ, is := range supported { 97 | assert.Equal(t, is, Supported(typ), typ) 98 | } 99 | 100 | assert.False(t, Supported("not-a-service")) 101 | } 102 | -------------------------------------------------------------------------------- /unit/slice/sub.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Active 9 | ) 10 | -------------------------------------------------------------------------------- /unit/socket/sub.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | StartPre 9 | StartChown 10 | StartPost 11 | Listening 12 | Running 13 | StopPre 14 | StopPreSigterm 15 | StopPreSigkill 16 | StopPost 17 | FinalSigterm 18 | FinalSigkill 19 | Failed 20 | ) 21 | -------------------------------------------------------------------------------- /unit/state.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | // values in this package correspond to the enums found in 4 | // src/basic/unit-name.h in the systemd library. 5 | 6 | // Activation status of a unit -- https://goo.gl/XHBVuC 7 | type Activation int 8 | 9 | //go:generate stringer -type=Activation state.go 10 | const ( 11 | Inactive Activation = iota 12 | Active 13 | Reloading 14 | Failed 15 | Activating 16 | Deactivating 17 | ) 18 | 19 | // Load status of a unit definition file -- https://goo.gl/NRBCVK 20 | type Load int 21 | 22 | //go:generate stringer -type=Load state.go 23 | const ( 24 | Stub Load = iota 25 | Loaded 26 | NotFound 27 | Error 28 | Merged 29 | Masked 30 | ) 31 | 32 | // Enable status of a unit 33 | type Enable int 34 | 35 | //go:generate stringer -type=Enable state.go 36 | const ( 37 | Disabled Enable = iota 38 | Static 39 | Indirect 40 | Enabled 41 | ) 42 | -------------------------------------------------------------------------------- /unit/state_test.go: -------------------------------------------------------------------------------- 1 | package unit_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/plasma-umass/systemgo/unit" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestStates(t *testing.T) { 12 | var activation unit.Activation 13 | var load unit.Load 14 | var enable unit.Enable 15 | 16 | states := map[fmt.Stringer]string{ 17 | unit.Loaded: "Loaded", 18 | unit.Active: "Active", 19 | unit.Static: "Static", 20 | 21 | activation: "Inactive", 22 | load: "Stub", 23 | enable: "Disabled", 24 | } 25 | 26 | for state, out := range states { 27 | assert.Equal(t, state.String(), out) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /unit/status.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | import "fmt" 4 | 5 | type Status struct { 6 | Load LoadStatus `json:"Load"` 7 | Activation ActivationStatus `json:"Activation"` 8 | 9 | Log []byte `json:"Log,omitempty"` 10 | } 11 | type ActivationStatus struct { 12 | State Activation `json:"State"` 13 | Sub string `json:"Sub"` 14 | } 15 | type LoadStatus struct { 16 | Path string `json:"Path"` 17 | Loaded Load `json:"Loaded"` 18 | State Enable `json:"Enabled"` 19 | Vendor Enable `json:"Vendor"` 20 | } 21 | 22 | func (s Status) String() (out string) { 23 | defer func() { 24 | if len(s.Log) > 0 { 25 | out += fmt.Sprintf("\nLog:\n%s", s.Log) 26 | } 27 | }() 28 | return fmt.Sprintf( 29 | `Loaded: %s (%s; %s; vendor preset: %s) 30 | Active: %s (%s)`, 31 | s.Load.Loaded, s.Load.Path, s.Load.State, s.Load.Vendor, 32 | s.Activation.State, s.Activation.Sub) 33 | } 34 | -------------------------------------------------------------------------------- /unit/status_test.go: -------------------------------------------------------------------------------- 1 | package unit_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/plasma-umass/systemgo/unit" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestStatus(t *testing.T) { 12 | 13 | st := unit.Status{ 14 | Load: unit.LoadStatus{ 15 | Path: "Path", 16 | Loaded: unit.Loaded, 17 | State: unit.Enabled, 18 | Vendor: unit.Enabled, 19 | }, 20 | Activation: unit.ActivationStatus{ 21 | State: unit.Active, 22 | Sub: "Sub", 23 | }, 24 | Log: []byte(`123456 test 25 | 654321 status`), 26 | } 27 | 28 | expected := fmt.Sprintf( 29 | `Loaded: %s (%s; %s; vendor preset: %s) 30 | Active: %s (%s) 31 | Log: 32 | %s`, 33 | st.Load.Loaded, st.Load.Path, st.Load.State, st.Load.Vendor, 34 | st.Activation.State, st.Activation.Sub, 35 | st.Log, 36 | ) 37 | 38 | assert.Equal(t, st.String(), expected) 39 | } 40 | -------------------------------------------------------------------------------- /unit/swap/sub.go: -------------------------------------------------------------------------------- 1 | package swap 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Activating // /sbin/swapon is running, but the swap not yet enabled 9 | ActivatingDone // /sbin/swapon is running, and the swap is done 10 | Active 11 | Deactivating 12 | ActivatingSigterm 13 | ActivatingSigkill 14 | DeactivatingSigterm 15 | DeactivatingSigkill 16 | Failed 17 | ) 18 | -------------------------------------------------------------------------------- /unit/timer/sub.go: -------------------------------------------------------------------------------- 1 | package timer 2 | 3 | type Sub int 4 | 5 | //go:generate stringer -type=Sub sub.go 6 | const ( 7 | Dead Sub = iota 8 | Waiting 9 | Running 10 | Elapsed 11 | Failed 12 | ) 13 | -------------------------------------------------------------------------------- /unit/unit.go: -------------------------------------------------------------------------------- 1 | package unit 2 | 3 | func IsActive(u Subber) bool { 4 | return u.Active() == Active 5 | } 6 | --------------------------------------------------------------------------------