1/* xscreensaver, Copyright (c) 2013-2018 Jamie Zawinski <jwz@jwz.org>
2 *
3 * Permission to use, copy, modify, distribute, and sell this software and its
4 * documentation for any purpose is hereby granted without fee, provided that
5 * the above copyright notice appear in all copies and that both that
6 * copyright notice and this permission notice appear in supporting
7 * documentation.  No representations are made about the suitability of this
8 * software for any purpose.  It is provided "as is" without express or
9 * implied warranty.
10 *
11 * XScreenSaverUpdater.app -- downloads and installs XScreenSaver updates
12 * via Sparkle.framework.
13 *
14 * Created: 7-Dec-2013
15 *
16 * NOTE: This does not work with Sparkle 1.5b6 -- it requires the "HEAD"
17 *       version 4-Dec-2013 or later.
18 */
19
20#define IN_UPDATER
21#import "Updater.h"
22#import "Sparkle/SUUpdater.h"
23
24@implementation XScreenSaverUpdater : NSObject
25
26- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
27{
28  NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
29  [defs registerDefaults:UPDATER_DEFAULTS];
30
31  // If it's not time to run the updater, then bail immediately.
32  // I'm not sure why this is necessary, but Sparkle seems to be
33  // checking too often.
34  //
35  if (! [self timeToCheck])
36    [[NSApplication sharedApplication] terminate:self];
37
38  // If the screen saver is not running, then launch the updater now.
39  // Otherwise, wait until the screen saver deactivates, and then do
40  // it.  This is because if the updater tries to pop up a dialog box
41  // while the screen saver is active, everything goes to hell and it
42  // never shows up.  You'd expect the dialog to just map below the
43  // screen saver window, but no.
44
45  if (! [self screenSaverActive]) {
46    [self runUpdater];
47  } else {
48    // Run the updater when the "screensaver.didstop" notification arrives.
49    [[NSDistributedNotificationCenter defaultCenter]
50      addObserver:self
51      selector:@selector(saverStoppedNotification:)
52      name:@"com.apple.screensaver.didstop"
53      object:nil];
54
55    // But I'm not sure I trust that, so also poll every couple minutes.
56    timer = [NSTimer scheduledTimerWithTimeInterval: 60 * 2
57                     target:self
58                     selector:@selector(pollSaverTermination:)
59                     userInfo:nil
60                     repeats:YES];
61  }
62}
63
64
65- (BOOL) timeToCheck
66{
67  NSUserDefaults *defs = [NSUserDefaults standardUserDefaults];
68  NSTimeInterval interval = [defs doubleForKey:@SUScheduledCheckIntervalKey];
69  NSDate *last = [defs objectForKey:@SULastCheckTimeKey];
70  if (!interval || !last)
71    return YES;
72  NSTimeInterval since = [[NSDate date] timeIntervalSinceDate:last];
73  return (since > interval);
74}
75
76
77// Whether ScreenSaverEngine is currently running, meaning screen is blanked.
78// There's no easy way to determine this other than scanning the process table.
79//
80- (BOOL) screenSaverActive
81{
82  BOOL found = NO;
83  NSString *target = @"/ScreenSaverEngine.app";
84  ProcessSerialNumber psn = { kNoProcess, kNoProcess };
85  while (GetNextProcess(&psn) == noErr) {
86    CFDictionaryRef cfdict =
87      ProcessInformationCopyDictionary (&psn,
88        kProcessDictionaryIncludeAllInformationMask);
89    if (cfdict) {
90      NSDictionary *dict = (NSDictionary *) cfdict;
91      NSString *path = [dict objectForKey:@"BundlePath"];
92      if (path && [path hasSuffix:target])
93        found = YES;
94      CFRelease (cfdict);
95    }
96    if (found)
97      break;
98  }
99  return found;
100}
101
102
103- (void) saverStoppedNotification:(NSNotification *)note
104{
105  [self runUpdater];
106}
107
108
109- (void) pollSaverTermination:(NSTimer *)t
110{
111  if (! [self screenSaverActive])
112    [self runUpdater];
113}
114
115
116- (void) runUpdater
117{
118  if (timer) {
119    [timer invalidate];
120    timer = nil;
121  }
122
123  SUUpdater *updater = [SUUpdater updaterForBundle:
124                                    [NSBundle bundleForClass:[self class]]];
125  [updater setDelegate:self];
126
127  // Launch the updater thread.
128  [updater checkForUpdatesInBackground];
129
130  // Now we need to wait for the Sparkle thread to finish before we can
131  // exit, so just poll waiting for it.
132  //
133  [NSTimer scheduledTimerWithTimeInterval:1
134           target:self
135           selector:@selector(pollUpdaterTermination:)
136           userInfo:updater
137           repeats:YES];
138}
139
140
141// Delegate method that lets us append extra info to the system-info URL.
142//
143- (NSArray *) feedParametersForUpdater:(SUUpdater *)updater
144                  sendingSystemProfile:(BOOL)sending
145{
146  // Get the name of the saver that invoked us, and include that in the
147  // system info.
148  NSString *saver = [[[NSProcessInfo processInfo] environment]
149                      objectForKey:@"XSCREENSAVER_CLASSPATH"];
150  if (! saver) return @[];
151  NSString *head = @"org.jwz.xscreensaver.";
152  if ([saver hasPrefix:head])
153    saver = [saver substringFromIndex:[head length]];
154
155  return @[ @{ @"key":		@"saver",
156               @"value":	saver,
157               @"displayKey":	@"Current Saver",
158               @"displayValue":	saver
159             }
160          ];
161}
162
163
164- (void) pollUpdaterTermination:(NSTimer *)t
165{
166  SUUpdater *updater = [t userInfo];
167  if (![updater updateInProgress])
168    [[NSApplication sharedApplication] terminate:self];
169}
170
171
172- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app
173{
174  return YES;
175}
176
177@end
178