Skip to content

Commit 01be46c

Browse files
committed
add CTRL-C handler for win32; workaround for FileSession congestion on win32; add requirements_win32.txt
1 parent d0701cb commit 01be46c

File tree

6 files changed

+204
-3
lines changed

6 files changed

+204
-3
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,4 @@ False: "n, no, f, false, off, 0".
133133
## Acknowledgments
134134
- Thanks to Tautulli for providing the [Javascript for proper authentication with Plex.TV](https://github.com/Tautulli/Tautulli/blob/master/data/interfaces/default/js/script.js).
135135
- Icon/Art based on Fan Icon from [http://www.malagatravelguide.net](http://www.malagatravelguide.net)
136+
- PyWin32 CherryPy Console/Service handler from [googleappengine](https://chromium.googlesource.com/external/googleappengine/python/+/master/lib/cherrypy/cherrypy/process/win32.py)

kitana.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from jinja2 import Environment, PackageLoader, select_autoescape
1717
from urllib.parse import urlparse
1818
from furl import furl
19-
from cherrypy.lib.static import serve_file
2019
from cherrypy.process.plugins import Monitor
2120
from requests import HTTPError, Timeout
2221
from distutils.util import strtobool
@@ -27,6 +26,7 @@
2726
from util.argparse import MultilineFormatter
2827
from util.messages import message, render_messages
2928
from util.update import update_check, StrictVersion
29+
from util.sessions import FileSession
3030

3131
env = Environment(
3232
loader=PackageLoader('kitana', 'templates'),
@@ -559,7 +559,7 @@ def default(self, *args, **kwargs):
559559
'server.socket_port': int(port),
560560
'engine.autoreload.on': args.autoreload,
561561
"tools.sessions.on": True,
562-
"tools.sessions.storage_class": cherrypy.lib.sessions.FileSession,
562+
"tools.sessions.storage_class": FileSession,
563563
"tools.sessions.storage_path": os.path.join(baseDir, "data", "sessions"),
564564
"tools.sessions.timeout": 525600,
565565
"tools.sessions.name": "kitana_session_id",
@@ -576,6 +576,10 @@ def default(self, *args, **kwargs):
576576
cherrypy.engine.autoreload.files.update(glob.glob(os.path.join(baseDir, "static", "sass", "**")))
577577
cherrypy.tools.baseurloverride = BaseUrlOverride()
578578

579+
if os.name == "nt":
580+
from util.win32 import ConsoleCtrlHandler
581+
ConsoleCtrlHandler(cherrypy.engine).subscribe()
582+
579583
conf = {
580584
"/": {
581585
"tools.sessions.on": True,

requirements_win32.txt

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-r requirements.txt
2+
pywin32>=223

util/http.py

-1
This file was deleted.

util/sessions.py

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# coding=utf-8
2+
import contextlib
3+
import os
4+
5+
from cherrypy.lib.sessions import FileSession as _FileSession
6+
7+
8+
class FileSession(_FileSession):
9+
def release_lock(self, path=None):
10+
"""Release the lock on the currently-loaded session data."""
11+
self.lock.close()
12+
with contextlib.suppress(FileNotFoundError, PermissionError):
13+
os.remove(self.lock._path)
14+
self.locked = False

util/win32.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# coding=utf-8
2+
3+
# borrowed from: https://chromium.googlesource.com/external/googleappengine/python/+/master/lib/cherrypy/cherrypy/process/win32.py
4+
5+
6+
"""Windows service. Requires pywin32."""
7+
import os
8+
import win32api
9+
import win32con
10+
import win32event
11+
import win32service
12+
import win32serviceutil
13+
from cherrypy.process import wspbus, plugins
14+
15+
16+
class ConsoleCtrlHandler(plugins.SimplePlugin):
17+
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
18+
19+
def __init__(self, bus):
20+
self.is_set = False
21+
plugins.SimplePlugin.__init__(self, bus)
22+
23+
def start(self):
24+
if self.is_set:
25+
self.bus.log('Handler for console events already set.', level=40)
26+
return
27+
28+
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
29+
if result == 0:
30+
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
31+
win32api.GetLastError(), level=40)
32+
else:
33+
self.bus.log('Set handler for console events.', level=40)
34+
self.is_set = True
35+
36+
def stop(self):
37+
if not self.is_set:
38+
self.bus.log('Handler for console events already off.', level=40)
39+
return
40+
41+
try:
42+
result = win32api.SetConsoleCtrlHandler(self.handle, 0)
43+
except ValueError:
44+
# "ValueError: The object has not been registered"
45+
result = 1
46+
47+
if result == 0:
48+
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
49+
win32api.GetLastError(), level=40)
50+
else:
51+
self.bus.log('Removed handler for console events.', level=40)
52+
self.is_set = False
53+
54+
def handle(self, event):
55+
"""Handle console control events (like Ctrl-C)."""
56+
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
57+
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
58+
win32con.CTRL_CLOSE_EVENT):
59+
self.bus.log('Console event %s: shutting down bus' % event)
60+
61+
# Remove self immediately so repeated Ctrl-C doesn't re-call it.
62+
try:
63+
self.stop()
64+
except ValueError:
65+
pass
66+
67+
self.bus.exit()
68+
# 'First to return True stops the calls'
69+
return 1
70+
return 0
71+
72+
73+
class Win32Bus(wspbus.Bus):
74+
"""A Web Site Process Bus implementation for Win32.
75+
76+
Instead of time.sleep, this bus blocks using native win32event objects.
77+
"""
78+
79+
def __init__(self):
80+
self.events = {}
81+
wspbus.Bus.__init__(self)
82+
83+
def _get_state_event(self, state):
84+
"""Return a win32event for the given state (creating it if needed)."""
85+
try:
86+
return self.events[state]
87+
except KeyError:
88+
event = win32event.CreateEvent(None, 0, 0,
89+
"WSPBus %s Event (pid=%r)" %
90+
(state.name, os.getpid()))
91+
self.events[state] = event
92+
return event
93+
94+
def _get_state(self):
95+
return self._state
96+
97+
def _set_state(self, value):
98+
self._state = value
99+
event = self._get_state_event(value)
100+
win32event.PulseEvent(event)
101+
102+
state = property(_get_state, _set_state)
103+
104+
def wait(self, state, interval=0.1, channel=None):
105+
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
106+
107+
Since this class uses native win32event objects, the interval
108+
argument is ignored.
109+
"""
110+
if isinstance(state, (tuple, list)):
111+
# Don't wait for an event that beat us to the punch ;)
112+
if self.state not in state:
113+
events = tuple([self._get_state_event(s) for s in state])
114+
win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
115+
else:
116+
# Don't wait for an event that beat us to the punch ;)
117+
if self.state != state:
118+
event = self._get_state_event(state)
119+
win32event.WaitForSingleObject(event, win32event.INFINITE)
120+
121+
122+
class _ControlCodes(dict):
123+
"""Control codes used to "signal" a service via ControlService.
124+
125+
User-defined control codes are in the range 128-255. We generally use
126+
the standard Python value for the Linux signal and add 128. Example:
127+
128+
>>> signal.SIGUSR1
129+
10
130+
control_codes['graceful'] = 128 + 10
131+
"""
132+
133+
def key_for(self, obj):
134+
"""For the given value, return its corresponding key."""
135+
for key, val in self.items():
136+
if val is obj:
137+
return key
138+
raise ValueError("The given object could not be found: %r" % obj)
139+
140+
141+
control_codes = _ControlCodes({'graceful': 138})
142+
143+
144+
def signal_child(service, command):
145+
if command == 'stop':
146+
win32serviceutil.StopService(service)
147+
elif command == 'restart':
148+
win32serviceutil.RestartService(service)
149+
else:
150+
win32serviceutil.ControlService(service, control_codes[command])
151+
152+
153+
class PyWebService(win32serviceutil.ServiceFramework):
154+
"""Python Web Service."""
155+
156+
_svc_name_ = "Python Web Service"
157+
_svc_display_name_ = "Python Web Service"
158+
_svc_deps_ = None # sequence of service names on which this depends
159+
_exe_name_ = "pywebsvc"
160+
_exe_args_ = None # Default to no arguments
161+
162+
# Only exists on Windows 2000 or later, ignored on windows NT
163+
_svc_description_ = "Python Web Service"
164+
165+
def SvcDoRun(self):
166+
from cherrypy import process
167+
process.bus.start()
168+
process.bus.block()
169+
170+
def SvcStop(self):
171+
from cherrypy import process
172+
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
173+
process.bus.exit()
174+
175+
def SvcOther(self, control):
176+
from cherrypy import process
177+
process.bus.publish(control_codes.key_for(control))
178+
179+
180+
if __name__ == '__main__':
181+
win32serviceutil.HandleCommandLine(PyWebService)

0 commit comments

Comments
 (0)