~cytrogen/gstack

ref: cdd6f7865d0edf741f658a256115cbf77dace61b gstack/browse/test/findport.test.ts -rw-r--r-- 6.2 KiB
cdd6f786 — Garry Tan feat: community wave — 7 fixes, relink, sidebar Write, discoverability (v0.13.5.0) (#641) 10 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import { describe, test, expect } from 'bun:test';
import * as net from 'net';
import * as path from 'path';

const polyfillPath = path.resolve(import.meta.dir, '../src/bun-polyfill.cjs');

// Helper: bind a port and hold it open, returning a cleanup function
function occupyPort(port: number): Promise<() => Promise<void>> {
  return new Promise((resolve, reject) => {
    const srv = net.createServer();
    srv.once('error', reject);
    srv.listen(port, '127.0.0.1', () => {
      resolve(() => new Promise<void>((r) => srv.close(() => r())));
    });
  });
}

// Helper: find a known-free port by binding to 0
function getFreePort(): Promise<number> {
  return new Promise((resolve, reject) => {
    const srv = net.createServer();
    srv.once('error', reject);
    srv.listen(0, '127.0.0.1', () => {
      const port = (srv.address() as net.AddressInfo).port;
      srv.close(() => resolve(port));
    });
  });
}

describe('findPort / isPortAvailable', () => {

  test('isPortAvailable returns true for a free port', async () => {
    // Use the same isPortAvailable logic from server.ts
    const port = await getFreePort();

    const available = await new Promise<boolean>((resolve) => {
      const srv = net.createServer();
      srv.once('error', () => resolve(false));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(true));
      });
    });

    expect(available).toBe(true);
  });

  test('isPortAvailable returns false for an occupied port', async () => {
    const port = await getFreePort();
    const release = await occupyPort(port);

    try {
      const available = await new Promise<boolean>((resolve) => {
        const srv = net.createServer();
        srv.once('error', () => resolve(false));
        srv.listen(port, '127.0.0.1', () => {
          srv.close(() => resolve(true));
        });
      });

      expect(available).toBe(false);
    } finally {
      await release();
    }
  });

  test('port is actually free after isPortAvailable returns true', async () => {
    // This is the core race condition test: after isPortAvailable says
    // a port is free, can we IMMEDIATELY bind to it?
    const port = await getFreePort();

    // Simulate isPortAvailable
    const isFree = await new Promise<boolean>((resolve) => {
      const srv = net.createServer();
      srv.once('error', () => resolve(false));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(true));
      });
    });

    expect(isFree).toBe(true);

    // Now immediately try to bind — this would fail with the old
    // Bun.serve() polyfill approach because the test server's
    // listen() would still be pending
    const canBind = await new Promise<boolean>((resolve) => {
      const srv = net.createServer();
      srv.once('error', () => resolve(false));
      srv.listen(port, '127.0.0.1', () => {
        srv.close(() => resolve(true));
      });
    });

    expect(canBind).toBe(true);
  });

  test('polyfill Bun.serve stop() is fire-and-forget (async)', async () => {
    // Verify that the polyfill's stop() does NOT wait for the socket
    // to actually close — this is the root cause of the race condition.
    // On macOS/Linux the OS reclaims the port fast enough that the race
    // rarely manifests, but on Windows TIME_WAIT makes it 100% repro.
    const result = Bun.spawnSync(['node', '-e', `
      require('${polyfillPath}');
      const net = require('net');

      async function test() {
        const port = 10000 + Math.floor(Math.random() * 50000);

        const testServer = Bun.serve({
          port,
          hostname: '127.0.0.1',
          fetch: () => new Response('ok'),
        });

        // stop() returns undefined — it does NOT return a Promise,
        // so callers cannot await socket teardown
        const retval = testServer.stop();
        console.log(typeof retval === 'undefined' ? 'FIRE_AND_FORGET' : 'AWAITABLE');
      }

      test();
    `], { stdout: 'pipe', stderr: 'pipe' });

    const output = result.stdout.toString().trim();
    // Confirms the polyfill's stop() is fire-and-forget — callers
    // cannot wait for the port to be released, hence the race
    expect(output).toBe('FIRE_AND_FORGET');
  });

  test('net.createServer approach does not have the race condition', async () => {
    // Prove the fix: net.createServer with proper async bind/close
    // releases the port cleanly
    const result = Bun.spawnSync(['node', '-e', `
      const net = require('net');

      async function testFix() {
        const port = 10000 + Math.floor(Math.random() * 50000);

        // Simulate the NEW isPortAvailable: proper async bind/close
        const isFree = await new Promise((resolve) => {
          const srv = net.createServer();
          srv.once('error', () => resolve(false));
          srv.listen(port, '127.0.0.1', () => {
            srv.close(() => resolve(true));
          });
        });

        if (!isFree) {
          console.log('PORT_BUSY');
          return;
        }

        // Immediately try to bind — should succeed because close()
        // completed before the Promise resolved
        const canBind = await new Promise((resolve) => {
          const srv = net.createServer();
          srv.once('error', () => resolve(false));
          srv.listen(port, '127.0.0.1', () => {
            srv.close(() => resolve(true));
          });
        });

        console.log(canBind ? 'FIX_WORKS' : 'FIX_BROKEN');
      }

      testFix();
    `], { stdout: 'pipe', stderr: 'pipe' });

    const output = result.stdout.toString().trim();
    expect(output).toBe('FIX_WORKS');
  });

  test('isPortAvailable handles rapid sequential checks', async () => {
    // Stress test: check the same port multiple times in sequence
    const port = await getFreePort();
    const results: boolean[] = [];

    for (let i = 0; i < 5; i++) {
      const available = await new Promise<boolean>((resolve) => {
        const srv = net.createServer();
        srv.once('error', () => resolve(false));
        srv.listen(port, '127.0.0.1', () => {
          srv.close(() => resolve(true));
        });
      });
      results.push(available);
    }

    // All 5 checks should succeed — no leaked sockets
    expect(results).toEqual([true, true, true, true, true]);
  });
});