3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Serves content for "script" handlers using the PHP runtime."""
28 from google
.appengine
.api
import appinfo
29 from google
.appengine
.tools
.devappserver2
import http_runtime
30 from google
.appengine
.tools
.devappserver2
import instance
32 from google
.appengine
.tools
.devappserver2
import safe_subprocess
35 _RUNTIME_PATH
= os
.path
.abspath(
36 os
.path
.join(os
.path
.dirname(sys
.argv
[0]), '_php_runtime.py'))
37 _CHECK_ENVIRONMENT_SCRIPT_PATH
= os
.path
.join(
38 os
.path
.dirname(__file__
), 'php', 'check_environment.php')
39 _RUNTIME_ARGS
= [sys
.executable
, _RUNTIME_PATH
]
42 class _PHPBinaryError(Exception):
46 class _PHPEnvironmentError(Exception):
50 class _BadPHPEnvironmentRuntimeProxy(instance
.RuntimeProxy
):
51 """Serves an error page describing the problem with the user's PHP setup."""
53 def __init__(self
, php_executable_path
, exception
):
54 self
._php
_executable
_path
= php_executable_path
55 self
._exception
= exception
63 def handle(self
, environ
, start_response
, url_map
, match
, request_id
,
65 """Serves a request by displaying an error page.
68 environ: An environ dict for the request as defined in PEP-333.
69 start_response: A function with semantics defined in PEP-333.
70 url_map: An appinfo.URLMap instance containing the configuration for the
71 handler matching this request.
72 match: A re.MatchObject containing the result of the matched URL pattern.
73 request_id: A unique string id associated with the request.
74 request_type: The type of the request. See instance.*_REQUEST module
78 A sequence of strings containing the body of the HTTP response.
80 start_response('500 Internal Server Error',
81 [('Content-Type', 'text/html')])
82 yield '<html><head><title>Invalid PHP Configuration</title></head>'
84 yield '<title>Invalid PHP Configuration</title>'
85 if isinstance(self
._exception
, _PHPEnvironmentError
):
86 yield '<b>The PHP interpreter specified with the --php_executable_path '
87 yield ' flag ("%s") is not compatible with the App Engine ' % (
88 self
._php
_executable
_path
)
89 yield 'PHP development environment.</b><br>'
91 yield '<pre>%s</pre>' % self
._exception
93 yield '<b>%s</b>' % cgi
.escape(str(self
._exception
))
95 yield '</body></html>'
98 class PHPRuntimeInstanceFactory(instance
.InstanceFactory
):
99 """A factory that creates new PHP runtime Instances."""
101 # A mapping from a php executable path to the _BadPHPEnvironmentRuntimeProxy
102 # descriping why it is not useable. If the php executable is usable then the
103 # path will map to None. Only one PHP executable will be used in a run of the
104 # development server but that is not necessarily the case for tests.
105 _php_binary_to_error_proxy
= {}
107 # TODO: Use real script values.
108 START_URL_MAP
= appinfo
.URLMap(
110 script
='$PHP_LIB/default_start_handler',
112 WARMUP_URL_MAP
= appinfo
.URLMap(
114 script
='$PHP_LIB/default_warmup_handler',
116 SUPPORTS_INTERACTIVE_REQUESTS
= True
117 FILE_CHANGE_INSTANCE_RESTART_POLICY
= instance
.NEVER
119 def __init__(self
, request_data
, runtime_config_getter
, module_configuration
):
120 """Initializer for PHPRuntimeInstanceFactory.
123 request_data: A wsgi_request_info.WSGIRequestInfo that will be provided
124 with request information for use by API stubs.
125 runtime_config_getter: A function that can be called without arguments
126 and returns the runtime_config_pb2.Config containing the configuration
128 module_configuration: An application_configuration.ModuleConfiguration
129 instance respresenting the configuration of the module that owns the
132 super(PHPRuntimeInstanceFactory
, self
).__init
__(
133 request_data
, 8 if runtime_config_getter().threadsafe
else 1)
134 self
._runtime
_config
_getter
= runtime_config_getter
135 self
._module
_configuration
= module_configuration
136 self
._bad
_environment
_proxy
= None
139 def _check_environment(php_executable_path
, application_root
):
140 if php_executable_path
is None:
141 raise _PHPBinaryError('The development server must be started with the '
142 '--php_executable_path flag set to the path of the '
145 if not os
.path
.exists(php_executable_path
):
146 raise _PHPBinaryError('The path specified with the --php_executable_path '
147 'flag (%s) does not exist.' % php_executable_path
)
149 if not os
.access(php_executable_path
, os
.X_OK
):
150 raise _PHPBinaryError('The path specified with the --php_executable_path '
151 'flag (%s) is not executable' % php_executable_path
)
154 # On Windows, in order to run a side-by-side assembly the specified env
155 # must include a valid SystemRoot.
156 if 'SYSTEMROOT' in os
.environ
:
157 env
['SYSTEMROOT'] = os
.environ
['SYSTEMROOT']
159 args
= [php_executable_path
, '-v', '-n']
160 version_process
= safe_subprocess
.start_process(args
,
161 stdout
=subprocess
.PIPE
,
162 stderr
=subprocess
.PIPE
,
164 version_stdout
, version_stderr
= version_process
.communicate()
165 if version_process
.returncode
:
166 raise _PHPEnvironmentError(
167 '"%s" returned an error [%d]\n%s%s' % (
169 version_process
.returncode
,
173 version_match
= re
.search(r
'PHP (\d+).(\d+)', version_stdout
)
174 if version_match
is None:
175 raise _PHPEnvironmentError(
176 '"%s" returned an unexpected version string:\n%s%s' % (
181 version
= tuple(int(v
) for v
in version_match
.groups())
183 raise _PHPEnvironmentError(
184 'The PHP interpreter must be version >= 5.4, %d.%d found' % version
)
186 args
= [php_executable_path
, '-c', application_root
, '-f',
187 _CHECK_ENVIRONMENT_SCRIPT_PATH
]
188 check_process
= safe_subprocess
.start_process(
190 stdout
=subprocess
.PIPE
,
191 stderr
=subprocess
.PIPE
,
193 check_process_stdout
, _
= check_process
.communicate()
194 if check_process
.returncode
:
195 raise _PHPEnvironmentError(check_process_stdout
)
197 def new_instance(self
, instance_id
, expect_ready_request
=False):
198 """Create and return a new Instance.
201 instance_id: A string or integer representing the unique (per module) id
203 expect_ready_request: If True then the instance will be sent a special
204 request (i.e. /_ah/warmup or /_ah/start) before it can handle external
208 The newly created instance.Instance.
211 def instance_config_getter():
212 runtime_config
= self
._runtime
_config
_getter
()
213 runtime_config
.instance_id
= str(instance_id
)
214 return runtime_config
216 php_executable_path
= (
217 self
._runtime
_config
_getter
().php_config
.php_executable_path
)
219 if php_executable_path
not in self
._php
_binary
_to
_error
_proxy
:
221 self
._check
_environment
(php_executable_path
,
222 self
._runtime
_config
_getter
().application_root
)
223 except Exception as e
:
224 self
._php
_binary
_to
_error
_proxy
[php_executable_path
] = (
225 _BadPHPEnvironmentRuntimeProxy(php_executable_path
, e
))
226 logging
.exception('The PHP runtime is not available')
228 self
._php
_binary
_to
_error
_proxy
[php_executable_path
] = None
230 proxy
= self
._php
_binary
_to
_error
_proxy
[php_executable_path
]
232 proxy
= http_runtime
.HttpRuntimeProxy(_RUNTIME_ARGS
,
233 instance_config_getter
,
234 self
._module
_configuration
)
235 return instance
.Instance(self
.request_data
,
238 self
.max_concurrent_requests
,
239 self
.max_background_threads
,
240 expect_ready_request
)