github.com/ethanhsieh/snapd@v0.0.0-20210615102523-3db9b8e4edc5/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 // This function is idempotent. 502 func EnsureSnapServices(snaps map[*snap.Info]*SnapServiceOptions, opts *EnsureSnapServicesOptions, observeChange ObserveChangeCallback, inter interacter) (err error) { 503 // note, sysd is not used when preseeding 504 sysd := systemd.New(systemd.SystemMode, inter) 505 506 if opts == nil { 507 opts = &EnsureSnapServicesOptions{} 508 } 509 510 // we only consider the global EnsureSnapServicesOptions to decide if we 511 // are preseeding or not to reduce confusion about which set of options 512 // determines whether we are preseeding or not during the ensure operation 513 preseeding := opts.Preseeding 514 515 // modifiedUnitsPreviousState is the set of units that were modified and the previous 516 // state of the unit before modification that we can roll back to if there 517 // are any issues. 518 // note that the rollback is best effort, if we are rebooted in the middle, 519 // there is no guarantee about the state of files, some may have been 520 // updated and some may have been rolled back, higher level tasks/changes 521 // should have do/undo handlers to properly handle the case where this 522 // function is interrupted midway 523 modifiedUnitsPreviousState := make(map[string]*osutil.MemoryFileState) 524 var modifiedSystem, modifiedUser bool 525 526 defer func() { 527 if err == nil { 528 return 529 } 530 for file, state := range modifiedUnitsPreviousState { 531 if state == nil { 532 // we don't have anything to rollback to, so just remove the 533 // file 534 if e := os.Remove(file); e != nil { 535 inter.Notify(fmt.Sprintf("while trying to remove %s due to previous failure: %v", file, e)) 536 } 537 } else { 538 // rollback the file to the previous state 539 if e := osutil.EnsureFileState(file, state); e != nil { 540 inter.Notify(fmt.Sprintf("while trying to rollback %s due to previous failure: %v", file, e)) 541 } 542 } 543 } 544 if modifiedSystem && !preseeding { 545 if e := sysd.DaemonReload(); e != nil { 546 inter.Notify(fmt.Sprintf("while trying to perform systemd daemon-reload due to previous failure: %v", e)) 547 } 548 } 549 if modifiedUser && !preseeding { 550 if e := userDaemonReload(); e != nil { 551 inter.Notify(fmt.Sprintf("while trying to perform user systemd daemon-reload due to previous failure: %v", e)) 552 } 553 } 554 }() 555 556 handleFileModification := func(app *snap.AppInfo, unitType string, name, path string, content []byte) error { 557 old, modifiedFile, err := tryFileUpdate(path, content) 558 if err != nil { 559 return err 560 } 561 562 if modifiedFile { 563 if observeChange != nil { 564 var oldContent []byte 565 if old != nil { 566 oldContent = old.Content 567 } 568 observeChange(app, nil, unitType, name, string(oldContent), string(content)) 569 } 570 modifiedUnitsPreviousState[path] = old 571 572 // also mark that we need to reload either the system or 573 // user instance of systemd 574 switch app.DaemonScope { 575 case snap.SystemDaemon: 576 modifiedSystem = true 577 case snap.UserDaemon: 578 modifiedUser = true 579 } 580 } 581 582 return nil 583 } 584 585 neededQuotaGrps := "a.QuotaGroupSet{} 586 587 for s, snapSvcOpts := range snaps { 588 if s.Type() == snap.TypeSnapd { 589 return fmt.Errorf("internal error: adding explicit services for snapd snap is unexpected") 590 } 591 592 // always use RequireMountedSnapdSnap options from the global options 593 genServiceOpts := &AddSnapServicesOptions{ 594 RequireMountedSnapdSnap: opts.RequireMountedSnapdSnap, 595 } 596 if snapSvcOpts != nil { 597 // and if there are per-snap options specified, use that for 598 // VitalityRank 599 genServiceOpts.VitalityRank = snapSvcOpts.VitalityRank 600 genServiceOpts.QuotaGroup = snapSvcOpts.QuotaGroup 601 602 if snapSvcOpts.QuotaGroup != nil { 603 if err := neededQuotaGrps.AddAllNecessaryGroups(snapSvcOpts.QuotaGroup); err != nil { 604 // this error can basically only be a circular reference 605 // in the quota group tree 606 return err 607 } 608 } 609 } 610 // note that the Preseeding option is not used here at all 611 612 for _, app := range s.Apps { 613 if !app.IsService() { 614 continue 615 } 616 617 // create services first; this doesn't trigger systemd 618 619 // Generate new service file state 620 path := app.ServiceFile() 621 content, err := generateSnapServiceFile(app, genServiceOpts) 622 if err != nil { 623 return err 624 } 625 626 if err := handleFileModification(app, "service", app.Name, path, content); err != nil { 627 return err 628 } 629 630 // Generate systemd .socket files if needed 631 socketFiles, err := generateSnapSocketFiles(app) 632 if err != nil { 633 return err 634 } 635 for name, content := range socketFiles { 636 path := app.Sockets[name].File() 637 if err := handleFileModification(app, "socket", name, path, content); err != nil { 638 return err 639 } 640 } 641 642 if app.Timer != nil { 643 content, err := generateSnapTimerFile(app) 644 if err != nil { 645 return err 646 } 647 path := app.Timer.File() 648 if err := handleFileModification(app, "timer", "", path, content); err != nil { 649 return err 650 } 651 } 652 } 653 } 654 655 handleSliceModification := func(grp *quota.Group, path string, content []byte) error { 656 old, modifiedFile, err := tryFileUpdate(path, content) 657 if err != nil { 658 return err 659 } 660 661 if modifiedFile { 662 if observeChange != nil { 663 var oldContent []byte 664 if old != nil { 665 oldContent = old.Content 666 } 667 observeChange(nil, grp, "slice", grp.Name, string(oldContent), string(content)) 668 } 669 670 modifiedUnitsPreviousState[path] = old 671 672 // also mark that we need to reload the system instance of systemd 673 // TODO: also handle reloading the user instance of systemd when 674 // needed 675 modifiedSystem = true 676 } 677 678 return nil 679 } 680 681 // now make sure that all of the slice units exist 682 for _, grp := range neededQuotaGrps.AllQuotaGroups() { 683 content, err := generateGroupSliceFile(grp) 684 if err != nil { 685 return err 686 } 687 688 sliceFileName := grp.SliceFileName() 689 path := filepath.Join(dirs.SnapServicesDir, sliceFileName) 690 if err := handleSliceModification(grp, path, content); err != nil { 691 return err 692 } 693 } 694 695 if !preseeding { 696 if modifiedSystem { 697 if err = sysd.DaemonReload(); err != nil { 698 return err 699 } 700 } 701 if modifiedUser { 702 if err = userDaemonReload(); err != nil { 703 return err 704 } 705 } 706 } 707 708 return nil 709 } 710 711 // AddSnapServicesOptions is a struct for controlling the generated service 712 // definition for a snap service. 713 type AddSnapServicesOptions struct { 714 // VitalityRank is the rank of all services in the specified snap used by 715 // the OOM killer when OOM conditions are reached. 716 VitalityRank int 717 718 // QuotaGroup is the quota group for all services in the specified snap. 719 QuotaGroup *quota.Group 720 721 // RequireMountedSnapdSnap is whether the generated units should depend on 722 // the snapd snap being mounted, this is specific to systems like UC18 and 723 // UC20 which have the snapd snap and need to have units generated 724 RequireMountedSnapdSnap bool 725 726 // Preseeding is whether the system is currently being preseeded, in which 727 // case there is not a running systemd for EnsureSnapServicesOptions to 728 // issue commands like systemctl daemon-reload to. 729 Preseeding bool 730 } 731 732 // AddSnapServices adds service units for the applications from the snap which 733 // are services. The services do not get enabled or started. 734 func AddSnapServices(s *snap.Info, opts *AddSnapServicesOptions, inter interacter) error { 735 m := map[*snap.Info]*SnapServiceOptions{ 736 s: {}, 737 } 738 ensureOpts := &EnsureSnapServicesOptions{} 739 if opts != nil { 740 // set the per-snap service options 741 m[s].VitalityRank = opts.VitalityRank 742 m[s].QuotaGroup = opts.QuotaGroup 743 744 // copy the globally applicable opts from AddSnapServicesOptions to 745 // EnsureSnapServicesOptions, since those options override the per-snap opts 746 // we put in the map argument 747 ensureOpts.Preseeding = opts.Preseeding 748 ensureOpts.RequireMountedSnapdSnap = opts.RequireMountedSnapdSnap 749 } 750 751 return EnsureSnapServices(m, ensureOpts, nil, inter) 752 } 753 754 // StopServicesFlags carries extra flags for StopServices. 755 type StopServicesFlags struct { 756 Disable bool 757 } 758 759 // StopServices stops and optionally disables service units for the applications 760 // from the snap which are services. 761 func StopServices(apps []*snap.AppInfo, flags *StopServicesFlags, reason snap.ServiceStopReason, inter interacter, tm timings.Measurer) error { 762 sysd := systemd.New(systemd.SystemMode, inter) 763 if flags == nil { 764 flags = &StopServicesFlags{} 765 } 766 767 if reason != snap.StopReasonOther { 768 logger.Debugf("StopServices called for %q, reason: %v", apps, reason) 769 } else { 770 logger.Debugf("StopServices called for %q", apps) 771 } 772 for _, app := range apps { 773 // Handle the case where service file doesn't exist and don't try to stop it as it will fail. 774 // This can happen with snap try when snap.yaml is modified on the fly and a daemon line is added. 775 if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { 776 continue 777 } 778 // Skip stop on refresh when refresh mode is set to something 779 // other than "restart" (or "" which is the same) 780 if reason == snap.StopReasonRefresh { 781 logger.Debugf(" %s refresh-mode: %v", app.Name, app.StopMode) 782 switch app.RefreshMode { 783 case "endure": 784 // skip this service 785 continue 786 } 787 } 788 789 var err error 790 timings.Run(tm, "stop-service", fmt.Sprintf("stop service %q", app.ServiceName()), func(nested timings.Measurer) { 791 err = stopService(sysd, app, inter) 792 if err == nil && flags.Disable { 793 err = sysd.Disable(app.ServiceName()) 794 } 795 }) 796 if err != nil { 797 return err 798 } 799 800 // ensure the service is really stopped on remove regardless 801 // of stop-mode 802 if reason == snap.StopReasonRemove && !app.StopMode.KillAll() && app.DaemonScope == snap.SystemDaemon { 803 // FIXME: make this smarter and avoid the killWait 804 // delay if not needed (i.e. if all processes 805 // have died) 806 sysd.Kill(app.ServiceName(), "TERM", "all") 807 time.Sleep(killWait) 808 sysd.Kill(app.ServiceName(), "KILL", "") 809 } 810 } 811 return nil 812 } 813 814 // ServicesEnableState returns a map of service names from the given snap, 815 // together with their enable/disable status. 816 func ServicesEnableState(s *snap.Info, inter interacter) (map[string]bool, error) { 817 sysd := systemd.New(systemd.SystemMode, inter) 818 819 // loop over all services in the snap, querying systemd for the current 820 // systemd state of the snaps 821 snapSvcsState := make(map[string]bool, len(s.Apps)) 822 for name, app := range s.Apps { 823 if !app.IsService() { 824 continue 825 } 826 // FIXME: handle user daemons 827 if app.DaemonScope != snap.SystemDaemon { 828 continue 829 } 830 state, err := sysd.IsEnabled(app.ServiceName()) 831 if err != nil { 832 return nil, err 833 } 834 snapSvcsState[name] = state 835 } 836 return snapSvcsState, nil 837 } 838 839 // RemoveQuotaGroup ensures that the slice file for a quota group is removed. It 840 // assumes that the slice corresponding to the group is not in use anymore by 841 // any services or sub-groups of the group when it is invoked. To remove a group 842 // with sub-groups, one must remove all the sub-groups first. 843 // This function is idempotent, if the slice file doesn't exist no error is 844 // returned. 845 func RemoveQuotaGroup(grp *quota.Group, inter interacter) error { 846 // TODO: it only works on leaf sub-groups currently 847 if len(grp.SubGroups) != 0 { 848 return fmt.Errorf("internal error: cannot remove quota group with sub-groups") 849 } 850 851 systemSysd := systemd.New(systemd.SystemMode, inter) 852 853 // remove the slice file 854 err := os.Remove(filepath.Join(dirs.SnapServicesDir, grp.SliceFileName())) 855 if err != nil && !os.IsNotExist(err) { 856 return err 857 } 858 859 if err == nil { 860 // we deleted the slice unit, so we need to daemon-reload 861 if err := systemSysd.DaemonReload(); err != nil { 862 return err 863 } 864 } 865 return nil 866 } 867 868 // RemoveSnapServices disables and removes service units for the applications 869 // from the snap which are services. The optional flag indicates whether 870 // services are removed as part of undoing of first install of a given snap. 871 func RemoveSnapServices(s *snap.Info, inter interacter) error { 872 if s.Type() == snap.TypeSnapd { 873 return fmt.Errorf("internal error: removing explicit services for snapd snap is unexpected") 874 } 875 systemSysd := systemd.New(systemd.SystemMode, inter) 876 userSysd := systemd.New(systemd.GlobalUserMode, inter) 877 var removedSystem, removedUser bool 878 879 for _, app := range s.Apps { 880 if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { 881 continue 882 } 883 884 var sysd systemd.Systemd 885 switch app.DaemonScope { 886 case snap.SystemDaemon: 887 sysd = systemSysd 888 removedSystem = true 889 case snap.UserDaemon: 890 sysd = userSysd 891 removedUser = true 892 } 893 serviceName := filepath.Base(app.ServiceFile()) 894 895 for _, socket := range app.Sockets { 896 path := socket.File() 897 socketServiceName := filepath.Base(path) 898 if err := sysd.Disable(socketServiceName); err != nil { 899 return err 900 } 901 902 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 903 logger.Noticef("Failed to remove socket file %q for %q: %v", path, serviceName, err) 904 } 905 } 906 907 if app.Timer != nil { 908 path := app.Timer.File() 909 910 timerName := filepath.Base(path) 911 if err := sysd.Disable(timerName); err != nil { 912 return err 913 } 914 915 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 916 logger.Noticef("Failed to remove timer file %q for %q: %v", path, serviceName, err) 917 } 918 } 919 920 if err := sysd.Disable(serviceName); err != nil { 921 return err 922 } 923 924 if err := os.Remove(app.ServiceFile()); err != nil && !os.IsNotExist(err) { 925 logger.Noticef("Failed to remove service file for %q: %v", serviceName, err) 926 } 927 928 } 929 930 // only reload if we actually had services 931 if removedSystem { 932 if err := systemSysd.DaemonReload(); err != nil { 933 return err 934 } 935 } 936 if removedUser { 937 if err := userDaemonReload(); err != nil { 938 return err 939 } 940 } 941 942 return nil 943 } 944 945 func genServiceNames(snap *snap.Info, appNames []string) []string { 946 names := make([]string, 0, len(appNames)) 947 948 for _, name := range appNames { 949 if app := snap.Apps[name]; app != nil { 950 names = append(names, app.ServiceName()) 951 } 952 } 953 return names 954 } 955 956 // TODO: this should not accept AddSnapServicesOptions, it should use some other 957 // subset of options, specifically it should not accept Preseeding as an option 958 // here 959 func genServiceFile(appInfo *snap.AppInfo, opts *AddSnapServicesOptions) ([]byte, error) { 960 if opts == nil { 961 opts = &AddSnapServicesOptions{} 962 } 963 964 // assemble all of the service directive snippets for all interfaces that 965 // this service needs to include in the generated systemd file 966 967 // use an ordered set to ensure we don't duplicate any keys from interfaces 968 // that specify the same snippet 969 970 // TODO: maybe we should error if multiple interfaces specify different 971 // values for the same directive, otherwise one of them will overwrite the 972 // other? What happens right now is that the snippet from the plug that 973 // comes last will win in the case of directives that can have only one 974 // value, but for some directives, systemd combines their values into a 975 // list. 976 ifaceServiceSnippets := &strutil.OrderedSet{} 977 978 for _, plug := range appInfo.Plugs { 979 iface, err := interfaces.ByName(plug.Interface) 980 if err != nil { 981 return nil, fmt.Errorf("error processing plugs while generating service unit for %v: %v", appInfo.SecurityTag(), err) 982 } 983 snips, err := interfaces.PermanentPlugServiceSnippets(iface, plug) 984 if err != nil { 985 return nil, fmt.Errorf("error processing plugs while generating service unit for %v: %v", appInfo.SecurityTag(), err) 986 } 987 for _, snip := range snips { 988 ifaceServiceSnippets.Put(snip) 989 } 990 } 991 992 // join the service snippets into one string to be included in the 993 // template 994 ifaceSpecifiedServiceSnippet := strings.Join(ifaceServiceSnippets.Items(), "\n") 995 996 serviceTemplate := `[Unit] 997 # Auto-generated, DO NOT EDIT 998 Description=Service for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 999 {{- if .MountUnit }} 1000 Requires={{.MountUnit}} 1001 {{- end }} 1002 {{- if .PrerequisiteTarget}} 1003 Wants={{.PrerequisiteTarget}} 1004 {{- end}} 1005 {{- if .After}} 1006 After={{ stringsJoin .After " " }} 1007 {{- end}} 1008 {{- if .Before}} 1009 Before={{ stringsJoin .Before " "}} 1010 {{- end}} 1011 {{- if .CoreMountedSnapdSnapDep}} 1012 Wants={{ stringsJoin .CoreMountedSnapdSnapDep " "}} 1013 After={{ stringsJoin .CoreMountedSnapdSnapDep " "}} 1014 {{- end}} 1015 X-Snappy=yes 1016 1017 [Service] 1018 EnvironmentFile=-/etc/environment 1019 ExecStart={{.App.LauncherCommand}} 1020 SyslogIdentifier={{.App.Snap.InstanceName}}.{{.App.Name}} 1021 Restart={{.Restart}} 1022 {{- if .App.RestartDelay}} 1023 RestartSec={{.App.RestartDelay.Seconds}} 1024 {{- end}} 1025 WorkingDirectory={{.WorkingDir}} 1026 {{- if .App.StopCommand}} 1027 ExecStop={{.App.LauncherStopCommand}} 1028 {{- end}} 1029 {{- if .App.ReloadCommand}} 1030 ExecReload={{.App.LauncherReloadCommand}} 1031 {{- end}} 1032 {{- if .App.PostStopCommand}} 1033 ExecStopPost={{.App.LauncherPostStopCommand}} 1034 {{- end}} 1035 {{- if .StopTimeout}} 1036 TimeoutStopSec={{.StopTimeout.Seconds}} 1037 {{- end}} 1038 {{- if .StartTimeout}} 1039 TimeoutStartSec={{.StartTimeout.Seconds}} 1040 {{- end}} 1041 Type={{.App.Daemon}} 1042 {{- if .Remain}} 1043 RemainAfterExit={{.Remain}} 1044 {{- end}} 1045 {{- if .BusName}} 1046 BusName={{.BusName}} 1047 {{- end}} 1048 {{- if .App.WatchdogTimeout}} 1049 WatchdogSec={{.App.WatchdogTimeout.Seconds}} 1050 {{- end}} 1051 {{- if .KillMode}} 1052 KillMode={{.KillMode}} 1053 {{- end}} 1054 {{- if .KillSignal}} 1055 KillSignal={{.KillSignal}} 1056 {{- end}} 1057 {{- if .OOMAdjustScore }} 1058 OOMScoreAdjust={{.OOMAdjustScore}} 1059 {{- end}} 1060 {{- if .InterfaceServiceSnippets}} 1061 {{.InterfaceServiceSnippets}} 1062 {{- end}} 1063 {{- if .SliceUnit}} 1064 Slice={{.SliceUnit}} 1065 {{- end}} 1066 {{- if not (or .App.Sockets .App.Timer .App.ActivatesOn) }} 1067 1068 [Install] 1069 WantedBy={{.ServicesTarget}} 1070 {{- end}} 1071 ` 1072 var templateOut bytes.Buffer 1073 tmpl := template.New("service-wrapper") 1074 tmpl.Funcs(template.FuncMap{ 1075 "stringsJoin": strings.Join, 1076 }) 1077 t := template.Must(tmpl.Parse(serviceTemplate)) 1078 1079 restartCond := appInfo.RestartCond.String() 1080 if restartCond == "" { 1081 restartCond = snap.RestartOnFailure.String() 1082 } 1083 1084 // use score -900+vitalityRank, where vitalityRank starts at 1 1085 // and considering snapd itself has OOMScoreAdjust=-900 1086 const baseOOMAdjustScore = -900 1087 var oomAdjustScore int 1088 if opts.VitalityRank > 0 { 1089 oomAdjustScore = baseOOMAdjustScore + opts.VitalityRank 1090 } 1091 1092 var remain string 1093 if appInfo.Daemon == "oneshot" { 1094 // any restart condition other than "no" is invalid for oneshot daemons 1095 restartCond = "no" 1096 // If StopExec is present for a oneshot service than we also need 1097 // RemainAfterExit=yes 1098 if appInfo.StopCommand != "" { 1099 remain = "yes" 1100 } 1101 } 1102 var killMode string 1103 if !appInfo.StopMode.KillAll() { 1104 killMode = "process" 1105 } 1106 1107 var busName string 1108 if appInfo.Daemon == "dbus" { 1109 busName = appInfo.BusName 1110 if busName == "" && len(appInfo.ActivatesOn) != 0 { 1111 slot := appInfo.ActivatesOn[len(appInfo.ActivatesOn)-1] 1112 if err := slot.Attr("name", &busName); err != nil { 1113 // This should be impossible for a valid AppInfo 1114 logger.Noticef("Cannot get 'name' attribute of dbus slot %q: %v", slot.Name, err) 1115 } 1116 } 1117 } 1118 1119 wrapperData := struct { 1120 App *snap.AppInfo 1121 1122 Restart string 1123 WorkingDir string 1124 StopTimeout time.Duration 1125 StartTimeout time.Duration 1126 ServicesTarget string 1127 PrerequisiteTarget string 1128 MountUnit string 1129 Remain string 1130 KillMode string 1131 KillSignal string 1132 OOMAdjustScore int 1133 BusName string 1134 Before []string 1135 After []string 1136 InterfaceServiceSnippets string 1137 SliceUnit string 1138 1139 Home string 1140 EnvVars string 1141 1142 CoreMountedSnapdSnapDep []string 1143 }{ 1144 App: appInfo, 1145 1146 InterfaceServiceSnippets: ifaceSpecifiedServiceSnippet, 1147 1148 Restart: restartCond, 1149 StopTimeout: serviceStopTimeout(appInfo), 1150 StartTimeout: time.Duration(appInfo.StartTimeout), 1151 Remain: remain, 1152 KillMode: killMode, 1153 KillSignal: appInfo.StopMode.KillSignal(), 1154 OOMAdjustScore: oomAdjustScore, 1155 BusName: busName, 1156 1157 Before: genServiceNames(appInfo.Snap, appInfo.Before), 1158 After: genServiceNames(appInfo.Snap, appInfo.After), 1159 1160 // systemd runs as PID 1 so %h will not work. 1161 Home: "/root", 1162 } 1163 switch appInfo.DaemonScope { 1164 case snap.SystemDaemon: 1165 wrapperData.ServicesTarget = systemd.ServicesTarget 1166 wrapperData.PrerequisiteTarget = systemd.PrerequisiteTarget 1167 wrapperData.MountUnit = filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())) 1168 wrapperData.WorkingDir = appInfo.Snap.DataDir() 1169 wrapperData.After = append(wrapperData.After, "snapd.apparmor.service") 1170 case snap.UserDaemon: 1171 wrapperData.ServicesTarget = systemd.UserServicesTarget 1172 // FIXME: ideally use UserDataDir("%h"), but then the 1173 // unit fails if the directory doesn't exist. 1174 wrapperData.WorkingDir = appInfo.Snap.DataDir() 1175 default: 1176 panic("unknown snap.DaemonScope") 1177 } 1178 1179 // check the quota group slice 1180 if opts.QuotaGroup != nil { 1181 wrapperData.SliceUnit = opts.QuotaGroup.SliceFileName() 1182 } 1183 1184 // Add extra "After" targets 1185 if wrapperData.PrerequisiteTarget != "" { 1186 wrapperData.After = append([]string{wrapperData.PrerequisiteTarget}, wrapperData.After...) 1187 } 1188 if wrapperData.MountUnit != "" { 1189 wrapperData.After = append([]string{wrapperData.MountUnit}, wrapperData.After...) 1190 } 1191 1192 if opts.RequireMountedSnapdSnap { 1193 // on core 18+ systems, the snapd tooling is exported 1194 // into the host system via a special mount unit, which 1195 // also adds an implicit dependency on the snapd snap 1196 // mount thus /usr/bin/snap points 1197 wrapperData.CoreMountedSnapdSnapDep = []string{SnapdToolingMountUnit} 1198 } 1199 1200 if err := t.Execute(&templateOut, wrapperData); err != nil { 1201 // this can never happen, except we forget a variable 1202 logger.Panicf("Unable to execute template: %v", err) 1203 } 1204 1205 return templateOut.Bytes(), nil 1206 } 1207 1208 func genServiceSocketFile(appInfo *snap.AppInfo, socketName string) []byte { 1209 socketTemplate := `[Unit] 1210 # Auto-generated, DO NOT EDIT 1211 Description=Socket {{.SocketName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 1212 {{- if .MountUnit}} 1213 Requires={{.MountUnit}} 1214 After={{.MountUnit}} 1215 {{- end}} 1216 X-Snappy=yes 1217 1218 [Socket] 1219 Service={{.ServiceFileName}} 1220 FileDescriptorName={{.SocketInfo.Name}} 1221 ListenStream={{.ListenStream}} 1222 {{- if .SocketInfo.SocketMode}} 1223 SocketMode={{.SocketInfo.SocketMode | printf "%04o"}} 1224 {{- end}} 1225 1226 [Install] 1227 WantedBy={{.SocketsTarget}} 1228 ` 1229 var templateOut bytes.Buffer 1230 t := template.Must(template.New("socket-wrapper").Parse(socketTemplate)) 1231 1232 socket := appInfo.Sockets[socketName] 1233 listenStream := renderListenStream(socket) 1234 wrapperData := struct { 1235 App *snap.AppInfo 1236 ServiceFileName string 1237 SocketsTarget string 1238 MountUnit string 1239 SocketName string 1240 SocketInfo *snap.SocketInfo 1241 ListenStream string 1242 }{ 1243 App: appInfo, 1244 ServiceFileName: filepath.Base(appInfo.ServiceFile()), 1245 SocketsTarget: systemd.SocketsTarget, 1246 SocketName: socketName, 1247 SocketInfo: socket, 1248 ListenStream: listenStream, 1249 } 1250 switch appInfo.DaemonScope { 1251 case snap.SystemDaemon: 1252 wrapperData.MountUnit = filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())) 1253 case snap.UserDaemon: 1254 // nothing 1255 default: 1256 panic("unknown snap.DaemonScope") 1257 } 1258 1259 if err := t.Execute(&templateOut, wrapperData); err != nil { 1260 // this can never happen, except we forget a variable 1261 logger.Panicf("Unable to execute template: %v", err) 1262 } 1263 1264 return templateOut.Bytes() 1265 } 1266 1267 func generateSnapSocketFiles(app *snap.AppInfo) (map[string][]byte, error) { 1268 if err := snap.ValidateApp(app); err != nil { 1269 return nil, err 1270 } 1271 1272 socketFiles := make(map[string][]byte) 1273 for name := range app.Sockets { 1274 socketFiles[name] = genServiceSocketFile(app, name) 1275 } 1276 return socketFiles, nil 1277 } 1278 1279 func renderListenStream(socket *snap.SocketInfo) string { 1280 s := socket.App.Snap 1281 listenStream := socket.ListenStream 1282 switch socket.App.DaemonScope { 1283 case snap.SystemDaemon: 1284 listenStream = strings.Replace(listenStream, "$SNAP_DATA", s.DataDir(), -1) 1285 // TODO: when we support User/Group in the generated 1286 // systemd unit, adjust this accordingly 1287 serviceUserUid := sys.UserID(0) 1288 runtimeDir := s.UserXdgRuntimeDir(serviceUserUid) 1289 listenStream = strings.Replace(listenStream, "$XDG_RUNTIME_DIR", runtimeDir, -1) 1290 listenStream = strings.Replace(listenStream, "$SNAP_COMMON", s.CommonDataDir(), -1) 1291 case snap.UserDaemon: 1292 listenStream = strings.Replace(listenStream, "$SNAP_USER_DATA", s.UserDataDir("%h"), -1) 1293 listenStream = strings.Replace(listenStream, "$SNAP_USER_COMMON", s.UserCommonDataDir("%h"), -1) 1294 // FIXME: find some way to share code with snap.UserXdgRuntimeDir() 1295 listenStream = strings.Replace(listenStream, "$XDG_RUNTIME_DIR", fmt.Sprintf("%%t/snap.%s", s.InstanceName()), -1) 1296 default: 1297 panic("unknown snap.DaemonScope") 1298 } 1299 return listenStream 1300 } 1301 1302 func generateSnapTimerFile(app *snap.AppInfo) ([]byte, error) { 1303 timerTemplate := `[Unit] 1304 # Auto-generated, DO NOT EDIT 1305 Description=Timer {{.TimerName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 1306 {{- if .MountUnit}} 1307 Requires={{.MountUnit}} 1308 After={{.MountUnit}} 1309 {{- end}} 1310 X-Snappy=yes 1311 1312 [Timer] 1313 Unit={{.ServiceFileName}} 1314 {{ range .Schedules }}OnCalendar={{ . }} 1315 {{ end }} 1316 [Install] 1317 WantedBy={{.TimersTarget}} 1318 ` 1319 var templateOut bytes.Buffer 1320 t := template.Must(template.New("timer-wrapper").Parse(timerTemplate)) 1321 1322 timerSchedule, err := timeutil.ParseSchedule(app.Timer.Timer) 1323 if err != nil { 1324 return nil, err 1325 } 1326 1327 schedules := generateOnCalendarSchedules(timerSchedule) 1328 1329 wrapperData := struct { 1330 App *snap.AppInfo 1331 ServiceFileName string 1332 TimersTarget string 1333 TimerName string 1334 MountUnit string 1335 Schedules []string 1336 }{ 1337 App: app, 1338 ServiceFileName: filepath.Base(app.ServiceFile()), 1339 TimersTarget: systemd.TimersTarget, 1340 TimerName: app.Name, 1341 Schedules: schedules, 1342 } 1343 switch app.DaemonScope { 1344 case snap.SystemDaemon: 1345 wrapperData.MountUnit = filepath.Base(systemd.MountUnitPath(app.Snap.MountDir())) 1346 case snap.UserDaemon: 1347 // nothing 1348 default: 1349 panic("unknown snap.DaemonScope") 1350 } 1351 1352 if err := t.Execute(&templateOut, wrapperData); err != nil { 1353 // this can never happen, except we forget a variable 1354 logger.Panicf("Unable to execute template: %v", err) 1355 } 1356 1357 return templateOut.Bytes(), nil 1358 } 1359 1360 func makeAbbrevWeekdays(start time.Weekday, end time.Weekday) []string { 1361 out := make([]string, 0, 7) 1362 for w := start; w%7 != (end + 1); w++ { 1363 out = append(out, time.Weekday(w % 7).String()[0:3]) 1364 } 1365 return out 1366 } 1367 1368 // daysRange generates a string representing a continuous range between given 1369 // day numbers, which due to compatiblilty with old systemd version uses a 1370 // verbose syntax of x,y,z instead of x..z 1371 func daysRange(start, end uint) string { 1372 var buf bytes.Buffer 1373 for i := start; i <= end; i++ { 1374 buf.WriteString(strconv.FormatInt(int64(i), 10)) 1375 if i < end { 1376 buf.WriteRune(',') 1377 } 1378 } 1379 return buf.String() 1380 } 1381 1382 // generateOnCalendarSchedules converts a schedule into OnCalendar schedules 1383 // suitable for use in systemd *.timer units using systemd.time(7) 1384 // https://www.freedesktop.org/software/systemd/man/systemd.time.html 1385 // XXX: old systemd versions do not support x..y ranges 1386 func generateOnCalendarSchedules(schedule []*timeutil.Schedule) []string { 1387 calendarEvents := make([]string, 0, len(schedule)) 1388 for _, sched := range schedule { 1389 days := make([]string, 0, len(sched.WeekSpans)) 1390 for _, week := range sched.WeekSpans { 1391 abbrev := strings.Join(makeAbbrevWeekdays(week.Start.Weekday, week.End.Weekday), ",") 1392 1393 if week.Start.Pos == timeutil.EveryWeek && week.End.Pos == timeutil.EveryWeek { 1394 // eg: mon, mon-fri, fri-mon 1395 days = append(days, fmt.Sprintf("%s *-*-*", abbrev)) 1396 continue 1397 } 1398 // examples: 1399 // mon1 - Mon *-*-1..7 (Monday during the first 7 days) 1400 // fri1 - Fri *-*-1..7 (Friday during the first 7 days) 1401 1402 // entries below will make systemd timer expire more 1403 // frequently than the schedule suggests, however snap 1404 // runner evaluates current time and gates the actual 1405 // action 1406 // 1407 // mon1-tue - *-*-1..7 *-*-8 (anchored at first 1408 // Monday; Monday happens during the 7 days, 1409 // Tuesday can possibly happen on the 8th day if 1410 // the month started on Tuesday) 1411 // 1412 // mon-tue1 - *-*~1 *-*-1..7 (anchored at first 1413 // Tuesday; matching Monday can happen on the 1414 // last day of previous month if Tuesday is the 1415 // 1st) 1416 // 1417 // mon5-tue - *-*~1..7 *-*-1 (anchored at last 1418 // Monday, the matching Tuesday can still happen 1419 // within the last 7 days, or on the 1st of the 1420 // next month) 1421 // 1422 // fri4-mon - *-*-22-31 *-*-1..7 (anchored at 4th 1423 // Friday, can span onto the next month, extreme case in 1424 // February when 28th is Friday) 1425 // 1426 // XXX: since old versions of systemd, eg. 229 available 1427 // in 16.04 does not support x..y ranges, days need to 1428 // be enumerated like so: 1429 // Mon *-*-1..7 -> Mon *-*-1,2,3,4,5,6,7 1430 // 1431 // XXX: old systemd versions do not support the last n 1432 // days syntax eg, *-*~1, thus the range needs to be 1433 // generated in more verbose way like so: 1434 // Mon *-*~1..7 -> Mon *-*-22,23,24,25,26,27,28,29,30,31 1435 // (22-28 is the last week, but the month can have 1436 // anywhere from 28 to 31 days) 1437 // 1438 startPos := week.Start.Pos 1439 endPos := startPos 1440 if !week.AnchoredAtStart() { 1441 startPos = week.End.Pos 1442 endPos = startPos 1443 } 1444 startDay := (startPos-1)*7 + 1 1445 endDay := (endPos) * 7 1446 1447 if week.IsSingleDay() { 1448 // single day, can use the 'weekday' filter 1449 if startPos == timeutil.LastWeek { 1450 // last week of a month, which can be 1451 // 22-28 in case of February, while 1452 // month can have between 28 and 31 days 1453 days = append(days, 1454 fmt.Sprintf("%s *-*-%s", abbrev, daysRange(22, 31))) 1455 } else { 1456 days = append(days, 1457 fmt.Sprintf("%s *-*-%s", abbrev, daysRange(startDay, endDay))) 1458 } 1459 continue 1460 } 1461 1462 if week.AnchoredAtStart() { 1463 // explore the edge cases first 1464 switch startPos { 1465 case timeutil.LastWeek: 1466 // starts in the last week of the month and 1467 // possibly spans into the first week of the 1468 // next month; 1469 // month can have between 28 and 31 1470 // days 1471 days = append(days, 1472 // trailing 29-31 that are not part of a full week 1473 fmt.Sprintf("*-*-%s", daysRange(29, 31)), 1474 fmt.Sprintf("*-*-%s", daysRange(1, 7))) 1475 case 4: 1476 // a range in the 4th week can span onto 1477 // the next week, which is either 28-31 1478 // or in extreme case (eg. February with 1479 // 28 days) 1-7 of the next month 1480 days = append(days, 1481 // trailing 29-31 that are not part of a full week 1482 fmt.Sprintf("*-*-%s", daysRange(29, 31)), 1483 fmt.Sprintf("*-*-%s", daysRange(1, 7))) 1484 default: 1485 // can possibly spill into the next week 1486 days = append(days, 1487 fmt.Sprintf("*-*-%s", daysRange(startDay+7, endDay+7))) 1488 } 1489 1490 if startDay < 28 { 1491 days = append(days, 1492 fmt.Sprintf("*-*-%s", daysRange(startDay, endDay))) 1493 } else { 1494 // from the end of the month 1495 days = append(days, 1496 fmt.Sprintf("*-*-%s", daysRange(startDay-7, endDay-7))) 1497 } 1498 } else { 1499 switch endPos { 1500 case timeutil.LastWeek: 1501 // month can have between 28 and 31 1502 // days, add trailing 29-31 that are not 1503 // part of a full week 1504 days = append(days, fmt.Sprintf("*-*-%s", daysRange(29, 31))) 1505 case 1: 1506 // possibly spans from the last week of the 1507 // previous month and ends in the first week of 1508 // current month 1509 days = append(days, fmt.Sprintf("*-*-%s", daysRange(22, 31))) 1510 default: 1511 // can possibly spill into the previous week 1512 days = append(days, 1513 fmt.Sprintf("*-*-%s", daysRange(startDay-7, endDay-7))) 1514 } 1515 if endDay < 28 { 1516 days = append(days, 1517 fmt.Sprintf("*-*-%s", daysRange(startDay, endDay))) 1518 } else { 1519 days = append(days, 1520 fmt.Sprintf("*-*-%s", daysRange(startDay-7, endDay-7))) 1521 } 1522 } 1523 } 1524 1525 if len(days) == 0 { 1526 // no weekday spec, meaning the timer runs every day 1527 days = []string{"*-*-*"} 1528 } 1529 1530 startTimes := make([]string, 0, len(sched.ClockSpans)) 1531 for _, clocks := range sched.ClockSpans { 1532 // use expanded clock spans 1533 for _, span := range clocks.ClockSpans() { 1534 when := span.Start 1535 if span.Spread { 1536 length := span.End.Sub(span.Start) 1537 if length < 0 { 1538 // span Start wraps around, so we have '00:00.Sub(23:45)' 1539 length = -length 1540 } 1541 if length > 5*time.Minute { 1542 // replicate what timeutil.Next() does 1543 // and cut some time at the end of the 1544 // window so that events do not happen 1545 // directly one after another 1546 length -= 5 * time.Minute 1547 } 1548 when = when.Add(randutil.RandomDuration(length)) 1549 } 1550 if when.Hour == 24 { 1551 // 24:00 for us means the other end of 1552 // the day, for systemd we need to 1553 // adjust it to the 0-23 hour range 1554 when.Hour -= 24 1555 } 1556 1557 startTimes = append(startTimes, when.String()) 1558 } 1559 } 1560 1561 for _, day := range days { 1562 if len(startTimes) == 0 { 1563 // current schedule is days only 1564 calendarEvents = append(calendarEvents, day) 1565 continue 1566 } 1567 1568 for _, startTime := range startTimes { 1569 calendarEvents = append(calendarEvents, fmt.Sprintf("%s %s", day, startTime)) 1570 } 1571 } 1572 } 1573 return calendarEvents 1574 } 1575 1576 type RestartServicesFlags struct { 1577 Reload bool 1578 } 1579 1580 // Restart or reload services; if reload flag is set then "systemctl reload-or-restart" is attempted. 1581 func RestartServices(svcs []*snap.AppInfo, flags *RestartServicesFlags, inter interacter, tm timings.Measurer) error { 1582 sysd := systemd.New(systemd.SystemMode, inter) 1583 1584 for _, srv := range svcs { 1585 // they're *supposed* to be all services, but checking doesn't hurt 1586 if !srv.IsService() { 1587 continue 1588 } 1589 1590 var err error 1591 timings.Run(tm, "restart-service", fmt.Sprintf("restart service %q", srv), func(nested timings.Measurer) { 1592 if flags != nil && flags.Reload { 1593 err = sysd.ReloadOrRestart(srv.ServiceName()) 1594 } else { 1595 // note: stop followed by start, not just 'restart' 1596 err = sysd.Restart(srv.ServiceName(), 5*time.Second) 1597 } 1598 }) 1599 if err != nil { 1600 // there is nothing we can do about failed service 1601 return err 1602 } 1603 } 1604 return nil 1605 } 1606 1607 // QueryDisabledServices returns a list of all currently disabled snap services 1608 // in the snap. 1609 func QueryDisabledServices(info *snap.Info, pb progress.Meter) ([]string, error) { 1610 // save the list of services that are in the disabled state before unlinking 1611 // and thus removing the snap services 1612 snapSvcStates, err := ServicesEnableState(info, pb) 1613 if err != nil { 1614 return nil, err 1615 } 1616 1617 disabledSnapSvcs := []string{} 1618 // add all disabled services to the list 1619 for svc, isEnabled := range snapSvcStates { 1620 if !isEnabled { 1621 disabledSnapSvcs = append(disabledSnapSvcs, svc) 1622 } 1623 } 1624 1625 // sort for easier testing 1626 sort.Strings(disabledSnapSvcs) 1627 1628 return disabledSnapSvcs, nil 1629 }