3434import { PassThrough } from 'node:stream' ;
3535import { createReadStream } from 'node:fs' ;
3636import { readFile } from 'node:fs/promises' ;
37+ import { createServer } from 'node:net' ;
3738import * as path from 'node:path' ;
3839import Docker from 'dockerode' ;
3940import * as tar from 'tar-stream' ;
@@ -215,6 +216,22 @@ async function statInProbe(container: Docker.Container, containerPath: string):
215216 return parseProbeStatResponse ( response ) ;
216217}
217218
219+ /** A TCP port that is free in this process's own network namespace right now - bound on loopback and
220+ * released again, for a sidecar about to share that namespace (`startBrowserViewer`). */
221+ async function allocateFreePort ( ) : Promise < number > {
222+ return new Promise ( ( resolve , reject ) => {
223+ const server = createServer ( ) ;
224+ server . once ( 'error' , reject ) ;
225+ server . listen ( 0 , '127.0.0.1' , ( ) => {
226+ const address = server . address ( ) ;
227+ server . close ( ( ) => {
228+ if ( address && typeof address === 'object' ) resolve ( address . port ) ;
229+ else reject ( new Error ( 'Could not allocate a free port for the browser-view sidecar' ) ) ;
230+ } ) ;
231+ } ) ;
232+ } ) ;
233+ }
234+
218235/**
219236 * Maps the `linkTarget` a symlink stat reports back to a host path. Docker resolves a link's target in
220237 * the scope of the container's root filesystem and reports that absolute container path: a target that
@@ -542,8 +559,8 @@ export class DockerDriver implements Driver {
542559 `Could not attach the runtime's own container to the ${ NETWORK_NAME } network: ${ ( error as Error ) . message } . ` +
543560 `Actor containers will reach this API through the host's published port ${ API_PORT } instead ` +
544561 `(${ CONTAINER_API_ALIAS } -> host-gateway), so keep -p ${ API_PORT } :${ API_PORT } published on all interfaces. ` +
545- `Browser view needs the direct route: pre-create the network (\`podman network create ${ NETWORK_NAME } \`) ` +
546- `and start the runtime container with \`--network ${ NETWORK_NAME } \`, which also restores the alias .` ,
562+ `To use the network alias anyway, pre-create the network (\`podman network create ${ NETWORK_NAME } \`) ` +
563+ `and start the runtime container with \`--network ${ NETWORK_NAME } \`.` ,
547564 ) ;
548565 }
549566 }
@@ -1191,6 +1208,15 @@ export class DockerDriver implements Driver {
11911208 Labels : labels ,
11921209 } ) ;
11931210
1211+ // How the console reaches the sidecar's VNC server. Normally the sidecar joins `apify-local` and is
1212+ // reached by its address there. When this process runs in a container that could not join that
1213+ // network (`apiReachableByAlias`'s doc comment - rootless Podman), the sidecar shares this
1214+ // container's own network namespace instead, so the console reaches it on localhost; every sidecar
1215+ // then needs a port of its own in that shared namespace, allocated here where it will be used.
1216+ const selfContainerId = process . env . HOSTNAME ;
1217+ const sharesRuntimeNetns = ! this . apiReachableByAlias && ! ! selfContainerId ;
1218+ const vncPort = sharesRuntimeNetns ? await allocateFreePort ( ) : BROWSER_VIEWER_VNC_PORT ;
1219+
11941220 let container : Docker . Container | undefined ;
11951221 try {
11961222 container = await this . docker . createContainer ( {
@@ -1199,11 +1225,11 @@ export class DockerDriver implements Driver {
11991225 Cmd : [ '/bin/sh' , BROWSER_VIEWER_SCRIPT ] ,
12001226 Env : [
12011227 `${ BROWSER_VIEWER_INTERACTIVE_ENV } =${ target . interactive ? '1' : '0' } ` ,
1202- `${ BROWSER_VIEWER_PORT_ENV } =${ BROWSER_VIEWER_VNC_PORT } ` ,
1228+ `${ BROWSER_VIEWER_PORT_ENV } =${ vncPort } ` ,
12031229 ] ,
12041230 Labels : labels ,
12051231 HostConfig : {
1206- NetworkMode : NETWORK_NAME ,
1232+ NetworkMode : sharesRuntimeNetns ? `container: ${ selfContainerId } ` : NETWORK_NAME ,
12071233 Memory : BROWSER_VIEWER_MEMORY_BYTES ,
12081234 AutoRemove : false ,
12091235 Mounts : [ { Type : 'volume' , Source : volumeName , Target : X11_SOCKET_DIR } ] ,
@@ -1213,12 +1239,15 @@ export class DockerDriver implements Driver {
12131239 this . browserViewers . set ( target . runId , { container, volumeName } ) ;
12141240 await container . start ( ) ;
12151241
1242+ if ( sharesRuntimeNetns ) {
1243+ return { vncHost : '127.0.0.1' , vncPort, x11SocketVolume : volumeName } ;
1244+ }
12161245 const info = await container . inspect ( ) ;
12171246 const address = info . NetworkSettings ?. Networks ?. [ NETWORK_NAME ] ?. IPAddress ;
12181247 return {
12191248 // The IP also works from a runtime running outside Docker; the name only resolves from inside.
12201249 vncHost : address || containerName ,
1221- vncPort : BROWSER_VIEWER_VNC_PORT ,
1250+ vncPort,
12221251 x11SocketVolume : volumeName ,
12231252 } ;
12241253 } catch ( error ) {
0 commit comments