Skip to content
Merged
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
5 changes: 5 additions & 0 deletions scripts/run-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { parseConfigFile, runServer } from 'verdaccio';

import { maxConcurrentTasks } from './utils/concurrency';
import { PACKS_DIRECTORY } from './utils/constants';
import { killProcessOnPort } from './utils/kill-process-on-port';
import { getWorkspaces } from './utils/workspace';

program
Expand All @@ -37,6 +38,10 @@ const pathExists = async (p: string) => {
};

const startVerdaccio = async () => {
// Kill Verdaccio related processes if they are already running
await killProcessOnPort(6001);
await killProcessOnPort(6002);

const ready = {
proxy: false,
verdaccio: false,
Expand Down
54 changes: 54 additions & 0 deletions scripts/utils/kill-process-on-port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// eslint-disable-next-line depend/ban-dependencies
import { execa } from 'execa';

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

/** Kills any process that is listening on the specified port. */
export const killProcessOnPort = async (port: number): Promise<void> => {
try {
let pids: string[] = [];

if (isWindows) {
// Windows: use netstat to find the process
try {
const { stdout } = await execa('netstat', ['-ano']);
const lines = stdout.split('\n');
const regex = new RegExp(`TCP.*:${port}.*LISTENING\\s+(\\d+)`, 'i');

for (const line of lines) {
const match = line.match(regex);
if (match && match[1]) {
pids.push(match[1]);
}
}
} catch {
Comment on lines +14 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix Windows PID parsing to avoid killing unrelated ports

The current regex matches any substring :${port}, so asking for port 6001 will also match 60012/60010 and can terminate the wrong process. Split the netstat columns and ensure the local address ends with the exact port before you add the PID to the kill list.

-        const { stdout } = await execa('netstat', ['-ano']);
-        const lines = stdout.split('\n');
-        const regex = new RegExp(`TCP.*:${port}.*LISTENING\\s+(\\d+)`, 'i');
-
-        for (const line of lines) {
-          const match = line.match(regex);
-          if (match && match[1]) {
-            pids.push(match[1]);
-          }
-        }
+        const { stdout } = await execa('netstat', ['-ano']);
+        const lines = stdout.split('\n');
+
+        for (const line of lines) {
+          const parts = line.trim().split(/\s+/);
+          if (
+            parts.length >= 5 &&
+            parts[0]?.toUpperCase().startsWith('TCP') &&
+            parts[1]?.endsWith(`:${port}`) &&
+            parts[3]?.toUpperCase() === 'LISTENING'
+          ) {
+            pids.push(parts[4]);
+          }
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { stdout } = await execa('netstat', ['-ano']);
const lines = stdout.split('\n');
const regex = new RegExp(`TCP.*:${port}.*LISTENING\\s+(\\d+)`, 'i');
for (const line of lines) {
const match = line.match(regex);
if (match && match[1]) {
pids.push(match[1]);
}
}
} catch {
const { stdout } = await execa('netstat', ['-ano']);
const lines = stdout.split('\n');
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (
parts.length >= 5 &&
parts[0]?.toUpperCase().startsWith('TCP') &&
parts[1]?.endsWith(`:${port}`) &&
parts[3]?.toUpperCase() === 'LISTENING'
) {
pids.push(parts[4]);
}
}
} catch {
🧰 Tools
🪛 ast-grep (0.39.9)

[warning] 15-15: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(TCP.*:${port}.*LISTENING\\s+(\\d+), 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
In scripts/utils/kill-process-on-port.ts around lines 14–24, the netstat parsing
regex can match substrings of ports (e.g., 6001 matching 60010), so replace the
regex approach with column-based parsing: split each netstat line on whitespace,
take the Local Address column (the second column), check that it ends exactly
with `:${port}` (so IPv4/IPV6 forms like [::]:80 still match), and only then
read the PID from the last column (or columns[columns.length-1]) after
validating it is numeric before pushing to pids; this prevents killing processes
bound to ports that merely contain the queried port as a substring.

// netstat failed, ignore
}
} else {
// Unix-like (macOS, Linux): use lsof
try {
const { stdout } = await execa('lsof', ['-ti', `:${port}`]);
pids = stdout.trim().split('\n').filter(Boolean);
} catch {
// lsof failed or no process found
}
}

if (pids.length > 0) {
console.log(`☠️ killing process(es) on port ${port}: ${pids.join(', ')}`);

if (isWindows) {
await Promise.all(
pids.map((pid) => execa('taskkill', ['/PID', pid, '/F']).catch(() => {}))
);
} else {
await Promise.all(pids.map((pid) => execa('kill', ['-9', pid]).catch(() => {})));
Comment on lines +37 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Swap to Storybook’s node logger

Per our scripts guidelines we should log through storybook/internal/node-logger, not console.log. Please route this message through the shared logger for consistency. As per coding guidelines

 import { execa } from 'execa';
+import { logger } from 'storybook/internal/node-logger';
@@
-      console.log(`☠️ killing process(es) on port ${port}: ${pids.join(', ')}`);
+      logger.info(`☠️ killing process(es) on port ${port}: ${pids.join(', ')}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (pids.length > 0) {
console.log(`☠️ killing process(es) on port ${port}: ${pids.join(', ')}`);
if (isWindows) {
await Promise.all(
pids.map((pid) => execa('taskkill', ['/PID', pid, '/F']).catch(() => {}))
);
} else {
await Promise.all(pids.map((pid) => execa('kill', ['-9', pid]).catch(() => {})));
import { execa } from 'execa';
import { logger } from 'storybook/internal/node-logger';
// ... other code ...
if (pids.length > 0) {
logger.info(`☠️ killing process(es) on port ${port}: ${pids.join(', ')}`);
if (isWindows) {
await Promise.all(
pids.map((pid) => execa('taskkill', ['/PID', pid, '/F']).catch(() => {}))
);
} else {
await Promise.all(pids.map((pid) => execa('kill', ['-9', pid]).catch(() => {})));
}
}
🤖 Prompt for AI Agents
In scripts/utils/kill-process-on-port.ts around lines 37 to 45, replace the
console.log call with Storybook’s shared node logger: import the logger from
'storybook/internal/node-logger' at the top of the file and use logger.info (or
logger.warn) to emit the "killing process(es) on port" message, keeping the same
string interpolation; leave the rest of the PID-killing logic unchanged and
ensure the import is added only once.

}

// Give the OS a moment to release the port
await new Promise((resolve) => setTimeout(resolve, 500));
}
} catch {
// No process found on port or command failed, which is fine
}
};
Loading