Skip to content

Commit ed09419

Browse files
authored
Fix port conflicts when running multiple NativePHP apps on Windows (#244)
1 parent 0c99364 commit ed09419

File tree

3 files changed

+63
-9
lines changed

3 files changed

+63
-9
lines changed

resources/js/electron-plugin/dist/server/php.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { promisify } from 'util';
1515
import { join } from 'path';
1616
import { app } from 'electron';
1717
import { execFile, spawn, spawnSync } from 'child_process';
18+
import { createServer } from 'net';
1819
import state from "./state.js";
1920
import getPort, { portNumbers } from 'get-port';
2021
const storagePath = join(app.getPath('userData'), 'storage');
@@ -42,10 +43,31 @@ function shouldOptimize(store) {
4243
}
4344
function getPhpPort() {
4445
return __awaiter(this, void 0, void 0, function* () {
45-
return yield getPort({
46+
const suggestedPort = yield getPort({
4647
host: '127.0.0.1',
4748
port: portNumbers(8100, 9000)
4849
});
50+
if (yield canBindToPort(suggestedPort)) {
51+
return suggestedPort;
52+
}
53+
console.warn(`Port ${suggestedPort} is not bindable, manually searching...`);
54+
for (let port = suggestedPort + 1; port < 9000; port++) {
55+
if (yield canBindToPort(port)) {
56+
return port;
57+
}
58+
}
59+
throw new Error('Could not find an available port in range 8100-9000');
60+
});
61+
}
62+
function canBindToPort(port) {
63+
return new Promise((resolve) => {
64+
const server = createServer();
65+
server.listen(port, '127.0.0.1', () => {
66+
server.close(() => resolve(true));
67+
});
68+
server.on('error', () => {
69+
resolve(false);
70+
});
4971
});
5072
}
5173
function retrievePhpIniSettings() {
@@ -235,8 +257,6 @@ function serveApp(secret, apiPort, phpIniSettings) {
235257
console.log('Skipping Database migration while in development.');
236258
console.log('You may migrate manually by running: php artisan native:migrate');
237259
}
238-
console.log('Starting PHP server...');
239-
const phpPort = yield getPhpPort();
240260
let serverPath;
241261
let cwd;
242262
if (runningSecureBuild()) {
@@ -247,6 +267,8 @@ function serveApp(secret, apiPort, phpIniSettings) {
247267
serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php');
248268
cwd = join(appPath, 'public');
249269
}
270+
console.log('Starting PHP server...');
271+
const phpPort = yield getPhpPort();
250272
const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], {
251273
cwd: cwd,
252274
env

resources/js/electron-plugin/src/server/php.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {promisify} from 'util'
88
import {join} from 'path'
99
import {app} from 'electron'
1010
import {execFile, spawn, spawnSync} from 'child_process'
11+
import {createServer} from 'net'
1112
import state from "./state.js";
1213
import getPort, {portNumbers} from 'get-port';
1314
import {ProcessResult} from "./ProcessResult.js";
@@ -50,10 +51,41 @@ function shouldOptimize(store) {
5051
}
5152

5253
async function getPhpPort() {
53-
return await getPort({
54+
// Try get-port first (fast path)
55+
const suggestedPort = await getPort({
5456
host: '127.0.0.1',
5557
port: portNumbers(8100, 9000)
5658
});
59+
60+
// Validate that we can actually bind to this port
61+
if (await canBindToPort(suggestedPort)) {
62+
return suggestedPort;
63+
}
64+
65+
// If get-port gave us a bad port, manually search starting from suggestedPort + 1
66+
console.warn(`Port ${suggestedPort} is not bindable, manually searching...`);
67+
68+
for (let port = suggestedPort + 1; port < 9000; port++) {
69+
if (await canBindToPort(port)) {
70+
return port;
71+
}
72+
}
73+
74+
throw new Error('Could not find an available port in range 8100-9000');
75+
}
76+
77+
function canBindToPort(port: number): Promise<boolean> {
78+
return new Promise((resolve) => {
79+
const server = createServer();
80+
81+
server.listen(port, '127.0.0.1', () => {
82+
server.close(() => resolve(true));
83+
});
84+
85+
server.on('error', () => {
86+
resolve(false);
87+
});
88+
});
5789
}
5890

5991
async function retrievePhpIniSettings() {
@@ -346,10 +378,6 @@ function serveApp(secret, apiPort, phpIniSettings): Promise<ProcessResult> {
346378
console.log('You may migrate manually by running: php artisan native:migrate')
347379
}
348380

349-
console.log('Starting PHP server...');
350-
const phpPort = await getPhpPort();
351-
352-
353381
let serverPath: string;
354382
let cwd: string;
355383

@@ -361,6 +389,8 @@ function serveApp(secret, apiPort, phpIniSettings): Promise<ProcessResult> {
361389
cwd = join(appPath, 'public');
362390
}
363391

392+
console.log('Starting PHP server...');
393+
const phpPort = await getPhpPort();
364394
const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], {
365395
cwd: cwd,
366396
env
@@ -371,7 +401,6 @@ function serveApp(secret, apiPort, phpIniSettings): Promise<ProcessResult> {
371401
// Show urls called
372402
phpServer.stdout.on('data', (data) => {
373403
// [Tue Jan 14 19:51:00 2025] 127.0.0.1:52779 [POST] URI: /_native/api/events
374-
375404
if (parseInt(process.env.SHELL_VERBOSITY) > 0) {
376405
console.log(data.toString().trim());
377406
}

resources/js/electron-plugin/tests/api.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ describe('API test', () => {
3131
});
3232

3333
it('starts API server on port 4000', async () => {
34+
// NOTE: If this fails it may be you have a NativePHP app running locally
35+
// and the port negotiation actually woks as expected (might be 4001).
36+
// Quit any running NativePHP apps to verify.
3437
expect(apiServer.port).toBe(4000);
3538
});
3639

0 commit comments

Comments
 (0)