-
Notifications
You must be signed in to change notification settings - Fork 5
/
auto_logout.py
executable file
·286 lines (230 loc) · 9.64 KB
/
auto_logout.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#!/usr/bin/env python3
# Copyright (C) 2014 Shea G Craig
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
auto_logout.py
Check for whether system idle time has exceeded some set amount of time,
as specified in seconds with global MAXIDLE.
If system is idle, prompt user for a chance to prevent logout.
If no user intervention prevents logout, either restart the computer or
shut down, depending on whether the time is past the scheduled shutdown
time.
Code for killing the loginwindow is included for reference purposes,
although this method of force-logging out is not recommended due to
graphics glitches.
Restarting the computer is the most stable way to forcibly logout a user
who may have applications preventing logout via the normal means.
"""
import datetime
import getpass
import re
import subprocess
import sys
import syslog
# pylint: disable=no-name-in-module
from AppKit import (NSImage, NSAlert, NSTimer, NSRunLoop, NSApplication,
NSSound, NSModalPanelRunLoopMode, NSApp,
NSRunAbortedResponse)
# pylint: enable=no-name-in-module
__version__ = "1.6.4"
# Number of seconds to wait before initiating a logout.
MAXIDLE = 1800
# Number of seconds user has to cancel logout.
LO_TIMEOUT = 120
# Icon displayed as the app icon for the alert dialog.
ICON_PATH = "/usr/local/share/EvilCloud.png"
# Sound played when alert is presented. See README.
ALERT_SOUND = "Submarine"
# Cocoa objects must use class func alloc().init(), so pylint doesn't
# see our init().
# pylint: disable=no-init
# Methods are named according to PyObjC/Cocoa style.
# pylint: disable=invalid-name
class Alert(NSAlert):
"""Subclasses NSAlert to include a timeout."""
def init(self): # pylint: disable=super-on-old-class
"""Add an instance variable for our timer."""
self = super(Alert, self).init()
self.timer = None
self.alert_sound = None
return self
def setIconWithContentsOfFile_(self, path):
"""Convenience method for adding an icon.
Args:
path: String path to a valid NSImage filetype (png)
"""
icon = NSImage.alloc().initWithContentsOfFile_(path)
self.setIcon_(icon) # pylint: disable=no-member
def setAlertSound_(self, name):
"""Set the sound to play when alert is presented.
Args:
name: String name of a system sound. See the README.
"""
self.alert_sound = name
def setTimeToGiveUp_(self, time):
"""Configure alert to give up after time seconds."""
# Cocoa objects must use class func alloc().init(), so pylint
# doesn't see our init().
# pylint: disable=attribute-defined-outside-init
self.timer = \
NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(
time, self, "_killWindow", None, False)
# pylint: enable=attribute-defined-outside-init
def present(self):
"""Present the Alert, giving up after configured time..
Returns: Int result code, based on PyObjC enums. See NSAlert
Class reference, but result should be one of:
User clicked the cancel button:
NSAlertFirstButtonReturn = 1000
Alert timed out:
NSRunAbortedResponse = -1001
"""
if self.timer:
NSRunLoop.currentRunLoop().addTimer_forMode_(
self.timer, NSModalPanelRunLoopMode)
# Start a Cocoa application by getting the shared app object.
# Make the python app the active app so alert is noticed.
app = NSApplication.sharedApplication()
app.activateIgnoringOtherApps_(True)
if self.alert_sound:
sound = NSSound.soundNamed_(self.alert_sound).play()
result = self.runModal() # pylint: disable=no-member
print(result)
return result
# pylint: disable=no-self-use
def _killWindow(self):
"""Abort the modal window as managed by NSApp."""
NSApp.abortModal()
# pylint: enable=no-self-use
# pylint: enable=no-init
# pylint: enable=invalid-name
def logout():
"""Forcibly log current user out of the gui.
This function is currently unused. killall loginwindow often results
in corrupted loginwindow graphics. The function remains more as
documentation of how to do these things.
"""
result = subprocess.check_output(["sudo", "-u", "root", "/usr/bin/killall",
"-9", "loginwindow"], text=True)
syslog.syslog(syslog.LOG_ALERT, result)
def restart():
"""Forcibly restart the computer."""
result = subprocess.check_output(
["sudo", "-u", "root", "/sbin/reboot", "-q"], text=True)
syslog.syslog(syslog.LOG_ALERT, result)
def fvrestart():
"""Forcibly restart a FV2 enabled computer."""
result = subprocess.check_output(
["sudo", "-u", "root", "/usr/bin/fdesetup", "authrestart"], text=True)
syslog.syslog(syslog.LOG_ALERT, result)
def shutdown():
"""Shutdown the computer immediately."""
result = subprocess.check_output(["sudo", "-u", "root", "/sbin/shutdown", "-h", "now"],
text=True)
syslog.syslog(syslog.LOG_ALERT, result)
def fv_active():
"""Get FileVault status."""
result = subprocess.check_output(["/usr/bin/fdesetup", "status"], text=True)
return result == "FileVault is On.\n"
def get_shutdown_time():
"""Return a system's shutdown time.
Returns:
A datetime.time object representing the time system is supposed
to shut itself down, or None if no schedule has been set.
"""
# Get the schedule items from pmset
result = subprocess.check_output(["pmset", "-g", "sched"], text=True)
# Get the shutdown time
pattern = re.compile(r"(shutdown at )(\d{1,2}:\d{2}[AP]M)")
final = pattern.search(result)
if final:
# Create a datetime object from the unhelpful apple format
today = datetime.date.today().strftime("%Y%m%d")
shutdown_time = datetime.datetime.strptime(
today + final.group(2), "%Y%m%d%I:%M%p")
else:
shutdown_time = None
return shutdown_time
def get_idle():
"""Check the IOREG for the idle time of the input devices.
Returns:
Float number of seconds computer has been idle.
"""
result = subprocess.check_output(["ioreg", "-c", "IOHIDSystem"], text=True)
# Strip out the first result (there are lots and lots of results;
# close enough!
pattern = re.compile(r'("HIDIdleTime" = )([0-9]*)')
final = pattern.search(result)
# Idle time is in really absurd units; convert to seconds.
idle_time = float(final.group(2)) / 1000000000
syslog.syslog(syslog.LOG_ALERT, "System Idle: %i seconds out of %i "
"allowed." % (idle_time, MAXIDLE))
return idle_time
def get_loginwindow_pid():
"""Get the pid for the user's loginwindow.
Currently unused since we need to use killall to pull this off.
Returns: An int process ID for the loginwindow.
"""
pid = None
result = subprocess.check_output(["ps", "-Axjc"], text=True)
pattern = re.compile(r".*loginwindow")
for line in result.splitlines():
match = pattern.search(line)
if match:
pid = match.group(0).split()[1]
return int(pid)
def build_alert():
"""Build an alert for auto-logout notifications."""
alert = Alert.alloc().init() # pylint: disable=no-member
alert.setMessageText_("Logging out idle user in %i seconds!" % LO_TIMEOUT)
alert.setInformativeText_("Click Cancel to prevent automatic logout.")
alert.addButtonWithTitle_("Cancel")
alert.setIconWithContentsOfFile_(ICON_PATH)
alert.setAlertSound_(ALERT_SOUND)
alert.setTimeToGiveUp_(LO_TIMEOUT)
return alert
def main():
"""Main program"""
idle_time = get_idle()
if idle_time > MAXIDLE:
syslog.syslog(syslog.LOG_ALERT, "System is idle.")
syslog.syslog(syslog.LOG_ALERT, "Idle user: %s" % getpass.getuser())
alert = build_alert()
if alert.present() != NSRunAbortedResponse:
# User cancelled
syslog.syslog(syslog.LOG_ALERT, "User cancelled auto logout.")
sys.exit()
else:
# If it's past shutdown time, go straight to shutting down.
# If there is no schedule, or it's before scheduled
# shutdown, restart.
shutdown_time = get_shutdown_time()
syslog.syslog(syslog.LOG_ALERT,
"Scheduled system shutdown time: %s" % shutdown_time)
if shutdown_time and datetime.datetime.now() > shutdown_time:
syslog.syslog(syslog.LOG_ALERT, "Shutdown time is nigh. "
"Shutting down.")
shutdown()
else:
syslog.syslog(syslog.LOG_ALERT, "Restarting")
if fv_active():
syslog.syslog(syslog.LOG_ALERT, "Authenticated Restart.")
fvrestart()
else:
restart()
else:
syslog.syslog(syslog.LOG_ALERT, "System is not idle.")
if __name__ == "__main__":
main()