Skip to content

Commit d8b05c2

Browse files
committedSep 2, 2013
[tests] Add support for locally spawned fcgi process
- No building of spawn-fcgi or managing of spawned process, yet
1 parent 9328843 commit d8b05c2

33 files changed

+3898
-128
lines changed
 

‎tests/src/python/qgis_local_server.py

Lines changed: 204 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import os
1818
import ConfigParser
1919
import urllib
20+
import urlparse
2021
import tempfile
2122

2223
# allow import error to be raised if qgis is not on sys.path
@@ -26,6 +27,19 @@
2627
raise ImportError(str(e) + '\n\nPlace path to pyqgis modules on sys.path,'
2728
' or assign to PYTHONPATH')
2829

30+
import qgis_local_server_spawn.flup_fcgi_client as fcgi_client
31+
32+
if sys.version_info[0:2] < (2, 7):
33+
try:
34+
from unittest2 import TestCase, expectedFailure
35+
import unittest2 as unittest
36+
except ImportError:
37+
print "You should install unittest2 to run the salt tests"
38+
sys.exit(0)
39+
else:
40+
from unittest import TestCase, expectedFailure
41+
import unittest
42+
2943

3044
class ServerNotAccessibleError(Exception):
3145

@@ -43,10 +57,11 @@ def __str__(self):
4357

4458
class QgisLocalServer(object):
4559

46-
def __init__(self, cgiurl, chkcapa=False):
60+
def __init__(self, cgiurl, chkcapa=False, spawn=False):
4761
self.cgiurl = cgiurl
4862
self.params = {}
4963
self.active = False
64+
self.spawn = spawn
5065

5166
# check capabilities to verify server is accessible
5267
if chkcapa:
@@ -74,12 +89,22 @@ def getCapabilities(self, params, browser=False):
7489
url = self.cgiurl + '?' + self._processParams()
7590
self.params = {}
7691

92+
xml = ''
93+
if self.spawn:
94+
xml = requestFromSpawn(url)[2]
95+
7796
if browser:
97+
tmp = tempfile.NamedTemporaryFile(suffix=".html", delete=False)
98+
tmp.write(xml)
99+
url = tmp.name
100+
tmp.close()
78101
openInBrowserTab(url)
79102
return False, ''
80103

81-
res = urllib.urlopen(url)
82-
xml = res.read()
104+
if not self.spawn:
105+
res = urllib.urlopen(url)
106+
xml = res.read()
107+
83108
success = ('perhaps you left off the .qgs extension' in xml or
84109
'WMS_Capabilities' in xml)
85110
return success, xml
@@ -98,18 +123,39 @@ def getMap(self, params, browser=False):
98123
url = self.cgiurl + '?' + self._processParams()
99124
self.params = {}
100125

101-
if browser:
126+
if browser and not self.spawn:
102127
openInBrowserTab(url)
103128
return False, ''
104129

105130
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
106-
tmp.close()
107-
res = urllib.urlretrieve(url, tmp.name)
108-
filepath = res[0]
109131
success = True
110-
if (res[1].getmaintype() != 'image' or
111-
res[1].getheader('Content-Type') != 'image/png'):
112-
success = False
132+
filepath = tmp.name
133+
# print 'filepath: ' + filepath
134+
135+
if self.spawn:
136+
status, headers, result, err = requestFromSpawn(url)
137+
tmp.write(result)
138+
tmp.close()
139+
140+
# print 'status: ' + status
141+
# print 'headers: ' + repr(headers)
142+
143+
if (status != '200 OK' or
144+
'content-type' not in headers or
145+
headers['content-type'] != 'image/png'):
146+
success = False
147+
148+
if browser:
149+
openInBrowserTab('file://' + filepath)
150+
return False, ''
151+
152+
else:
153+
tmp.close()
154+
filepath2, headers = urllib.urlretrieve(url, tmp.name)
155+
156+
if (headers.getmaintype() != 'image' or
157+
headers.getheader('Content-Type') != 'image/png'):
158+
success = False
113159

114160
return success, filepath
115161

@@ -146,7 +192,7 @@ def _convertInstances(self):
146192
class ServerConfigNotAccessibleError(Exception):
147193

148194
def __init__(self, err=''):
149-
self.msg = '\n\n' + str(err) + '\n'
195+
self.msg = '\n' + ('\n' + str(err) + '\n' if err else '')
150196
self.msg += """
151197
#----------------------------------------------------------------#
152198
Local test QGIS Server is not accessible
@@ -172,32 +218,59 @@ def __str__(self):
172218
return self.msg
173219

174220

221+
class ServerSpawnNotAccessibleError(Exception):
222+
223+
def __init__(self, cgiurl, err=''):
224+
self.msg = '\n' + ('\n' + str(err) + '\n' if err else '')
225+
self.msg += """
226+
#----------------------------------------------------------------#
227+
Locally spawned test QGIS Server is not accessible at:
228+
{0}
229+
#----------------------------------------------------------------#
230+
""".format(cgiurl)
231+
232+
def __str__(self):
233+
return self.msg
234+
235+
175236
class QgisLocalServerConfig(QgisLocalServer):
176237

177-
def __init__(self, cfgdir, chkcapa=False):
238+
def __init__(self, cfgdir, chkcapa=False, spawn=False):
178239
msg = 'Server configuration directory required'
179240
assert cfgdir, msg
180241

181-
self.cfgdir = cfgdir
182-
self.cfg = os.path.normpath(os.path.join(self.cfgdir,
183-
'qgis_local_server.cfg'))
184-
if not os.path.exists(self.cfg):
185-
msg = ('Default server configuration file could not be written'
186-
' to {0}'.format(self.cfg))
187-
assert self._writeDefaultServerConfig(), msg
188-
189-
self._checkItemFound('file', self.cfg)
190-
self._checkItemReadable('file', self.cfg)
191-
192-
cgiurl, self.projdir = self._readServerConfig()
242+
# cfgdir is either path to temp project dir (spawned) or settings dir
243+
# temp dir for spawned server is used lke 'projdir' in configured
244+
if spawn:
245+
# predefined and runs in user space
246+
cgiurl = 'http://127.0.0.1:8448/qgis_mapserv.fcgi'
247+
self.projdir = cfgdir
248+
else:
249+
self.cfgdir = cfgdir
250+
self.cfg = os.path.normpath(os.path.join(self.cfgdir,
251+
'qgis_local_server.cfg'))
252+
if not os.path.exists(self.cfg):
253+
msg = ('Default server configuration file could not be written'
254+
' to {0}'.format(self.cfg))
255+
assert self._writeDefaultServerConfig(), msg
256+
257+
_checkItemFound('file', self.cfg)
258+
_checkItemReadable('file', self.cfg)
259+
260+
cgiurl, self.projdir = self._readServerConfig()
193261

194262
try:
195-
self._checkItemFound('project directory', self.projdir)
196-
self._checkItemReadable('project directory', self.projdir)
197-
self._checkItemWriteable('project directory', self.projdir)
198-
super(QgisLocalServerConfig, self).__init__(cgiurl, chkcapa)
263+
_checkItemFound('project directory', self.projdir)
264+
_checkItemReadable('project directory', self.projdir)
265+
_checkItemWriteable('project directory', self.projdir)
266+
super(QgisLocalServerConfig, self).__init__(cgiurl,
267+
chkcapa=chkcapa,
268+
spawn=spawn)
199269
except Exception, err:
200-
raise ServerConfigNotAccessibleError(err)
270+
if spawn:
271+
raise ServerSpawnNotAccessibleError(cgiurl, '')
272+
else:
273+
raise ServerConfigNotAccessibleError(err)
201274

202275
def projectDir(self):
203276
return self.projdir
@@ -218,22 +291,8 @@ def getMap(self, params, browser=False):
218291
msg = 'Project could not be found at {0}'.format(proj)
219292
assert os.path.exists(proj), msg
220293

221-
return super(QgisLocalServerConfig, self).getMap(params, browser)
222-
223-
def _checkItemFound(self, item, path):
224-
msg = ('Server configuration {0} could not be found at:\n'
225-
' {1}'.format(item, path))
226-
assert os.path.exists(path), msg
227-
228-
def _checkItemReadable(self, item, path):
229-
msg = ('Server configuration {0} is not readable from:\n'
230-
' {1}'.format(item, path))
231-
assert os.access(path, os.R_OK), msg
232-
233-
def _checkItemWriteable(self, item, path):
234-
msg = ('Server configuration {0} is not writeable from:\n'
235-
' {1}'.format(item, path))
236-
assert os.access(path, os.W_OK), msg
294+
return super(QgisLocalServerConfig, self).getMap(params,
295+
browser=browser)
237296

238297
def _writeDefaultServerConfig(self):
239298
"""Overwrites any existing server configuration file with default"""
@@ -268,6 +327,50 @@ def _readServerConfig(self):
268327
return url, projdir
269328

270329

330+
def _checkItemFound(item, path):
331+
msg = ('Server configuration {0} could not be found at:\n'
332+
' {1}'.format(item, path))
333+
assert os.path.exists(path), msg
334+
335+
336+
def _checkItemReadable(item, path):
337+
msg = ('Server configuration {0} is not readable from:\n'
338+
' {1}'.format(item, path))
339+
assert os.access(path, os.R_OK), msg
340+
341+
342+
def _checkItemWriteable(item, path):
343+
msg = ('Server configuration {0} is not writeable from:\n'
344+
' {1}'.format(item, path))
345+
assert os.access(path, os.W_OK), msg
346+
347+
348+
def requestFromSpawn(url):
349+
""" load fastcgi page """
350+
prl = urlparse.urlparse(url)
351+
fcgi_host, fcgi_port = prl.hostname, prl.port
352+
try:
353+
fcgi = fcgi_client.FCGIApp(host=fcgi_host, port=fcgi_port)
354+
env = {
355+
'SCRIPT_FILENAME': prl.path[1:],
356+
'QUERY_STRING': prl.query,
357+
'REQUEST_METHOD': 'GET',
358+
'SCRIPT_NAME': prl.path[1:],
359+
'REQUEST_URI': prl.path + '?' + prl.query,
360+
'GATEWAY_INTERFACE': 'CGI/1.1',
361+
# 'DOCUMENT_ROOT': '/qgisserver/',
362+
'SERVER_ADDR': fcgi_host,
363+
'SERVER_PORT': str(fcgi_port),
364+
'SERVER_PROTOCOL': 'HTTP/1.0',
365+
'SERVER_NAME': fcgi_host
366+
}
367+
ret = fcgi(env)
368+
return ret
369+
except:
370+
print 'Locally spawned fcgi server not accessible'
371+
return '500', [], '', ''
372+
373+
271374
def openInBrowserTab(url):
272375
if sys.platform[:3] in ('win', 'dar'):
273376
import webbrowser
@@ -282,39 +385,60 @@ def openInBrowserTab(url):
282385
stderr=subprocess.STDOUT).pid
283386

284387

388+
class TestQgisLocalServerConfig(TestCase):
389+
390+
def config_dir(self):
391+
# for when testing local writes to projdir
392+
if TESTSPAWN:
393+
return tempfile.mkdtemp()
394+
else:
395+
return os.path.join(os.path.expanduser('~'), '.qgis2')
396+
397+
# @unittest.skip('')
398+
def test_check_capabilities(self):
399+
server = QgisLocalServerConfig(self.config_dir(),
400+
chkcapa=True,
401+
spawn=TESTSPAWN)
402+
# print '\nServer accessible and returned capabilities'
403+
404+
# @unittest.skip('')
405+
def test_get_map_serverconfig(self):
406+
server = QgisLocalServerConfig(self.config_dir(), spawn=TESTSPAWN)
407+
408+
# creating crs needs app instance to access /resources/srs.db
409+
# crs = QgsCoordinateReferenceSystem()
410+
# # default for labeling test data sources: WGS 84 / UTM zone 13N
411+
# crs.createFromSrid(32613)
412+
ext = QgsRectangle(606510, 4823130, 612510, 4827130)
413+
params = {
414+
'SERVICE': 'WMS',
415+
'VERSION': '1.3.0',
416+
'REQUEST': 'GetMap',
417+
'MAP': TESTPROJ,
418+
# layer stacking order for rendering: bottom,to,top
419+
'LAYERS': ['background', 'point'], # or 'background,point'
420+
'STYLES': ',',
421+
'CRS': 'EPSG:32613', # or: QgsCoordinateReferenceSystem obj
422+
'BBOX': ext, # or: '606510,4823130,612510,4827130'
423+
'FORMAT': 'image/png', # or: 'image/png; mode=8bit'
424+
'WIDTH': '600',
425+
'HEIGHT': '400',
426+
'DPI': '72',
427+
'MAP_RESOLUTION': '72',
428+
'FORMAT_OPTIONS': 'dpi:72',
429+
'TRANSPARENT': 'FALSE',
430+
'IgnoreGetMapUrl': '1'
431+
}
432+
if 'QGISSERVER_PNG' in os.environ and not TESTSPAWN:
433+
# open resultant png with system
434+
res, filepath = server.getMap(params, False)
435+
openInBrowserTab('file://' + filepath)
436+
else:
437+
# open GetMap url in browser
438+
res, filepath = server.getMap(params, True)
439+
440+
285441
if __name__ == '__main__':
286-
qgishome = os.path.join(os.path.expanduser('~'), '.qgis2')
287-
server = QgisLocalServerConfig(qgishome, True)
288-
# print '\nServer accessible and returned capabilities'
289-
290-
# creating crs needs app instance to access /resources/srs.db
291-
# crs = QgsCoordinateReferenceSystem()
292-
# # default for labeling test data sources: WGS 84 / UTM zone 13N
293-
# crs.createFromSrid(32613)
294-
ext = QgsRectangle(606510, 4823130, 612510, 4827130)
295-
params = {
296-
'SERVICE': 'WMS',
297-
'VERSION': '1.3.0',
298-
'REQUEST': 'GetMap',
299-
'MAP': '/test-projects/tests/tests.qgs',
300-
# layer stacking order for rendering: bottom,to,top
301-
'LAYERS': ['background', 'point'], # or 'background,point'
302-
'STYLES': ',',
303-
'CRS': 'EPSG:32613', # or: QgsCoordinateReferenceSystem obj
304-
'BBOX': ext, # or: '606510,4823130,612510,4827130'
305-
'FORMAT': 'image/png', # or: 'image/png; mode=8bit'
306-
'WIDTH': '600',
307-
'HEIGHT': '400',
308-
'DPI': '72',
309-
'MAP_RESOLUTION': '72',
310-
'FORMAT_OPTIONS': 'dpi:72',
311-
'TRANSPARENT': 'TRUE',
312-
'IgnoreGetMapUrl': '1'
313-
}
314-
if 'QGISSERVER_PNG' in os.environ:
315-
# open resultant png with system
316-
res, filepath = server.getMap(params, False)
317-
openInBrowserTab('file://' + filepath)
318-
else:
319-
# open GetMap url in browser
320-
res, filepath = server.getMap(params, True)
442+
TESTSPAWN = True
443+
TESTPROJ = '/test-projects/tests/pal_test.qgs'
444+
unittest.main(verbosity=2)

‎tests/src/python/qgis_local_server_spawn/__init__.py

Whitespace-only changes.
Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
2011-04-04 Allan Saddi <allan@saddi.com>
2+
3+
* Add threadpool options to Paste factories.
4+
5+
2011-02-19 Allan Saddi <allan@saddi.com>
6+
7+
* When deriving PATH_INFO from REQUEST_URI, take SCRIPT_NAME into account.
8+
9+
2011-01-11 Allan Saddi <allan@saddi.com>
10+
11+
* Use HTTP status code 500 for error pages. Thanks to
12+
Yohann Gabory for pointing out this issue and providing a patch.
13+
14+
2010-10-14 Allan Saddi <allan@saddi.com>
15+
16+
* Don't try to restore signal handlers if they weren't installed in
17+
the first place.
18+
19+
2010-10-05 Allan Saddi <allan@saddi.com>
20+
21+
* Improvements to *_app._getConnection methods suggested by
22+
Andrej A Antonov. Thanks!
23+
24+
2009-10-27 Allan Saddi <allan@saddi.com>
25+
26+
* Exit gracefully if a thread cannot be started when adding a new
27+
job.
28+
29+
2009-10-21 Allan Saddi <allan@saddi.com>
30+
31+
* Add configurable timeout (default: no timeout) to be used when the
32+
WSGI application is called. Only applies to forked servers!
33+
34+
2009-06-05 Allan Saddi <allan@saddi.com>
35+
36+
* Fix bug in scgi servers that occurs when SCRIPT_NAME is missing.
37+
Thanks to Jon Nelson for finding the problem!
38+
39+
2009-05-29 Allan Saddi <allan@saddi.com>
40+
41+
* Let all the active requests to finish before quitting. Thanks
42+
to Anand Chitipothu for the patch!
43+
44+
2009-05-26 Allan Saddi <allan@saddi.com>
45+
46+
* Release 1.0.2
47+
48+
2009-05-18 Allan Saddi <allan@saddi.com>
49+
50+
* Import Paste factories (and dependencies...) from PasteScript
51+
52+
2009-05-04 Allan Saddi <allan@saddi.com>
53+
54+
* Be tolerant of EAGAIN when sending messages to parent process.
55+
56+
2009-02-02 Allan Saddi <allan@saddi.com>
57+
58+
* Add forceCGI keyword argument to FastCGI servers to
59+
programmatically force CGI behavior.
60+
61+
* Merge Tommi Virtanen's "single server" (sequential server)
62+
patch.
63+
64+
2008-12-03 Allan Saddi <allan@saddi.com>
65+
66+
* Update ez_setup.py.
67+
68+
2008-09-26 Allan Saddi <allan@saddi.com>
69+
70+
* Re-seed random module after each fork.
71+
72+
2008-09-11 Allan Saddi <allan@saddi.com>
73+
74+
* Add an indication as to which header fails assertion when
75+
passing in non-string header names and/or values.
76+
77+
2008-08-20 Allan Saddi <allan@saddi.com>
78+
79+
* Add support for setting umask for UNIX domain sockets from
80+
paste.server_factory implementations. Thanks to Michal Suszko
81+
for the patch.
82+
83+
2008-07-23 Allan Saddi <allan@saddi.com>
84+
85+
* Add support for configuring UNIX domain sockets (for servers that
86+
support them) in the paste.server_factory implementations. Thanks
87+
to Dan Roberts for the code.
88+
89+
2008-07-22 Allan Saddi <allan@saddi.com>
90+
91+
* Release 1.0.1
92+
93+
* Attempt to deduce missing PATH_INFO and/or QUERY_STRING from
94+
REQUEST_URI, if present. Patch provided by Richard Davies.
95+
96+
2007-09-10 Allan Saddi <allan@saddi.com>
97+
98+
* Fix readline implementations so size argument is checked
99+
earlier.
100+
101+
2007-07-14 Allan Saddi <allan@saddi.com>
102+
103+
* Prevent ThreadPool inconsistences if an exception is
104+
actually raised. Thanks to Tim Chen for the patch.
105+
106+
2007-06-05 Allan Saddi <allan@saddi.com>
107+
108+
* Remove publisher and middleware packages.
109+
* Add cgi server for completeness.
110+
111+
2007-05-17 Allan Saddi <allan@saddi.com>
112+
113+
* Fix fcgi_fork so it can run on Solaris. Thanks to
114+
Basil Crow for the patch.
115+
116+
2007-01-22 Allan Saddi <allan@saddi.com>
117+
118+
* Fix eunuchs import issue.
119+
120+
2007-01-10 Allan Saddi <allan@saddi.com>
121+
122+
* Support gzip compression of XHTML pages using the
123+
correct MIME type.
124+
125+
2006-12-29 Allan Saddi <allan@saddi.com>
126+
127+
* Deprecate WSGI_SCRIPT_NAME and scriptName in scgi_base.
128+
Modern versions of mod_scgi correctly set SCRIPT_NAME &
129+
PATH_INFO.
130+
131+
2006-12-13 Allan Saddi <allan@saddi.com>
132+
133+
* Fix problem in session.py seen when optimization is on.
134+
135+
2006-12-05 Allan Saddi <allan@saddi.com>
136+
137+
* Update servers to default to an empty QUERY_STRING if
138+
not present in the environ.
139+
* Update gzip.py: compresslevel -> compress_level
140+
* Update gzip.py by updating docstrings and renaming
141+
classes/methods/functions to better follow Python naming
142+
conventions. NB: mimeTypes keyword parameter is now
143+
mime_types.
144+
145+
2006-12-02 Allan Saddi <allan@saddi.com>
146+
147+
* Change intra-package imports into absolute imports.
148+
149+
2006-12-02 Allan Saddi <allan@saddi.com>
150+
151+
* Add forceCookieOutput attribute to SessionService to
152+
force Set-Cookie output for the current request.
153+
154+
2006-12-01 Allan Saddi <allan@saddi.com>
155+
156+
* Update setup script.
157+
158+
2006-11-26 Allan Saddi <allan@saddi.com>
159+
160+
* Don't attempt to install signal handlers under Windows
161+
to improve compatibility.
162+
163+
2006-11-24 Allan Saddi <allan@saddi.com>
164+
165+
* Add *_thread egg entry-point aliases.
166+
* Add UNIX domain socket support to scgi, scgi_fork,
167+
scgi_app.
168+
* Add flup.client package which contains various
169+
WSGI -> connector client implentations. (So far: FastCGI,
170+
and SCGI.)
171+
172+
2006-11-19 Allan Saddi <allan@saddi.com>
173+
174+
* Change mime-type matching algorithm in GzipMiddleware.
175+
Strip parameters (e.g. "encoding") and accept a list of
176+
regexps. By default, compress 'text/.*' mime-types.
177+
178+
2006-11-10 Allan Saddi <allan@saddi.com>
179+
180+
* Add cookieAttributes to SessionService to make it easier
181+
to customize the generated cookie's attributes.
182+
183+
2006-08-28 Allan Saddi <allan@saddi.com>
184+
185+
* Add support for FastCGI roles other than FCGI_RESPONDER.
186+
Patch provided by Seairth Jacobs.
187+
188+
2006-08-02 Allan Saddi <allan@saddi.com>
189+
190+
* Add cookieExpiration keyword to SessionService /
191+
SessionMiddleware to adjust the session cookie's expiration.
192+
Thanks to Blaise Laflamme for the suggestion.
193+
194+
2006-06-27 Allan Saddi <allan@saddi.com>
195+
196+
* Set close-on-exec flag on all server sockets. Thanks to
197+
Ralf Schmitt for reporting the problem.
198+
199+
2006-06-18 Allan Saddi <allan@saddi.com>
200+
201+
* Stop ignoring EPIPE exceptions, as this is probably the
202+
wrong thing to do. (Application is unaware of disconnected
203+
clients and the CPU spins when sending large files to a
204+
disconnected client.) Thanks to Ivan Sagalaev for bringing
205+
this to my attention.
206+
207+
NB: Existing applications that use the flup servers may begin
208+
seeing socket.error exceptions...
209+
210+
2006-05-18 Allan Saddi <allan@saddi.com>
211+
212+
* Added umask keyword parameter to fcgi and fcgi_fork,
213+
for use when binding to a UNIX socket.
214+
215+
2006-05-03 Allan Saddi <allan@saddi.com>
216+
217+
* Fix illusive problem with AJP implementation. Thanks to
218+
Moshe Van der Sterre for explaining the problem and
219+
providing a fix.
220+
221+
2006-04-06 Allan Saddi <allan@saddi.com>
222+
223+
* Catch a strange FieldStorage case. Seen in production.
224+
Not quite sure what causes it.
225+
226+
2006-03-21 Allan Saddi <allan@saddi.com>
227+
228+
* Add maxRequests option to PreforkServer. Patch provided by
229+
Wojtek Sobczuk.
230+
231+
2006-02-23 Allan Saddi <allan@saddi.com>
232+
233+
* Add paste.server_factory-compliant factories and respective
234+
egg entry points. Thanks to Luis Bruno for the code.
235+
236+
Add debug option to servers, which is True by default.
237+
Currently, only server-level error handling is affected.
238+
239+
2006-01-15 Allan Saddi <allan@saddi.com>
240+
241+
* Change the behavior of ImportingModuleResolver when dealing
242+
with ImportErrors. Previously, it would act as if the module
243+
did not exist. Now, it propagates the exception to another
244+
level (outer middleware or WSGI). Reported by Scot Doyle.
245+
246+
2006-01-05 Allan Saddi <allan@saddi.com>
247+
248+
* Improve Windows compatibility by conditionally installing
249+
SIGHUP handler. Thanks to Brad Miller for pointing out the
250+
problem and providing a fix.
251+
252+
2005-12-19 Allan Saddi <allan@saddi.com>
253+
254+
* Fix socket leak in eunuchs socketpair() wrapper. Thanks to
255+
Georg Bauer for pointing this out.
256+
257+
2005-12-16 Allan Saddi <allan@saddi.com>
258+
259+
* Switch to setuptools for egg support.
260+
* Add higher-level 404 error page support. Thanks to Scot Doyle
261+
for suggesting the idea and providing code. If you previously
262+
subclassed Publisher to provide a custom 404 error page, this
263+
is now broken. It will have to be massaged to fit the new
264+
calling convention.
265+
266+
2005-11-28 Allan Saddi <allan@saddi.com>
267+
268+
* Fix issue with FCGI_GET_VALUES handling. Thanks to
269+
Timothy Wright for pointing this out.
270+
271+
2005-11-18 Allan Saddi <allan@saddi.com>
272+
273+
* When running under Python < 2.4, attempt to use socketpair()
274+
from eunuchs module.
275+
276+
2005-09-07 Allan Saddi <allan@saddi.com>
277+
278+
* Python 2.3 doesn't define socket.SHUT_WR, which affected
279+
the closing of the FastCGI socket with the server. This would
280+
cause output to hang. Thanks to Eugene Lazutkin for bringing
281+
the problem to my attention and going out of his way to help
282+
me debug it!
283+
284+
2005-07-03 Allan Saddi <allan@saddi.com>
285+
286+
* Ensure session identifiers only contain ASCII characters when
287+
using a non-ASCII locale. Thanks to Ksenia Marasanova for the
288+
the fix.
289+
290+
2005-06-12 Allan Saddi <allan@saddi.com>
291+
292+
* Cleanly close connection socket to avoid sending a TCP RST to
293+
the web server. (fcgi_base) Fix suggested by Dima Barsky.
294+
295+
2005-05-31 Allan Saddi <allan@saddi.com>
296+
297+
* Take scriptName from the WSGI_SCRIPT_NAME environment variable
298+
passed from the web server, if present.
299+
* Check if scriptName is None, and if so, don't modify SCRIPT_NAME
300+
& PATH_INFO. For better compatibility with cgi2scgi. (scgi_base)
301+
302+
2005-05-18 Allan Saddi <allan@saddi.com>
303+
304+
* Change default allowedServers for ajp and scgi to ['127.0.0.1'].
305+
* Accept PATH_INFO from environment for scgi servers, in case
306+
cgi2scgi is being used. Submitted by Ian Bicking.
307+
* Change threaded servers so wsgi.multiprocess is False by default.
308+
Allow it to be changed by keyword argument.
309+
* Fix wsgi.multiprocess for scgi_fork. (Set to True.)
310+
311+
2005-05-15 Allan Saddi <allan@saddi.com>
312+
313+
* Prevent possible deadlock related to DiskSessionStore locking.
314+
* Add logic to SessionStore so that it will block if attempting to
315+
check out a Session that's already been checked out.
316+
317+
2005-05-14 Allan Saddi <allan@saddi.com>
318+
319+
* Convert the use of decorators in session.py to something
320+
compatible with Python <2.4.
321+
322+
2005-04-23 Allan Saddi <allan@saddi.com>
323+
324+
* Ensure that SessionStore.checkOutSession() never returns an
325+
invalidated Session. Reported by Rene Dudfield.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Metadata-Version: 1.0
2+
Name: flup
3+
Version: 1.0.3.dev-20110405
4+
Summary: Random assortment of WSGI servers
5+
Home-page: http://www.saddi.com/software/flup/
6+
Author: Allan Saddi
7+
Author-email: allan@saddi.com
8+
License: BSD
9+
Description: UNKNOWN
10+
Platform: UNKNOWN
11+
Classifier: Development Status :: 5 - Production/Stable
12+
Classifier: Environment :: Web Environment
13+
Classifier: Intended Audience :: Developers
14+
Classifier: License :: OSI Approved :: BSD License
15+
Classifier: Operating System :: OS Independent
16+
Classifier: Programming Language :: Python
17+
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server
18+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2013-09-01, Larry Shaffer <larrys@dakotacarto.com>
2+
3+
This is a stripped-down source of flup-1.0.3.dev-20110405, specifically to act
4+
only as a client to a locally spawned QGIS Server fcgi process, for unit tests.
5+
6+
flup available at http://trac.saddi.com/flup under a BSD-style license
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#
2+
class NoDefault(object):
3+
pass
4+
5+
__all__ = [ 'NoDefault', ]
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""
2+
.. highlight:: python
3+
:linenothreshold: 5
4+
5+
.. highlight:: bash
6+
:linenothreshold: 5
7+
8+
ajp - an AJP 1.3/WSGI gateway.
9+
10+
:copyright: Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com>
11+
All rights reserved.
12+
:license:
13+
14+
Redistribution and use in source and binary forms, with or without
15+
modification, are permitted provided that the following conditions
16+
are met:
17+
18+
1. Redistributions of source code must retain the above copyright
19+
notice, this list of conditions and the following disclaimer.
20+
2. Redistributions in binary form must reproduce the above copyright
21+
notice, this list of conditions and the following disclaimer in the
22+
documentation and/or other materials provided with the distribution.
23+
24+
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS **AS IS** AND
25+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
28+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
30+
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
31+
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
32+
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
33+
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
34+
SUCH DAMAGE.
35+
36+
For more information about AJP and AJP connectors for your web server, see
37+
http://jakarta.apache.org/tomcat/connectors-doc/.
38+
39+
For more information about the Web Server Gateway Interface, see
40+
http://www.python.org/peps/pep-0333.html.
41+
42+
Example usage::
43+
44+
#!/usr/bin/env python
45+
import sys
46+
from myapplication import app # Assume app is your WSGI application object
47+
from ajp import WSGIServer
48+
ret = WSGIServer(app).run()
49+
sys.exit(ret and 42 or 0)
50+
51+
See the documentation for WSGIServer for more information.
52+
53+
About the bit of logic at the end:
54+
Upon receiving SIGHUP, the python script will exit with status code 42. This
55+
can be used by a wrapper script to determine if the python script should be
56+
re-run. When a SIGINT or SIGTERM is received, the script exits with status
57+
code 0, possibly indicating a normal exit.
58+
59+
Example wrapper script::
60+
61+
#!/bin/sh
62+
STATUS=42
63+
while test $STATUS -eq 42; do
64+
python "$@" that_script_above.py
65+
STATUS=$?
66+
done
67+
68+
Example workers.properties (for mod_jk)::
69+
70+
worker.list=foo
71+
worker.foo.port=8009
72+
worker.foo.host=localhost
73+
worker.foo.type=ajp13
74+
75+
Example httpd.conf (for mod_jk)::
76+
77+
JkWorkersFile /path/to/workers.properties
78+
JkMount /* foo
79+
80+
Note that if you mount your ajp application anywhere but the root ("/"), you
81+
SHOULD specifiy scriptName to the WSGIServer constructor. This will ensure
82+
that SCRIPT_NAME/PATH_INFO are correctly deduced.
83+
"""
84+
85+
__author__ = 'Allan Saddi <allan@saddi.com>'
86+
__version__ = '$Revision$'
87+
88+
import socket
89+
import logging
90+
91+
from flup.server.ajp_base import BaseAJPServer, Connection
92+
from flup.server.threadedserver import ThreadedServer
93+
94+
__all__ = ['WSGIServer']
95+
96+
class WSGIServer(BaseAJPServer, ThreadedServer):
97+
"""
98+
AJP1.3/WSGI server. Runs your WSGI application as a persistant program
99+
that understands AJP1.3. Opens up a TCP socket, binds it, and then
100+
waits for forwarded requests from your webserver.
101+
102+
Why AJP? Two good reasons are that AJP provides load-balancing and
103+
fail-over support. Personally, I just wanted something new to
104+
implement. :)
105+
106+
Of course you will need an AJP1.3 connector for your webserver (e.g.
107+
mod_jk) - see http://jakarta.apache.org/tomcat/connectors-doc/.
108+
"""
109+
def __init__(self, application, scriptName='', environ=None,
110+
multithreaded=True, multiprocess=False,
111+
bindAddress=('localhost', 8009), allowedServers=None,
112+
loggingLevel=logging.INFO, debug=False, **kw):
113+
"""
114+
scriptName is the initial portion of the URL path that "belongs"
115+
to your application. It is used to determine PATH_INFO (which doesn't
116+
seem to be passed in). An empty scriptName means your application
117+
is mounted at the root of your virtual host.
118+
119+
environ, which must be a dictionary, can contain any additional
120+
environment variables you want to pass to your application.
121+
122+
bindAddress is the address to bind to, which must be a tuple of
123+
length 2. The first element is a string, which is the host name
124+
or IPv4 address of a local interface. The 2nd element is the port
125+
number.
126+
127+
allowedServers must be None or a list of strings representing the
128+
IPv4 addresses of servers allowed to connect. None means accept
129+
connections from anywhere.
130+
131+
loggingLevel sets the logging level of the module-level logger.
132+
"""
133+
BaseAJPServer.__init__(self, application,
134+
scriptName=scriptName,
135+
environ=environ,
136+
multithreaded=multithreaded,
137+
multiprocess=multiprocess,
138+
bindAddress=bindAddress,
139+
allowedServers=allowedServers,
140+
loggingLevel=loggingLevel,
141+
debug=debug)
142+
for key in ('jobClass', 'jobArgs'):
143+
if kw.has_key(key):
144+
del kw[key]
145+
ThreadedServer.__init__(self, jobClass=Connection,
146+
jobArgs=(self, None), **kw)
147+
148+
def run(self):
149+
"""
150+
Main loop. Call this after instantiating WSGIServer. SIGHUP, SIGINT,
151+
SIGQUIT, SIGTERM cause it to cleanup and return. (If a SIGHUP
152+
is caught, this method returns True. Returns False otherwise.)
153+
"""
154+
self.logger.info('%s starting up', self.__class__.__name__)
155+
156+
try:
157+
sock = self._setupSocket()
158+
except socket.error, e:
159+
self.logger.error('Failed to bind socket (%s), exiting', e[1])
160+
return False
161+
162+
ret = ThreadedServer.run(self, sock)
163+
164+
self._cleanupSocket(sock)
165+
# AJP connections are more or less persistent. .shutdown() will
166+
# not return until the web server lets go. So don't bother calling
167+
# it...
168+
#self.shutdown()
169+
170+
self.logger.info('%s shutting down%s', self.__class__.__name__,
171+
self._hupReceived and ' (reload requested)' or '')
172+
173+
return ret
174+
175+
if __name__ == '__main__':
176+
def test_app(environ, start_response):
177+
"""Probably not the most efficient example."""
178+
import cgi
179+
start_response('200 OK', [('Content-Type', 'text/html')])
180+
yield '<html><head><title>Hello World!</title></head>\n' \
181+
'<body>\n' \
182+
'<p>Hello World!</p>\n' \
183+
'<table border="1">'
184+
names = environ.keys()
185+
names.sort()
186+
for name in names:
187+
yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
188+
name, cgi.escape(`environ[name]`))
189+
190+
form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ,
191+
keep_blank_values=1)
192+
if form.list:
193+
yield '<tr><th colspan="2">Form data</th></tr>'
194+
195+
for field in form.list:
196+
yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
197+
field.name, field.value)
198+
199+
yield '</table>\n' \
200+
'</body></html>\n'
201+
202+
from wsgiref import validate
203+
test_app = validate.validator(test_app)
204+
# Explicitly set bindAddress to *:8009 for testing.
205+
WSGIServer(test_app,
206+
bindAddress=('', 8009), allowedServers=None,
207+
loggingLevel=logging.DEBUG).run()

‎tests/src/python/qgis_local_server_spawn/flup/server/ajp_base.py

Lines changed: 978 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions
6+
# are met:
7+
# 1. Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
# 2. Redistributions in binary form must reproduce the above copyright
10+
# notice, this list of conditions and the following disclaimer in the
11+
# documentation and/or other materials provided with the distribution.
12+
#
13+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19+
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22+
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23+
# SUCH DAMAGE.
24+
#
25+
# $Id$
26+
27+
__author__ = 'Allan Saddi <allan@saddi.com>'
28+
__version__ = '$Revision$'
29+
30+
import sys
31+
import socket
32+
import select
33+
import signal
34+
import errno
35+
36+
try:
37+
import fcntl
38+
except ImportError:
39+
def setCloseOnExec(sock):
40+
pass
41+
else:
42+
def setCloseOnExec(sock):
43+
fcntl.fcntl(sock.fileno(), fcntl.F_SETFD, fcntl.FD_CLOEXEC)
44+
45+
from flup.server.threadpool import ThreadPool
46+
47+
__all__ = ['ThreadedServer']
48+
49+
class ThreadedServer(object):
50+
def __init__(self, jobClass=None, jobArgs=(), **kw):
51+
self._jobClass = jobClass
52+
self._jobArgs = jobArgs
53+
54+
self._threadPool = ThreadPool(**kw)
55+
56+
def run(self, sock, timeout=1.0):
57+
"""
58+
The main loop. Pass a socket that is ready to accept() client
59+
connections. Return value will be True or False indiciating whether
60+
or not the loop was exited due to SIGHUP.
61+
"""
62+
# Set up signal handlers.
63+
self._keepGoing = True
64+
self._hupReceived = False
65+
66+
# Might need to revisit this?
67+
if not sys.platform.startswith('win'):
68+
self._installSignalHandlers()
69+
70+
# Set close-on-exec
71+
setCloseOnExec(sock)
72+
73+
# Main loop.
74+
while self._keepGoing:
75+
try:
76+
r, w, e = select.select([sock], [], [], timeout)
77+
except select.error, e:
78+
if e[0] == errno.EINTR:
79+
continue
80+
raise
81+
82+
if r:
83+
try:
84+
clientSock, addr = sock.accept()
85+
except socket.error, e:
86+
if e[0] in (errno.EINTR, errno.EAGAIN):
87+
continue
88+
raise
89+
90+
setCloseOnExec(clientSock)
91+
92+
if not self._isClientAllowed(addr):
93+
clientSock.close()
94+
continue
95+
96+
# Hand off to Connection.
97+
conn = self._jobClass(clientSock, addr, *self._jobArgs)
98+
if not self._threadPool.addJob(conn, allowQueuing=False):
99+
# No thread left, immediately close the socket to hopefully
100+
# indicate to the web server that we're at our limit...
101+
# and to prevent having too many opened (and useless)
102+
# files.
103+
clientSock.close()
104+
105+
self._mainloopPeriodic()
106+
107+
# Restore signal handlers.
108+
if not sys.platform.startswith('win'):
109+
self._restoreSignalHandlers()
110+
111+
# Return bool based on whether or not SIGHUP was received.
112+
return self._hupReceived
113+
114+
def shutdown(self):
115+
"""Wait for running threads to finish."""
116+
self._threadPool.shutdown()
117+
118+
def _mainloopPeriodic(self):
119+
"""
120+
Called with just about each iteration of the main loop. Meant to
121+
be overridden.
122+
"""
123+
pass
124+
125+
def _exit(self, reload=False):
126+
"""
127+
Protected convenience method for subclasses to force an exit. Not
128+
really thread-safe, which is why it isn't public.
129+
"""
130+
if self._keepGoing:
131+
self._keepGoing = False
132+
self._hupReceived = reload
133+
134+
def _isClientAllowed(self, addr):
135+
"""Override to provide access control."""
136+
return True
137+
138+
# Signal handlers
139+
140+
def _hupHandler(self, signum, frame):
141+
self._hupReceived = True
142+
self._keepGoing = False
143+
144+
def _intHandler(self, signum, frame):
145+
self._keepGoing = False
146+
147+
def _installSignalHandlers(self):
148+
supportedSignals = [signal.SIGINT, signal.SIGTERM]
149+
if hasattr(signal, 'SIGHUP'):
150+
supportedSignals.append(signal.SIGHUP)
151+
152+
self._oldSIGs = [(x,signal.getsignal(x)) for x in supportedSignals]
153+
154+
for sig in supportedSignals:
155+
if hasattr(signal, 'SIGHUP') and sig == signal.SIGHUP:
156+
signal.signal(sig, self._hupHandler)
157+
else:
158+
signal.signal(sig, self._intHandler)
159+
160+
def _restoreSignalHandlers(self):
161+
for signum,handler in self._oldSIGs:
162+
signal.signal(signum, handler)
163+
164+
if __name__ == '__main__':
165+
class TestJob(object):
166+
def __init__(self, sock, addr):
167+
self._sock = sock
168+
self._addr = addr
169+
def run(self):
170+
print "Client connection opened from %s:%d" % self._addr
171+
self._sock.send('Hello World!\n')
172+
self._sock.setblocking(1)
173+
self._sock.recv(1)
174+
self._sock.close()
175+
print "Client connection closed from %s:%d" % self._addr
176+
sock = socket.socket()
177+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
178+
sock.bind(('', 8080))
179+
sock.listen(socket.SOMAXCONN)
180+
ThreadedServer(maxThreads=10, jobClass=TestJob).run(sock)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright (c) 2005 Allan Saddi <allan@saddi.com>
2+
# All rights reserved.
3+
#
4+
# Redistribution and use in source and binary forms, with or without
5+
# modification, are permitted provided that the following conditions
6+
# are met:
7+
# 1. Redistributions of source code must retain the above copyright
8+
# notice, this list of conditions and the following disclaimer.
9+
# 2. Redistributions in binary form must reproduce the above copyright
10+
# notice, this list of conditions and the following disclaimer in the
11+
# documentation and/or other materials provided with the distribution.
12+
#
13+
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16+
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17+
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18+
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19+
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21+
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22+
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23+
# SUCH DAMAGE.
24+
#
25+
# $Id$
26+
27+
__author__ = 'Allan Saddi <allan@saddi.com>'
28+
__version__ = '$Revision$'
29+
30+
import sys
31+
import thread
32+
import threading
33+
34+
class ThreadPool(object):
35+
"""
36+
Thread pool that maintains the number of idle threads between
37+
minSpare and maxSpare inclusive. By default, there is no limit on
38+
the number of threads that can be started, but this can be controlled
39+
by maxThreads.
40+
"""
41+
def __init__(self, minSpare=1, maxSpare=5, maxThreads=sys.maxint):
42+
self._minSpare = minSpare
43+
self._maxSpare = maxSpare
44+
self._maxThreads = max(minSpare, maxThreads)
45+
46+
self._lock = threading.Condition()
47+
self._workQueue = []
48+
self._idleCount = self._workerCount = maxSpare
49+
50+
self._threads = []
51+
self._stop = False
52+
53+
# Start the minimum number of worker threads.
54+
for i in range(maxSpare):
55+
self._start_new_thread()
56+
57+
def _start_new_thread(self):
58+
t = threading.Thread(target=self._worker)
59+
self._threads.append(t)
60+
t.setDaemon(True)
61+
t.start()
62+
return t
63+
64+
def shutdown(self):
65+
"""shutdown all workers."""
66+
self._lock.acquire()
67+
self._stop = True
68+
self._lock.notifyAll()
69+
self._lock.release()
70+
71+
# wait for all threads to finish
72+
for t in self._threads[:]:
73+
t.join()
74+
75+
def addJob(self, job, allowQueuing=True):
76+
"""
77+
Adds a job to the work queue. The job object should have a run()
78+
method. If allowQueuing is True (the default), the job will be
79+
added to the work queue regardless if there are any idle threads
80+
ready. (The only way for there to be no idle threads is if maxThreads
81+
is some reasonable, finite limit.)
82+
83+
Otherwise, if allowQueuing is False, and there are no more idle
84+
threads, the job will not be queued.
85+
86+
Returns True if the job was queued, False otherwise.
87+
"""
88+
self._lock.acquire()
89+
try:
90+
# Maintain minimum number of spares.
91+
while self._idleCount < self._minSpare and \
92+
self._workerCount < self._maxThreads:
93+
try:
94+
self._start_new_thread()
95+
except thread.error:
96+
return False
97+
self._workerCount += 1
98+
self._idleCount += 1
99+
100+
# Hand off the job.
101+
if self._idleCount or allowQueuing:
102+
self._workQueue.append(job)
103+
self._lock.notify()
104+
return True
105+
else:
106+
return False
107+
finally:
108+
self._lock.release()
109+
110+
def _worker(self):
111+
"""
112+
Worker thread routine. Waits for a job, executes it, repeat.
113+
"""
114+
self._lock.acquire()
115+
try:
116+
while True:
117+
while not self._workQueue and not self._stop:
118+
self._lock.wait()
119+
120+
if self._stop:
121+
return
122+
123+
# We have a job to do...
124+
job = self._workQueue.pop(0)
125+
126+
assert self._idleCount > 0
127+
self._idleCount -= 1
128+
129+
self._lock.release()
130+
131+
try:
132+
job.run()
133+
except:
134+
# FIXME: This should really be reported somewhere.
135+
# But we can't simply report it to stderr because of fcgi
136+
pass
137+
138+
self._lock.acquire()
139+
140+
if self._idleCount == self._maxSpare:
141+
break # NB: lock still held
142+
self._idleCount += 1
143+
assert self._idleCount <= self._maxSpare
144+
145+
# Die off...
146+
assert self._workerCount > self._maxSpare
147+
self._threads.remove(threading.currentThread())
148+
self._workerCount -= 1
149+
finally:
150+
self._lock.release()

‎tests/src/python/qgis_local_server_spawn/flup_fcgi_client.py

Lines changed: 473 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#!/usr/bin/env python
2+
"""
3+
################################################################################
4+
# 2013-09-01 Larry Shaffer <larrys@dakotacarto.com>
5+
#
6+
# NOTE: THIS IS ONLY A DEVELOPMENT TESTING SCRIPT. TO BE REMOVED AT LATER DATE.
7+
################################################################################
8+
9+
<description>
10+
11+
This file is part of ZTC and distributed under the same license.
12+
http://bitbucket.org/rvs/ztc/
13+
14+
Copyright (c) 2011 Vladimir Rusinov <vladimir@greenmice.info>
15+
"""
16+
17+
import flup_fcgi_client as fcgi_client
18+
# from flup.client.fcgi_app import FCGIApp as fcgi_client
19+
20+
def load_page(fcgi_host,fcgi_port, script, query):
21+
""" load fastcgi page """
22+
try:
23+
fcgi = fcgi_client.FCGIApp(host = fcgi_host,
24+
port = fcgi_port)
25+
env = {
26+
'SCRIPT_FILENAME': script,
27+
'QUERY_STRING': query,
28+
'REQUEST_METHOD': 'GET',
29+
'SCRIPT_NAME': script,
30+
'REQUEST_URI': script + '?' + query,
31+
'GATEWAY_INTERFACE': 'CGI/1.1',
32+
'SERVER_SOFTWARE': '',
33+
'REDIRECT_STATUS': '200',
34+
'CONTENT_TYPE': '',
35+
'CONTENT_LENGTH': '0',
36+
'DOCUMENT_ROOT': '/qgisserver/',
37+
'SERVER_ADDR': fcgi_host,
38+
'SERVER_PORT': str(fcgi_port),
39+
'SERVER_PROTOCOL': 'HTTP/1.0',
40+
'SERVER_NAME': fcgi_host
41+
}
42+
ret = fcgi(env)
43+
return ret
44+
except:
45+
print 'fastcgi load failed'
46+
return '500', [], '', ''
47+
48+
49+
if __name__ == "__main__":
50+
fcgi_host = '127.0.0.1'
51+
fcgi_port = '8448'
52+
script = 'qgis_mapserv.fcgi'
53+
query = 'SERVICE=WMS&VERSION=1.3.0&REQUEST=GetCapabilities&MAP=/test-projects/tests/pal_test.qgs'
54+
print load_page(fcgi_host,fcgi_port, script, query)[2]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
jan kneschke <jan@kneschke.de>
2+
stefan bühler <lighttpd@stbuehler.de>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
CMAKE_MINIMUM_REQUIRED(VERSION 2.6.0 FATAL_ERROR)
2+
3+
cmake_policy(VERSION 2.6.0)
4+
5+
PROJECT(spawn-fcgi)
6+
SET(PACKAGE_NAME ${CMAKE_PROJECT_NAME})
7+
SET(PACKAGE_VERSION 1.6.4)
8+
9+
SET(CMAKE_MAN_DIR "share/man" CACHE STRING
10+
"Install location for man pages (relative to prefix).")
11+
MARK_AS_ADVANCED(CMAKE_MAN_DIR)
12+
13+
# Set Default build type to RelWithDebInfo
14+
IF("${CMAKE_BUILD_TYPE}" STREQUAL "")
15+
SET(CMAKE_BUILD_TYPE RelWithDebInfo CACHE STRING "Choose the type of build, options are: None(CMAKE_CXX_FLAGS or CMAKE_C_FLAGS used) Debug Release RelWithDebInfo MinSizeRel." FORCE)
16+
ENDIF("${CMAKE_BUILD_TYPE}" STREQUAL "")
17+
18+
ADD_SUBDIRECTORY(src)
19+
20+
INSTALL(FILES spawn-fcgi.1 DESTINATION ${CMAKE_MAN_DIR}/man1)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
3+
Copyright (c) 2004, Jan Kneschke, incremental
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
- Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
- Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
- Neither the name of the 'incremental' nor the names of its contributors may
17+
be used to endorse or promote products derived from this software without
18+
specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
24+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
30+
THE POSSIBILITY OF SUCH DAMAGE.
31+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
SUBDIRS=src doc
2+
3+
EXTRA_DIST=autogen.sh spawn-fcgi.1 CMakeLists.txt
4+
man1_MANS=spawn-fcgi.1
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
====
3+
NEWS
4+
====
5+
6+
- 1.6.4 -
7+
* Use octal mode for -M (patch by dfjoerg)
8+
* Add -b backlog option (fixes #2422, patch by aschmitz)
9+
10+
- 1.6.3 - 2009-09-23
11+
* Fix unix socket mode change to work without specifying user/group for socket
12+
* Add some ./run script examples for use with daemontools or runit
13+
* Fix Invalid Argument in chmod if mode=-1 (fixes #2033)
14+
* Add deprecated and /bin/sh info for -f option; wrap syntax output (fixes #2044)
15+
* Add run script examples in automake dist build
16+
17+
- 1.6.2 - 2009-04-18
18+
* Add homepage to README
19+
* Add IPv6 support
20+
* Fix problems with usernames starting with a digit and non-existent uids; add warning if only user privs are dropped. (fixes #1959)
21+
* Add check to link against socket/nsl if needed (fixes #1960)
22+
* List IPv6 as feature after the version if it is supported
23+
24+
- 1.6.1 - 2009-03-29
25+
26+
* Add build date to show-version
27+
* Added options to chown/chmod the socket and to create the socket before chroot() (fixes #1906)
28+
* Updated man page
29+
* Add proper SUID bit detection
30+
* Added option to change the directory before spawning (fixes #1847)
31+
32+
- 1.6.0 - 2009-02-28
33+
34+
* Separated spawn-fcgi from lighttpd
35+
* Remove limits for php children; per default the PHP_FCGI_CHILDREN var is not changed (php defaults to no children, one worker)
36+
* Modified the log messages format (more details on errors, no source line)
37+
* Only try to connect to unix socket (not tcp) before spawning (fixes again #1575)
38+
* Only disconnect from terminal in fork mode (keep stderr/stdout open in nofork mode)
39+
* Allow numerical user and group ids for -u/-g (fixes #1141)
40+
* Ignore pid-file option in no-fork mode (instead of producing empty file)
41+
* Fix error handling for unix-socket-connect test
42+
* Man page update
43+
* Use header include order from 1.4.x
44+
* Fix segfault due to uninitialized var
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
2+
==========
3+
spawn-fcgi
4+
==========
5+
6+
:authors: Jan Kneschke, Stefan Bühler
7+
8+
:homepage:
9+
http://redmine.lighttpd.net/projects/spawn-fcgi
10+
11+
:abstract:
12+
spawn-fcgi is used to spawn FastCGI applications
13+
14+
Features
15+
--------
16+
- binds to IPv4/IPv6 and Unix domain sockets
17+
- supports privilege separation: chmod/chown socket, drop to uid/gid
18+
- supports chroot
19+
- supports daemontools supervise
20+
21+
Build
22+
=====
23+
24+
If ./configure is missing, run ./autogen.sh.
25+
26+
./configure
27+
make
28+
make install
29+
30+
Alternatively you can use the cmake build system (may not work
31+
on every platform):
32+
33+
cmake .
34+
make
35+
make install
36+
37+
38+
Usage
39+
=====
40+
41+
See man page.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/sh
2+
# Run this to generate all the initial makefiles, etc.
3+
4+
ACLOCAL=${ACLOCAL:-aclocal}
5+
AUTOHEADER=${AUTOHEADER:-autoheader}
6+
AUTOMAKE=${AUTOMAKE:-automake}
7+
AUTOMAKE_FLAGS="--add-missing --copy"
8+
AUTOCONF=${AUTOCONF:-autoconf}
9+
10+
ARGV0=$0
11+
12+
set -e
13+
14+
15+
run() {
16+
echo "$ARGV0: running \`$@'"
17+
$@
18+
}
19+
20+
run $ACLOCAL $ACLOCAL_FLAGS
21+
run $AUTOHEADER
22+
run $AUTOMAKE $AUTOMAKE_FLAGS
23+
run $AUTOCONF
24+
echo "Now type './configure ...' and 'make' to compile."
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# -*- Autoconf -*-
2+
# Process this file with autoconf to produce a configure script.
3+
4+
AC_PREREQ(2.61)
5+
AC_INIT([spawn-fcgi],[1.6.4])
6+
AC_CONFIG_SRCDIR([src/spawn-fcgi.c])
7+
AC_CONFIG_HEADER([config.h])
8+
9+
AM_INIT_AUTOMAKE([-Wall -Werror foreign])
10+
11+
# Checks for programs.
12+
AC_PROG_CC
13+
AC_PROG_MAKE_SET
14+
15+
# Checks for libraries.
16+
17+
# Checks for header files.
18+
AC_HEADER_STDC
19+
AC_HEADER_SYS_WAIT
20+
AC_CHECK_HEADERS([arpa/inet.h errno.h fcntl.h getopt.h grp.h netdb.h \
21+
netinet/in.h netinet/tcp.h pwd.h stdio.h stdlib.h \
22+
string.h sys/ioctl.h sys/socket.h sys/stat.h sys/time.h \
23+
sys/types.h sys/un.h sys/wait.h unistd.h])
24+
25+
26+
# Checks for typedefs, structures, and compiler characteristics.
27+
AC_C_CONST
28+
AC_TYPE_UID_T
29+
AC_TYPE_PID_T
30+
AC_HEADER_TIME
31+
AC_CHECK_TYPES(socklen_t,,,[#include <sys/types.h>
32+
#include <sys/socket.h>])
33+
34+
## solaris needs -lsocket -lnsl
35+
AC_SEARCH_LIBS([socket],[socket])
36+
AC_SEARCH_LIBS([inet_addr],[nsl socket])
37+
38+
# Checks for library functions.
39+
AC_FUNC_CHOWN
40+
AC_FUNC_FORK
41+
AC_FUNC_MALLOC
42+
AC_FUNC_SELECT_ARGTYPES
43+
AC_FUNC_STAT
44+
AC_CHECK_FUNCS([dup2 memset putenv select socket strerror strtol issetugid inet_pton])
45+
46+
47+
# Check for IPv6 support
48+
49+
AC_ARG_ENABLE(ipv6,
50+
AC_HELP_STRING([--disable-ipv6],[disable IPv6 support]),
51+
[case "${enableval}" in
52+
yes) ipv6=true ;;
53+
no) ipv6=false ;;
54+
*) AC_MSG_ERROR(bad value ${enableval} for --enable-ipv6) ;;
55+
esac],[ipv6=true])
56+
57+
if test x$ipv6 = xtrue; then
58+
AC_CACHE_CHECK([for IPv6 support], ac_cv_ipv6_support,
59+
[AC_TRY_LINK([ #include <sys/types.h>
60+
#include <sys/socket.h>
61+
#include <netinet/in.h>], [struct sockaddr_in6 s; struct in6_addr t=in6addr_any; int i=AF_INET6; s; t.s6_addr[0] = 0; ],
62+
[ac_cv_ipv6_support=yes], [ac_cv_ipv6_support=no])])
63+
64+
if test "$ac_cv_ipv6_support" = yes; then
65+
AC_DEFINE(HAVE_IPV6,1,[Whether to enable IPv6 support])
66+
fi
67+
fi
68+
69+
70+
# check for extra compiler options (warning options)
71+
if test "${GCC}" = "yes"; then
72+
CFLAGS="${CFLAGS} -Wall -W -Wshadow -pedantic -std=gnu99"
73+
fi
74+
75+
AC_ARG_ENABLE(extra-warnings,
76+
AC_HELP_STRING([--enable-extra-warnings],[enable extra warnings (gcc specific)]),
77+
[case "${enableval}" in
78+
yes) extrawarnings=true ;;
79+
no) extrawarnings=false ;;
80+
*) AC_MSG_ERROR(bad value ${enableval} for --enable-extra-warnings) ;;
81+
esac],[extrawarnings=false])
82+
83+
if test x$extrawarnings = xtrue; then
84+
CFLAGS="${CFLAGS} -g -O2 -g2 -Wall -Wmissing-declarations -Wdeclaration-after-statement -Wcast-align -Winline -Wsign-compare -Wnested-externs -Wpointer-arith -Wl,--as-needed -Wformat-security"
85+
fi
86+
87+
AC_CONFIG_FILES([Makefile src/Makefile doc/Makefile])
88+
AC_OUTPUT
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
EXTRA_DIST=run-generic run-php run-rails
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash
2+
# Use this as a run script with daemontools or runit
3+
4+
## ABSOLUTE path to the spawn-fcgi binary
5+
SPAWNFCGI="/usr/bin/spawn-fcgi"
6+
7+
## ABSOLUTE path to the FastCGI application (php-cgi, dispatch.fcgi, ...)
8+
FCGIPROGRAM="/usr/bin/php5-cgi"
9+
10+
## bind to unix socket
11+
FCGISOCKET="/var/run/lighttpd/your-fcgi-app.sock"
12+
13+
# allowed environment variables separated by spaces
14+
ALLOWED_ENV="PATH USER"
15+
16+
## if this script is run as root switch to the following user
17+
USERID=xxx
18+
SOCKUSERID=www-data
19+
#CHROOT=/home/www/
20+
21+
#RAILS_ENV="production"
22+
#export RAILS_ENV
23+
24+
25+
################## no config below this line
26+
27+
exec 2>&1
28+
29+
if test x$PHP_FCGI_CHILDREN = x; then
30+
PHP_FCGI_CHILDREN=4
31+
fi
32+
33+
ALLOWED_ENV="$ALLOWED_ENV RAILS_ENV"
34+
35+
if test x$UID = x0; then
36+
EX="$SPAWNFCGI -n -s $FCGISOCKET -u $USERID -U $SOCKUSERID -C $PHP_FCGI_CHILDREN -- $FCGIPROGRAM"
37+
else
38+
EX="$SPAWNFCGI -n -s $FCGISOCKET -C $PHP_FCGI_CHILDREN -- $FCGIPROGRAM"
39+
fi
40+
41+
# copy the allowed environment variables
42+
E=
43+
44+
for i in $ALLOWED_ENV; do
45+
E="$E $i=${!i}"
46+
done
47+
48+
# clean environment and set up a new one
49+
exec env - $E $EX
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
# Use this as a ./run script with daemontools or runit
3+
# You should replace xxx with the user you want php to run as (and www-data with the user lighty runs as)
4+
5+
exec 2>&1
6+
PHP_FCGI_CHILDREN=2 \
7+
PHP_FCGI_MAX_REQUESTS=1000 \
8+
exec /usr/bin/spawn-fcgi -n -s /var/run/lighttpd/php-xxx.sock -n -u xxx -U www-data -- /usr/bin/php5-cgi
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
# Use this as a ./run script with daemontools or runit
3+
# You should replace xxx with the user you want rails to run as (and www-data with the user lighty runs as)
4+
# /path-to-rails should be replaced with the correct path too :)
5+
6+
exec 2>&1
7+
RAILS_ENV="production" \
8+
exec /usr/bin/spawn-fcgi -n -s /var/run/lighttpd/rails-xxx.sock -u xxx -U www-data -- /path-to-rails/public/dispatch.fcgi
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
.TH spawn-fcgi 1 "21 November 2012"
2+
.
3+
.SH NAME
4+
.
5+
spawn-fcgi \- Spawns FastCGI processes
6+
.
7+
.SH SYNOPSIS
8+
.
9+
.B spawn-fcgi
10+
[options] [ -- <fcgiapp> [fcgi app arguments]]
11+
.P
12+
.B spawn-fcgi
13+
\-v
14+
.P
15+
.B spawn-fcgi
16+
\-h
17+
.
18+
.SH DESCRIPTION
19+
.
20+
\fIspawn-fcgi\fP is used to spawn remote and local FastCGI processes.
21+
.P
22+
While it is obviously needed to spawn remote FastCGI backends (the web server
23+
can only spawn local ones), it is recommended to spawn local backends
24+
with spawn-fcgi, too.
25+
.P
26+
Reasons why you may want to use spawn-fcgi instead of something else:
27+
.IP * 3
28+
Privilege separation without needing a suid-binary or running a server as root.
29+
.IP * 3
30+
You can restart your web server and the FastCGI applications without restarting the others.
31+
.IP * 3
32+
You can run them in different chroot()s.
33+
.IP * 3
34+
Running your FastCGI applications doesn't depend on the web server you are running,
35+
which allows for easier testing of other web servers.
36+
.
37+
.SH OPTIONS
38+
.
39+
\fIspawn-fcgi\fP accepts the following options:
40+
.TP 8
41+
.B \-f <path>
42+
Filename of the FastCGI application to spawn. This option is deprecated and it
43+
is recommend to always specify the application (absolute path) and its parameters after "--";
44+
the fcgiapp parameter is directly used for the exec() call, while for starting the binary given
45+
with \-f /bin/sh is needed (which may not be available in a chroot).
46+
.IP
47+
This option is ignored if fcgiapp is given.
48+
.TP 8
49+
.B \-d <path>
50+
Change the current directory before spawning the application.
51+
.TP 8
52+
.B \-a <address>
53+
IPv4/IPv6 address to bind to; only used if \-p is given too. Defaults to "0.0.0.0" (IPv4).
54+
.TP 8
55+
.B \-p <port>
56+
TCP port to bind to; you cannot combine this with the \-s option.
57+
.TP 8
58+
.B \-s <path>
59+
Path to the Unix domain socket to bind to; you cannot combine this with the \-p option.
60+
.TP 8
61+
.B \-C <children>
62+
(PHP only) Number of children to spawn by setting the PHP_FCGI_CHILDREN
63+
environment variable. Default is not to overwrite the environment variable;
64+
php will spawn no children if the variable is not set (same as setting it to 0).
65+
.TP 8
66+
.B \-F <children>
67+
Number of children to fork, defaults to 1. This option doesn't work with \-n,
68+
have a look at
69+
.BR multiwatch(1)
70+
if you want to supervise multiple forks on the same socket.
71+
.TP 8
72+
.B \-b <backlog>
73+
backlog to allow on the socket (default 1024). This is usually limited by the kernel too,
74+
check sysctl net.core.somaxconn (default 128) for linux.
75+
.IP
76+
backlog is the queue of connections that the kernel accepts before the userspace application sees them.
77+
.TP 8
78+
.B \-P <path>
79+
Name of the PID file for spawned processes (ignored in no-fork mode)
80+
.TP 8
81+
.B \-n
82+
No forking should take place (for daemontools)
83+
.TP 8
84+
.B \-M <mode>
85+
Change file mode of the Unix domain socket; only used if \-s is given too.
86+
.TP 8
87+
.B \-?, \-h
88+
General usage instructions
89+
.TP 8
90+
.B \-v
91+
Shows version information and exits
92+
.P
93+
.
94+
The following options are only available if you invoke spawn-fcgi as root:
95+
.TP 8
96+
.B \-c <directory>
97+
Chroot to specified directory; the Unix domain socket is created inside the chroot unless \-S is given.
98+
.TP 8
99+
.B \-S
100+
Create Unix domain socket before chroot().
101+
.TP 8
102+
.B \-u
103+
User ID to change to.
104+
.TP 8
105+
.B \-g
106+
Group ID to change to. Defaults to primary group of the user given for \-u.
107+
.TP 8
108+
.B \-U
109+
Change user of the Unix domain socket, defaults to the value of \-u. (only used if \-s is given)
110+
.TP 8
111+
.B \-G
112+
Change group of the Unix domain socket, defaults to the primary group of the user given for \-U;
113+
if \-U wasn't given, defaults to the value of \-g. (only used if \-s is given)
114+
.
115+
.SH "SEE ALSO"
116+
.
117+
.BR svc(8),
118+
.BR supervise(8),
119+
see http://cr.yp.to/daemontools.html
120+
.P
121+
.BR multiwatch(1),
122+
see http://cgit.stbuehler.de/gitosis/multiwatch/about/
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
INCLUDE(CheckCSourceCompiles)
2+
INCLUDE(CheckIncludeFiles)
3+
INCLUDE(CheckFunctionExists)
4+
INCLUDE(CheckVariableExists)
5+
INCLUDE(CheckTypeSize)
6+
INCLUDE(CMakeDetermineCCompiler)
7+
8+
IF(CMAKE_COMPILER_IS_GNUCC)
9+
OPTION(BUILD_EXTRA_WARNINGS "extra warnings")
10+
11+
IF(BUILD_EXTRA_WARNINGS)
12+
SET(WARN_FLAGS "-g -O2 -g2 -Wall -Wmissing-declarations -Wdeclaration-after-statement -Wcast-align -Winline -Wsign-compare -Wnested-externs -Wpointer-arith -Wl,--as-needed -Wformat-security")
13+
# -Wno-pointer-sign -Werror -Wbad-function-cast -Wmissing-prototypes
14+
ELSE(BUILD_EXTRA_WARNINGS)
15+
SET(WARN_FLAGS "")
16+
ENDIF(BUILD_EXTRA_WARNINGS)
17+
ENDIF(CMAKE_COMPILER_IS_GNUCC)
18+
19+
# awk '/#include <(.*)>/ { h = substr($2,2,length($2)-2); h2=toupper(h); gsub("\\.|\\/", "_", h2); printf "%s%s%s%s%s", "CHECK_INCLUDE_FILES(", h, " HAVE_", h2, ")\n" }' spawn-fcgi.c | sort
20+
CHECK_INCLUDE_FILES(arpa/inet.h HAVE_ARPA_INET_H)
21+
CHECK_INCLUDE_FILES(errno.h HAVE_ERRNO_H)
22+
CHECK_INCLUDE_FILES(fcntl.h HAVE_FCNTL_H)
23+
CHECK_INCLUDE_FILES(getopt.h HAVE_GETOPT_H)
24+
CHECK_INCLUDE_FILES(grp.h HAVE_GRP_H)
25+
CHECK_INCLUDE_FILES(netdb.h HAVE_NETDB_H)
26+
CHECK_INCLUDE_FILES(netinet/in.h HAVE_NETINET_IN_H)
27+
CHECK_INCLUDE_FILES(netinet/tcp.h HAVE_NETINET_TCP_H)
28+
CHECK_INCLUDE_FILES(pwd.h HAVE_PWD_H)
29+
CHECK_INCLUDE_FILES(stdio.h HAVE_STDIO_H)
30+
CHECK_INCLUDE_FILES(stdlib.h HAVE_STDLIB_H)
31+
CHECK_INCLUDE_FILES(string.h HAVE_STRING_H)
32+
CHECK_INCLUDE_FILES(sys/ioctl.h HAVE_SYS_IOCTL_H)
33+
CHECK_INCLUDE_FILES(sys/socket.h HAVE_SYS_SOCKET_H)
34+
CHECK_INCLUDE_FILES(sys/stat.h HAVE_SYS_STAT_H)
35+
CHECK_INCLUDE_FILES(sys/time.h HAVE_SYS_TIME_H)
36+
CHECK_INCLUDE_FILES(sys/types.h HAVE_SYS_TYPES_H)
37+
CHECK_INCLUDE_FILES(sys/un.h HAVE_SYS_UN_H)
38+
CHECK_INCLUDE_FILES(sys/wait.h HAVE_SYS_WAIT_H)
39+
CHECK_INCLUDE_FILES(unistd.h HAVE_UNISTD_H)
40+
CHECK_INCLUDE_FILES(winsock2.h HAVE_WINSOCK2_H)
41+
42+
CHECK_FUNCTION_EXISTS(issetugid HAVE_ISSETUGID)
43+
CHECK_FUNCTION_EXISTS(inet_pton HAVE_INET_PTON)
44+
45+
CHECK_C_SOURCE_COMPILES("
46+
#include <sys/types.h>
47+
#include <sys/socket.h>
48+
#include <netinet/in.h>
49+
50+
int main() {
51+
struct sockaddr_in6 s; struct in6_addr t=in6addr_any; int i=AF_INET6; s; t.s6_addr[0] = 0;
52+
return 0;
53+
}" HAVE_IPV6)
54+
55+
SET(CMAKE_EXTRA_INCLUDE_FILES sys/socket.h)
56+
CHECK_TYPE_SIZE(socklen_t HAVE_SOCKLEN_T)
57+
SET(CMAKE_EXTRA_INCLUDE_FILES)
58+
59+
## Write out config.h
60+
CONFIGURE_FILE(${CMAKE_CURRENT_SOURCE_DIR}/config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h)
61+
62+
ADD_DEFINITIONS(-DHAVE_CONFIG_H)
63+
64+
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
65+
66+
ADD_EXECUTABLE(spawn-fcgi spawn-fcgi.c)
67+
68+
IF(CMAKE_COMPILER_IS_GNUCC)
69+
SET_TARGET_PROPERTIES(spawn-fcgi PROPERTIES COMPILE_FLAGS "-std=gnu99 -Wall -g -Wshadow -W -pedantic -fPIC -D_GNU_SOURCE ${WARN_FLAGS}")
70+
ENDIF(CMAKE_COMPILER_IS_GNUCC)
71+
72+
INSTALL(TARGETS spawn-fcgi DESTINATION bin)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
bin_PROGRAMS=spawn-fcgi
3+
spawn_fcgi_SOURCES=spawn-fcgi.c
4+
5+
EXTRA_DIST=CMakeLists.txt config.h.cmake
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
/* generated by cmake, do not modify. modify config.h.cmake instead */
3+
4+
/* Package data */
5+
6+
#define PACKAGE_NAME "${PACKAGE_NAME}"
7+
#define PACKAGE_VERSION "${PACKAGE_VERSION}"
8+
9+
/* grep HAVE_ CMakeLists.txt | sed -e 's/.*\(HAVE_[A-Z_]*\).*/#cmakedefine \1/' */
10+
11+
#cmakedefine HAVE_ARPA_INET_H
12+
#cmakedefine HAVE_ERRNO_H
13+
#cmakedefine HAVE_FCNTL_H
14+
#cmakedefine HAVE_GETOPT_H
15+
#cmakedefine HAVE_GRP_H
16+
#cmakedefine HAVE_NETDB_H
17+
#cmakedefine HAVE_NETINET_IN_H
18+
#cmakedefine HAVE_NETINET_TCP_H
19+
#cmakedefine HAVE_PWD_H
20+
#cmakedefine HAVE_STDIO_H
21+
#cmakedefine HAVE_STDLIB_H
22+
#cmakedefine HAVE_STRING_H
23+
#cmakedefine HAVE_SYS_IOCTL_H
24+
#cmakedefine HAVE_SYS_SOCKET_H
25+
#cmakedefine HAVE_SYS_STAT_H
26+
#cmakedefine HAVE_SYS_TIME_H
27+
#cmakedefine HAVE_SYS_TYPES_H
28+
#cmakedefine HAVE_SYS_UN_H
29+
#cmakedefine HAVE_SYS_WAIT_H
30+
#cmakedefine HAVE_UNISTD_H
31+
#cmakedefine HAVE_WINSOCK
32+
#cmakedefine HAVE_SOCKLEN_T
33+
34+
#cmakedefine HAVE_ISSETUGID
35+
#cmakedefine HAVE_INET_PTON
36+
37+
#cmakedefine HAVE_IPV6

‎tests/src/python/qgis_local_server_spawn/spawn-fcgi/src/spawn-fcgi.c

Lines changed: 630 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/bin/bash
2+
3+
################################################################################
4+
# 2013-09-01 Larry Shaffer <larrys@dakotacarto.com>
5+
#
6+
# NOTE: THIS IS ONLY A DEVELOPMENT TESTING SCRIPT. TO BE REMOVED AT LATER DATE.
7+
################################################################################
8+
9+
/usr/local/bin/spawn-fcgi -p 8448 -u $UID -- /qgisserver/qgis_mapserv.fcgi
10+
11+

‎tests/src/python/test_qgspallabeling_base.py

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ class TestQgsPalLabeling(TestCase):
6666
_PalDataDir = os.path.join(_TestDataDir, 'labeling')
6767
_PalFeaturesDb = os.path.join(_PalDataDir, 'pal_features_v3.sqlite')
6868
_TestFont = TESTFONT
69-
_TestProj = None
7069
_MapRegistry = None
7170
_MapRenderer = None
7271
_Canvas = None
@@ -242,38 +241,6 @@ def renderCheck(self, mismatch=0, imgpath='', grpprefix=''):
242241
msg = '\nRender check failed for "{0}"'.format(self._Test)
243242
return res, msg
244243

245-
def defaultWmsParams(self, projpath, layername):
246-
return {
247-
'SERVICE': 'WMS',
248-
'VERSION': '1.3.0',
249-
'REQUEST': 'GetMap',
250-
'MAP': str(projpath).strip(),
251-
# layer stacking order for rendering: bottom,to,top
252-
'LAYERS': ['background', str(layername).strip()], # or 'name,name'
253-
'STYLES': ',',
254-
'CRS': 'EPSG:32613', # self._CRS, # authid str or QgsCoordinateReferenceSystem obj
255-
'BBOX': '606510,4823130,612510,4827130', # self.aoiExtent(),
256-
'FORMAT': 'image/png', # or: 'image/png; mode=8bit'
257-
'WIDTH': '600',
258-
'HEIGHT': '400',
259-
'DPI': '72',
260-
'MAP_RESOLUTION': '72',
261-
'FORMAT_OPTIONS': 'dpi:72',
262-
'TRANSPARENT': 'FALSE',
263-
'IgnoreGetMapUrl': '1'
264-
}
265-
266-
@classmethod
267-
def setUpServerProjectAndDir(cls, testprojpath, testdir):
268-
cls._TestProj = QgsProject.instance()
269-
cls._TestProj.setFileName(testprojpath)
270-
try:
271-
shutil.copy(cls._PalFeaturesDb, testdir)
272-
for qml in glob.glob(cls._PalDataDir + os.sep + '*.qml'):
273-
shutil.copy(qml, testdir)
274-
except IOError, e:
275-
raise IOError(str(e) + '\nCould not set up test server directory')
276-
277244

278245
class TestPALConfig(TestQgsPalLabeling):
279246

‎tests/src/python/test_qgspallabeling_server.py

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222

2323
import sys
2424
import os
25+
import glob
26+
import shutil
27+
import tempfile
2528
from PyQt4.QtCore import *
2629
from PyQt4.QtGui import *
2730

@@ -35,18 +38,43 @@
3538
from test_qgspallabeling_base import TestQgsPalLabeling, runSuite
3639
from test_qgspallabeling_tests import TestPointBase
3740
from qgis_local_server import (QgisLocalServerConfig,
38-
ServerConfigNotAccessibleError)
41+
ServerConfigNotAccessibleError,
42+
ServerSpawnNotAccessibleError)
3943

4044
SERVER = None
45+
SPAWN = False
4146
TESTPROJDIR = ''
4247
TESTPROJPATH = ''
48+
49+
# TODO [LS]: attempt to spawn local server; add function in qgis_local_server.py
50+
4351
try:
44-
SERVER = \
45-
QgisLocalServerConfig(str(QgsApplication.qgisSettingsDirPath()), True)
52+
# first try to connect to locally spawned server
53+
# http://127.0.0.1:8448/qgis_mapserv.fcgi (see qgis_local_server.py)
54+
SERVER = QgisLocalServerConfig(
55+
tempfile.mkdtemp(),
56+
chkcapa=True, spawn=True)
57+
SPAWN = True
58+
print '\n------------ Using SPAWNED local test server ------------\n'
59+
except ServerSpawnNotAccessibleError, e:
60+
SERVER = None
61+
# print e
62+
# TODO [LS]: add some error output if server is spawned, but not working
63+
pass # may have no local spawn fcgi setup
64+
65+
if SERVER is None:
66+
try:
67+
# next try to connect to configured local server
68+
SERVER = QgisLocalServerConfig(
69+
str(QgsApplication.qgisSettingsDirPath()),
70+
chkcapa=True, spawn=False)
71+
except ServerConfigNotAccessibleError, e:
72+
SERVER = None
73+
print e
74+
75+
if SERVER is not None:
4676
TESTPROJDIR = SERVER.projectDir()
4777
TESTPROJPATH = os.path.join(TESTPROJDIR, 'pal_test.qgs')
48-
except ServerConfigNotAccessibleError, e:
49-
print e
5078

5179

5280
def skipUnlessHasServer(): # skip test class decorator
@@ -55,23 +83,76 @@ def skipUnlessHasServer(): # skip test class decorator
5583
return unittest.skip('\nConfigured local QGIS Server is not accessible\n\n')
5684

5785

58-
@skipUnlessHasServer()
59-
class TestServerPoint(TestQgsPalLabeling, TestPointBase):
86+
class TestServerBase(TestQgsPalLabeling):
87+
88+
_TestProj = None
89+
""":type: QgsProject"""
90+
_TestProjSetup = False
6091

6192
@classmethod
6293
def setUpClass(cls):
6394
TestQgsPalLabeling.setUpClass()
64-
cls.setUpServerProjectAndDir(TESTPROJPATH, TESTPROJDIR)
65-
cls.layer = TestQgsPalLabeling.loadFeatureLayer('background')
95+
96+
cls._TestProj = QgsProject.instance()
97+
cls._TestProj.setFileName(str(TESTPROJPATH).strip())
98+
if not cls._TestProjSetup:
99+
try:
100+
shutil.copy(cls._PalFeaturesDb, TESTPROJDIR)
101+
for qml in glob.glob(cls._PalDataDir + os.sep + '*.qml'):
102+
shutil.copy(qml, TESTPROJDIR)
103+
except IOError, e:
104+
raise IOError(str(e) +
105+
'\nCould not set up test server directory')
106+
cls._TestProjSetup = True
107+
108+
# the blue background (set via layer style) to match renderchecker's
109+
cls._BkgrdLayer = TestQgsPalLabeling.loadFeatureLayer('background')
110+
cls._CheckMismatch = 200 # default for server tests; mismatch expected
111+
cls._CheckGroup = '' # default '' will check against server control
112+
113+
@classmethod
114+
def tearDownClass(cls):
115+
"""Run after all tests"""
116+
TestQgsPalLabeling.tearDownClass()
117+
# layers removed, save empty project file
118+
cls._TestProj.write()
119+
120+
def defaultWmsParams(self, layername):
121+
return {
122+
'SERVICE': 'WMS',
123+
'VERSION': '1.3.0',
124+
'REQUEST': 'GetMap',
125+
'MAP': str(TESTPROJPATH).strip(),
126+
# layer stacking order for rendering: bottom,to,top
127+
'LAYERS': ['background', str(layername).strip()], # or 'name,name'
128+
'STYLES': ',',
129+
# authid str or QgsCoordinateReferenceSystem obj
130+
'CRS': 'EPSG:32613', # self._CRS
131+
'BBOX': '606510,4823130,612510,4827130', # self.aoiExtent(),
132+
'FORMAT': 'image/png', # or: 'image/png; mode=8bit'
133+
'WIDTH': '600',
134+
'HEIGHT': '400',
135+
'DPI': '72',
136+
'MAP_RESOLUTION': '72',
137+
'FORMAT_OPTIONS': 'dpi:72',
138+
'TRANSPARENT': 'FALSE',
139+
'IgnoreGetMapUrl': '1'
140+
}
141+
142+
143+
@skipUnlessHasServer()
144+
class TestServerPoint(TestServerBase, TestPointBase):
145+
146+
@classmethod
147+
def setUpClass(cls):
148+
TestServerBase.setUpClass()
66149
cls.layer = TestQgsPalLabeling.loadFeatureLayer('point')
67-
cls.checkmismatch = 1000
68-
cls.checkgroup = ''
69150

70151
def setUp(self):
71152
"""Run before each test."""
72153
self.configTest('pal_server', 'sp')
73154
self.lyr = self.defaultSettings()
74-
self.params = self.defaultWmsParams(TESTPROJPATH, 'point')
155+
self.params = self.defaultWmsParams('point')
75156
self._TestImage = ''
76157

77158
def tearDown(self):
@@ -83,13 +164,15 @@ def checkTest(self, **kwargs):
83164
# save project file
84165
self._TestProj.write()
85166
# get server results
167+
# print self.params.__repr__()
86168
res, self._TestImage = SERVER.getMap(self.params, False)
169+
# print self._TestImage.__repr__()
87170
self.saveContolImage(self._TestImage)
88171
self.assertTrue(res, 'Failed to retrieve/save image from test server')
89172
# gp = kwargs['grpprefix'] if 'grpprefix' in kwargs else ''
90-
self.assertTrue(*self.renderCheck(mismatch=self.checkmismatch,
173+
self.assertTrue(*self.renderCheck(mismatch=self._CheckMismatch,
91174
imgpath=self._TestImage,
92-
grpprefix=self.checkgroup))
175+
grpprefix=self._CheckGroup))
93176

94177

95178
@skipUnlessHasServer()
@@ -98,7 +181,7 @@ class TestServerVsCanvasPoint(TestServerPoint):
98181
@classmethod
99182
def setUpClass(cls):
100183
TestServerPoint.setUpClass()
101-
cls.checkgroup = 'pal_canvas'
184+
cls._CheckGroup = 'pal_canvas'
102185

103186

104187
if __name__ == '__main__':
@@ -108,4 +191,6 @@ def setUpClass(cls):
108191
'TestServerVsCanvasPoint.test_text_size_map_unit'
109192
]
110193
res = runSuite(sys.modules[__name__], suite)
194+
# if SPAWN:
195+
# os.remove(TESTPROJDIR) # remove temp directory (why does this error?)
111196
sys.exit(not res.wasSuccessful())

0 commit comments

Comments
 (0)
Please sign in to comment.