Skip to content

Fix port conflicts when running multiple NativePHP apps on Windows #244

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions resources/js/electron-plugin/dist/server/php.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { promisify } from 'util';
import { join } from 'path';
import { app } from 'electron';
import { execFile, spawn, spawnSync } from 'child_process';
import { createServer } from 'net';
import state from "./state.js";
import getPort, { portNumbers } from 'get-port';
const storagePath = join(app.getPath('userData'), 'storage');
Expand Down Expand Up @@ -42,10 +43,31 @@ function shouldOptimize(store) {
}
function getPhpPort() {
return __awaiter(this, void 0, void 0, function* () {
return yield getPort({
const suggestedPort = yield getPort({
host: '127.0.0.1',
port: portNumbers(8100, 9000)
});
if (yield canBindToPort(suggestedPort)) {
return suggestedPort;
}
console.warn(`Port ${suggestedPort} is not bindable, manually searching...`);
for (let port = suggestedPort + 1; port < 9000; port++) {
if (yield canBindToPort(port)) {
return port;
}
}
throw new Error('Could not find an available port in range 8100-9000');
});
}
function canBindToPort(port) {
return new Promise((resolve) => {
const server = createServer();
server.listen(port, '127.0.0.1', () => {
server.close(() => resolve(true));
});
server.on('error', () => {
resolve(false);
});
});
}
function retrievePhpIniSettings() {
Expand Down Expand Up @@ -235,8 +257,6 @@ function serveApp(secret, apiPort, phpIniSettings) {
console.log('Skipping Database migration while in development.');
console.log('You may migrate manually by running: php artisan native:migrate');
}
console.log('Starting PHP server...');
const phpPort = yield getPhpPort();
let serverPath;
let cwd;
if (runningSecureBuild()) {
Expand All @@ -247,6 +267,8 @@ function serveApp(secret, apiPort, phpIniSettings) {
serverPath = join(appPath, 'vendor', 'laravel', 'framework', 'src', 'Illuminate', 'Foundation', 'resources', 'server.php');
cwd = join(appPath, 'public');
}
console.log('Starting PHP server...');
const phpPort = yield getPhpPort();
const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], {
cwd: cwd,
env
Expand Down
41 changes: 35 additions & 6 deletions resources/js/electron-plugin/src/server/php.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {promisify} from 'util'
import {join} from 'path'
import {app} from 'electron'
import {execFile, spawn, spawnSync} from 'child_process'
import {createServer} from 'net'
import state from "./state.js";
import getPort, {portNumbers} from 'get-port';
import {ProcessResult} from "./ProcessResult.js";
Expand Down Expand Up @@ -50,10 +51,41 @@ function shouldOptimize(store) {
}

async function getPhpPort() {
return await getPort({
// Try get-port first (fast path)
const suggestedPort = await getPort({
host: '127.0.0.1',
port: portNumbers(8100, 9000)
});

// Validate that we can actually bind to this port
if (await canBindToPort(suggestedPort)) {
return suggestedPort;
}

// If get-port gave us a bad port, manually search starting from suggestedPort + 1
console.warn(`Port ${suggestedPort} is not bindable, manually searching...`);

for (let port = suggestedPort + 1; port < 9000; port++) {
if (await canBindToPort(port)) {
return port;
}
}

throw new Error('Could not find an available port in range 8100-9000');
}

function canBindToPort(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = createServer();

server.listen(port, '127.0.0.1', () => {
server.close(() => resolve(true));
});

server.on('error', () => {
resolve(false);
});
});
}

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

console.log('Starting PHP server...');
const phpPort = await getPhpPort();


let serverPath: string;
let cwd: string;

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

console.log('Starting PHP server...');
const phpPort = await getPhpPort();
const phpServer = callPhp(['-S', `127.0.0.1:${phpPort}`, serverPath], {
cwd: cwd,
env
Expand All @@ -371,7 +401,6 @@ function serveApp(secret, apiPort, phpIniSettings): Promise<ProcessResult> {
// Show urls called
phpServer.stdout.on('data', (data) => {
// [Tue Jan 14 19:51:00 2025] 127.0.0.1:52779 [POST] URI: /_native/api/events

if (parseInt(process.env.SHELL_VERBOSITY) > 0) {
console.log(data.toString().trim());
}
Expand Down
3 changes: 3 additions & 0 deletions resources/js/electron-plugin/tests/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ describe('API test', () => {
});

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

Expand Down