github.com/stulluk/snapd@v0.0.0-20210611110309-f6d5d5bd24b0/wrappers/services.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2021 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 wrappers 21 22 import ( 23 "bytes" 24 "context" 25 "fmt" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "sort" 30 "strconv" 31 "strings" 32 "text/template" 33 "time" 34 35 "github.com/snapcore/snapd/dirs" 36 "github.com/snapcore/snapd/interfaces" 37 "github.com/snapcore/snapd/logger" 38 "github.com/snapcore/snapd/osutil" 39 "github.com/snapcore/snapd/osutil/sys" 40 "github.com/snapcore/snapd/progress" 41 "github.com/snapcore/snapd/randutil" 42 "github.com/snapcore/snapd/snap" 43 "github.com/snapcore/snapd/snap/quota" 44 "github.com/snapcore/snapd/strutil" 45 "github.com/snapcore/snapd/systemd" 46 "github.com/snapcore/snapd/timeout" 47 "github.com/snapcore/snapd/timeutil" 48 "github.com/snapcore/snapd/timings" 49 "github.com/snapcore/snapd/usersession/client" 50 ) 51 52 type interacter interface { 53 Notify(status string) 54 } 55 56 // wait this time between TERM and KILL 57 var killWait = 5 * time.Second 58 59 func serviceStopTimeout(app *snap.AppInfo) time.Duration { 60 tout := app.StopTimeout 61 if tout == 0 { 62 tout = timeout.DefaultTimeout 63 } 64 return time.Duration(tout) 65 } 66 67 // TODO: this should not accept AddSnapServicesOptions, it should use some other 68 // subset of options, specifically it should not accept Preseeding as an option 69 // here 70 func generateSnapServiceFile(app *snap.AppInfo, opts *AddSnapServicesOptions) ([]byte, error) { 71 if err := snap.ValidateApp(app); err != nil { 72 return nil, err 73 } 74 75 return genServiceFile(app, opts) 76 } 77 78 // generateGroupSliceFile generates a systemd slice unit definition for the 79 // specified quota group. 80 func generateGroupSliceFile(grp *quota.Group) ([]byte, error) { 81 buf := bytes.Buffer{} 82 83 template := `[Unit] 84 Description=Slice for snap quota group %[1]s 85 Before=slices.target 86 X-Snappy=yes 87 88 [Slice] 89 # Always enable memory accounting otherwise the MemoryMax setting does nothing. 90 MemoryAccounting=true 91 MemoryMax=%[2]d 92 # for compatibility with older versions of systemd 93 MemoryLimit=%[2]d 94 95 # Always enable task accounting in order to be able to count the processes/ 96 # threads, etc for a slice 97 TasksAccounting=true 98 ` 99 100 fmt.Fprintf(&buf, template, grp.Name, grp.MemoryLimit) 101 102 return buf.Bytes(), nil 103 } 104 105 func stopUserServices(cli *client.Client, inter interacter, services ...string) error { 106 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout.DefaultTimeout)) 107 defer cancel() 108 failures, err := cli.ServicesStop(ctx, services) 109 for _, f := range failures { 110 inter.Notify(fmt.Sprintf("Could not stop service %q for uid %d: %s", f.Service, f.Uid, f.Error)) 111 } 112 return err 113 } 114 115 func startUserServices(cli *client.Client, inter interacter, services ...string) error { 116 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout.DefaultTimeout)) 117 defer cancel() 118 startFailures, stopFailures, err := cli.ServicesStart(ctx, services) 119 for _, f := range startFailures { 120 inter.Notify(fmt.Sprintf("Could not start service %q for uid %d: %s", f.Service, f.Uid, f.Error)) 121 } 122 for _, f := range stopFailures { 123 inter.Notify(fmt.Sprintf("While trying to stop previously started service %q for uid %d: %s", f.Service, f.Uid, f.Error)) 124 } 125 return err 126 } 127 128 func stopService(sysd systemd.Systemd, app *snap.AppInfo, inter interacter) error { 129 serviceName := app.ServiceName() 130 tout := serviceStopTimeout(app) 131 132 var extraServices []string 133 for _, socket := range app.Sockets { 134 extraServices = append(extraServices, filepath.Base(socket.File())) 135 } 136 if app.Timer != nil { 137 extraServices = append(extraServices, filepath.Base(app.Timer.File())) 138 } 139 140 switch app.DaemonScope { 141 case snap.SystemDaemon: 142 stopErrors := []error{} 143 for _, service := range extraServices { 144 if err := sysd.Stop(service, tout); err != nil { 145 stopErrors = append(stopErrors, err) 146 } 147 } 148 149 if err := sysd.Stop(serviceName, tout); err != nil { 150 if !systemd.IsTimeout(err) { 151 return err 152 } 153 inter.Notify(fmt.Sprintf("%s refused to stop, killing.", serviceName)) 154 // ignore errors for kill; nothing we'd do differently at this point 155 sysd.Kill(serviceName, "TERM", "") 156 time.Sleep(killWait) 157 sysd.Kill(serviceName, "KILL", "") 158 } 159 160 if len(stopErrors) > 0 { 161 return stopErrors[0] 162 } 163 164 case snap.UserDaemon: 165 extraServices = append(extraServices, serviceName) 166 cli := client.New() 167 return stopUserServices(cli, inter, extraServices...) 168 } 169 170 return nil 171 } 172 173 // enableServices enables services specified by apps. On success the returned 174 // disable function can be used to undo all the actions. On error all the 175 // services get disabled automatically (disable is nil). 176 func enableServices(apps []*snap.AppInfo, inter interacter) (disable func(), err error) { 177 var enabled []string 178 var userEnabled []string 179 180 systemSysd := systemd.New(systemd.SystemMode, inter) 181 userSysd := systemd.New(systemd.GlobalUserMode, inter) 182 183 disableEnabledServices := func() { 184 for _, srvName := range enabled { 185 if e := systemSysd.Disable(srvName); e != nil { 186 inter.Notify(fmt.Sprintf("While trying to disable previously enabled service %q: %v", srvName, e)) 187 } 188 } 189 for _, s := range userEnabled { 190 if e := userSysd.Disable(s); e != nil { 191 inter.Notify(fmt.Sprintf("while trying to disable %s due to previous failure: %v", s, e)) 192 } 193 } 194 } 195 196 defer func() { 197 if err != nil { 198 disableEnabledServices() 199 } 200 }() 201 202 for _, app := range apps { 203 var sysd systemd.Systemd 204 switch app.DaemonScope { 205 case snap.SystemDaemon: 206 sysd = systemSysd 207 case snap.UserDaemon: 208 sysd = userSysd 209 } 210 211 svcName := app.ServiceName() 212 213 switch app.DaemonScope { 214 case snap.SystemDaemon: 215 if err = sysd.Enable(svcName); err != nil { 216 return nil, err 217 218 } 219 enabled = append(enabled, svcName) 220 case snap.UserDaemon: 221 if err = userSysd.Enable(svcName); err != nil { 222 return nil, err 223 } 224 userEnabled = append(userEnabled, svcName) 225 } 226 } 227 228 return disableEnabledServices, nil 229 } 230 231 // StartServicesFlags carries extra flags for StartServices. 232 type StartServicesFlags struct { 233 Enable bool 234 } 235 236 // StartServices starts service units for the applications from the snap which 237 // are services. Service units will be started in the order provided by the 238 // caller. 239 func StartServices(apps []*snap.AppInfo, disabledSvcs []string, flags *StartServicesFlags, inter interacter, tm timings.Measurer) (err error) { 240 if flags == nil { 241 flags = &StartServicesFlags{} 242 } 243 244 systemSysd := systemd.New(systemd.SystemMode, inter) 245 userSysd := systemd.New(systemd.GlobalUserMode, inter) 246 cli := client.New() 247 248 var disableEnabledServices func() 249 250 defer func() { 251 if err == nil { 252 return 253 } 254 if disableEnabledServices != nil { 255 disableEnabledServices() 256 } 257 }() 258 259 var toEnable []*snap.AppInfo 260 systemServices := make([]string, 0, len(apps)) 261 userServices := make([]string, 0, len(apps)) 262 263 // gather all non-sockets, non-timers, and non-dbus activated 264 // services to enable first 265 for _, app := range apps { 266 // they're *supposed* to be all services, but checking doesn't hurt 267 if !app.IsService() { 268 continue 269 } 270 // sockets and timers are enabled and started separately (and unconditionally) further down. 271 // dbus activatable services are started on first use. 272 if len(app.Sockets) == 0 && app.Timer == nil && len(app.ActivatesOn) == 0 { 273 if strutil.ListContains(disabledSvcs, app.Name) { 274 continue 275 } 276 svcName := app.ServiceName() 277 switch app.DaemonScope { 278 case snap.SystemDaemon: 279 systemServices = append(systemServices, svcName) 280 case snap.UserDaemon: 281 userServices = append(userServices, svcName) 282 } 283 if flags.Enable { 284 toEnable = append(toEnable, app) 285 } 286 } 287 } 288 289 disableEnabledServices, err = enableServices(toEnable, inter) 290 if err != nil { 291 return err 292 } 293 294 // handle sockets and timers 295 for _, app := range apps { 296 // they're *supposed* to be all services, but checking doesn't hurt 297 if !app.IsService() { 298 continue 299 } 300 301 var sysd systemd.Systemd 302 switch app.DaemonScope { 303 case snap.SystemDaemon: 304 sysd = systemSysd 305 case snap.UserDaemon: 306 sysd = userSysd 307 } 308 309 defer func(app *snap.AppInfo) { 310 if err == nil { 311 return 312 } 313 314 if e := stopService(sysd, app, inter); e != nil { 315 inter.Notify(fmt.Sprintf("While trying to stop previously started service %q: %v", app.ServiceName(), e)) 316 } 317 for _, socket := range app.Sockets { 318 socketService := filepath.Base(socket.File()) 319 if e := sysd.Disable(socketService); e != nil { 320 inter.Notify(fmt.Sprintf("While trying to disable previously enabled socket service %q: %v", socketService, e)) 321 } 322 } 323 if app.Timer != nil { 324 timerService := filepath.Base(app.Timer.File()) 325 if e := sysd.Disable(timerService); e != nil { 326 inter.Notify(fmt.Sprintf("While trying to disable previously enabled timer service %q: %v", timerService, e)) 327 } 328 } 329 }(app) 330 331 for _, socket := range app.Sockets { 332 socketService := filepath.Base(socket.File()) 333 // enable the socket 334 if err = sysd.Enable(socketService); err != nil { 335 return err 336 } 337 338 switch app.DaemonScope { 339 case snap.SystemDaemon: 340 timings.Run(tm, "start-system-socket-service", fmt.Sprintf("start system socket service %q", socketService), func(nested timings.Measurer) { 341 err = sysd.Start(socketService) 342 }) 343 case snap.UserDaemon: 344 timings.Run(tm, "start-user-socket-service", fmt.Sprintf("start user socket service %q", socketService), func(nested timings.Measurer) { 345 err = startUserServices(cli, inter, socketService) 346 }) 347 } 348 if err != nil { 349 return err 350 } 351 } 352 353 if app.Timer != nil { 354 timerService := filepath.Base(app.Timer.File()) 355 // enable the timer 356 if err = sysd.Enable(timerService); err != nil { 357 return err 358 } 359 360 switch app.DaemonScope { 361 case snap.SystemDaemon: 362 timings.Run(tm, "start-system-timer-service", fmt.Sprintf("start system timer service %q", timerService), func(nested timings.Measurer) { 363 err = sysd.Start(timerService) 364 }) 365 case snap.UserDaemon: 366 timings.Run(tm, "start-user-timer-service", fmt.Sprintf("start user timer service %q", timerService), func(nested timings.Measurer) { 367 err = startUserServices(cli, inter, timerService) 368 }) 369 } 370 if err != nil { 371 return err 372 } 373 } 374 } 375 376 for _, srv := range systemServices { 377 // starting all services at once does not create a single 378 // transaction, but instead spawns multiple jobs, make sure the 379 // services started in the original order by bring them up one 380 // by one, see: 381 // https://github.com/systemd/systemd/issues/8102 382 // https://lists.freedesktop.org/archives/systemd-devel/2018-January/040152.html 383 timings.Run(tm, "start-service", fmt.Sprintf("start service %q", srv), func(nested timings.Measurer) { 384 err = systemSysd.Start(srv) 385 }) 386 if err != nil { 387 // cleanup was set up by iterating over apps 388 return err 389 } 390 } 391 392 if len(userServices) != 0 { 393 timings.Run(tm, "start-user-services", "start user services", func(nested timings.Measurer) { 394 err = startUserServices(cli, inter, userServices...) 395 }) 396 if err != nil { 397 return err 398 } 399 } 400 401 return nil 402 } 403 404 func userDaemonReload() error { 405 cli := client.New() 406 ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout.DefaultTimeout)) 407 defer cancel() 408 return cli.ServicesDaemonReload(ctx) 409 } 410 411 func tryFileUpdate(path string, desiredContent []byte) (old *osutil.MemoryFileState, modified bool, err error) { 412 newFileState := osutil.MemoryFileState{ 413 Content: desiredContent, 414 Mode: os.FileMode(0644), 415 } 416 417 // get the existing content (if any) of the file to have something to 418 // rollback to if we have any errors 419 420 // note we can't use FileReference here since we may be modifying 421 // the file, and the FileReference.State() wouldn't be evaluated 422 // until _after_ we attempted modification 423 oldFileState := osutil.MemoryFileState{} 424 425 if st, err := os.Stat(path); err == nil { 426 b, err := ioutil.ReadFile(path) 427 if err != nil { 428 return nil, false, err 429 } 430 oldFileState.Content = b 431 oldFileState.Mode = st.Mode() 432 newFileState.Mode = st.Mode() 433 434 // save the old state of the file 435 old = &oldFileState 436 } 437 438 if mkdirErr := os.MkdirAll(filepath.Dir(path), 0755); mkdirErr != nil { 439 return nil, false, mkdirErr 440 } 441 ensureErr := osutil.EnsureFileState(path, &newFileState) 442 switch ensureErr { 443 case osutil.ErrSameState: 444 // didn't change the file 445 return old, false, nil 446 case nil: 447 // we successfully modified the file 448 return old, true, nil 449 default: 450 // some other fatal error trying to write the file 451 return nil, false, ensureErr 452 } 453 } 454 455 type SnapServiceOptions struct { 456 // VitalityRank is the rank of all services in the specified snap used by 457 // the OOM killer when OOM conditions are reached. 458 VitalityRank int 459 460 // QuotaGroup is the quota group for all services in the specified snap. 461 QuotaGroup *quota.Group 462 } 463 464 // ObserveChangeCallback can be invoked by EnsureSnapServices to observe 465 // the previous content of a unit and the new on a change. 466 // unitType can be "service", "socket", "timer". name is empty for a timer. 467 type ObserveChangeCallback func(app *snap.AppInfo, grp *quota.Group, unitType string, name, old, new string) 468 469 // EnsureSnapServicesOptions is the set of options applying to the 470 // EnsureSnapServices operation. It does not include per-snap specific options 471 // such as VitalityRank or RequireMountedSnapdSnap from AddSnapServiceOptions, 472 // since those are expected to be provided in the snaps argument. 473 type EnsureSnapServicesOptions struct { 474 // Preseeding is whether the system is currently being preseeded, in which 475 // case there is not a running systemd for EnsureSnapServicesOptions to 476 // issue commands like systemctl daemon-reload to. 477 Preseeding bool 478 479 // RequireMountedSnapdSnap is whether the generated units should depend on 480 // the snapd snap being mounted, this is specific to systems like UC18 and 481 // UC20 which have the snapd snap and need to have units generated 482 RequireMountedSnapdSnap bool 483 } 484 485 // EnsureSnapServices will ensure that the specified snap services' file states 486 // are up to date with the specified options and infos. It will add new services 487 // if those units don't already exist, but it does not delete existing service 488 // units that are not present in the snap's Info structures. 489 // There are two sets of options; there are global options which apply to the 490 // entire transaction and to every snap service that is ensured, and options 491 // which are per-snap service and specified in the map argument. 492 // If any errors are encountered trying to update systemd units, then all 493 // changes performed up to that point are rolled back, meaning newly written 494 // units are deleted and modified units are attempted to be restored to their 495 // previous state. 496 // To observe which units were added or modified a 497 // ObserveChangeCallback calllback can be provided. The callback is 498 // invoked while processing the changes. Because of that it should not 499 // produce immediate side-effects, as the changes are in effect only 500 // if the function did not return an error. 501 func EnsureSnapServices(snaps map[*snap.Info]*SnapServiceOptions, opts *EnsureSnapServicesOptions, observeChange ObserveChangeCallback, inter interacter) (err error) { 502 // note, sysd is not used when preseeding 503 sysd := systemd.New(systemd.SystemMode, inter) 504 505 if opts == nil { 506 opts = &EnsureSnapServicesOptions{} 507 } 508 509 // we only consider the global EnsureSnapServicesOptions to decide if we 510 // are preseeding or not to reduce confusion about which set of options 511 // determines whether we are preseeding or not during the ensure operation 512 preseeding := opts.Preseeding 513 514 // modifiedUnitsPreviousState is the set of units that were modified and the previous 515 // state of the unit before modification that we can roll back to if there 516 // are any issues. 517 // note that the rollback is best effort, if we are rebooted in the middle, 518 // there is no guarantee about the state of files, some may have been 519 // updated and some may have been rolled back, higher level tasks/changes 520 // should have do/undo handlers to properly handle the case where this 521 // function is interrupted midway 522 modifiedUnitsPreviousState := make(map[string]*osutil.MemoryFileState) 523 var modifiedSystem, modifiedUser bool 524 525 defer func() { 526 if err == nil { 527 return 528 } 529 for file, state := range modifiedUnitsPreviousState { 530 if state == nil { 531 // we don't have anything to rollback to, so just remove the 532 // file 533 if e := os.Remove(file); e != nil { 534 inter.Notify(fmt.Sprintf("while trying to remove %s due to previous failure: %v", file, e)) 535 } 536 } else { 537 // rollback the file to the previous state 538 if e := osutil.EnsureFileState(file, state); e != nil { 539 inter.Notify(fmt.Sprintf("while trying to rollback %s due to previous failure: %v", file, e)) 540 } 541 } 542 } 543 if modifiedSystem && !preseeding { 544 if e := sysd.DaemonReload(); e != nil { 545 inter.Notify(fmt.Sprintf("while trying to perform systemd daemon-reload due to previous failure: %v", e)) 546 } 547 } 548 if modifiedUser && !preseeding { 549 if e := userDaemonReload(); e != nil { 550 inter.Notify(fmt.Sprintf("while trying to perform user systemd daemon-reload due to previous failure: %v", e)) 551 } 552 } 553 }() 554 555 handleFileModification := func(app *snap.AppInfo, unitType string, name, path string, content []byte) error { 556 old, modifiedFile, err := tryFileUpdate(path, content) 557 if err != nil { 558 return err 559 } 560 561 if modifiedFile { 562 if observeChange != nil { 563 var oldContent []byte 564 if old != nil { 565 oldContent = old.Content 566 } 567 observeChange(app, nil, unitType, name, string(oldContent), string(content)) 568 } 569 modifiedUnitsPreviousState[path] = old 570 571 // also mark that we need to reload either the system or 572 // user instance of systemd 573 switch app.DaemonScope { 574 case snap.SystemDaemon: 575 modifiedSystem = true 576 case snap.UserDaemon: 577 modifiedUser = true 578 } 579 } 580 581 return nil 582 } 583 584 neededQuotaGrps := "a.QuotaGroupSet{} 585 586 for s, snapSvcOpts := range snaps { 587 if s.Type() == snap.TypeSnapd { 588 return fmt.Errorf("internal error: adding explicit services for snapd snap is unexpected") 589 } 590 591 // always use RequireMountedSnapdSnap options from the global options 592 genServiceOpts := &AddSnapServicesOptions{ 593 RequireMountedSnapdSnap: opts.RequireMountedSnapdSnap, 594 } 595 if snapSvcOpts != nil { 596 // and if there are per-snap options specified, use that for 597 // VitalityRank 598 genServiceOpts.VitalityRank = snapSvcOpts.VitalityRank 599 genServiceOpts.QuotaGroup = snapSvcOpts.QuotaGroup 600 601 if snapSvcOpts.QuotaGroup != nil { 602 if err := neededQuotaGrps.AddAllNecessaryGroups(snapSvcOpts.QuotaGroup); err != nil { 603 // this error can basically only be a circular reference 604 // in the quota group tree 605 return err 606 } 607 } 608 } 609 // note that the Preseeding option is not used here at all 610 611 for _, app := range s.Apps { 612 if !app.IsService() { 613 continue 614 } 615 616 // create services first; this doesn't trigger systemd 617 618 // Generate new service file state 619 path := app.ServiceFile() 620 content, err := generateSnapServiceFile(app, genServiceOpts) 621 if err != nil { 622 return err 623 } 624 625 if err := handleFileModification(app, "service", app.Name, path, content); err != nil { 626 return err 627 } 628 629 // Generate systemd .socket files if needed 630 socketFiles, err := generateSnapSocketFiles(app) 631 if err != nil { 632 return err 633 } 634 for name, content := range socketFiles { 635 path := app.Sockets[name].File() 636 if err := handleFileModification(app, "socket", name, path, content); err != nil { 637 return err 638 } 639 } 640 641 if app.Timer != nil { 642 content, err := generateSnapTimerFile(app) 643 if err != nil { 644 return err 645 } 646 path := app.Timer.File() 647 if err := handleFileModification(app, "timer", "", path, content); err != nil { 648 return err 649 } 650 } 651 } 652 } 653 654 handleSliceModification := func(grp *quota.Group, path string, content []byte) error { 655 old, modifiedFile, err := tryFileUpdate(path, content) 656 if err != nil { 657 return err 658 } 659 660 if modifiedFile { 661 if observeChange != nil { 662 var oldContent []byte 663 if old != nil { 664 oldContent = old.Content 665 } 666 observeChange(nil, grp, "slice", grp.Name, string(oldContent), string(content)) 667 } 668 669 modifiedUnitsPreviousState[path] = old 670 671 // also mark that we need to reload the system instance of systemd 672 // TODO: also handle reloading the user instance of systemd when 673 // needed 674 modifiedSystem = true 675 } 676 677 return nil 678 } 679 680 // now make sure that all of the slice units exist 681 for _, grp := range neededQuotaGrps.AllQuotaGroups() { 682 content, err := generateGroupSliceFile(grp) 683 if err != nil { 684 return err 685 } 686 687 sliceFileName := grp.SliceFileName() 688 path := filepath.Join(dirs.SnapServicesDir, sliceFileName) 689 if err := handleSliceModification(grp, path, content); err != nil { 690 return err 691 } 692 } 693 694 if !preseeding { 695 if modifiedSystem { 696 if err = sysd.DaemonReload(); err != nil { 697 return err 698 } 699 } 700 if modifiedUser { 701 if err = userDaemonReload(); err != nil { 702 return err 703 } 704 } 705 } 706 707 return nil 708 } 709 710 // AddSnapServicesOptions is a struct for controlling the generated service 711 // definition for a snap service. 712 type AddSnapServicesOptions struct { 713 // VitalityRank is the rank of all services in the specified snap used by 714 // the OOM killer when OOM conditions are reached. 715 VitalityRank int 716 717 // QuotaGroup is the quota group for all services in the specified snap. 718 QuotaGroup *quota.Group 719 720 // RequireMountedSnapdSnap is whether the generated units should depend on 721 // the snapd snap being mounted, this is specific to systems like UC18 and 722 // UC20 which have the snapd snap and need to have units generated 723 RequireMountedSnapdSnap bool 724 725 // Preseeding is whether the system is currently being preseeded, in which 726 // case there is not a running systemd for EnsureSnapServicesOptions to 727 // issue commands like systemctl daemon-reload to. 728 Preseeding bool 729 } 730 731 // AddSnapServices adds service units for the applications from the snap which 732 // are services. The services do not get enabled or started. 733 func AddSnapServices(s *snap.Info, opts *AddSnapServicesOptions, inter interacter) error { 734 m := map[*snap.Info]*SnapServiceOptions{ 735 s: {}, 736 } 737 ensureOpts := &EnsureSnapServicesOptions{} 738 if opts != nil { 739 // set the per-snap service options 740 m[s].VitalityRank = opts.VitalityRank 741 m[s].QuotaGroup = opts.QuotaGroup 742 743 // copy the globally applicable opts from AddSnapServicesOptions to 744 // EnsureSnapServicesOptions, since those options override the per-snap opts 745 // we put in the map argument 746 ensureOpts.Preseeding = opts.Preseeding 747 ensureOpts.RequireMountedSnapdSnap = opts.RequireMountedSnapdSnap 748 } 749 750 return EnsureSnapServices(m, ensureOpts, nil, inter) 751 } 752 753 // StopServicesFlags carries extra flags for StopServices. 754 type StopServicesFlags struct { 755 Disable bool 756 } 757 758 // StopServices stops and optionally disables service units for the applications 759 // from the snap which are services. 760 func StopServices(apps []*snap.AppInfo, flags *StopServicesFlags, reason snap.ServiceStopReason, inter interacter, tm timings.Measurer) error { 761 sysd := systemd.New(systemd.SystemMode, inter) 762 if flags == nil { 763 flags = &StopServicesFlags{} 764 } 765 766 if reason != snap.StopReasonOther { 767 logger.Debugf("StopServices called for %q, reason: %v", apps, reason) 768 } else { 769 logger.Debugf("StopServices called for %q", apps) 770 } 771 for _, app := range apps { 772 // Handle the case where service file doesn't exist and don't try to stop it as it will fail. 773 // This can happen with snap try when snap.yaml is modified on the fly and a daemon line is added. 774 if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { 775 continue 776 } 777 // Skip stop on refresh when refresh mode is set to something 778 // other than "restart" (or "" which is the same) 779 if reason == snap.StopReasonRefresh { 780 logger.Debugf(" %s refresh-mode: %v", app.Name, app.StopMode) 781 switch app.RefreshMode { 782 case "endure": 783 // skip this service 784 continue 785 } 786 } 787 788 var err error 789 timings.Run(tm, "stop-service", fmt.Sprintf("stop service %q", app.ServiceName()), func(nested timings.Measurer) { 790 err = stopService(sysd, app, inter) 791 if err == nil && flags.Disable { 792 err = sysd.Disable(app.ServiceName()) 793 } 794 }) 795 if err != nil { 796 return err 797 } 798 799 // ensure the service is really stopped on remove regardless 800 // of stop-mode 801 if reason == snap.StopReasonRemove && !app.StopMode.KillAll() && app.DaemonScope == snap.SystemDaemon { 802 // FIXME: make this smarter and avoid the killWait 803 // delay if not needed (i.e. if all processes 804 // have died) 805 sysd.Kill(app.ServiceName(), "TERM", "all") 806 time.Sleep(killWait) 807 sysd.Kill(app.ServiceName(), "KILL", "") 808 } 809 } 810 return nil 811 } 812 813 // ServicesEnableState returns a map of service names from the given snap, 814 // together with their enable/disable status. 815 func ServicesEnableState(s *snap.Info, inter interacter) (map[string]bool, error) { 816 sysd := systemd.New(systemd.SystemMode, inter) 817 818 // loop over all services in the snap, querying systemd for the current 819 // systemd state of the snaps 820 snapSvcsState := make(map[string]bool, len(s.Apps)) 821 for name, app := range s.Apps { 822 if !app.IsService() { 823 continue 824 } 825 // FIXME: handle user daemons 826 if app.DaemonScope != snap.SystemDaemon { 827 continue 828 } 829 state, err := sysd.IsEnabled(app.ServiceName()) 830 if err != nil { 831 return nil, err 832 } 833 snapSvcsState[name] = state 834 } 835 return snapSvcsState, nil 836 } 837 838 // RemoveQuotaGroup ensures that the slice file for a quota group is removed. It 839 // assumes that the slice corresponding to the group is not in use anymore by 840 // any services or sub-groups of the group when it is invoked. 841 // group with sub-groups, one must remove all the sub-groups first. 842 func RemoveQuotaGroup(grp *quota.Group, inter interacter) error { 843 // TODO: it only works on leaf sub-groups currently 844 if len(grp.SubGroups) != 0 { 845 return fmt.Errorf("internal error: cannot remove quota group with sub-groups") 846 } 847 848 systemSysd := systemd.New(systemd.SystemMode, inter) 849 850 // remove the slice file 851 err := os.Remove(filepath.Join(dirs.SnapServicesDir, grp.SliceFileName())) 852 if err != nil && !os.IsNotExist(err) { 853 return err 854 } 855 856 if err == nil { 857 // we deleted the slice unit, so we need to daemon-reload 858 if err := systemSysd.DaemonReload(); err != nil { 859 return err 860 } 861 } 862 return nil 863 } 864 865 // RemoveSnapServices disables and removes service units for the applications 866 // from the snap which are services. The optional flag indicates whether 867 // services are removed as part of undoing of first install of a given snap. 868 func RemoveSnapServices(s *snap.Info, inter interacter) error { 869 if s.Type() == snap.TypeSnapd { 870 return fmt.Errorf("internal error: removing explicit services for snapd snap is unexpected") 871 } 872 systemSysd := systemd.New(systemd.SystemMode, inter) 873 userSysd := systemd.New(systemd.GlobalUserMode, inter) 874 var removedSystem, removedUser bool 875 876 for _, app := range s.Apps { 877 if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { 878 continue 879 } 880 881 var sysd systemd.Systemd 882 switch app.DaemonScope { 883 case snap.SystemDaemon: 884 sysd = systemSysd 885 removedSystem = true 886 case snap.UserDaemon: 887 sysd = userSysd 888 removedUser = true 889 } 890 serviceName := filepath.Base(app.ServiceFile()) 891 892 for _, socket := range app.Sockets { 893 path := socket.File() 894 socketServiceName := filepath.Base(path) 895 if err := sysd.Disable(socketServiceName); err != nil { 896 return err 897 } 898 899 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 900 logger.Noticef("Failed to remove socket file %q for %q: %v", path, serviceName, err) 901 } 902 } 903 904 if app.Timer != nil { 905 path := app.Timer.File() 906 907 timerName := filepath.Base(path) 908 if err := sysd.Disable(timerName); err != nil { 909 return err 910 } 911 912 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 913 logger.Noticef("Failed to remove timer file %q for %q: %v", path, serviceName, err) 914 } 915 } 916 917 if err := sysd.Disable(serviceName); err != nil { 918 return err 919 } 920 921 if err := os.Remove(app.ServiceFile()); err != nil && !os.IsNotExist(err) { 922 logger.Noticef("Failed to remove service file for %q: %v", serviceName, err) 923 } 924 925 } 926 927 // only reload if we actually had services 928 if removedSystem { 929 if err := systemSysd.DaemonReload(); err != nil { 930 return err 931 } 932 } 933 if removedUser { 934 if err := userDaemonReload(); err != nil { 935 return err 936 } 937 } 938 939 return nil 940 } 941 942 func genServiceNames(snap *snap.Info, appNames []string) []string { 943 names := make([]string, 0, len(appNames)) 944 945 for _, name := range appNames { 946 if app := snap.Apps[name]; app != nil { 947 names = append(names, app.ServiceName()) 948 } 949 } 950 return names 951 } 952 953 // TODO: this should not accept AddSnapServicesOptions, it should use some other 954 // subset of options, specifically it should not accept Preseeding as an option 955 // here 956 func genServiceFile(appInfo *snap.AppInfo, opts *AddSnapServicesOptions) ([]byte, error) { 957 if opts == nil { 958 opts = &AddSnapServicesOptions{} 959 } 960 961 // assemble all of the service directive snippets for all interfaces that 962 // this service needs to include in the generated systemd file 963 964 // use an ordered set to ensure we don't duplicate any keys from interfaces 965 // that specify the same snippet 966 967 // TODO: maybe we should error if multiple interfaces specify different 968 // values for the same directive, otherwise one of them will overwrite the 969 // other? What happens right now is that the snippet from the plug that 970 // comes last will win in the case of directives that can have only one 971 // value, but for some directives, systemd combines their values into a 972 // list. 973 ifaceServiceSnippets := &strutil.OrderedSet{} 974 975 for _, plug := range appInfo.Plugs { 976 iface, err := interfaces.ByName(plug.Interface) 977 if err != nil { 978 return nil, fmt.Errorf("error processing plugs while generating service unit for %v: %v", appInfo.SecurityTag(), err) 979 } 980 snips, err := interfaces.PermanentPlugServiceSnippets(iface, plug) 981 if err != nil { 982 return nil, fmt.Errorf("error processing plugs while generating service unit for %v: %v", appInfo.SecurityTag(), err) 983 } 984 for _, snip := range snips { 985 ifaceServiceSnippets.Put(snip) 986 } 987 } 988 989 // join the service snippets into one string to be included in the 990 // template 991 ifaceSpecifiedServiceSnippet := strings.Join(ifaceServiceSnippets.Items(), "\n") 992 993 serviceTemplate := `[Unit] 994 # Auto-generated, DO NOT EDIT 995 Description=Service for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 996 {{- if .MountUnit }} 997 Requires={{.MountUnit}} 998 {{- end }} 999 {{- if .PrerequisiteTarget}} 1000 Wants={{.PrerequisiteTarget}} 1001 {{- end}} 1002 {{- if .After}} 1003 After={{ stringsJoin .After " " }} 1004 {{- end}} 1005 {{- if .Before}} 1006 Before={{ stringsJoin .Before " "}} 1007 {{- end}} 1008 {{- if .CoreMountedSnapdSnapDep}} 1009 Wants={{ stringsJoin .CoreMountedSnapdSnapDep " "}} 1010 After={{ stringsJoin .CoreMountedSnapdSnapDep " "}} 1011 {{- end}} 1012 X-Snappy=yes 1013 1014 [Service] 1015 EnvironmentFile=-/etc/environment 1016 ExecStart={{.App.LauncherCommand}} 1017 SyslogIdentifier={{.App.Snap.InstanceName}}.{{.App.Name}} 1018 Restart={{.Restart}} 1019 {{- if .App.RestartDelay}} 1020 RestartSec={{.App.RestartDelay.Seconds}} 1021 {{- end}} 1022 WorkingDirectory={{.WorkingDir}} 1023 {{- if .App.StopCommand}} 1024 ExecStop={{.App.LauncherStopCommand}} 1025 {{- end}} 1026 {{- if .App.ReloadCommand}} 1027 ExecReload={{.App.LauncherReloadCommand}} 1028 {{- end}} 1029 {{- if .App.PostStopCommand}} 1030 ExecStopPost={{.App.LauncherPostStopCommand}} 1031 {{- end}} 1032 {{- if .StopTimeout}} 1033 TimeoutStopSec={{.StopTimeout.Seconds}} 1034 {{- end}} 1035 {{- if .StartTimeout}} 1036 TimeoutStartSec={{.StartTimeout.Seconds}} 1037 {{- end}} 1038 Type={{.App.Daemon}} 1039 {{- if .Remain}} 1040 RemainAfterExit={{.Remain}} 1041 {{- end}} 1042 {{- if .BusName}} 1043 BusName={{.BusName}} 1044 {{- end}} 1045 {{- if .App.WatchdogTimeout}} 1046 WatchdogSec={{.App.WatchdogTimeout.Seconds}} 1047 {{- end}} 1048 {{- if .KillMode}} 1049 KillMode={{.KillMode}} 1050 {{- end}} 1051 {{- if .KillSignal}} 1052 KillSignal={{.KillSignal}} 1053 {{- end}} 1054 {{- if .OOMAdjustScore }} 1055 OOMScoreAdjust={{.OOMAdjustScore}} 1056 {{- end}} 1057 {{- if .InterfaceServiceSnippets}} 1058 {{.InterfaceServiceSnippets}} 1059 {{- end}} 1060 {{- if .SliceUnit}} 1061 Slice={{.SliceUnit}} 1062 {{- end}} 1063 {{- if not (or .App.Sockets .App.Timer .App.ActivatesOn) }} 1064 1065 [Install] 1066 WantedBy={{.ServicesTarget}} 1067 {{- end}} 1068 ` 1069 var templateOut bytes.Buffer 1070 tmpl := template.New("service-wrapper") 1071 tmpl.Funcs(template.FuncMap{ 1072 "stringsJoin": strings.Join, 1073 }) 1074 t := template.Must(tmpl.Parse(serviceTemplate)) 1075 1076 restartCond := appInfo.RestartCond.String() 1077 if restartCond == "" { 1078 restartCond = snap.RestartOnFailure.String() 1079 } 1080 1081 // use score -900+vitalityRank, where vitalityRank starts at 1 1082 // and considering snapd itself has OOMScoreAdjust=-900 1083 const baseOOMAdjustScore = -900 1084 var oomAdjustScore int 1085 if opts.VitalityRank > 0 { 1086 oomAdjustScore = baseOOMAdjustScore + opts.VitalityRank 1087 } 1088 1089 var remain string 1090 if appInfo.Daemon == "oneshot" { 1091 // any restart condition other than "no" is invalid for oneshot daemons 1092 restartCond = "no" 1093 // If StopExec is present for a oneshot service than we also need 1094 // RemainAfterExit=yes 1095 if appInfo.StopCommand != "" { 1096 remain = "yes" 1097 } 1098 } 1099 var killMode string 1100 if !appInfo.StopMode.KillAll() { 1101 killMode = "process" 1102 } 1103 1104 var busName string 1105 if appInfo.Daemon == "dbus" { 1106 busName = appInfo.BusName 1107 if busName == "" && len(appInfo.ActivatesOn) != 0 { 1108 slot := appInfo.ActivatesOn[len(appInfo.ActivatesOn)-1] 1109 if err := slot.Attr("name", &busName); err != nil { 1110 // This should be impossible for a valid AppInfo 1111 logger.Noticef("Cannot get 'name' attribute of dbus slot %q: %v", slot.Name, err) 1112 } 1113 } 1114 } 1115 1116 wrapperData := struct { 1117 App *snap.AppInfo 1118 1119 Restart string 1120 WorkingDir string 1121 StopTimeout time.Duration 1122 StartTimeout time.Duration 1123 ServicesTarget string 1124 PrerequisiteTarget string 1125 MountUnit string 1126 Remain string 1127 KillMode string 1128 KillSignal string 1129 OOMAdjustScore int 1130 BusName string 1131 Before []string 1132 After []string 1133 InterfaceServiceSnippets string 1134 SliceUnit string 1135 1136 Home string 1137 EnvVars string 1138 1139 CoreMountedSnapdSnapDep []string 1140 }{ 1141 App: appInfo, 1142 1143 InterfaceServiceSnippets: ifaceSpecifiedServiceSnippet, 1144 1145 Restart: restartCond, 1146 StopTimeout: serviceStopTimeout(appInfo), 1147 StartTimeout: time.Duration(appInfo.StartTimeout), 1148 Remain: remain, 1149 KillMode: killMode, 1150 KillSignal: appInfo.StopMode.KillSignal(), 1151 OOMAdjustScore: oomAdjustScore, 1152 BusName: busName, 1153 1154 Before: genServiceNames(appInfo.Snap, appInfo.Before), 1155 After: genServiceNames(appInfo.Snap, appInfo.After), 1156 1157 // systemd runs as PID 1 so %h will not work. 1158 Home: "/root", 1159 } 1160 switch appInfo.DaemonScope { 1161 case snap.SystemDaemon: 1162 wrapperData.ServicesTarget = systemd.ServicesTarget 1163 wrapperData.PrerequisiteTarget = systemd.PrerequisiteTarget 1164 wrapperData.MountUnit = filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())) 1165 wrapperData.WorkingDir = appInfo.Snap.DataDir() 1166 wrapperData.After = append(wrapperData.After, "snapd.apparmor.service") 1167 case snap.UserDaemon: 1168 wrapperData.ServicesTarget = systemd.UserServicesTarget 1169 // FIXME: ideally use UserDataDir("%h"), but then the 1170 // unit fails if the directory doesn't exist. 1171 wrapperData.WorkingDir = appInfo.Snap.DataDir() 1172 default: 1173 panic("unknown snap.DaemonScope") 1174 } 1175 1176 // check the quota group slice 1177 if opts.QuotaGroup != nil { 1178 wrapperData.SliceUnit = opts.QuotaGroup.SliceFileName() 1179 } 1180 1181 // Add extra "After" targets 1182 if wrapperData.PrerequisiteTarget != "" { 1183 wrapperData.After = append([]string{wrapperData.PrerequisiteTarget}, wrapperData.After...) 1184 } 1185 if wrapperData.MountUnit != "" { 1186 wrapperData.After = append([]string{wrapperData.MountUnit}, wrapperData.After...) 1187 } 1188 1189 if opts.RequireMountedSnapdSnap { 1190 // on core 18+ systems, the snapd tooling is exported 1191 // into the host system via a special mount unit, which 1192 // also adds an implicit dependency on the snapd snap 1193 // mount thus /usr/bin/snap points 1194 wrapperData.CoreMountedSnapdSnapDep = []string{SnapdToolingMountUnit} 1195 } 1196 1197 if err := t.Execute(&templateOut, wrapperData); err != nil { 1198 // this can never happen, except we forget a variable 1199 logger.Panicf("Unable to execute template: %v", err) 1200 } 1201 1202 return templateOut.Bytes(), nil 1203 } 1204 1205 func genServiceSocketFile(appInfo *snap.AppInfo, socketName string) []byte { 1206 socketTemplate := `[Unit] 1207 # Auto-generated, DO NOT EDIT 1208 Description=Socket {{.SocketName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 1209 {{- if .MountUnit}} 1210 Requires={{.MountUnit}} 1211 After={{.MountUnit}} 1212 {{- end}} 1213 X-Snappy=yes 1214 1215 [Socket] 1216 Service={{.ServiceFileName}} 1217 FileDescriptorName={{.SocketInfo.Name}} 1218 ListenStream={{.ListenStream}} 1219 {{- if .SocketInfo.SocketMode}} 1220 SocketMode={{.SocketInfo.SocketMode | printf "%04o"}} 1221 {{- end}} 1222 1223 [Install] 1224 WantedBy={{.SocketsTarget}} 1225 ` 1226 var templateOut bytes.Buffer 1227 t := template.Must(template.New("socket-wrapper").Parse(socketTemplate)) 1228 1229 socket := appInfo.Sockets[socketName] 1230 listenStream := renderListenStream(socket) 1231 wrapperData := struct { 1232 App *snap.AppInfo 1233 ServiceFileName string 1234 SocketsTarget string 1235 MountUnit string 1236 SocketName string 1237 SocketInfo *snap.SocketInfo 1238 ListenStream string 1239 }{ 1240 App: appInfo, 1241 ServiceFileName: filepath.Base(appInfo.ServiceFile()), 1242 SocketsTarget: systemd.SocketsTarget, 1243 SocketName: socketName, 1244 SocketInfo: socket, 1245 ListenStream: listenStream, 1246 } 1247 switch appInfo.DaemonScope { 1248 case snap.SystemDaemon: 1249 wrapperData.MountUnit = filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())) 1250 case snap.UserDaemon: 1251 // nothing 1252 default: 1253 panic("unknown snap.DaemonScope") 1254 } 1255 1256 if err := t.Execute(&templateOut, wrapperData); err != nil { 1257 // this can never happen, except we forget a variable 1258 logger.Panicf("Unable to execute template: %v", err) 1259 } 1260 1261 return templateOut.Bytes() 1262 } 1263 1264 func generateSnapSocketFiles(app *snap.AppInfo) (map[string][]byte, error) { 1265 if err := snap.ValidateApp(app); err != nil { 1266 return nil, err 1267 } 1268 1269 socketFiles := make(map[string][]byte) 1270 for name := range app.Sockets { 1271 socketFiles[name] = genServiceSocketFile(app, name) 1272 } 1273 return socketFiles, nil 1274 } 1275 1276 func renderListenStream(socket *snap.SocketInfo) string { 1277 s := socket.App.Snap 1278 listenStream := socket.ListenStream 1279 switch socket.App.DaemonScope { 1280 case snap.SystemDaemon: 1281 listenStream = strings.Replace(listenStream, "$SNAP_DATA", s.DataDir(), -1) 1282 // TODO: when we support User/Group in the generated 1283 // systemd unit, adjust this accordingly 1284 serviceUserUid := sys.UserID(0) 1285 runtimeDir := s.UserXdgRuntimeDir(serviceUserUid) 1286 listenStream = strings.Replace(listenStream, "$XDG_RUNTIME_DIR", runtimeDir, -1) 1287 listenStream = strings.Replace(listenStream, "$SNAP_COMMON", s.CommonDataDir(), -1) 1288 case snap.UserDaemon: 1289 listenStream = strings.Replace(listenStream, "$SNAP_USER_DATA", s.UserDataDir("%h"), -1) 1290 listenStream = strings.Replace(listenStream, "$SNAP_USER_COMMON", s.UserCommonDataDir("%h"), -1) 1291 // FIXME: find some way to share code with snap.UserXdgRuntimeDir() 1292 listenStream = strings.Replace(listenStream, "$XDG_RUNTIME_DIR", fmt.Sprintf("%%t/snap.%s", s.InstanceName()), -1) 1293 default: 1294 panic("unknown snap.DaemonScope") 1295 } 1296 return listenStream 1297 } 1298 1299 func generateSnapTimerFile(app *snap.AppInfo) ([]byte, error) { 1300 timerTemplate := `[Unit] 1301 # Auto-generated, DO NOT EDIT 1302 Description=Timer {{.TimerName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 1303 {{- if .MountUnit}} 1304 Requires={{.MountUnit}} 1305 After={{.MountUnit}} 1306 {{- end}} 1307 X-Snappy=yes 1308 1309 [Timer] 1310 Unit={{.ServiceFileName}} 1311 {{ range .Schedules }}OnCalendar={{ . }} 1312 {{ end }} 1313 [Install] 1314 WantedBy={{.TimersTarget}} 1315 ` 1316 var templateOut bytes.Buffer 1317 t := template.Must(template.New("timer-wrapper").Parse(timerTemplate)) 1318 1319 timerSchedule, err := timeutil.ParseSchedule(app.Timer.Timer) 1320 if err != nil { 1321 return nil, err 1322 } 1323 1324 schedules := generateOnCalendarSchedules(timerSchedule) 1325 1326 wrapperData := struct { 1327 App *snap.AppInfo 1328 ServiceFileName string 1329 TimersTarget string 1330 TimerName string 1331 MountUnit string 1332 Schedules []string 1333 }{ 1334 App: app, 1335 ServiceFileName: filepath.Base(app.ServiceFile()), 1336 TimersTarget: systemd.TimersTarget, 1337 TimerName: app.Name, 1338 Schedules: schedules, 1339 } 1340 switch app.DaemonScope { 1341 case snap.SystemDaemon: 1342 wrapperData.MountUnit = filepath.Base(systemd.MountUnitPath(app.Snap.MountDir())) 1343 case snap.UserDaemon: 1344 // nothing 1345 default: 1346 panic("unknown snap.DaemonScope") 1347 } 1348 1349 if err := t.Execute(&templateOut, wrapperData); err != nil { 1350 // this can never happen, except we forget a variable 1351 logger.Panicf("Unable to execute template: %v", err) 1352 } 1353 1354 return templateOut.Bytes(), nil 1355 } 1356 1357 func makeAbbrevWeekdays(start time.Weekday, end time.Weekday) []string { 1358 out := make([]string, 0, 7) 1359 for w := start; w%7 != (end + 1); w++ { 1360 out = append(out, time.Weekday(w % 7).String()[0:3]) 1361 } 1362 return out 1363 } 1364 1365 // daysRange generates a string representing a continuous range between given 1366 // day numbers, which due to compatiblilty with old systemd version uses a 1367 // verbose syntax of x,y,z instead of x..z 1368 func daysRange(start, end uint) string { 1369 var buf bytes.Buffer 1370 for i := start; i <= end; i++ { 1371 buf.WriteString(strconv.FormatInt(int64(i), 10)) 1372 if i < end { 1373 buf.WriteRune(',') 1374 } 1375 } 1376 return buf.String() 1377 } 1378 1379 // generateOnCalendarSchedules converts a schedule into OnCalendar schedules 1380 // suitable for use in systemd *.timer units using systemd.time(7) 1381 // https://www.freedesktop.org/software/systemd/man/systemd.time.html 1382 // XXX: old systemd versions do not support x..y ranges 1383 func generateOnCalendarSchedules(schedule []*timeutil.Schedule) []string { 1384 calendarEvents := make([]string, 0, len(schedule)) 1385 for _, sched := range schedule { 1386 days := make([]string, 0, len(sched.WeekSpans)) 1387 for _, week := range sched.WeekSpans { 1388 abbrev := strings.Join(makeAbbrevWeekdays(week.Start.Weekday, week.End.Weekday), ",") 1389 1390 if week.Start.Pos == timeutil.EveryWeek && week.End.Pos == timeutil.EveryWeek { 1391 // eg: mon, mon-fri, fri-mon 1392 days = append(days, fmt.Sprintf("%s *-*-*", abbrev)) 1393 continue 1394 } 1395 // examples: 1396 // mon1 - Mon *-*-1..7 (Monday during the first 7 days) 1397 // fri1 - Fri *-*-1..7 (Friday during the first 7 days) 1398 1399 // entries below will make systemd timer expire more 1400 // frequently than the schedule suggests, however snap 1401 // runner evaluates current time and gates the actual 1402 // action 1403 // 1404 // mon1-tue - *-*-1..7 *-*-8 (anchored at first 1405 // Monday; Monday happens during the 7 days, 1406 // Tuesday can possibly happen on the 8th day if 1407 // the month started on Tuesday) 1408 // 1409 // mon-tue1 - *-*~1 *-*-1..7 (anchored at first 1410 // Tuesday; matching Monday can happen on the 1411 // last day of previous month if Tuesday is the 1412 // 1st) 1413 // 1414 // mon5-tue - *-*~1..7 *-*-1 (anchored at last 1415 // Monday, the matching Tuesday can still happen 1416 // within the last 7 days, or on the 1st of the 1417 // next month) 1418 // 1419 // fri4-mon - *-*-22-31 *-*-1..7 (anchored at 4th 1420 // Friday, can span onto the next month, extreme case in 1421 // February when 28th is Friday) 1422 // 1423 // XXX: since old versions of systemd, eg. 229 available 1424 // in 16.04 does not support x..y ranges, days need to 1425 // be enumerated like so: 1426 // Mon *-*-1..7 -> Mon *-*-1,2,3,4,5,6,7 1427 // 1428 // XXX: old systemd versions do not support the last n 1429 // days syntax eg, *-*~1, thus the range needs to be 1430 // generated in more verbose way like so: 1431 // Mon *-*~1..7 -> Mon *-*-22,23,24,25,26,27,28,29,30,31 1432 // (22-28 is the last week, but the month can have 1433 // anywhere from 28 to 31 days) 1434 // 1435 startPos := week.Start.Pos 1436 endPos := startPos 1437 if !week.AnchoredAtStart() { 1438 startPos = week.End.Pos 1439 endPos = startPos 1440 } 1441 startDay := (startPos-1)*7 + 1 1442 endDay := (endPos) * 7 1443 1444 if week.IsSingleDay() { 1445 // single day, can use the 'weekday' filter 1446 if startPos == timeutil.LastWeek { 1447 // last week of a month, which can be 1448 // 22-28 in case of February, while 1449 // month can have between 28 and 31 days 1450 days = append(days, 1451 fmt.Sprintf("%s *-*-%s", abbrev, daysRange(22, 31))) 1452 } else { 1453 days = append(days, 1454 fmt.Sprintf("%s *-*-%s", abbrev, daysRange(startDay, endDay))) 1455 } 1456 continue 1457 } 1458 1459 if week.AnchoredAtStart() { 1460 // explore the edge cases first 1461 switch startPos { 1462 case timeutil.LastWeek: 1463 // starts in the last week of the month and 1464 // possibly spans into the first week of the 1465 // next month; 1466 // month can have between 28 and 31 1467 // days 1468 days = append(days, 1469 // trailing 29-31 that are not part of a full week 1470 fmt.Sprintf("*-*-%s", daysRange(29, 31)), 1471 fmt.Sprintf("*-*-%s", daysRange(1, 7))) 1472 case 4: 1473 // a range in the 4th week can span onto 1474 // the next week, which is either 28-31 1475 // or in extreme case (eg. February with 1476 // 28 days) 1-7 of the next month 1477 days = append(days, 1478 // trailing 29-31 that are not part of a full week 1479 fmt.Sprintf("*-*-%s", daysRange(29, 31)), 1480 fmt.Sprintf("*-*-%s", daysRange(1, 7))) 1481 default: 1482 // can possibly spill into the next week 1483 days = append(days, 1484 fmt.Sprintf("*-*-%s", daysRange(startDay+7, endDay+7))) 1485 } 1486 1487 if startDay < 28 { 1488 days = append(days, 1489 fmt.Sprintf("*-*-%s", daysRange(startDay, endDay))) 1490 } else { 1491 // from the end of the month 1492 days = append(days, 1493 fmt.Sprintf("*-*-%s", daysRange(startDay-7, endDay-7))) 1494 } 1495 } else { 1496 switch endPos { 1497 case timeutil.LastWeek: 1498 // month can have between 28 and 31 1499 // days, add trailing 29-31 that are not 1500 // part of a full week 1501 days = append(days, fmt.Sprintf("*-*-%s", daysRange(29, 31))) 1502 case 1: 1503 // possibly spans from the last week of the 1504 // previous month and ends in the first week of 1505 // current month 1506 days = append(days, fmt.Sprintf("*-*-%s", daysRange(22, 31))) 1507 default: 1508 // can possibly spill into the previous week 1509 days = append(days, 1510 fmt.Sprintf("*-*-%s", daysRange(startDay-7, endDay-7))) 1511 } 1512 if endDay < 28 { 1513 days = append(days, 1514 fmt.Sprintf("*-*-%s", daysRange(startDay, endDay))) 1515 } else { 1516 days = append(days, 1517 fmt.Sprintf("*-*-%s", daysRange(startDay-7, endDay-7))) 1518 } 1519 } 1520 } 1521 1522 if len(days) == 0 { 1523 // no weekday spec, meaning the timer runs every day 1524 days = []string{"*-*-*"} 1525 } 1526 1527 startTimes := make([]string, 0, len(sched.ClockSpans)) 1528 for _, clocks := range sched.ClockSpans { 1529 // use expanded clock spans 1530 for _, span := range clocks.ClockSpans() { 1531 when := span.Start 1532 if span.Spread { 1533 length := span.End.Sub(span.Start) 1534 if length < 0 { 1535 // span Start wraps around, so we have '00:00.Sub(23:45)' 1536 length = -length 1537 } 1538 if length > 5*time.Minute { 1539 // replicate what timeutil.Next() does 1540 // and cut some time at the end of the 1541 // window so that events do not happen 1542 // directly one after another 1543 length -= 5 * time.Minute 1544 } 1545 when = when.Add(randutil.RandomDuration(length)) 1546 } 1547 if when.Hour == 24 { 1548 // 24:00 for us means the other end of 1549 // the day, for systemd we need to 1550 // adjust it to the 0-23 hour range 1551 when.Hour -= 24 1552 } 1553 1554 startTimes = append(startTimes, when.String()) 1555 } 1556 } 1557 1558 for _, day := range days { 1559 if len(startTimes) == 0 { 1560 // current schedule is days only 1561 calendarEvents = append(calendarEvents, day) 1562 continue 1563 } 1564 1565 for _, startTime := range startTimes { 1566 calendarEvents = append(calendarEvents, fmt.Sprintf("%s %s", day, startTime)) 1567 } 1568 } 1569 } 1570 return calendarEvents 1571 } 1572 1573 type RestartServicesFlags struct { 1574 Reload bool 1575 } 1576 1577 // Restart or reload services; if reload flag is set then "systemctl reload-or-restart" is attempted. 1578 func RestartServices(svcs []*snap.AppInfo, flags *RestartServicesFlags, inter interacter, tm timings.Measurer) error { 1579 sysd := systemd.New(systemd.SystemMode, inter) 1580 1581 for _, srv := range svcs { 1582 // they're *supposed* to be all services, but checking doesn't hurt 1583 if !srv.IsService() { 1584 continue 1585 } 1586 1587 var err error 1588 timings.Run(tm, "restart-service", fmt.Sprintf("restart service %q", srv), func(nested timings.Measurer) { 1589 if flags != nil && flags.Reload { 1590 err = sysd.ReloadOrRestart(srv.ServiceName()) 1591 } else { 1592 // note: stop followed by start, not just 'restart' 1593 err = sysd.Restart(srv.ServiceName(), 5*time.Second) 1594 } 1595 }) 1596 if err != nil { 1597 // there is nothing we can do about failed service 1598 return err 1599 } 1600 } 1601 return nil 1602 } 1603 1604 // QueryDisabledServices returns a list of all currently disabled snap services 1605 // in the snap. 1606 func QueryDisabledServices(info *snap.Info, pb progress.Meter) ([]string, error) { 1607 // save the list of services that are in the disabled state before unlinking 1608 // and thus removing the snap services 1609 snapSvcStates, err := ServicesEnableState(info, pb) 1610 if err != nil { 1611 return nil, err 1612 } 1613 1614 disabledSnapSvcs := []string{} 1615 // add all disabled services to the list 1616 for svc, isEnabled := range snapSvcStates { 1617 if !isEnabled { 1618 disabledSnapSvcs = append(disabledSnapSvcs, svc) 1619 } 1620 } 1621 1622 // sort for easier testing 1623 sort.Strings(disabledSnapSvcs) 1624 1625 return disabledSnapSvcs, nil 1626 }