Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

failing to successfully launch Spectron headless in Docker on Jenkins #1034

Open
DrewHagenAtInfiniteCampus opened this issue Sep 22, 2021 · 0 comments

Comments

@DrewHagenAtInfiniteCampus

Hello! I'm looking for some help. I've been spinning my wheels at this for a while. I am newcomer to Electron app development and am starting to get very lower level.

I'm trying to set up a development phase CI pipeline for my team using Jenkins (CI platform my company uses) with Docker. Part of this includes a quality gate with our unit tests and also our Spectron E2E tests.

So I'm trying to run our existing Spectron suite automatically headless within a Docker container on Jenkins. For the base image doing this, I've followed the Provided Docker images in the Electron Build documentation and am building my Docker container from electronuserland/builder:wine-chrome.

Right now, I am trying to start simple by just testing start of the Spectron Application and launching a window

MY SPEC:

import { appUtil } from '../utils/app-utils';

fdescribe('Application launch', () => {
    jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;

    const app = appUtil.app;
    console.log('/---------/ SPECTRON APP AFTER INIT /---------/')
    console.log(app);
    
    
    beforeAll(async () => {
        await appUtil.startApp();
        //await app.client.waitUntilWindowLoaded();
        console.log('/---------/ SPECTRON APP AFTER START /---------/')
        console.log(app);
    });

    afterAll(async () =>
        await appUtil.stopApp()
    );

    it('Should show a window', async () => {
        console.log('/---------/ SPECTRON APP CLIENT /---------/');
        console.log(app.client);
        console.log('/---------/ APP CLIENT STATUS /---------/');
        console.log(await app.client.status());
        console.log('/---------/ APP CLIENT DESIRED CAPABILITIES CHROME OPTIONS /---------/');
        console.log(await app.client.desiredCapabilities.chromeOptions);

        let count = await app.client.getWindowCount();
        expect(count).toBe(1);
    });
});

APPUTIL (where we have Spectron Application initialized):

import * as electron from 'electron';
import { Application } from 'spectron';
import { join } from 'path';

export class AppUtil {

    app = new Application({
        path: '' + electron,
        args: [join(__dirname, '../../../..')],
        chromeDriverLogPath: '../../chromeDriver.log',
        webdriverLogPath: '../../webdriver.log'
    });  
    
    public async startApp(): Promise<Application> {
        if (this.appIsRunning()) {
           const result = await this.waitForAppClosure();
           if(!result){
               throw new Error("Application failed to close within the allotted timeout");
           };
        };
        await this.app.start();
        this.app.browserWindow.focus();
        this.app.browserWindow.setAlwaysOnTop(true);

        return this.app;
    }

    public async stopApp(): Promise<Application> {
        if (this.appIsRunning()) {
            this.app.stop();
            this.app.mainProcess.abort()
        };
        //allow time for terminal windows to close
        await sleep(1000);
        return Promise.resolve(this.app);
    }

    private appIsRunning(): boolean {
        return !!(this.app && this.app.isRunning());
    }

    /**
     * Wait for the app to close for a max of 5 secs
     */
    private async waitForAppClosure(): Promise<boolean> {
        let count = 0;
        while(this.appIsRunning() && (count < 5)){
            await sleep(1000);
            count++;
        };

        return !this.appIsRunning();
    }
}

export const appUtil = new AppUtil();

/**
 * Sleeps for a specified amount of time
 */
 export async function sleep(time: number): Promise<void> {
    return new Promise(r => setTimeout(r, time));
}

When I run these, I get the following result in Jenkins output:

Failures:
1) Application launch Should show a window
  Message:
    Error: A session id is required for this command but wasn't found in the response payload
  Stack:
    error properties: Object({ type: 'NoSessionIdError' })
    Error: A session id is required for this command but wasn't found in the response payload
        at windowHandles() - application.js:274:17

Suite error: Application launch
  Message:
    Error: Client initialization failed after 10 attempts: RuntimeError Client initialization failed after 10 attempts:
  Stack:
    error properties: Object({ details: undefined, type: 'RuntimeError', seleniumStack: Object({ type: 'UnknownError', message: 'An unknown server-side error occurred while processing the command.', orgStatusMessage: 'unknown error: Chrome failed to start: exited abnormally.
      (unknown error: DevToolsActivePort file doesn't exist)
      (The process started from chrome location /root/repo/apps/myapp/node_modules/spectron/lib/launcher.js is no longer running, so ChromeDriver is assuming that Chrome has crashed.)
      (Driver info: chromedriver=80.0.3987.86 (4c2763461dfdcdda46516e2a9060c22a3c1a2525-refs/branch-heads/3987@{#796}),platform=Linux 3.10.0-1127.19.1.el7.x86_64 x86_64)' }) })
    Error: unknown error: Chrome failed to start: exited abnormally.
        at <Jasmine>
        at new RuntimeError (/root/repo/apps/myapp/node_modules/spectron/node_modules/webdriverio/build/lib/utils/ErrorHandler.js:143:12)
        at Request._callback (/root/repo/apps/myapp/node_modules/spectron/node_modules/webdriverio/build/lib/utils/RequestHandler.js:318:39)
        at Request.self.callback (/root/repo/apps/myapp/node_modules/request/request.js:185:22)
        at Request.emit (events.js:376:20)
        at Request.emit (domain.js:470:12)
        at Request.<anonymous> (/root/repo/apps/myapp/node_modules/request/request.js:1154:10)
        at Request.emit (events.js:376:20)
        at Request.emit (domain.js:470:12)
        at IncomingMessage.<anonymous> (/root/repo/apps/myapp/node_modules/request/request.js:1076:12)
        at Object.onceWrapper (events.js:482:28)

Ran 1 of 219 specs
1 spec, 2 failures
Finished in 4.578 seconds

It seems like the highlight here is DevToolsActivePort file doesn't exist.
When I've googled around about this, many people discuss passing arguments to Chrome Driver.

Passing in Chrome Driver Args

I noticed Spectron's README the Application API shows chromeDriverArgs. So I've been passing an array of the arguments into the Application constructor like so:

app = new Application({
        path: '' + electron,
        args: [join(__dirname, '../../../..')],
        chromeDriverLogPath: '../../chromeDriver.log',
        webdriverLogPath: '../../webdriver.log',
        chromeDriverArgs: [
            '--no-sandbox',
            '--disable-dev-shm-usage',
            '--headless',
            'start-maximized',
            'disable-infobars',
            '--disable-gpu',
            '--window-size=1920,1080'
        ]
    });

I've played around with passing various combinations of these arguments:

'--no-sandbox',
'--disable-dev-shm-usage',
'--headless',
'--remote-debugging-port=9222',
'--user-data-dir=/home/ubuntujenkins/userData',
'start-maximized',
'disable-infobars',
'--disable-gpu',
'--window-size=1920,1080'

But still, no success. Only '--remote-debugging-port=9222' and '--user-data-dir=/home/ubuntujenkins/userData' makes the ``DevToolsActivePort file doesn't existerror disappear, but then it's replaced bychrome unreachable`.

Xvfb Configuration?

I'm also unsure if maybe this can relate to an Xvfb misconfiguration?
I've tried:

  1. running Xvfb directly... adding sleep after like I've seen in some examples:
stage('test e2e') {
    steps {
        setupXvfbVariablesAndE2ETest()
    }
}

//...

void setupXvfbVariablesAndE2ETest() {
    // Similar to Travis CI example here: https://www.electronjs.org/docs/tutorial/testing-on-headless-ci
    runCommand('Xvfb :99 -screen 0 1920x1080x24 > /dev/null 2>&1 '
        + '& export DISPLAY=:99.0 '
        + '&& sleep 3 ' //give xvfb somem time to start
        + '&& npm run test-e2e')
}
  1. using xvfb-run... in my package.json:
"test-xvfb": "xvfb-run -al --error-file='./xvfb-error.log' --server-args='-screen 0, 1920x1080x24' sleep 5 && npm run test-e2e"
  1. using xvfb-maybe
"test-xvfb": "xvfb-maybe -al --error-file='./xvfb-error.log' --server-args='-screen 0, 1920x1080x24' -- npm run test-e2e

or more simply, I also tried:

"test-xvfb": "xvfb-maybe npm run test-e2e
  1. trying to run Xvfb and set DISPLAY environment variable in Dockerfile and/or Jenkinsfile before running npm run test-e2e:
  • a line in Dockerfile:
RUN Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & export DISPLAY=':99.0'
  • in Jenkinsfile:
    have tried running shell commands in this line:
sh 'Xvfb :99 -screen 0 1920x1080x24 > /dev/null 2>&1 && sleep 5 && npm run test-e2e'

also tried setting environment variable first:

pipeline {
    environment {
        DISPLAY = ':99.0'
    }
    //...
  1. try running Xvfb in a Docker entrypoint:
FROM electronuserland/builder:wine-chrome

ENV DISPLAY=:99

ADD docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT [ "/docker-entrypoint.sh" ]

with

#!/usr/bin/env bash

echo "Starting Xvfb"
Xvfb :99 -ac &
sleep 5

echo "Executing command $@"
export DISPLAY=:99

exec "$@"

Lastly, I noticed the official Electron documentation mentions a plugin for Jenkins.
Is it possible on Jenkins, installing and configuring that plugin is necessary for Xvfb to behave as expected, even on a Docker container?

None of what I'm trying seems to be working. If it appears obvious to anyone that I am missing something, I'd be delighted if you let me know! I can also post some logs if requested (I have to scrub proprietary data references first).
Thanks so much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant