Expand Minimize Picture-in-picture Power Device Status Voice Recognition Skip Back Skip Forward Minus Plus Play Search
Internet Explorer alert
This browser is not recommended for use with smartdevicelink.com, and may not function properly. Upgrade to a different browser to guarantee support of all features.
close alert
To Top Created with Sketch. To Top
To Bottom Created with Sketch. To Bottom
WebEngine Support

WebEngine Support

With the introduction of WebEngine applications, the Policy Server requires additional setup to be able to support them. This is because WebEngine apps need to be uploaded and servable for execution by supported HMIs, and the Policy Server provides the information necessary for downloading these app bundles. The implementation details are up to the Policy Server maintainer, as the method of hosting these WebEngine app bundles may depend on the environment and manner in which the Policy Server is running.

Getting Started

The entrypoint for the custom implementation starts in the customizable/webengine-bundle/index.js file in the project. In the single function stub handleBundle the URL of the app bundle is passed in. The goal is for the Policy Server to download the bundle from the passed in URL, extract the bundle to get the compressed and uncompressed file size data, and to host it in a publicly accessible location. The new URL for the WebEngine app bundle and its size information is expected to be returned in the cb argument for the handleBundle function, and that information will automatically be reflected in future calls to the /api/v1/applications/store route. Check the customizable/webengine-bundle/index.js file comments for specifics.

It is recommended that the app bundles are hosted in a dedicated online file-sharing service such as AWS's S3 buckets. These URLs are expected to be persistent and unchanging, even after Policy Server restarts or migrations.

S3 Storage Code Example

You may use this code snippet for reference on how to implement the handleBundle function. This implementation stores the app bundles on an S3 bucket, and assumes that the bucket exists, and that your computer's credentials are set up to be authenticated with AWS, and that you have installed the node-stream-zip and aws-sdk node modules to the Policy Server. Note that AWS has changed how new buckets are configured as of April 2023, so make sure you have your bucket set up to be public and to enable ACLs in the Object Ownership settings.

// skeleton function for customized downloading and extracting of package information
const http = require('http');
const https = require('https');
const fs = require('fs');
const UUID = require('uuid');
const AWS = require('aws-sdk');
const StreamZip = require('node-stream-zip');
AWS.config.update({region: 'us-east-1'});
// assumes the bucket already exists. make sure it is set up to allow writing objects to it from remote sources!
const BUCKET_NAME = 'webengine-bundles';

/**
 * asynchronous function for downloading the bundle from the given url and extracting its size information
 * @param package_url - a publicly accessible external url that's used to download the bundle onto the Policy Server
 * @param cb - a callback function that expects two arguments
 *      if there was a failure in the process, it should be sent as the first argument. the Policy Server will log it
 *      the second argument to return must follow the formatted object below
 *      {
 *          url: the Policy Server should save a copy of the app bundle somewhere publicly accessible
 *              this url must be a full resolved url
 *          size_compressed_bytes: the number of bytes of the compressed downloaded bundle
 *          size_decompressed_bytes: the number of bytes of the extracted downloaded bundle
 *      }
 */
exports.handleBundle = function (package_url, cb) {
    let compressedSize = 0;
    let bucketUrl = '';
    const TMP_FILE_NAME = `${UUID.v4()}.zip`;

    // read the URL and save it to a buffer variable
    readUrlToBuffer(package_url)
        .then(zipBuffer => { // submit the file contents to S3
            compressedSize = zipBuffer.length;
            const randomString = UUID.v4();
            const fileName = `${randomString}.zip`;
            bucketUrl = `https://${BUCKET_NAME}.s3.amazonaws.com/${fileName}`;
            // make the bundle publicly accessible
            const objectParams = {Bucket: BUCKET_NAME, ACL: 'public-read', Key: fileName, Body: zipBuffer};
            // Create object upload promise
            return new AWS.S3().putObject(objectParams).promise();
        })
        .then(() => { // unzip the contents of the bundle to get its uncompressed data information
            return streamUrlToTmpFile(bucketUrl, TMP_FILE_NAME);
        })
        .then(() => {
            return unzipAndGetUncompressedSize(TMP_FILE_NAME);
        })
        .then(uncompressedSize => {
            // delete the tmp zip file
            fs.unlink(TMP_FILE_NAME, () => {
                // all the information has been collected
                cb(null, {
                    url: bucketUrl,
                    size_compressed_bytes: compressedSize,
                    size_decompressed_bytes: uncompressedSize
                });
            });
        })
        .catch(err => {
            // delete the tmp zip file
            fs.unlink(TMP_FILE_NAME, () => {
                cb(err);
            });
        });
}

function unzipAndGetUncompressedSize (fileName) {
    let uncompressedSize = 0;

    return new Promise((resolve, reject) => {
        const zip = new StreamZip({
            file: fileName,
            skipEntryNameValidation: true
        });
        zip.on('ready', () => {
            // iterate through every unzipped entry and count up the file sizes
            for (const entry of Object.values(zip.entries())) {
                if (!entry.isDirectory) {
                    uncompressedSize += entry.size;
                }
            }
            // close the file once you're done
            zip.close()
            resolve(uncompressedSize);
        });

        // Handle errors
        zip.on('error', err => { reject(err) });
    });
}

function streamUrlToTmpFile (url, fileName) {
    const urlObj = new URL(url);
    return new Promise((resolve, reject) => {
        function resCallback (res) {
            res.pipe(fs.createWriteStream(fileName)).on('close', resolve);
        }
        if (urlObj.protocol === "https:") {
            https.get(url, resCallback).end();
        } else {
            http.get(url, resCallback).end();
        }
    });
}

function readUrlToBuffer (url) {
    const urlObj = new URL(url);
    return new Promise((resolve, reject) => {
        let zipBuffer = [];
        function resCallback (res) {
            res.on('data', data => {
                zipBuffer.push(data);
            })
            .on('close', function () { // file fully downloaded
                // put the zip contents to a buffer 
                resolve(Buffer.concat(zipBuffer));
            });
        }

        if (urlObj.protocol === "https:") {
            https.get(url, resCallback).end();
        } else {
            http.get(url, resCallback).end();
        }
    })
}
View on GitHub.com
Previous Section Next Section