github.com/rigado/snapd@v2.42.5-go-mod+incompatible/systemd/systemd.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2015 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package systemd 21 22 import ( 23 "errors" 24 "fmt" 25 "io" 26 "os" 27 "os/exec" 28 "path/filepath" 29 "regexp" 30 "strconv" 31 "strings" 32 "sync" 33 "sync/atomic" 34 "time" 35 36 _ "github.com/snapcore/squashfuse" 37 38 "github.com/snapcore/snapd/dirs" 39 "github.com/snapcore/snapd/osutil" 40 "github.com/snapcore/snapd/osutil/squashfs" 41 "github.com/snapcore/snapd/release" 42 "github.com/snapcore/snapd/selinux" 43 ) 44 45 var ( 46 // the output of "show" must match this for Stop to be done: 47 isStopDone = regexp.MustCompile(`(?m)\AActiveState=(?:failed|inactive)$`).Match 48 49 // how much time should Stop wait between calls to show 50 stopCheckDelay = 250 * time.Millisecond 51 52 // how much time should Stop wait between notifying the user of the waiting 53 stopNotifyDelay = 20 * time.Second 54 55 // daemonReloadLock is a package level lock to ensure that we 56 // do not run any `systemd daemon-reload` while a 57 // daemon-reload is in progress or a mount unit is 58 // generated/activated. 59 // 60 // See https://github.com/systemd/systemd/issues/10872 for the 61 // upstream systemd bug 62 daemonReloadLock extMutex 63 ) 64 65 // mu is a sync.Mutex that also supports to check if the lock is taken 66 type extMutex struct { 67 lock sync.Mutex 68 muC int32 69 } 70 71 // Lock acquires the mutex 72 func (m *extMutex) Lock() { 73 m.lock.Lock() 74 atomic.AddInt32(&m.muC, 1) 75 } 76 77 // Unlock releases the mutex 78 func (m *extMutex) Unlock() { 79 atomic.AddInt32(&m.muC, -1) 80 m.lock.Unlock() 81 } 82 83 // Taken will panic with the given error message if the lock is not 84 // taken when this code runs. This is useful to internally check if 85 // something is accessed without a valid lock. 86 func (m *extMutex) Taken(errMsg string) { 87 if atomic.LoadInt32(&m.muC) != 1 { 88 panic("internal error: " + errMsg) 89 } 90 } 91 92 // systemctlCmd calls systemctl with the given args, returning its standard output (and wrapped error) 93 var systemctlCmd = func(args ...string) ([]byte, error) { 94 bs, err := exec.Command("systemctl", args...).CombinedOutput() 95 if err != nil { 96 exitCode, _ := osutil.ExitCode(err) 97 return nil, &Error{cmd: args, exitCode: exitCode, msg: bs} 98 } 99 100 return bs, nil 101 } 102 103 // MockSystemctl is called from the commands to actually call out to 104 // systemctl. It's exported so it can be overridden by testing. 105 func MockSystemctl(f func(args ...string) ([]byte, error)) func() { 106 oldSystemctlCmd := systemctlCmd 107 systemctlCmd = f 108 return func() { 109 systemctlCmd = oldSystemctlCmd 110 } 111 } 112 113 // MockStopDelays is used from tests so that Stop can be less 114 // forgiving there. 115 func MockStopDelays(checkDelay, notifyDelay time.Duration) func() { 116 oldCheckDelay := stopCheckDelay 117 oldNotifyDelay := stopNotifyDelay 118 stopCheckDelay = checkDelay 119 stopNotifyDelay = notifyDelay 120 return func() { 121 stopCheckDelay = oldCheckDelay 122 stopNotifyDelay = oldNotifyDelay 123 } 124 } 125 126 func Available() error { 127 _, err := systemctlCmd("--version") 128 return err 129 } 130 131 var osutilStreamCommand = osutil.StreamCommand 132 133 // jctl calls journalctl to get the JSON logs of the given services. 134 var jctl = func(svcs []string, n int, follow bool) (io.ReadCloser, error) { 135 // args will need two entries per service, plus a fixed number (give or take 136 // one) for the initial options. 137 args := make([]string, 0, 2*len(svcs)+6) // the fixed number is 6 138 args = append(args, "-o", "json", "--no-pager") // 3... 139 if n < 0 { 140 args = append(args, "--no-tail") // < 2 141 } else { 142 args = append(args, "-n", strconv.Itoa(n)) // ... + 2 ... 143 } 144 if follow { 145 args = append(args, "-f") // ... + 1 == 6 146 } 147 148 for i := range svcs { 149 args = append(args, "-u", svcs[i]) // this is why 2× 150 } 151 152 return osutilStreamCommand("journalctl", args...) 153 } 154 155 func MockJournalctl(f func(svcs []string, n int, follow bool) (io.ReadCloser, error)) func() { 156 oldJctl := jctl 157 jctl = f 158 return func() { 159 jctl = oldJctl 160 } 161 } 162 163 // Systemd exposes a minimal interface to manage systemd via the systemctl command. 164 type Systemd interface { 165 DaemonReload() error 166 Enable(service string) error 167 Disable(service string) error 168 Start(service ...string) error 169 StartNoBlock(service ...string) error 170 Stop(service string, timeout time.Duration) error 171 Kill(service, signal, who string) error 172 Restart(service string, timeout time.Duration) error 173 Status(units ...string) ([]*UnitStatus, error) 174 IsEnabled(service string) (bool, error) 175 IsActive(service string) (bool, error) 176 LogReader(services []string, n int, follow bool) (io.ReadCloser, error) 177 AddMountUnitFile(name, revision, what, where, fstype string) (string, error) 178 RemoveMountUnitFile(baseDir string) error 179 Mask(service string) error 180 Unmask(service string) error 181 } 182 183 // A Log is a single entry in the systemd journal 184 type Log map[string]string 185 186 const ( 187 // the default target for systemd units that we generate 188 ServicesTarget = "multi-user.target" 189 190 // the target prerequisite for systemd units we generate 191 PrerequisiteTarget = "network.target" 192 193 // the default target for systemd socket units that we generate 194 SocketsTarget = "sockets.target" 195 196 // the default target for systemd timer units that we generate 197 TimersTarget = "timers.target" 198 ) 199 200 type reporter interface { 201 Notify(string) 202 } 203 204 // New returns a Systemd that uses the given rootDir 205 func New(rootDir string, mode InstanceMode, rep reporter) Systemd { 206 return &systemd{rootDir: rootDir, mode: mode, reporter: rep} 207 } 208 209 // InstanceMode determines which instance of systemd to control. 210 // 211 // SystemMode refers to the system instance (i.e. pid 1). UserMode 212 // refers to the the instance launched to manage the user's desktop 213 // session. GlobalUserMode controls configuration respected by all 214 // user instances on the system. 215 // 216 // As GlobalUserMode does not refer to a single instance of systemd, 217 // some operations are not supported such as starting and stopping 218 // daemons. 219 type InstanceMode int 220 221 const ( 222 SystemMode InstanceMode = iota 223 UserMode 224 GlobalUserMode 225 ) 226 227 type systemd struct { 228 rootDir string 229 reporter reporter 230 mode InstanceMode 231 } 232 233 func (s *systemd) systemctl(args ...string) ([]byte, error) { 234 switch s.mode { 235 case SystemMode: 236 case UserMode: 237 args = append([]string{"--user"}, args...) 238 case GlobalUserMode: 239 args = append([]string{"--user", "--global"}, args...) 240 default: 241 panic("unknown InstanceMode") 242 } 243 return systemctlCmd(args...) 244 } 245 246 // DaemonReload reloads systemd's configuration. 247 func (s *systemd) DaemonReload() error { 248 if s.mode == GlobalUserMode { 249 panic("cannot call daemon-reload with GlobalUserMode") 250 } 251 daemonReloadLock.Lock() 252 defer daemonReloadLock.Unlock() 253 254 return s.daemonReloadNoLock() 255 } 256 257 func (s *systemd) daemonReloadNoLock() error { 258 daemonReloadLock.Taken("cannot use daemon-reload without lock") 259 260 _, err := s.systemctl("daemon-reload") 261 return err 262 } 263 264 // Enable the given service 265 func (s *systemd) Enable(serviceName string) error { 266 _, err := s.systemctl("--root", s.rootDir, "enable", serviceName) 267 return err 268 } 269 270 // Unmask the given service 271 func (s *systemd) Unmask(serviceName string) error { 272 _, err := s.systemctl("--root", s.rootDir, "unmask", serviceName) 273 return err 274 } 275 276 // Disable the given service 277 func (s *systemd) Disable(serviceName string) error { 278 _, err := s.systemctl("--root", s.rootDir, "disable", serviceName) 279 return err 280 } 281 282 // Mask the given service 283 func (s *systemd) Mask(serviceName string) error { 284 _, err := s.systemctl("--root", s.rootDir, "mask", serviceName) 285 return err 286 } 287 288 // Start the given service or services 289 func (s *systemd) Start(serviceNames ...string) error { 290 if s.mode == GlobalUserMode { 291 panic("cannot call start with GlobalUserMode") 292 } 293 _, err := s.systemctl(append([]string{"start"}, serviceNames...)...) 294 return err 295 } 296 297 // StartNoBlock starts the given service or services non-blocking 298 func (s *systemd) StartNoBlock(serviceNames ...string) error { 299 if s.mode == GlobalUserMode { 300 panic("cannot call start with GlobalUserMode") 301 } 302 _, err := s.systemctl(append([]string{"start", "--no-block"}, serviceNames...)...) 303 return err 304 } 305 306 // LogReader for the given services 307 func (*systemd) LogReader(serviceNames []string, n int, follow bool) (io.ReadCloser, error) { 308 return jctl(serviceNames, n, follow) 309 } 310 311 var statusregex = regexp.MustCompile(`(?m)^(?:(.+?)=(.*)|(.*))?$`) 312 313 type UnitStatus struct { 314 Daemon string 315 UnitName string 316 Enabled bool 317 Active bool 318 } 319 320 var baseProperties = []string{"Id", "ActiveState", "UnitFileState"} 321 var extendedProperties = []string{"Id", "ActiveState", "UnitFileState", "Type"} 322 var unitProperties = map[string][]string{ 323 ".timer": baseProperties, 324 ".socket": baseProperties, 325 // in service units, Type is the daemon type 326 ".service": extendedProperties, 327 // in mount units, Type is the fs type 328 ".mount": extendedProperties, 329 } 330 331 func (s *systemd) getUnitStatus(properties []string, unitNames []string) ([]*UnitStatus, error) { 332 cmd := make([]string, len(unitNames)+2) 333 cmd[0] = "show" 334 // ask for all properties, regardless of unit type 335 cmd[1] = "--property=" + strings.Join(properties, ",") 336 copy(cmd[2:], unitNames) 337 bs, err := s.systemctl(cmd...) 338 if err != nil { 339 return nil, err 340 } 341 342 sts := make([]*UnitStatus, 0, len(unitNames)) 343 cur := &UnitStatus{} 344 seen := map[string]bool{} 345 346 for _, bs := range statusregex.FindAllSubmatch(bs, -1) { 347 if len(bs[0]) == 0 { 348 // systemctl separates data pertaining to particular services by an empty line 349 unitType := filepath.Ext(cur.UnitName) 350 expected := unitProperties[unitType] 351 if expected == nil { 352 expected = baseProperties 353 } 354 355 missing := make([]string, 0, len(expected)) 356 for _, k := range expected { 357 if !seen[k] { 358 missing = append(missing, k) 359 } 360 } 361 if len(missing) > 0 { 362 return nil, fmt.Errorf("cannot get unit %q status: missing %s in ‘systemctl show’ output", cur.UnitName, strings.Join(missing, ", ")) 363 } 364 sts = append(sts, cur) 365 if len(sts) > len(unitNames) { 366 break // wut 367 } 368 if cur.UnitName != unitNames[len(sts)-1] { 369 return nil, fmt.Errorf("cannot get unit status: queried status of %q but got status of %q", unitNames[len(sts)-1], cur.UnitName) 370 } 371 372 cur = &UnitStatus{} 373 seen = map[string]bool{} 374 continue 375 } 376 if len(bs[3]) > 0 { 377 return nil, fmt.Errorf("cannot get unit status: bad line %q in ‘systemctl show’ output", bs[3]) 378 } 379 k := string(bs[1]) 380 v := string(bs[2]) 381 382 if v == "" { 383 return nil, fmt.Errorf("cannot get unit status: empty field %q in ‘systemctl show’ output", k) 384 } 385 386 switch k { 387 case "Id": 388 cur.UnitName = v 389 case "Type": 390 cur.Daemon = v 391 case "ActiveState": 392 // made to match “systemctl is-active” behaviour, at least at systemd 229 393 cur.Active = v == "active" || v == "reloading" 394 case "UnitFileState": 395 // "static" means it can't be disabled 396 cur.Enabled = v == "enabled" || v == "static" 397 default: 398 return nil, fmt.Errorf("cannot get unit status: unexpected field %q in ‘systemctl show’ output", k) 399 } 400 401 if seen[k] { 402 return nil, fmt.Errorf("cannot get unit status: duplicate field %q in ‘systemctl show’ output", k) 403 } 404 seen[k] = true 405 } 406 407 if len(sts) != len(unitNames) { 408 return nil, fmt.Errorf("cannot get unit status: expected %d results, got %d", len(unitNames), len(sts)) 409 } 410 return sts, nil 411 } 412 413 // Status fetches the status of given units. Statuses are returned in the same 414 // order as unit names passed in argument. 415 func (s *systemd) Status(unitNames ...string) ([]*UnitStatus, error) { 416 if s.mode == GlobalUserMode { 417 panic("cannot call status with GlobalUserMode") 418 } 419 unitToStatus := make(map[string]*UnitStatus, len(unitNames)) 420 421 var limitedUnits []string 422 var extendedUnits []string 423 424 for _, name := range unitNames { 425 if strings.HasSuffix(name, ".timer") || strings.HasSuffix(name, ".socket") { 426 limitedUnits = append(limitedUnits, name) 427 } else { 428 extendedUnits = append(extendedUnits, name) 429 } 430 } 431 432 for _, set := range []struct { 433 units []string 434 properties []string 435 }{ 436 {units: extendedUnits, properties: extendedProperties}, 437 {units: limitedUnits, properties: baseProperties}, 438 } { 439 if len(set.units) == 0 { 440 continue 441 } 442 sts, err := s.getUnitStatus(set.properties, set.units) 443 if err != nil { 444 return nil, err 445 } 446 for _, status := range sts { 447 unitToStatus[status.UnitName] = status 448 } 449 } 450 451 // unpack to preserve the promised order 452 sts := make([]*UnitStatus, len(unitNames)) 453 for idx, name := range unitNames { 454 var ok bool 455 sts[idx], ok = unitToStatus[name] 456 if !ok { 457 return nil, fmt.Errorf("cannot determine status of unit %q", name) 458 } 459 } 460 461 return sts, nil 462 } 463 464 // IsEnabled checkes whether the given service is enabled 465 func (s *systemd) IsEnabled(serviceName string) (bool, error) { 466 if s.mode == GlobalUserMode { 467 panic("cannot call is-enabled with GlobalUserMode") 468 } 469 _, err := s.systemctl("--root", s.rootDir, "is-enabled", serviceName) 470 if err == nil { 471 return true, nil 472 } 473 // "systemctl is-enabled <name>" prints `disabled\n` to stderr and returns exit code 1 474 // for disabled services 475 sysdErr, ok := err.(*Error) 476 if ok && sysdErr.exitCode == 1 && strings.TrimSpace(string(sysdErr.msg)) == "disabled" { 477 return false, nil 478 } 479 return false, err 480 } 481 482 // IsActive checkes whether the given service is Active 483 func (s *systemd) IsActive(serviceName string) (bool, error) { 484 if s.mode == GlobalUserMode { 485 panic("cannot call is-active with GlobalUserMode") 486 } 487 _, err := s.systemctl("--root", s.rootDir, "is-active", serviceName) 488 if err == nil { 489 return true, nil 490 } 491 // "systemctl is-active <name>" prints `inactive\n` to stderr and returns exit code 1 for inactive services 492 sysdErr, ok := err.(*Error) 493 if ok && sysdErr.exitCode > 0 && strings.TrimSpace(string(sysdErr.msg)) == "inactive" { 494 return false, nil 495 } 496 return false, err 497 } 498 499 // Stop the given service, and wait until it has stopped. 500 func (s *systemd) Stop(serviceName string, timeout time.Duration) error { 501 if s.mode == GlobalUserMode { 502 panic("cannot call stop with GlobalUserMode") 503 } 504 if _, err := s.systemctl("stop", serviceName); err != nil { 505 return err 506 } 507 508 // and now wait for it to actually stop 509 giveup := time.NewTimer(timeout) 510 notify := time.NewTicker(stopNotifyDelay) 511 defer notify.Stop() 512 check := time.NewTicker(stopCheckDelay) 513 defer check.Stop() 514 515 firstCheck := true 516 loop: 517 for { 518 select { 519 case <-giveup.C: 520 break loop 521 case <-check.C: 522 bs, err := s.systemctl("show", "--property=ActiveState", serviceName) 523 if err != nil { 524 return err 525 } 526 if isStopDone(bs) { 527 return nil 528 } 529 if !firstCheck { 530 continue loop 531 } 532 firstCheck = false 533 case <-notify.C: 534 } 535 // after notify delay or after a failed first check 536 s.reporter.Notify(fmt.Sprintf("Waiting for %s to stop.", serviceName)) 537 } 538 539 return &Timeout{action: "stop", service: serviceName} 540 } 541 542 // Kill all processes of the unit with the given signal 543 func (s *systemd) Kill(serviceName, signal, who string) error { 544 if s.mode == GlobalUserMode { 545 panic("cannot call kill with GlobalUserMode") 546 } 547 if who == "" { 548 who = "all" 549 } 550 _, err := s.systemctl("kill", serviceName, "-s", signal, "--kill-who="+who) 551 return err 552 } 553 554 // Restart the service, waiting for it to stop before starting it again. 555 func (s *systemd) Restart(serviceName string, timeout time.Duration) error { 556 if s.mode == GlobalUserMode { 557 panic("cannot call restart with GlobalUserMode") 558 } 559 if err := s.Stop(serviceName, timeout); err != nil { 560 return err 561 } 562 return s.Start(serviceName) 563 } 564 565 // Error is returned if the systemd action failed 566 type Error struct { 567 cmd []string 568 msg []byte 569 exitCode int 570 } 571 572 func (e *Error) Error() string { 573 return fmt.Sprintf("%v failed with exit status %d: %s", e.cmd, e.exitCode, e.msg) 574 } 575 576 // Timeout is returned if the systemd action failed to reach the 577 // expected state in a reasonable amount of time 578 type Timeout struct { 579 action string 580 service string 581 } 582 583 func (e *Timeout) Error() string { 584 return fmt.Sprintf("%v failed to %v: timeout", e.service, e.action) 585 } 586 587 // IsTimeout checks whether the given error is a Timeout 588 func IsTimeout(err error) bool { 589 _, isTimeout := err.(*Timeout) 590 return isTimeout 591 } 592 593 // Time returns the time the Log was received by the journal. 594 func (l Log) Time() (time.Time, error) { 595 sus, ok := l["__REALTIME_TIMESTAMP"] 596 if !ok { 597 return time.Time{}, errors.New("no timestamp") 598 } 599 // according to systemd.journal-fields(7) it's microseconds as a decimal string 600 us, err := strconv.ParseInt(sus, 10, 64) 601 if err != nil { 602 return time.Time{}, fmt.Errorf("timestamp not a decimal number: %#v", sus) 603 } 604 605 return time.Unix(us/1000000, 1000*(us%1000000)).UTC(), nil 606 } 607 608 // Message of the Log, if any; otherwise, "-". 609 func (l Log) Message() string { 610 if msg, ok := l["MESSAGE"]; ok { 611 return msg 612 } 613 614 return "-" 615 } 616 617 // SID is the syslog identifier of the Log, if any; otherwise, "-". 618 func (l Log) SID() string { 619 if sid, ok := l["SYSLOG_IDENTIFIER"]; ok { 620 return sid 621 } 622 623 return "-" 624 } 625 626 // PID is the pid of the client pid, if any; otherwise, "-". 627 func (l Log) PID() string { 628 if pid, ok := l["_PID"]; ok { 629 return pid 630 } 631 if pid, ok := l["SYSLOG_PID"]; ok { 632 return pid 633 } 634 635 return "-" 636 } 637 638 // MountUnitPath returns the path of a {,auto}mount unit 639 func MountUnitPath(baseDir string) string { 640 escapedPath := EscapeUnitNamePath(baseDir) 641 return filepath.Join(dirs.SnapServicesDir, escapedPath+".mount") 642 } 643 644 // AddMountUnitFile adds/enables/starts a mount unit. 645 func (s *systemd) AddMountUnitFile(snapName, revision, what, where, fstype string) (string, error) { 646 daemonReloadLock.Lock() 647 defer daemonReloadLock.Unlock() 648 649 options := []string{"nodev"} 650 if fstype == "squashfs" { 651 newFsType, newOptions, err := squashfs.FsType() 652 if err != nil { 653 return "", err 654 } 655 options = append(options, newOptions...) 656 fstype = newFsType 657 if release.SELinuxLevel() != release.NoSELinux { 658 if mountCtx := selinux.SnapMountContext(); mountCtx != "" { 659 options = append(options, "context="+mountCtx) 660 } 661 } 662 } 663 if osutil.IsDirectory(what) { 664 options = append(options, "bind") 665 fstype = "none" 666 } 667 668 c := fmt.Sprintf(`[Unit] 669 Description=Mount unit for %s, revision %s 670 Before=snapd.service 671 672 [Mount] 673 What=%s 674 Where=%s 675 Type=%s 676 Options=%s 677 LazyUnmount=yes 678 679 [Install] 680 WantedBy=multi-user.target 681 `, snapName, revision, what, where, fstype, strings.Join(options, ",")) 682 683 mu := MountUnitPath(where) 684 mountUnitName, err := filepath.Base(mu), osutil.AtomicWriteFile(mu, []byte(c), 0644, 0) 685 if err != nil { 686 return "", err 687 } 688 689 // we need to do a daemon-reload here to ensure that systemd really 690 // knows about this new mount unit file 691 if err := s.daemonReloadNoLock(); err != nil { 692 return "", err 693 } 694 695 if err := s.Enable(mountUnitName); err != nil { 696 return "", err 697 } 698 if err := s.Start(mountUnitName); err != nil { 699 return "", err 700 } 701 702 return mountUnitName, nil 703 } 704 705 func (s *systemd) RemoveMountUnitFile(mountedDir string) error { 706 daemonReloadLock.Lock() 707 defer daemonReloadLock.Unlock() 708 709 unit := MountUnitPath(dirs.StripRootDir(mountedDir)) 710 if !osutil.FileExists(unit) { 711 return nil 712 } 713 714 // use umount -d (cleanup loopback devices) -l (lazy) to ensure that even busy mount points 715 // can be unmounted. 716 // note that the long option --lazy is not supported on trusty. 717 // the explicit -d is only needed on trusty. 718 isMounted, err := osutil.IsMounted(mountedDir) 719 if err != nil { 720 return err 721 } 722 if isMounted { 723 if output, err := exec.Command("umount", "-d", "-l", mountedDir).CombinedOutput(); err != nil { 724 return osutil.OutputErr(output, err) 725 } 726 727 if err := s.Stop(filepath.Base(unit), time.Duration(1*time.Second)); err != nil { 728 return err 729 } 730 } 731 if err := s.Disable(filepath.Base(unit)); err != nil { 732 return err 733 } 734 if err := os.Remove(unit); err != nil { 735 return err 736 } 737 // daemon-reload to ensure that systemd actually really 738 // forgets about this mount unit 739 if err := s.daemonReloadNoLock(); err != nil { 740 return err 741 } 742 743 return nil 744 }