Skip to content

Commit d5714ce

Browse files
committed
Improved server termination.
1 parent c873fa1 commit d5714ce

File tree

3 files changed

+136
-7
lines changed

3 files changed

+136
-7
lines changed

README.md

+55-5
Original file line numberDiff line numberDiff line change
@@ -357,20 +357,70 @@ The variables available to normal PHP scripts are also available to extensions v
357357

358358
Always use the `ProcessHelper::StartProcess()` static function when starting external, long-running processes inside an extension. The [ProcessHelper](https://github.com/cubiclesoft/php-misc/blob/master/docs/process_helper.md) class is designed to start non-blocking processes in the background across all platforms. Note that the preferred way to start long-running processes is to use the long-running processes extension.
359359

360+
Server Termination
361+
------------------
362+
363+
For certain tasks, it is important to tell PHP App Server to exit. For example, when upgrading a PHP App Server application on Windows, PHP itself needs to be updated and therefore can't be running during the upgrade. It's also generally good behavior to exit an application not too long after the last browser tab is closed.
364+
365+
There are two available methods for triggering early termination of the server:
366+
367+
* Use the Exit App extension. To use it, have the web browser establish a WebSocket connection to `/exit-app/` and send a valid `authtoken` and a `delay` in a JSON object that specifies the number of seconds to wait to terminate the server. Once used, every page of the app must connect to `/exit-app/` to keep the server alive. A minimum delay of 3 seconds is recommended. Web browsers tend to drop WebSocket connections as soon as they leave the page or the tab is closed.
368+
* Send a `X-Exit-App` header in the response of a PHP script. The value of the header is the integer number of seconds to wait to terminate the server. A minimum delay of 3 seconds is recommended. This special header is not passed to the web browser but handled internally.
369+
370+
Example PHP code for the Exit App extension method:
371+
372+
```php
373+
<script type="text/javascript">
374+
// NOTE: Always put WebSocket class instances in a Javascript closure like this one to limit the XSRF attack surface.
375+
(function() {
376+
function InitExitApp()
377+
{
378+
var ws = new WebSocket((window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host + '/exit-app/');
379+
380+
ws.addEventListener('open', function(e) {
381+
var msg = {
382+
authtoken: '<?=hash_hmac("sha256", "/exit-app/", $_SERVER["PAS_SECRET"])?>',
383+
delay: 3
384+
};
385+
386+
ws.send(JSON.stringify(msg));
387+
});
388+
389+
ws.addEventListener('close', function(e) {
390+
setTimeout(InitExitApp, 500);
391+
});
392+
}
393+
394+
InitExitApp();
395+
})();
396+
</script>
397+
```
398+
399+
Example PHP code for the header method:
400+
401+
```php
402+
<?php
403+
// User clicked an "Exit application" link or something.
404+
header("X-Exit-App: 3");
405+
?>
406+
```
407+
408+
The extension is a more reliable method of detecting that all browser tabs to the application have been closed. However, if the application is unable to support the extension for some reason, then use the header method instead. The header method is best used on pages where it makes sense (e.g. a page with upgrade information).
409+
360410
Pre-Installer Tasks
361411
-------------------
362412

363-
Before running the various scripts that generate installer packages, various files need to be created, renamed, and/or modified. Every file that starts with "yourapp" needs to be renamed to your application name, preferably restricted to all lowercase a-z and hyphens. This is done so that updates to the software don't accidentally overwrite your work and so that any nosy users poking around the directory structure see the application's actual name instead of "yourapp".
413+
Before running the various scripts that generate installer packages, various files need to be created, renamed, and/or modified. Every file that starts with "yourapp" needs to be renamed to your application name, preferably restricted to all lowercase a-z and hyphens. This needs to be done so that updates to the software don't accidentally overwrite your work and so that any nosy users poking around the directory structure see the application's actual name instead of "yourapp".
364414

365415
* yourapp.png - A 512x512 pixel PNG image containing your application icon. It should be fairly easy to tell what the icon represents when shrunk to 24x24 pixels. The default icon works for testing but should be replaced with your own icon before deploying.
366-
* yourapp.ico - A Windows .ico file containing your application icon at as many resolutions and sizes as possible. The default icon works for testing but should be replaced with your own icon before deploying.
416+
* yourapp.ico - This file can be deleted. It's only included to have a higher quality default ICO file. See the Windows EXE packaging instructions for more details.
367417
* yourapp.phpapp - This file needs to be modified. More on this file in a moment.
368-
* yourapp-license.txt - Replace the text within with an actual End User License Agreement (EULA) written and approved by a real lawyer.
418+
* yourapp-license.txt - Replace the text in the file with an actual End User License Agreement (EULA) that's been written and approved by a real lawyer.
369419

370420
The 'yourapp.phpapp' file is a PHP file that performs the actual application startup sequence of starting the web server (server.php) and then launching the user's web browser. There is an `$options` array in the file that should be modified for your application's needs:
371421

372-
* business - A string containing your business or your name (Default is "CubicleSoft", which is probably not really what is desired). Shown under some OSes when displaying a program listing - notably Linux.
373-
* appname - A boolean of false or a string containing your application's name (Default is false, which attempts to automatically determine the app's name based on the directory it is installed in). Shown under some OSes when displaying a program listing - notably Linux.
422+
* business - A string containing your business or your name (Default is "Your Business or Name"). Shown under some OSes when displaying a program listing - notably Linux.
423+
* appname - A boolean of false or a string containing your application's name (Default is "Your App"). When a boolean of false, the code attempts to automatically determine the app's name based on the directory it is installed in. This string is shown under some OSes when displaying a program listing - notably Linux.
374424
* home - An optional string containing the directory to use as the "home" directory. Could be useful for implementing a "portable" version of the application.
375425
* host - A string containing the IP address to bind to (Default is "127.0.0.1"). In general, don't change this.
376426
* port - An integer containing the port number to bind to (Default is 0, which selects a random port number). In general, don't change this.

extensions/10_exit_app.php

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
// Terminate the web server in a timely fashion after the user closes the last browser tab.
3+
// (C) 2019 CubicleSoft. All Rights Reserved.
4+
5+
class PAS_Extension_10_exit_app
6+
{
7+
private $delay, $lastclient;
8+
9+
public function InitServer()
10+
{
11+
$this->delay = false;
12+
$this->lastclient = 0;
13+
}
14+
15+
public function ServerReady()
16+
{
17+
}
18+
19+
public function UpdateStreamsAndTimeout($prefix, &$timeout, &$readfps, &$writefps)
20+
{
21+
global $wsserver, $cgis, $fcgis, $running;
22+
23+
if ($this->delay !== false)
24+
{
25+
if ($wsserver->NumClients() || count($cgis) || count($fcgis)) $this->lastclient = microtime(true);
26+
else if ($this->lastclient < microtime(true) - $this->delay)
27+
{
28+
$timeout = 0;
29+
$running = false;
30+
}
31+
else
32+
{
33+
$timeout = (int)($this->delay - (microtime(true) - $this->lastclient)) + 1;
34+
}
35+
}
36+
}
37+
38+
public function CanHandleRequest($method, $url, $path, $client)
39+
{
40+
if ($path === "/exit-app/") return true;
41+
42+
return false;
43+
}
44+
45+
public function RequireAuthToken()
46+
{
47+
return true;
48+
}
49+
50+
public function ProcessRequest($method, $path, $client, &$data)
51+
{
52+
if (!is_array($data)) return false;
53+
54+
$this->delay = (isset($data["delay"]) && $data["delay"] > 0 ? (int)$data["delay"] : false);
55+
56+
$result = array("success" => true);
57+
58+
// WebSocket expected.
59+
if ($method === false) return $result;
60+
61+
// Prevent browsers and proxies from doing bad things.
62+
$client->SetResponseNoCache();
63+
64+
$client->SetResponseContentType("application/json");
65+
$client->AddResponseContent(json_encode($result, JSON_UNESCAPED_SLASHES));
66+
$client->FinalizeResponse();
67+
68+
return true;
69+
}
70+
71+
public function ServerDone()
72+
{
73+
}
74+
}
75+
?>

server.php

+6-2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@
6868
// Load MIME types.
6969
$mimetypemap = json_decode(file_get_contents($rootpath . "/support/mime_types.json"), true);
7070

71+
if (isset($args["opts"]["quit"]) && $args["opts"]["quit"] > 0 && $args["opts"]["quit"] < 60) $args["opts"]["quit"] = 60;
72+
7173
// Load all server extensions.
7274
if (isset($args["opts"]["exts"])) $extspath = $args["opts"]["exts"];
7375
else $extspath = $rootpath . "/extensions";
@@ -1077,6 +1079,7 @@ function SendHTTPErrorResponse($client)
10771079
if ($name === "status" && (int)$val >= 200) $client->SetResponseCode((int)$val);
10781080
else if ($name === "content-length" && (int)$val >= 0) $client->SetResponseContentLength((int)$val);
10791081
else if ($name === "content-type") $client->AddResponseHeader("Content-Type", $val, true);
1082+
else if ($name === "x-exit-app") $args["opts"]["quit"] = (int)$val;
10801083
else $client->AddResponseHeader(HTTP::HeaderNameCleanup($name), $val);
10811084
}
10821085

@@ -1182,6 +1185,7 @@ function SendHTTPErrorResponse($client)
11821185
if ($name === "status" && (int)$val >= 200) $client->SetResponseCode((int)$val);
11831186
else if ($name === "content-length" && (int)$val >= 0) $client->SetResponseContentLength((int)$val);
11841187
else if ($name === "content-type") $client->AddResponseHeader("Content-Type", $val, true);
1188+
else if ($name === "x-exit-app") $args["opts"]["quit"] = (int)$val;
11851189
else $client->AddResponseHeader(HTTP::HeaderNameCleanup($name), $val);
11861190
}
11871191

@@ -1458,9 +1462,9 @@ function SendHTTPErrorResponse($client)
14581462
}
14591463

14601464
// Automatically quit the server at the configured time (if any).
1461-
if (isset($args["opts"]["quit"]) && $args["opts"]["quit"] >= 60)
1465+
if (isset($args["opts"]["quit"]) && $args["opts"]["quit"] > 0)
14621466
{
1463-
if ($webserver->NumClients() || $wsserver->NumClients()) $lastclient = microtime(true);
1467+
if ($wsserver->NumClients() || count($cgis) || count($fcgis)) $lastclient = microtime(true);
14641468
else if ($lastclient < microtime(true) - (int)$args["opts"]["quit"]) $running = false;
14651469
}
14661470

0 commit comments

Comments
 (0)