פתרון בעיות: Puppeteer

Chrome ללא ממשק גרפי לא מופעל ב-Windows

כללי מדיניות מסוימים של Chrome עשויים לאלץ את ההפעלה של Chrome או של Chromium עם תוספים מסוימים.

ה-Puppeteer מעביר את הדגל --disable-extensions כברירת מחדל, ולכן הוא לא פועל כאשר כללי מדיניות כאלה פעילים.

כדי לעקוף את הבעיה, אפשר לנסות לפעול בלי הדגל:

const browser = await puppeteer.launch({
  ignoreDefaultArgs: ['--disable-extensions'],
});

הקשר: issue 3681.

Chrome ללא ממשק גרפי לא מופעל ב-UNIX

צריך לוודא שכל יחסי התלות הנחוצים מותקנים. אפשר להריץ את ldd chrome | grep not במחשב Linux כדי לבדוק אילו יחסי תלות חסרים.

יחסי תלות של Debian (Ubuntu)

ca-certificates
fonts-liberation
libappindicator3-1
libasound2
libatk-bridge2.0-0
libatk1.0-0
libc6
libcairo2
libcups2
libdbus-1-3
libexpat1
libfontconfig1
libgbm1
libgcc1
libglib2.0-0
libgtk-3-0
libnspr4
libnss3
libpango-1.0-0
libpangocairo-1.0-0
libstdc++6
libx11-6
libx11-xcb1
libxcb1
libxcomposite1
libxcursor1
libxdamage1
libxext6
libxfixes3
libxi6
libxrandr2
libxrender1
libxss1
libxtst6
lsb-release
wget
xdg-utils

תלות של CentOS

alsa-lib.x86_64
atk.x86_64
cups-libs.x86_64
gtk3.x86_64
ipa-gothic-fonts
libXcomposite.x86_64
libXcursor.x86_64
libXdamage.x86_64
libXext.x86_64
libXi.x86_64
libXrandr.x86_64
libXScrnSaver.x86_64
libXtst.x86_64
pango.x86_64
xorg-x11-fonts-100dpi
xorg-x11-fonts-75dpi
xorg-x11-fonts-cyrillic
xorg-x11-fonts-misc
xorg-x11-fonts-Type1
xorg-x11-utils

אחרי שמתקינים יחסי תלות צריך לעדכן את ספריית nss באמצעות הפקודה הזו

yum update nss -y

עיינו בדיונים:

  • #290 - פתרון בעיות ב-Debian
  • #391 - פתרון בעיות ב-CentOS
  • #379 - פתרון בעיות באלפים

Chrome ללא ממשק גרפי משבית את איחוד ה-GPU

ל-Chrome ול-Chromium נדרש --use-gl=egl כדי להפעיל שיפור מהירות באמצעות GPU במצב 'דפדפן ללא GUI'.

const browser = await puppeteer.launch({
  headless: true,
  args: ['--use-gl=egl'],
});

מתבצעת הורדה של Chrome, אבל הפעלתו ב-Node.js נכשלה

אם מופיעה הודעת שגיאה שנראית כך כשמנסים להפעיל את Chromium:

(node:15505) UnhandledPromiseRejectionWarning: Error: Failed to launch the browser process!
spawn /Users/.../node_modules/puppeteer/.local-chromium/mac-756035/chrome-mac/Chromium.app/Contents/MacOS/Chromium ENOENT

המשמעות היא שהדפדפן הורד אבל החילוץ נכשל. הסיבה הנפוצה ביותר היא באג ב-Node.js בגרסה 14.0.0 ששבר את extract-zip, והמודול Puppeteer משתמש בו כדי לחלץ הורדות של הדפדפן למקום הנכון. הבאג תוקן ב-Node.js בגרסה 14.1.0, לכן חשוב לוודא שאתם משתמשים בגרסה הזו ואילך.

הגדרת ארגז חול של Chrome Linux

כדי להגן על סביבת המארח מפני תוכן אינטרנט לא מהימן, ב-Chrome משתמשים בשכבות מרובות של הרצה בארגז חול. כדי שהקטע הזה יפעל כראוי, קודם צריך להגדיר את המארח. אם אין ארגז חול (sandbox) מתאים לשימוש ב-Chrome, הוא יקרוס עם השגיאה No usable sandbox!.

אם אתם סומכים לחלוטין על התוכן שאתם פותחים ב-Chrome, תוכלו להפעיל את Chrome באמצעות הארגומנט --no-sandbox:

const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox'],
});

יש שתי דרכים להגדיר ארגז חול ב-Chromium.

שכפול מרחב שמות Sser נתמך רק בליבות מבוססות מודרניות. בדרך כלל אפשר להפעיל מרחבי שמות של משתמשים ללא הרשאות, אבל הם יכולים לפתוח שטח התקפה נוסף בליבה עבור תהליכים שאינם בארגז החול (ללא ארגז חול) כדי להעלות אותם להרשאות ליבה.

sudo sysctl -w kernel.unprivileged_userns_clone=1

[חלופה] הגדרת ארגז חול של הגדרה

ארגז החול setuid מגיע כקובץ הפעלה עצמאי וממוקם לצד Chromium שממנו ה-Puppeteer מוריד. אפשר להשתמש שוב באותו קובץ הפעלה ב-Sandbox בגרסאות שונות של Chromium, לכן אפשר לבצע את הפעולות הבאות פעם אחת בלבד בכל סביבת מארח:

# cd to the downloaded instance
cd <project-dir-path>/node_modules/puppeteer/.local-chromium/linux-<revision>/chrome-linux/
sudo chown root:root chrome_sandbox
sudo chmod 4755 chrome_sandbox
# copy sandbox executable to a shared location
sudo cp -p chrome_sandbox /usr/local/sbin/chrome-devel-sandbox
# export CHROME_DEVEL_SANDBOX env variable
export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

כדאי לייצא את משתנה ה-env CHROME_DEVEL_SANDBOX כברירת מחדל. במקרה כזה, צריך להוסיף את הקוד הבא ל-~/.bashrc או ל-.zshenv:

export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox

הפעלת Puppeteer ב-Travis CI

ביצענו את הבדיקות של Puppeteer ב-Travis CI עד גרסה 6.0.0, ולאחר מכן עברנו ל-GitHub Actions. תוכלו לעיין במאמר .travis.yml (גרסה 5.5.0).

הנה כמה שיטות עבודה מומלצות:

  • יש להפעיל את שירות xvfb כדי להפעיל את Chromium במצב ללא אוזניות
  • פועל ב-Xenial Linux ב-Travis כברירת מחדל
  • פועל ב-npm install כברירת מחדל
  • הקובץ node_modules נשמר במטמון כברירת מחדל

.travis.yml עשוי להיראות כך:

language: node_js
node_js: node
services: xvfb

script:
  - npm run test

הפעלת Puppeteer ב-CircleCI

  1. מתחילים עם תמונת NodeJS בהגדרות. yaml docker: - image: circleci/node:14 # Use your desired version environment: NODE_ENV: development # Only needed if puppeteer is in `devDependencies`
  2. ככל הנראה תלויים כמו libXtst6 צריך להתקין בעזרת apt-get, לכן צריך להשתמש ב-hirtreeslight/puppeteer (instructions) או להדביק חלקים מהמקור בהגדרה שלכם.
  3. לסיום, אם משתמשים ב-Puppeteer דרך Jest, יכול להיות שתוצג שגיאה בהובלה של תהליכים צאצאים: shell [00:00.0] jest args: --e2e --spec --max-workers=36 Error: spawn ENOMEM at ChildProcess.spawn (internal/child_process.js:394:11) סביר להניח שהסיבה לכך היא שהמערכת של Jest זיהתה אוטומטית את מספר התהליכים במחשב כולו (36) ולא את המספר המותר במאגר התגים (2). כדי לתקן את הבעיה, צריך להגדיר את jest --maxWorkers=2 בפקודת הבדיקה.

הפעילו את Puppeteer ב-Docker

לא פשוט להפעיל את Chrome ללא GUI ב-Docker. בחבילה של Chromium שמתקינים את Puppeteer חסרים יחסי התלות הנחוצים של הספרייה המשותפת.

כדי לפתור את הבעיה, צריך להתקין את יחסי התלות החסרים ואת חבילת Chromium העדכנית ב-Dockerfile:

FROM node:14-slim

# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
# Note: this installs the necessary libs to make the bundled version of Chromium that Puppeteer
# installs, work.
RUN apt-get update \
    && apt-get install -y wget gnupg \
    && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
    && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
    && apt-get update \
    && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
      --no-install-recommends \
    && rm -rf /var/lib/apt/lists/*

# If running Docker >= 1.13.0 use docker run's --init arg to reap zombie processes, otherwise
# uncomment the following lines to have `dumb-init` as PID 1
# ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_x86_64 /usr/local/bin/dumb-init
# RUN chmod +x /usr/local/bin/dumb-init
# ENTRYPOINT ["dumb-init", "--"]

# Uncomment to skip the chromium download when installing puppeteer. If you do,
# you'll need to launch puppeteer with:
#     browser.launch({executablePath: 'google-chrome-stable'})
# ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true

# Install puppeteer so it's available in the container.
RUN npm init -y &&  \
    npm i puppeteer \
    # Add user so we don't need --no-sandbox.
    # same layer as npm install to keep re-chowned files from using up several hundred MBs more space
    && groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
    && mkdir -p /home/pptruser/Downloads \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /node_modules \
    && chown -R pptruser:pptruser /package.json \
    && chown -R pptruser:pptruser /package-lock.json

# Run everything after as non-privileged user.
USER pptruser

CMD ["google-chrome-stable"]

יוצרים את הקונטיינר:

docker build -t puppeteer-chrome-linux .

כדי להפעיל את הקונטיינר, מעבירים את הפקודה node -e "<yourscript.js content as a string>" בתור הפקודה:

 docker run -i --init --rm --cap-add=SYS_ADMIN \
   --name puppeteer-chrome puppeteer-chrome-linux \
   node -e "`cat yourscript.js`"

בכתובת https://github.com/ebidel/try-puppeteer יש דוגמה מלאה שמראה איך להפעיל את ה-Dockerfile משרת אינטרנט שפועל ב-App Engine Flex (Node).

ריצה באלפים

חבילת Chromium החדשה ביותר שנתמכת ב-Alpine היא 100, שתואמת לPuppeteer גרסה 13.5.0.

דוגמה לקובץ Docker:

FROM alpine

# Installs latest Chromium (100) package.
RUN apk add --no-cache \
      chromium \
      nss \
      freetype \
      harfbuzz \
      ca-certificates \
      ttf-freefont \
      nodejs \
      yarn

...

# Tell Puppeteer to skip installing Chrome. We'll be using the installed package.
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
    PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser

# Puppeteer v13.5.0 works with Chromium 100.
RUN yarn add puppeteer@13.5.0

# Add user so we don't need --no-sandbox.
RUN addgroup -S pptruser && adduser -S -G pptruser pptruser \
    && mkdir -p /home/pptruser/Downloads /app \
    && chown -R pptruser:pptruser /home/pptruser \
    && chown -R pptruser:pptruser /app

# Run everything after as non-privileged user.
USER pptruser

...

שיטות מומלצות עם Docker

כברירת מחדל, Docker מריץ קונטיינר עם נפח זיכרון משותף של /dev/shm בנפח 64MB. השם הזה בדרך כלל קטן מדי ל-Chrome והוא יגרום ל-Chrome לקרוס במהלך רינדור דפים גדולים. כדי לפתור את הבעיה, מריצים את הקונטיינר עם docker run --shm-size=1gb כדי להגדיל את /dev/shm. אין יותר צורך בכך החל מגרסה 65 של Chrome. במקום זאת, הפעילו את הדפדפן עם הדגל --disable-dev-shm-usage:

const browser = await puppeteer.launch({
  args: ['--disable-dev-shm-usage'],
});

הפעולה הזו כותבת קובצי זיכרון משותפים ב-/tmp במקום ב-/dev/shm. מעיינים בכתובת crbug.com/736452.

האם מופיעות שגיאות מוזרות נוספות בעת הפעלת Chrome? כשאתם מפתחים באופן מקומי, כדאי להריץ את הקונטיינר באמצעות docker run --cap-add=SYS_ADMIN. מכיוון שקובץ Docker מוסיף משתמש pptr כמשתמש ללא הרשאות, יכול להיות שאין לו את כל ההרשאות הנדרשות.

dumb-init כדאי לבדוק אם נתקלת בהרבה זומבים תהליכים של Chrome שנמשכים מסביב. יש טיפול מיוחד בתהליכים ב-PID=1, ולכן קשה לסגור את Chrome בצורה נכונה במקרים מסוימים (למשל באמצעות Docker).

מפעילים את Puppeteer בענן

ב-Google App Engine

זמן הריצה של Node.js של הסביבה הסטנדרטית של App Engine כולל את כל חבילות המערכת שדרושות להפעלת Chrome ללא ממשק גרפי.

כדי להשתמש ב-puppeteer, צריך לרשום את המודול בתור תלות ב-package.json ולפרוס אותו ב-Google App Engine. מידע נוסף על השימוש ב-puppeteer ב-App Engine זמין במדריך הרשמי.

ב-Google Cloud Functions

זמן הריצה של Node.js 10 של Google Cloud Functions כולל את כל חבילות המערכת שדרושות לצורך הפעלת Chrome ללא ממשק גרפי.

כדי להשתמש ב-puppeteer, צריך לרשום את המודול כתלות ב-package.json ולפרוס את הפונקציה ב-Google Cloud Functions באמצעות זמן הריצה של nodejs10.

הפעלת Puppeteer ב-Google Cloud Run

זמן הריצה של Node.js המוגדר כברירת מחדל ל-Google Cloud Run לא כולל את חבילות המערכת שדרושות להפעלת Chrome ללא ממשק גרפי. מגדירים Dockerfile משלכם וכוללים את יחסי התלות החסרים.

על הרוקו

ההפעלה של Puppeteer ב-Heroku מצריכה יחסי תלות נוספים שלא נכללים בתיבת Linux שה-Heroku מייצר. כדי להוסיף את יחסי התלות בפריסה, מוסיפים את ה-buildpack של Puppeteer Heroku לרשימת ה-buildpacks באפליקציה בקטע 'הגדרות' > 'Buildpacks'.

כתובת ה-URL של ה-buildpack היא https://github.com/jontewks/puppeteer-heroku-buildpack

חשוב להשתמש במצב '--no-sandbox' כשמפעילים את Puppeteer. כדי לעשות זאת, אפשר להעביר אותה כארגומנט לקריאה של .launch(): puppeteer.launch({ args: ['--no-sandbox'] });.

כשלוחצים על Addpack (הוספת buildpack), מדביקים את כתובת ה-URL הזו בקלט ולוחצים על Save (שמירה). בפריסה הבאה, האפליקציה תתקין גם את יחסי התלות שדרושים להפעלת Puppeteer.

אם אתם צריכים לעבד תווים בסינית, ביפנית או בקוריאנית, ייתכן שתצטרכו להשתמש ב-buildpack עם קובצי גופנים נוספים כמו https://github.com/CoffeeAndCode/puppeteer-heroku-buildpack

יש גם מדריך נוסף מ- @timleland שכולל פרויקט לדוגמה.

ב-AWS Lambda

AWS Lambda מגביל את הגודל של חבילות הפריסה לכ-50MB. כתוצאה מכך, ההפעלה של Chrome ללא דפדפן GUI (ולכן גם Puppeteer) ב-Lambda. חברי הקהילה גיבשו כמה משאבים לפתרון הבעיות:

מופע של AWS EC2 עם Amazon-Linux

אם יש לכם מכונה של EC2 שמריצה את Amazon-linux בצינור עיבוד הנתונים של CI/CD, ואתם רוצים להריץ בדיקות של Puppeteer ב-Amazon-linux, צריך לבצע את הפעולות הבאות.

  1. כדי להתקין את Chromium צריך קודם להפעיל את amazon-linux-extras, שהוא חלק מ-EPEL (חבילות נוספות ל-Enterprise Linux):

    sudo amazon-linux-extras install epel -y
    
  2. לאחר מכן, מתקינים את Chromium:

    sudo yum install -y chromium
    

עכשיו Puppeteer יכול להפעיל את Chromium כדי להריץ את הבדיקות שלך. אם לא מפעילים EPEL וממשיכים להתקין את Chromium כחלק מ-npm install, Puppeteer לא יכולה להפעיל את Chromium כי libatk-1.0.so.0 וחבילות רבות נוספות לא זמינות.

בעיות בתרגום קוד

אם אתם משתמשים בממיר JavaScript כמו babel או TypeScript, ייתכן שקריאה ל-evaluate() באמצעות פונקציית אסינכרונית לא תפעל. הסיבה לכך היא ש-puppeteer משתמש ב-Function.prototype.toString() כדי לסדר פונקציות בסדרה, אבל יכול להיות שטרנספורמרים משנים את קוד הפלט באופן שלא תואם ל-puppeteer.

תוכלו לנסות לעקוף את הבעיה על ידי מתן הוראה לממיר לא לשבש את הקוד. לדוגמה, להגדיר את TypeScript לשימוש בגרסת ה-ECma העדכנית ביותר ("target": "es2018"). פתרון עקיף נוסף יכול להיות שימוש בתבניות מחרוזות במקום בפונקציות:

await page.evaluate(`(async() => {
   console.log('1');
})()`);