Skip to content

Commit

Permalink
Fix: Exit http server on php exit (#1714)
Browse files Browse the repository at this point in the history
## Motivation for the change, related issues
Calling the `php.exit` caused a hanging connection. This had affect in
tests, and leaving open connections around.
## Implementation details
Attach the HTTP server that we spawn to handle websocket connections to
the PHP instance.
When we call `.exit` we use the attached instance to close the
connections.

## Testing Instructions (or ideally a Blueprint)
1. Create a project with the following test code:

```import { getPHPLoaderModule, withNetworking } from '@php-wasm/node';
import { PHP, __private__dont__use, loadPHPRuntime } from '@php-wasm/universal';

const isWindows = process.platform === 'win32';

const dostuff = async function() {
	const id =  await loadPHPRuntime(
		await getPHPLoaderModule('8.3'),
		await withNetworking({})
	);
	const php = new PHP(id);

	const privateSymbol = Object.getOwnPropertySymbols(php)
		.find(sym => String(sym).includes('__private__dont__use'));


	const result = await php.run({ code: `<?php echo "HI!"; `} );
	console.log(result.text);
	php.exit();

}

dostuff().catch(console.error);
```
2. Build `php-wasm/node` and `php-wasm/universal`

```
npx nx run php-wasm-universal:build
npx nx run php-wasm-node:build
```
4. Cd into `/dist` php-wasm folder and run `npm link`
5. Go to the project and run `npm link @php-wasm/node` and `npm link
@php-wam/universal`
6. Navigate inside node module, in each package and run `npm install`
7. Run the script you have created
8. Ensure that exits successfully
  • Loading branch information
kozer authored Aug 29, 2024
1 parent 6447b01 commit a0abc36
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 2 deletions.
5 changes: 4 additions & 1 deletion packages/php-wasm/node/src/lib/networking/with-networking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ export async function withNetworking(
const [inboundProxyWsServerPort, outboundProxyWsServerPort] =
await findFreePorts(2);

await initOutboundWebsocketProxyServer(outboundProxyWsServerPort);
const outboundNetworkProxyServer = await initOutboundWebsocketProxyServer(
outboundProxyWsServerPort
);

return {
...phpModuleArgs,
outboundNetworkProxyServer,
websocket: {
...(phpModuleArgs['websocket'] || {}),
url: (_: any, host: string, port: string) => {
Expand Down
23 changes: 23 additions & 0 deletions packages/php-wasm/node/src/test/php-networking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
PHP,
SupportedPHPVersions,
setPhpIniEntries,
getLoadedRuntime,
} from '@php-wasm/universal';
import express from 'express';
import { rootCertificates } from 'tls';
import { loadNodeRuntime } from '../lib';
import http from 'http';

describe.each(SupportedPHPVersions)(
'PHP %s',
Expand Down Expand Up @@ -169,6 +171,27 @@ describe.each(SupportedPHPVersions)(
console.log(text);
expect(text).toContain('bool(true)');
});

it('should close server when runtime is exited', async () => {
const id = await loadNodeRuntime(phpVersion);
const php = new PHP(id);
const rt = getLoadedRuntime(id);

expect(rt.outboundNetworkProxyServer).toBeDefined();
expect(rt.outboundNetworkProxyServer).toBeInstanceOf(
http.Server
);

expect(rt.outboundNetworkProxyServer.listening).toBe(true);
php.exit();

// Wait for the server to close
await new Promise((resolve) => {
rt.outboundNetworkProxyServer.on('close', resolve);
});

expect(rt.outboundNetworkProxyServer.listening).toBe(false);
});
});
},
1000
Expand Down
2 changes: 1 addition & 1 deletion packages/php-wasm/universal/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export type {
} from './supported-php-extensions';
export { PHP, __private__dont__use } from './php';
export type { MountHandler, UnmountFunction } from './php';
export { loadPHPRuntime } from './load-php-runtime';
export { loadPHPRuntime, getLoadedRuntime } from './load-php-runtime';
export type * from './emscripten-types';
export type {
DataModule,
Expand Down
9 changes: 9 additions & 0 deletions packages/php-wasm/universal/src/lib/load-php-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { logger } from '@php-wasm/logger';
import { IncomingMessage, Server, ServerResponse } from 'http';

const RuntimeId = Symbol('RuntimeId');
const loadedRuntimes: Map<number, PHPRuntime> = new Map();
Expand Down Expand Up @@ -160,6 +161,10 @@ export async function loadPHPRuntime(
PHPRuntime.originalExit = PHPRuntime._exit;

PHPRuntime._exit = function (code: number) {
if (PHPRuntime.outboundNetworkProxyServer) {
PHPRuntime.outboundNetworkProxyServer.close();
PHPRuntime.outboundNetworkProxyServer.closeAllConnections();
}
loadedRuntimes.delete(id);
return PHPRuntime.originalExit(code);
};
Expand Down Expand Up @@ -238,6 +243,10 @@ export type EmscriptenOptions = {
onRuntimeInitialized?: () => void;
monitorRunDependencies?: (left: number) => void;
onMessage?: (listener: EmscriptenMessageListener) => void;
outboundNetworkProxyServer?: Server<
typeof IncomingMessage,
typeof ServerResponse
>;
instantiateWasm?: (
info: WebAssembly.Imports,
receiveInstance: (
Expand Down

0 comments on commit a0abc36

Please sign in to comment.