1// Package merlin implements the ASUS-Merlin init system. 2 3package merlin 4 5import ( 6 "bufio" 7 "bytes" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "strings" 13 14 "github.com/nextdns/nextdns/host/service" 15 "github.com/nextdns/nextdns/host/service/internal" 16) 17 18type Service struct { 19 service.Config 20 service.ConfigFileStorer 21 Path string 22 JFFSScript string 23} 24 25func New(c service.Config) (Service, error) { 26 if b, err := exec.Command("uname", "-o").Output(); err != nil || 27 !strings.HasPrefix(string(b), "ASUSWRT-Merlin") { 28 return Service{}, service.ErrNotSuported 29 } 30 ep, err := os.Executable() 31 if err != nil { 32 return Service{}, err 33 } 34 return Service{ 35 Config: c, 36 ConfigFileStorer: service.ConfigFileStorer{File: ep + ".conf"}, 37 Path: ep + ".init", 38 JFFSScript: "/jffs/scripts/services-start", 39 }, nil 40} 41 42func (s Service) Install() error { 43 if err := internal.CreateWithTemplate(s.Path, tmpl, 0755, s.Config); err != nil { 44 return err 45 } 46 47 out, err := internal.RunOutput("nvram", "get", "jffs2_scripts") 48 if err != nil { 49 return fmt.Errorf("check jffs2_scripts: %v", err) 50 } 51 if !strings.HasPrefix(out, "1") { 52 if err := internal.Run("nvram", "set", "jffs2_scripts=1"); err != nil { 53 return fmt.Errorf("enable jffs2_scripts: %v", err) 54 } 55 if err := internal.Run("nvram", "commit"); err != nil { 56 return fmt.Errorf("nvram commit: %v", err) 57 } 58 } 59 if err := addLine(s.JFFSScript, s.Path+" start"); err != nil { 60 return err 61 } 62 return nil 63} 64 65func (s Service) Uninstall() error { 66 _ = removeLine(s.JFFSScript, s.Path+" start") 67 if err := os.Remove(s.Path); err != nil { 68 if os.IsNotExist(err) { 69 return service.ErrNoInstalled 70 } 71 return err 72 } 73 return nil 74} 75 76func (s Service) Status() (service.Status, error) { 77 if _, err := os.Stat(s.Path); os.IsNotExist(err) { 78 return service.StatusNotInstalled, nil 79 } 80 81 err := internal.Run(s.Path, "status") 82 if internal.ExitCode(err) == 1 { 83 return service.StatusStopped, nil 84 } else if err != nil { 85 return service.StatusUnknown, err 86 } 87 return service.StatusRunning, nil 88} 89 90func (s Service) Start() error { 91 return internal.Run(s.Path, "start") 92} 93 94func (s Service) Stop() error { 95 return internal.Run(s.Path, "stop") 96} 97 98func (s Service) Restart() error { 99 return internal.Run(s.Path, "restart") 100} 101 102func excludeLine(file, line string) (found bool, out []byte, err error) { 103 f, err := os.Open(file) 104 if err != nil { 105 return false, nil, err 106 } 107 defer f.Close() 108 s := bufio.NewScanner(f) 109 for s.Scan() { 110 if s.Text() == line { 111 found = true 112 } else { 113 out = append(out, s.Bytes()...) 114 out = append(out, '\n') 115 } 116 } 117 if err := s.Err(); err != nil { 118 return false, nil, err 119 } 120 return 121} 122 123func addLine(file, line string) error { 124 found, _, err := excludeLine(file, line) 125 if os.IsNotExist(err) { 126 return ioutil.WriteFile(file, []byte("#!/bin/sh\n"+line+"\n"), 0755) 127 } 128 if err != nil { 129 return err 130 } 131 if found { 132 return service.ErrAlreadyInstalled 133 } 134 b, err := ioutil.ReadFile(file) 135 if err != nil { 136 return err 137 } 138 f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 139 if err != nil { 140 return err 141 } 142 defer f.Close() 143 s := bufio.NewScanner(bytes.NewReader(b)) 144 firstLine := true 145 for s.Scan() { 146 l := s.Text() 147 if firstLine { 148 firstLine = false 149 if strings.HasPrefix(l, "#!") { 150 _, err = fmt.Fprintf(f, "%s\n\n%s\n", l, line) 151 } else { 152 // missing shebang 153 _, err = fmt.Fprintf(f, "#!/bin/sh\n\n%s\n%s\n", line, l) 154 } 155 } else { 156 _, err = fmt.Fprintln(f, l) 157 } 158 if err != nil { 159 return err 160 } 161 } 162 if err := s.Err(); err != nil { 163 return err 164 } 165 if firstLine { 166 // Empty file 167 return ioutil.WriteFile(file, []byte("#!/bin/sh\n"+line+"\n"), 0755) 168 } 169 return err 170} 171 172func removeLine(file, line string) error { 173 found, out, err := excludeLine(file, line) 174 if err != nil { 175 return err 176 } 177 if !found { 178 return service.ErrNoInstalled 179 } 180 if bytes.Equal(bytes.TrimSpace(out), []byte("#!/bin/sh")) { 181 return os.Remove(file) 182 } 183 return ioutil.WriteFile(file, out, 0755) 184} 185 186var tmpl = `#!/bin/sh 187 188name="{{.Name}}" 189exe="{{.Executable}}" 190cmd="$exe{{range .Arguments}} {{.}}{{end}}" 191pid_file="/tmp/$name.pid" 192 193get_pid() { 194 cat "$pid_file" 195} 196 197is_running() { 198 test -f "$pid_file" && ps | grep -q "^ *$(get_pid) " 199} 200 201log() { 202 logger -s -t "${name}.init" "$@" 203} 204 205setup_tz() { 206 tz="$(nvram get time_zone)" 207 tz_dir="/jffs/zoneinfo" 208 tz_file="$tz_dir/$tz" 209 tz_url="https://github.com/nextdns/nextdns/raw/master/router/merlin/tz/$tz" 210 if [ "$(readlink /etc/localtime)" != "$tz_file" ]; then 211 if [ -f "$tz_file" ]; then 212 ln -sf "$tz_file" /etc/localtime 213 else 214 mkdir -p "$tz_dir" 215 if curl -sLo "$tz_file" "$tz_url"; then 216 ln -sf "$tz_file" /etc/localtime 217 fi 218 fi 219 fi 220} 221 222case "$1" in 223 start) 224 if is_running; then 225 log "Already started" 226 else 227 if [ -f /rom/ca-bundle.crt ]; then 228 # John’s fork 39E3j9527 has trust store in non-standard location 229 export SSL_CERT_FILE=/rom/ca-bundle.crt 230 fi 231 setup_tz 232 unset TZ 233 export {{.RunModeEnv}}=1 234 $cmd & 235 echo $! > "$pid_file" 236 if ! is_running; then 237 log "Unable to start" 238 exit 1 239 fi 240 fi 241 242 # Install a symlink of the service into the path if not already present 243 if [ -z "$(which $(basename $exe))" ]; then 244 # /home/$USER is in the path and does not seem to conflict with stuff 245 # like entware. 246 mkdir -p /home/$USER 247 ln -s "$exe" "/home/$USER/$(basename $exe)" 248 fi 249 ;; 250 stop) 251 if is_running; then 252 kill $(get_pid) 253 for i in 1 2 3 4 5 6 7 8 9 10; do 254 if ! is_running; then 255 break 256 fi 257 sleep 1 258 done 259 if is_running; then 260 log "Not stopped; may still be shutting down or shutdown may have failed" 261 exit 1 262 else 263 log "Stopped" 264 if [ -f "$pid_file" ]; then 265 rm "$pid_file" 266 fi 267 fi 268 else 269 log "Not running" 270 fi 271 ;; 272 restart) 273 $0 stop 274 if is_running; then 275 log "Unable to stop, will not attempt to start" 276 exit 1 277 fi 278 $0 start 279 ;; 280 status) 281 if is_running; then 282 log "Running" 283 else 284 log "Stopped" 285 exit 1 286 fi 287 ;; 288 *) 289 log "Usage: $0 {start|stop|restart|status}" 290 exit 1 291 ;; 292esac 293exit 0 294` 295