github.com/rigado/snapd@v2.42.5-go-mod+incompatible/wrappers/services.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2016 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 "fmt" 25 "math/rand" 26 "os" 27 "path/filepath" 28 "strings" 29 "text/template" 30 "time" 31 32 "github.com/snapcore/snapd/dirs" 33 "github.com/snapcore/snapd/logger" 34 "github.com/snapcore/snapd/osutil" 35 "github.com/snapcore/snapd/osutil/sys" 36 "github.com/snapcore/snapd/snap" 37 "github.com/snapcore/snapd/systemd" 38 "github.com/snapcore/snapd/timeout" 39 "github.com/snapcore/snapd/timeutil" 40 "github.com/snapcore/snapd/timings" 41 ) 42 43 type interacter interface { 44 Notify(status string) 45 } 46 47 // wait this time between TERM and KILL 48 var killWait = 5 * time.Second 49 50 func serviceStopTimeout(app *snap.AppInfo) time.Duration { 51 tout := app.StopTimeout 52 if tout == 0 { 53 tout = timeout.DefaultTimeout 54 } 55 return time.Duration(tout) 56 } 57 58 func generateSnapServiceFile(app *snap.AppInfo) ([]byte, error) { 59 if err := snap.ValidateApp(app); err != nil { 60 return nil, err 61 } 62 63 return genServiceFile(app), nil 64 } 65 66 func stopService(sysd systemd.Systemd, app *snap.AppInfo, inter interacter) error { 67 serviceName := app.ServiceName() 68 tout := serviceStopTimeout(app) 69 70 stopErrors := []error{} 71 for _, socket := range app.Sockets { 72 if err := sysd.Stop(filepath.Base(socket.File()), tout); err != nil { 73 stopErrors = append(stopErrors, err) 74 } 75 } 76 77 if app.Timer != nil { 78 if err := sysd.Stop(filepath.Base(app.Timer.File()), tout); err != nil { 79 stopErrors = append(stopErrors, err) 80 } 81 } 82 83 if err := sysd.Stop(serviceName, tout); err != nil { 84 if !systemd.IsTimeout(err) { 85 return err 86 } 87 inter.Notify(fmt.Sprintf("%s refused to stop, killing.", serviceName)) 88 // ignore errors for kill; nothing we'd do differently at this point 89 sysd.Kill(serviceName, "TERM", "") 90 time.Sleep(killWait) 91 sysd.Kill(serviceName, "KILL", "") 92 93 } 94 95 if len(stopErrors) > 0 { 96 return stopErrors[0] 97 } 98 99 return nil 100 } 101 102 // StartServices starts service units for the applications from the snap which 103 // are services. Service units will be started in the order provided by the 104 // caller. 105 func StartServices(apps []*snap.AppInfo, inter interacter, tm timings.Measurer) (err error) { 106 sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) 107 108 services := make([]string, 0, len(apps)) 109 for _, app := range apps { 110 // they're *supposed* to be all services, but checking doesn't hurt 111 if !app.IsService() { 112 continue 113 } 114 115 defer func(app *snap.AppInfo) { 116 if err == nil { 117 return 118 } 119 if e := stopService(sysd, app, inter); e != nil { 120 inter.Notify(fmt.Sprintf("While trying to stop previously started service %q: %v", app.ServiceName(), e)) 121 } 122 for _, socket := range app.Sockets { 123 socketService := filepath.Base(socket.File()) 124 if e := sysd.Disable(socketService); e != nil { 125 inter.Notify(fmt.Sprintf("While trying to disable previously enabled socket service %q: %v", socketService, e)) 126 } 127 } 128 if app.Timer != nil { 129 timerService := filepath.Base(app.Timer.File()) 130 if e := sysd.Disable(timerService); e != nil { 131 inter.Notify(fmt.Sprintf("While trying to disable previously enabled timer service %q: %v", timerService, e)) 132 } 133 } 134 }(app) 135 136 if len(app.Sockets) == 0 && app.Timer == nil { 137 // check if the service is disabled, if so don't start it up 138 // this could happen for example if the service was disabled in 139 // the install hook by snapctl or if the service was disabled in 140 // the previous installation 141 isEnabled, err := sysd.IsEnabled(app.ServiceName()) 142 if err != nil { 143 return err 144 } 145 146 if isEnabled { 147 services = append(services, app.ServiceName()) 148 } 149 } 150 151 for _, socket := range app.Sockets { 152 socketService := filepath.Base(socket.File()) 153 // enable the socket 154 if err := sysd.Enable(socketService); err != nil { 155 return err 156 } 157 158 timings.Run(tm, "start-socket-service", fmt.Sprintf("start socket service %q", socketService), func(nested timings.Measurer) { 159 err = sysd.Start(socketService) 160 }) 161 if err != nil { 162 return err 163 } 164 } 165 166 if app.Timer != nil { 167 timerService := filepath.Base(app.Timer.File()) 168 // enable the timer 169 if err := sysd.Enable(timerService); err != nil { 170 return err 171 } 172 173 timings.Run(tm, "start-timer-service", fmt.Sprintf("start timer service %q", timerService), func(nested timings.Measurer) { 174 err = sysd.Start(timerService) 175 }) 176 if err != nil { 177 return err 178 } 179 } 180 } 181 182 for _, srv := range services { 183 // starting all services at once does not create a single 184 // transaction, but instead spawns multiple jobs, make sure the 185 // services started in the original order by bring them up one 186 // by one, see: 187 // https://github.com/systemd/systemd/issues/8102 188 // https://lists.freedesktop.org/archives/systemd-devel/2018-January/040152.html 189 timings.Run(tm, "start-service", fmt.Sprintf("start service %q", srv), func(nested timings.Measurer) { 190 err = sysd.Start(srv) 191 }) 192 if err != nil { 193 // cleanup was set up by iterating over apps 194 return err 195 } 196 } 197 198 return nil 199 } 200 201 // AddSnapServices adds service units for the applications from the snap which are services. 202 func AddSnapServices(s *snap.Info, inter interacter) (err error) { 203 if s.GetType() == snap.TypeSnapd { 204 return writeSnapdServicesOnCore(s, inter) 205 } 206 207 sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) 208 var written []string 209 var enabled []string 210 defer func() { 211 if err == nil { 212 return 213 } 214 for _, s := range enabled { 215 if e := sysd.Disable(s); e != nil { 216 inter.Notify(fmt.Sprintf("while trying to disable %s due to previous failure: %v", s, e)) 217 } 218 } 219 for _, s := range written { 220 if e := os.Remove(s); e != nil { 221 inter.Notify(fmt.Sprintf("while trying to remove %s due to previous failure: %v", s, e)) 222 } 223 } 224 if len(written) > 0 { 225 if e := sysd.DaemonReload(); e != nil { 226 inter.Notify(fmt.Sprintf("while trying to perform systemd daemon-reload due to previous failure: %v", e)) 227 } 228 } 229 }() 230 231 for _, app := range s.Apps { 232 if !app.IsService() { 233 continue 234 } 235 // Generate service file 236 content, err := generateSnapServiceFile(app) 237 if err != nil { 238 return err 239 } 240 svcFilePath := app.ServiceFile() 241 os.MkdirAll(filepath.Dir(svcFilePath), 0755) 242 if err := osutil.AtomicWriteFile(svcFilePath, content, 0644, 0); err != nil { 243 return err 244 } 245 written = append(written, svcFilePath) 246 247 // Generate systemd .socket files if needed 248 socketFiles, err := generateSnapSocketFiles(app) 249 if err != nil { 250 return err 251 } 252 for path, content := range *socketFiles { 253 os.MkdirAll(filepath.Dir(path), 0755) 254 if err := osutil.AtomicWriteFile(path, content, 0644, 0); err != nil { 255 return err 256 } 257 written = append(written, path) 258 } 259 260 if app.Timer != nil { 261 content, err := generateSnapTimerFile(app) 262 if err != nil { 263 return err 264 } 265 path := app.Timer.File() 266 os.MkdirAll(filepath.Dir(path), 0755) 267 if err := osutil.AtomicWriteFile(path, content, 0644, 0); err != nil { 268 return err 269 } 270 written = append(written, path) 271 } 272 273 if app.Timer != nil || len(app.Sockets) != 0 { 274 // service is socket or timer activated, not during the 275 // boot 276 continue 277 } 278 279 svcName := app.ServiceName() 280 if err := sysd.Enable(svcName); err != nil { 281 return err 282 } 283 enabled = append(enabled, svcName) 284 } 285 286 if len(written) > 0 { 287 if err := sysd.DaemonReload(); err != nil { 288 return err 289 } 290 } 291 292 return nil 293 } 294 295 // StopServices stops service units for the applications from the snap which are services. 296 func StopServices(apps []*snap.AppInfo, reason snap.ServiceStopReason, inter interacter, tm timings.Measurer) error { 297 sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) 298 299 logger.Debugf("StopServices called for %q, reason: %v", apps, reason) 300 for _, app := range apps { 301 // Handle the case where service file doesn't exist and don't try to stop it as it will fail. 302 // This can happen with snap try when snap.yaml is modified on the fly and a daemon line is added. 303 if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { 304 continue 305 } 306 // Skip stop on refresh when refresh mode is set to something 307 // other than "restart" (or "" which is the same) 308 if reason == snap.StopReasonRefresh { 309 logger.Debugf(" %s refresh-mode: %v", app.Name, app.StopMode) 310 switch app.RefreshMode { 311 case "endure": 312 // skip this service 313 continue 314 } 315 } 316 317 var err error 318 timings.Run(tm, "stop-service", fmt.Sprintf("stop service %q", app.ServiceName()), func(nested timings.Measurer) { 319 err = stopService(sysd, app, inter) 320 }) 321 if err != nil { 322 return err 323 } 324 325 // ensure the service is really stopped on remove regardless 326 // of stop-mode 327 if reason == snap.StopReasonRemove && !app.StopMode.KillAll() { 328 // FIXME: make this smarter and avoid the killWait 329 // delay if not needed (i.e. if all processes 330 // have died) 331 sysd.Kill(app.ServiceName(), "TERM", "all") 332 time.Sleep(killWait) 333 sysd.Kill(app.ServiceName(), "KILL", "") 334 } 335 } 336 337 return nil 338 } 339 340 // ServicesEnableState returns a map of service names from the given snap, 341 // together with their enable/disable status. 342 func ServicesEnableState(s *snap.Info, inter interacter) (map[string]bool, error) { 343 sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) 344 345 // loop over all services in the snap, querying systemd for the current 346 // systemd state of the snaps 347 snapSvcsState := make(map[string]bool, len(s.Apps)) 348 for name, app := range s.Apps { 349 if !app.IsService() { 350 continue 351 } 352 state, err := sysd.IsEnabled(app.ServiceName()) 353 if err != nil { 354 return nil, err 355 } 356 snapSvcsState[name] = state 357 } 358 return snapSvcsState, nil 359 } 360 361 // RemoveSnapServices disables and removes service units for the applications from the snap which are services. 362 func RemoveSnapServices(s *snap.Info, inter interacter) error { 363 sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) 364 nservices := 0 365 366 for _, app := range s.Apps { 367 if !app.IsService() || !osutil.FileExists(app.ServiceFile()) { 368 continue 369 } 370 nservices++ 371 372 serviceName := filepath.Base(app.ServiceFile()) 373 374 for _, socket := range app.Sockets { 375 path := socket.File() 376 socketServiceName := filepath.Base(path) 377 if err := sysd.Disable(socketServiceName); err != nil { 378 return err 379 } 380 381 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 382 logger.Noticef("Failed to remove socket file %q for %q: %v", path, serviceName, err) 383 } 384 } 385 386 if app.Timer != nil { 387 path := app.Timer.File() 388 389 timerName := filepath.Base(path) 390 if err := sysd.Disable(timerName); err != nil { 391 return err 392 } 393 394 if err := os.Remove(path); err != nil && !os.IsNotExist(err) { 395 logger.Noticef("Failed to remove timer file %q for %q: %v", path, serviceName, err) 396 } 397 } 398 399 if err := sysd.Disable(serviceName); err != nil { 400 return err 401 } 402 403 if err := os.Remove(app.ServiceFile()); err != nil && !os.IsNotExist(err) { 404 logger.Noticef("Failed to remove service file for %q: %v", serviceName, err) 405 } 406 407 } 408 409 // only reload if we actually had services 410 if nservices > 0 { 411 if err := sysd.DaemonReload(); err != nil { 412 return err 413 } 414 } 415 416 return nil 417 } 418 419 func genServiceNames(snap *snap.Info, appNames []string) []string { 420 names := make([]string, 0, len(appNames)) 421 422 for _, name := range appNames { 423 if app := snap.Apps[name]; app != nil { 424 names = append(names, app.ServiceName()) 425 } 426 } 427 return names 428 } 429 430 func genServiceFile(appInfo *snap.AppInfo) []byte { 431 serviceTemplate := `[Unit] 432 # Auto-generated, DO NOT EDIT 433 Description=Service for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 434 Requires={{.MountUnit}} 435 Wants={{.PrerequisiteTarget}} 436 After={{.MountUnit}} {{.PrerequisiteTarget}}{{if .After}} {{ stringsJoin .After " " }}{{end}} 437 {{- if .Before}} 438 Before={{ stringsJoin .Before " "}} 439 {{- end}} 440 X-Snappy=yes 441 442 [Service] 443 ExecStart={{.App.LauncherCommand}} 444 SyslogIdentifier={{.App.Snap.InstanceName}}.{{.App.Name}} 445 Restart={{.Restart}} 446 {{- if .App.RestartDelay}} 447 RestartSec={{.App.RestartDelay.Seconds}} 448 {{- end}} 449 WorkingDirectory={{.App.Snap.DataDir}} 450 {{- if .App.StopCommand}} 451 ExecStop={{.App.LauncherStopCommand}} 452 {{- end}} 453 {{- if .App.ReloadCommand}} 454 ExecReload={{.App.LauncherReloadCommand}} 455 {{- end}} 456 {{- if .App.PostStopCommand}} 457 ExecStopPost={{.App.LauncherPostStopCommand}} 458 {{- end}} 459 {{- if .StopTimeout}} 460 TimeoutStopSec={{.StopTimeout.Seconds}} 461 {{- end}} 462 {{- if .StartTimeout}} 463 TimeoutStartSec={{.StartTimeout.Seconds}} 464 {{- end}} 465 Type={{.App.Daemon}} 466 {{- if .Remain}} 467 RemainAfterExit={{.Remain}} 468 {{- end}} 469 {{- if .App.BusName}} 470 BusName={{.App.BusName}} 471 {{- end}} 472 {{- if .App.WatchdogTimeout}} 473 WatchdogSec={{.App.WatchdogTimeout.Seconds}} 474 {{- end}} 475 {{- if .KillMode}} 476 KillMode={{.KillMode}} 477 {{- end}} 478 {{- if .KillSignal}} 479 KillSignal={{.KillSignal}} 480 {{- end}} 481 {{- if not .App.Sockets}} 482 483 [Install] 484 WantedBy={{.ServicesTarget}} 485 {{- end}} 486 ` 487 var templateOut bytes.Buffer 488 tmpl := template.New("service-wrapper") 489 tmpl.Funcs(template.FuncMap{ 490 "stringsJoin": strings.Join, 491 }) 492 t := template.Must(tmpl.Parse(serviceTemplate)) 493 494 restartCond := appInfo.RestartCond.String() 495 if restartCond == "" { 496 restartCond = snap.RestartOnFailure.String() 497 } 498 499 var remain string 500 if appInfo.Daemon == "oneshot" { 501 // any restart condition other than "no" is invalid for oneshot daemons 502 restartCond = "no" 503 // If StopExec is present for a oneshot service than we also need 504 // RemainAfterExit=yes 505 if appInfo.StopCommand != "" { 506 remain = "yes" 507 } 508 } 509 var killMode string 510 if !appInfo.StopMode.KillAll() { 511 killMode = "process" 512 } 513 514 wrapperData := struct { 515 App *snap.AppInfo 516 517 Restart string 518 StopTimeout time.Duration 519 StartTimeout time.Duration 520 ServicesTarget string 521 PrerequisiteTarget string 522 MountUnit string 523 Remain string 524 KillMode string 525 KillSignal string 526 Before []string 527 After []string 528 529 Home string 530 EnvVars string 531 }{ 532 App: appInfo, 533 534 Restart: restartCond, 535 StopTimeout: serviceStopTimeout(appInfo), 536 StartTimeout: time.Duration(appInfo.StartTimeout), 537 ServicesTarget: systemd.ServicesTarget, 538 PrerequisiteTarget: systemd.PrerequisiteTarget, 539 MountUnit: filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())), 540 Remain: remain, 541 KillMode: killMode, 542 KillSignal: appInfo.StopMode.KillSignal(), 543 544 Before: genServiceNames(appInfo.Snap, appInfo.Before), 545 After: genServiceNames(appInfo.Snap, appInfo.After), 546 547 // systemd runs as PID 1 so %h will not work. 548 Home: "/root", 549 } 550 551 if err := t.Execute(&templateOut, wrapperData); err != nil { 552 // this can never happen, except we forget a variable 553 logger.Panicf("Unable to execute template: %v", err) 554 } 555 556 return templateOut.Bytes() 557 } 558 559 func genServiceSocketFile(appInfo *snap.AppInfo, socketName string) []byte { 560 socketTemplate := `[Unit] 561 # Auto-generated, DO NOT EDIT 562 Description=Socket {{.SocketName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 563 Requires={{.MountUnit}} 564 After={{.MountUnit}} 565 X-Snappy=yes 566 567 [Socket] 568 Service={{.ServiceFileName}} 569 FileDescriptorName={{.SocketInfo.Name}} 570 ListenStream={{.ListenStream}} 571 {{- if .SocketInfo.SocketMode}} 572 SocketMode={{.SocketInfo.SocketMode | printf "%04o"}} 573 {{- end}} 574 575 [Install] 576 WantedBy={{.SocketsTarget}} 577 ` 578 var templateOut bytes.Buffer 579 t := template.Must(template.New("socket-wrapper").Parse(socketTemplate)) 580 581 socket := appInfo.Sockets[socketName] 582 listenStream := renderListenStream(socket) 583 wrapperData := struct { 584 App *snap.AppInfo 585 ServiceFileName string 586 SocketsTarget string 587 MountUnit string 588 SocketName string 589 SocketInfo *snap.SocketInfo 590 ListenStream string 591 }{ 592 App: appInfo, 593 ServiceFileName: filepath.Base(appInfo.ServiceFile()), 594 SocketsTarget: systemd.SocketsTarget, 595 MountUnit: filepath.Base(systemd.MountUnitPath(appInfo.Snap.MountDir())), 596 SocketName: socketName, 597 SocketInfo: socket, 598 ListenStream: listenStream, 599 } 600 601 if err := t.Execute(&templateOut, wrapperData); err != nil { 602 // this can never happen, except we forget a variable 603 logger.Panicf("Unable to execute template: %v", err) 604 } 605 606 return templateOut.Bytes() 607 } 608 609 func generateSnapSocketFiles(app *snap.AppInfo) (*map[string][]byte, error) { 610 if err := snap.ValidateApp(app); err != nil { 611 return nil, err 612 } 613 614 socketFiles := make(map[string][]byte) 615 for name, socket := range app.Sockets { 616 socketFiles[socket.File()] = genServiceSocketFile(app, name) 617 } 618 return &socketFiles, nil 619 } 620 621 func renderListenStream(socket *snap.SocketInfo) string { 622 snap := socket.App.Snap 623 listenStream := strings.Replace(socket.ListenStream, "$SNAP_DATA", snap.DataDir(), -1) 624 // TODO: when we support User/Group in the generated systemd unit, 625 // adjust this accordingly 626 serviceUserUid := sys.UserID(0) 627 runtimeDir := snap.UserXdgRuntimeDir(serviceUserUid) 628 listenStream = strings.Replace(listenStream, "$XDG_RUNTIME_DIR", runtimeDir, -1) 629 return strings.Replace(listenStream, "$SNAP_COMMON", snap.CommonDataDir(), -1) 630 } 631 632 func generateSnapTimerFile(app *snap.AppInfo) ([]byte, error) { 633 timerTemplate := `[Unit] 634 # Auto-generated, DO NOT EDIT 635 Description=Timer {{.TimerName}} for snap application {{.App.Snap.InstanceName}}.{{.App.Name}} 636 Requires={{.MountUnit}} 637 After={{.MountUnit}} 638 X-Snappy=yes 639 640 [Timer] 641 Unit={{.ServiceFileName}} 642 {{ range .Schedules }}OnCalendar={{ . }} 643 {{ end }} 644 [Install] 645 WantedBy={{.TimersTarget}} 646 ` 647 var templateOut bytes.Buffer 648 t := template.Must(template.New("timer-wrapper").Parse(timerTemplate)) 649 650 timerSchedule, err := timeutil.ParseSchedule(app.Timer.Timer) 651 if err != nil { 652 return nil, err 653 } 654 655 schedules := generateOnCalendarSchedules(timerSchedule) 656 657 wrapperData := struct { 658 App *snap.AppInfo 659 ServiceFileName string 660 TimersTarget string 661 TimerName string 662 MountUnit string 663 Schedules []string 664 }{ 665 App: app, 666 ServiceFileName: filepath.Base(app.ServiceFile()), 667 TimersTarget: systemd.TimersTarget, 668 TimerName: app.Name, 669 MountUnit: filepath.Base(systemd.MountUnitPath(app.Snap.MountDir())), 670 Schedules: schedules, 671 } 672 673 if err := t.Execute(&templateOut, wrapperData); err != nil { 674 // this can never happen, except we forget a variable 675 logger.Panicf("Unable to execute template: %v", err) 676 } 677 678 return templateOut.Bytes(), nil 679 680 } 681 682 func makeAbbrevWeekdays(start time.Weekday, end time.Weekday) []string { 683 out := make([]string, 0, 7) 684 for w := start; w%7 != (end + 1); w++ { 685 out = append(out, time.Weekday(w % 7).String()[0:3]) 686 } 687 return out 688 } 689 690 // generateOnCalendarSchedules converts a schedule into OnCalendar schedules 691 // suitable for use in systemd *.timer units using systemd.time(7) 692 // https://www.freedesktop.org/software/systemd/man/systemd.time.html 693 func generateOnCalendarSchedules(schedule []*timeutil.Schedule) []string { 694 calendarEvents := make([]string, 0, len(schedule)) 695 for _, sched := range schedule { 696 days := make([]string, 0, len(sched.WeekSpans)) 697 for _, week := range sched.WeekSpans { 698 abbrev := strings.Join(makeAbbrevWeekdays(week.Start.Weekday, week.End.Weekday), ",") 699 switch week.Start.Pos { 700 case timeutil.EveryWeek: 701 // eg: mon, mon-fri, fri-mon 702 days = append(days, fmt.Sprintf("%s *-*-*", abbrev)) 703 case timeutil.LastWeek: 704 // eg: mon5 705 days = append(days, fmt.Sprintf("%s *-*~7/1", abbrev)) 706 default: 707 // eg: mon1, fri1, mon1-tue2 708 startDay := (week.Start.Pos-1)*7 + 1 709 endDay := week.End.Pos * 7 710 711 // NOTE: schedule mon1-tue2 (all weekdays 712 // between the first Monday of the month, until 713 // the second Tuesday of the month) is not 714 // translatable to systemd.time(7) format, for 715 // this assume all weekdays and allow the runner 716 // to do the filtering 717 if week.Start != week.End { 718 days = append(days, 719 fmt.Sprintf("*-*-%d..%d/1", startDay, endDay)) 720 } else { 721 days = append(days, 722 fmt.Sprintf("%s *-*-%d..%d/1", abbrev, startDay, endDay)) 723 } 724 725 } 726 } 727 728 if len(days) == 0 { 729 // no weekday spec, meaning the timer runs every day 730 days = []string{"*-*-*"} 731 } 732 733 startTimes := make([]string, 0, len(sched.ClockSpans)) 734 for _, clocks := range sched.ClockSpans { 735 // use expanded clock spans 736 for _, span := range clocks.ClockSpans() { 737 when := span.Start 738 if span.Spread { 739 length := span.End.Sub(span.Start) 740 if length < 0 { 741 // span Start wraps around, so we have '00:00.Sub(23:45)' 742 length = -length 743 } 744 if length > 5*time.Minute { 745 // replicate what timeutil.Next() does 746 // and cut some time at the end of the 747 // window so that events do not happen 748 // directly one after another 749 length -= 5 * time.Minute 750 } 751 when = when.Add(time.Duration(rand.Int63n(int64(length)))) 752 } 753 if when.Hour == 24 { 754 // 24:00 for us means the other end of 755 // the day, for systemd we need to 756 // adjust it to the 0-23 hour range 757 when.Hour -= 24 758 } 759 760 startTimes = append(startTimes, when.String()) 761 } 762 } 763 764 for _, day := range days { 765 for _, startTime := range startTimes { 766 calendarEvents = append(calendarEvents, fmt.Sprintf("%s %s", day, startTime)) 767 } 768 } 769 } 770 return calendarEvents 771 }