0

I was trying to launch chromium browser using remote-debugging-pipe in python, but there is a problem everytime I tried to do so,

I am using this code, I got help from gemini and chatgpt but they don't have enough data to help

here is my code

import os
import json
import subprocess
import select
import sys
# Create two pipes:
# 1) Parent writes to write_fd_w, Chrome reads from fd 3
# 2) Chrome writes to fd 4, parent reads from read_fd_r

read_fd_r, write_fd_w = os.pipe() # Chrome writes here (FD 4), parent reads here
write_fd_r, read_fd_w = os.pipe() # Parent writes here, Chrome reads here (FD 3)

# Function to duplicate FDs inside child process
def preexec():
    # Close parent's ends in child and dup to fd 3 and 4
    os.close(read_fd_r)
    os.close(write_fd_w)
    os.dup2(read_fd_w, 3) # Chrome reads commands on fd 3
    os.dup2(write_fd_r, 4) # Chrome writes responses on fd 4
    # Close original fds after dup
    os.close(read_fd_w)
    os.close(write_fd_r)

# Launch Chromium with remote debugging pipe on fds 3 (input) and 4 (output)
proc = subprocess.Popen(
    ['/Applications/Chromium.app/Contents/MacOS/Chromium', '--headless', '--remote-debugging-pipe'],
    stdin=subprocess.DEVNULL,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.PIPE,
    # capture stderr for debugging
    preexec_fn=preexec,
    pass_fds=(read_fd_w, write_fd_r)
) # Close child-side ends in parent
os.close(read_fd_w)
os.close(write_fd_r)

def send_message(message_id, method, params=None):
    message = {
        "id": message_id,
        "method": method,
    }
    if params:
        message["params"] = params
    json_str = json.dumps(message) + '\0' # Null-terminated
    os.write(write_fd_w, json_str.encode('utf-8'))

def read_message(timeout=5.0):
    buffer = b""
    while True:
        rlist, _, _ = select.select([read_fd_r], [], [], timeout)
        if not rlist:
            raise TimeoutError("Timeout waiting for message")
        byte = os.read(read_fd_r, 1)
        if byte == b'\0':
            break
        if not byte:
            raise EOFError("Chromium closed pipe")
        buffer += byte
    return json.loads(buffer.decode('utf-8'))

try:
    send_message(1, "Page.enable")
    print("Response to Page.enable:", read_message())
    send_message(2, "Page.navigate", {"url": "https://example.com"})
    print("Response to Page.navigate:", read_message())
except Exception as e:
    print("Error:", e, file=sys.stderr)
    # Read Chromium stderr output
    err = proc.stderr.read().decode('utf-8')
    print("Chromium stderr:\n", err, file=sys.stderr)
finally:
    proc.terminate()
    proc.wait()
    os.close(read_fd_r)
    os.close(write_fd_w)

using this script, I only receives my own input back, the response I receive is this

Response to Page.enable: {'id': 1, 'method': 'Page.enable'}
Response to Page.navigate: {'id': 2, 'method': 'Page.navigate', 'params': {'url': 'https://example.com'}}

like my inputs don't reach chromium at all

if I change the code to this

import os
import json
import subprocess
import select
import sys

# Corrected pipe setup for clarity:
# 1) Parent writes to Chromium's read-pipe (fd 3)
# 2) Chromium writes to parent's read-pipe (fd 4)

# Pipe 1: Parent writes here, Chromium reads here (FD 3)
parent_write_fd, chromium_read_fd = os.pipe() 

# Pipe 2: Chromium writes here, Parent reads here (FD 4)
chromium_write_fd, parent_read_fd = os.pipe() 

# Function to duplicate FDs inside child process
def preexec():
    # Close parent's ends of the pipes
    os.close(parent_write_fd)
    os.close(parent_read_fd)
    
    # Duplicate FDs to match Chromium's expectations
    os.dup2(chromium_read_fd, 3) # Chrome reads commands on fd 3
    os.dup2(chromium_write_fd, 4) # Chrome writes responses on fd 4
    
    # Close original FDs after duplication
    os.close(chromium_read_fd)
    os.close(chromium_write_fd)

# Launch Chromium with remote debugging pipe on fds 3 (input) and 4 (output)
proc = subprocess.Popen(
    ['/Applications/Chromium.app/Contents/MacOS/Chromium', '--headless', '--remote-debugging-pipe'],
    stdin=subprocess.DEVNULL,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.PIPE,
    preexec_fn=preexec,
    pass_fds=(chromium_read_fd, chromium_write_fd)
) 

# Close child-side ends in parent
os.close(chromium_read_fd)
os.close(chromium_write_fd)

def send_message(message_id, method, params=None):
    message = {
        "id": message_id,
        "method": method,
    }
    if params:
        message["params"] = params
    json_str = json.dumps(message) + '\0' # Null-terminated
    os.write(parent_write_fd, json_str.encode('utf-8'))

def read_message(timeout=5.0):
    buffer = b""
    while True:
        rlist, _, _ = select.select([parent_read_fd], [], [], timeout)
        if not rlist:
            raise TimeoutError("Timeout waiting for message")
        byte = os.read(parent_read_fd, 1)
        if byte == b'\0':
            break
        if not byte:
            raise EOFError("Chromium closed pipe")
        buffer += byte
    return json.loads(buffer.decode('utf-8'))

try:
    send_message(1, "Page.enable")
    print("Response to Page.enable:", read_message())
    send_message(2, "Page.navigate", {"url": "https://example.com"})
    print("Response to Page.navigate:", read_message())
except Exception as e:
    print("Error:", e, file=sys.stderr)
    err = proc.stderr.read().decode('utf-8')
    print("Chromium stderr:\n", err, file=sys.stderr)
finally:
    proc.terminate()
    proc.wait()
    os.close(parent_read_fd)
    os.close(parent_write_fd)

it gives the following error

Error: [Errno 9] Bad file descriptor
Chromium stderr:
 [0914/135115.337200:ERROR:chrome/app/chrome_main_delegate.cc:1082] Remote debugging pipe file descriptors are not open.

I tried to find out how playwright is doing this by looking at their code from github, but I think they are using node js under their python wrapper

1 Answer 1

1

Maybe this code will be helpful to you.

import os
import json
import subprocess
import select
import sys
import time

parent_write, child_read = os.pipe()
child_write, parent_read = os.pipe()

def preexec():
    os.dup2(child_read, 3)
    os.dup2(child_write, 4)
    if child_read not in (3, 4):
        os.close(child_read)
    if child_write not in (3, 4):
        os.close(child_write)

proc = subprocess.Popen(
    [
        '/Applications/Chromium.app/Contents/MacOS/Chromium',
        '--headless',
        '--remote-debugging-pipe',
        '--no-first-run',
        '--disable-gpu'
    ],
    stdin=subprocess.DEVNULL,
    stdout=subprocess.DEVNULL,
    stderr=subprocess.PIPE,
    preexec_fn=preexec,
    pass_fds=(child_read, child_write)
)

os.close(child_read)
os.close(child_write)

def send_message(message_id, method, params=None):
    message = {"id": message_id, "method": method}
    if params:
        message["params"] = params
    data = (json.dumps(message) + '\0').encode('utf-8')
    os.write(parent_write, data)

def read_message(timeout=5.0):
    buf = bytearray()
    start = time.time()
    while True:
        now = time.time()
        remaining = max(0.0, timeout - (now - start))
        if remaining == 0.0:
            raise TimeoutError("timeout waiting for message")
        rlist, _, _ = select.select([parent_read], [], [], remaining)
        if not rlist:
            raise TimeoutError("timeout waiting for message")
        b = os.read(parent_read, 1)
        if not b:
            raise EOFError("chromium closed pipe")
        if b == b'\0':
            break
        buf.extend(b)
    return json.loads(buf.decode('utf-8'))

try:
    try:
        time.sleep(0.1)
        while True:
            msg = read_message(timeout=0.01)
            print("startup event:", msg)
    except TimeoutError:
        pass

    send_message(1, "Page.enable")
    resp = read_message(timeout=5.0)
    print("Response to Page.enable:", resp)

    send_message(2, "Page.navigate", {"url": "https://example.com"})
    while True:
        resp = read_message(timeout=5.0)
        print("event:", resp)
        if isinstance(resp, dict) and resp.get("id") == 2:
            print("Response to Page.navigate:", resp)
            break

except Exception as e:
    print("Error:", e, file=sys.stderr)
    try:
        err = proc.stderr.read().decode('utf-8', errors='ignore')
        print("Chromium stderr:\n", err, file=sys.stderr)
    except Exception:
        pass
finally:
    try:
        proc.terminate()
        proc.wait(timeout=2)
    except Exception:
        pass
    try:
        os.close(parent_read)
    except Exception:
        pass
    try:
        os.close(parent_write)
    except Exception:
        pass

Sign up to request clarification or add additional context in comments.

1 Comment

it still gives this error Error: [Errno 9] Bad file descriptor Chromium stderr: [0914/184332.772905:ERROR:chrome/app/chrome_main_delegate.cc:1082] Remote debugging pipe file descriptors are not open.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.