Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Making WhatsApp desktop application portable

Portable desktop WhatsApp

You start multiple instances of WhatsApp using --user-data-dir flag providing the full path to the directory. For example:

E:\Temp\Whatsapp>WhatsApp.exe --user-data-dir=E:\Temp\Whatsapp\number1

or by creating a shortcut with the flag.

If directory does not exist it will be created (tested with WhatsApp-2.2019.6-full.nupkg on Windows x64).

The rest of the document is just for historic purposes.


!!This tutorial is no longer applicable!!

This tutorial will explain how to make WhatsApp desktop application portable on Windows platform. Maybe this can work for other platforms as well.

For this to work NodeJs and asar package are required.

Download WhatsApp package

Firstly download latest version of WhatsApp. The following link contains all Windows (x64) releases:

https://web.whatsapp.com/desktop/windows/release/x64/RELEASES

Find the release with the highest number that has full suffix. For example WhatsApp-0.2.2478-full.nupkg

The download link would be:

https://web.whatsapp.com/desktop/windows/release/x64/WhatsApp-0.2.2478-full.nupkg

Once downloaded unpack it (it's standard zip file), and go to the \lib\net45 folder. This folder contains the actual application. Copy the contents of that folder to your portable destination. For this example E:\WhatsAppPortable.

Within that folder delete the squirrel.exe to disable automatic updates.

Download NodeJs and install asar

If Node and asar are already installed, this step can be skipped.

Go to https://nodejs.org/download/release/latest/ and download version that ends with -win-x64.7z.

Unpack that archive to E:\node for example. Open cmd.exe, go to Node's folder and install asar.

C:\>cd /d E:\node
E:\node>npm install asar

Make WhatsApp portable

While at cmd unpack electron.asar package from E:\WhatsAppPortable\resources\ folder using asar's extract archive

asar extract E:\WhatsAppPortable\resources\electron.asar E:\WhatsAppPortable\resources\electron_extract

Now go to E:\WhatsAppPortable\resources\electron_extract\browser\ and edit the init.js file using any text editor.

UPDATE 2020-04-01

Lines below are now in the electron_extract\browser\ app.js file and not in the init.js file.

In addition WhatsApp now has additional code with support for the --user-data-dir which can be used instead of this tutorial. Provided path has to be absolute for this to work. See comments below.

Now go to E:\WhatsAppPortable\resources\electron_extract\browser\ and edit the app.js file using any text editor.

Find the following lines:

app.setPath('userData', path.join(app.getPath('appData'), app.getName()))
app.setPath('userCache', path.join(app.getPath('cache'), app.getName()))

Replace them with:

var appProfileDir = "Profile"
for (let arg of process.argv) {
  if (arg.indexOf('--profile-dir=') === 0) {
    appProfileDir = arg.substr(arg.indexOf('=') + 1)
  }
}
var profilePath = path.join(path.dirname(process.execPath), appProfileDir)
app.setPath('userData', profilePath)
app.setPath('userCache', profilePath)

Save the file and pack it with asar:

asar pack E:\WhatsAppPortable\resources\electron_extract E:\WhatsAppPortable\resources\electron.asar

The electron_extract folder in E:\WhatsAppPortable\resources\ can be deleted now.

Running the portable version

In the previous step the --profile-dir switch is added and Profile is set as default folder. If the WhatsApp is started without the switch it will create and use Profile folder.

If it is started like this:

WhatsApp.exe --profile-dir=MySecondNumber

it will create MySecondNumber folder ad use it as a profile folder.

This provides the ability to run multiple instances of WhatsApp with different profiles on the same computer.

Final thoughts

Adding something like this profile-dir switch can probably be used to make any Electron application portable as long it is not using appData path elsewhere the code.

@jduartedj

This comment has been minimized.

Copy link

@jduartedj jduartedj commented Apr 4, 2018

Hi,

As the lazy person I am I ran the application as soon as I extracted it... and it is running ok!
So if anyone is looking for an easy way to run wahtsapp with user privileges (like me) just run the exe inside net45 folder after extraction!
Also I copied the net45 folder somewhere else and renamed it whitout any issues.

Hope this helps anyone out there 😄

EDIT: also here is an easier way, download the normal installer, extract it like a zip and the latest nupkg file is there.

@timea-techgirl

This comment has been minimized.

Copy link

@timea-techgirl timea-techgirl commented Apr 6, 2018

@milolav Do I have to edit the electron.asar after each update?

@bluemufa

This comment has been minimized.

Copy link

@bluemufa bluemufa commented May 9, 2018

How i do custom theme for this whatsapp

@fatemehjohari

This comment has been minimized.

Copy link

@fatemehjohari fatemehjohari commented Jun 13, 2018

thanks

@kmiyon

This comment has been minimized.

Copy link

@kmiyon kmiyon commented Jun 19, 2018

Hello jduartedj i downloaded WhatsApp-0.2.9737-using 7-zip extracted
all till i got net45 folder that's it i did click the WhatsApp.exe just like
you said WhatsApp does run there are 4 instances in the windows task manager
but i got is a white screen and by the way did rename the squirrel.exe to
squirrel.exe.old so what did i do wrong ?

@idzuwan

This comment has been minimized.

Copy link

@idzuwan idzuwan commented Jul 25, 2018

@kmiyon did you actually follow all the step? including unpacking electron and repacking it after editing init.js? the instruction still valid for version 0.3.3

@homerybart

This comment has been minimized.

Copy link

@homerybart homerybart commented Dec 16, 2018

Hello
I try extract electron.asar from nodejs and he returns me: " "asar" it is not recognized as an internal or external command, program or batch file executable."
How can I extract electron.asar?

@homerybart

This comment has been minimized.

Copy link

@homerybart homerybart commented Dec 16, 2018

Solved, I installed asar globally with option -g

nmp install -g asar

@weklost

This comment has been minimized.

Copy link

@weklost weklost commented Mar 29, 2019

@selcukduman

This comment has been minimized.

Copy link

@selcukduman selcukduman commented Mar 23, 2020

Now the file to change is electron_extract\browser\api\app.js instead of electron_extract\browser\init.js. The lines to change are the same.

@milolav

This comment has been minimized.

Copy link
Owner Author

@milolav milolav commented Mar 23, 2020

@selcukduman Looks like there is now support for --user-data-dir in the app.js

app._setDefaultAppPaths = (packagePath) => {
    // Set the user path according to application's name.
    app.setPath('userData', path.join(app.getPath('appData'), app.getName()));
    app.setPath('userCache', path.join(app.getPath('cache'), app.getName()));
    app.setAppPath(packagePath);
    // Add support for --user-data-dir=
    const userDataDirFlag = '--user-data-dir=';
    const userDataArg = process.argv.find(arg => arg.startsWith(userDataDirFlag));
    if (userDataArg) {
        const userDataDir = userDataArg.substr(userDataDirFlag.length);
        if (path.isAbsolute(userDataDir))
            app.setPath('userData', userDataDir);
    }
};

The only difference I see here is that it accepts only absolute paths and doesn't set userCache. I've tried using this switch and it seems to be working fine. Everything is stored within provided folder, and %APPDATA%\WhatsApp folder is not created.

I've also noticed that if provided folder does not exist, it will be created empty and the application will not start but throw an error instead:

events.js:177
      throw er; // Unhandled 'error' event
      ^

Error [ERR_STREAM_DESTROYED]: Cannot call write after a stream was destroyed
    at doWrite (_stream_writable.js:413:19)
    at writeOrBuffer (_stream_writable.js:401:5)
    at WriteStream.Writable.write (_stream_writable.js:301:11)
    at q.write (E:\Temp\WhatsApp\resources\app.asar\main.js:148:34858)
    at N._emit (E:\Temp\WhatsApp\resources\app.asar\main.js:148:29866)
    at N.error (E:\Temp\WhatsApp\resources\app.asar\main.js:148:26179)
    at process.<anonymous> (E:\Temp\WhatsApp\resources\app.asar\main.js:248:109177)
    at process.emit (events.js:205:15)
    at process._fatalException (internal/process/execution.js:146:25)

Starting it the second time folder will be populated and the application will start correctly.

On that note, the same thing happens after applying my code as well, so I guess that folder creation issue might be a bug in WhatsApp's code.

Tested with WhatsApp-0.4.1307-full x64 version.on Windows 10.

@efraim-il

This comment has been minimized.

Copy link

@efraim-il efraim-il commented Apr 1, 2020

Thank you

When I open init.js, I am unable to find the lines you mentioned:
app.setPath('userData', path.join(app.getPath('appData'), app.getName()))
app.setPath('userCache', path.join(app.getPath('cache'), app.getName()))


"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const buffer_1 = require("buffer");
const fs = require("fs");
const path = require("path");
const util = require("util");
const v8 = require("v8");
const Module = require('module');
// We modified the original process.argv to let node.js load the init.js,
// we need to restore it here.
process.argv.splice(1, 1);
// Clear search paths.
require('../common/reset-search-paths');
// Import common settings.
require('@electron/internal/common/init');
const globalPaths = Module.globalPaths;
// Expose public APIs.
globalPaths.push(path.join(__dirname, 'api', 'exports'));
if (process.platform === 'win32') {
    // Redirect node's console to use our own implementations, since node can not
    // handle console output when running as GUI program.
    const consoleLog = (format, ...args) => {
        return process.log(util.format(format, ...args) + '\n');
    };
    const streamWrite = function (chunk, encoding, callback) {
        if (buffer_1.Buffer.isBuffer(chunk)) {
            chunk = chunk.toString(encoding);
        }
        process.log(chunk);
        if (callback) {
            callback();
        }
        return true;
    };
    console.log = console.error = console.warn = consoleLog;
    process.stdout.write = process.stderr.write = streamWrite;
}
// Don't quit on fatal error.
process.on('uncaughtException', function (error) {
    // Do nothing if the user has a custom uncaught exception handler.
    if (process.listeners('uncaughtException').length > 1) {
        return;
    }
    // Show error in GUI.
    // We can't import { dialog } at the top of this file as this file is
    // responsible for setting up the require hook for the "electron" module
    // so we import it inside the handler down here
    Promise.resolve().then(() => require('electron')).then(({ dialog }) => {
        const stack = error.stack ? error.stack : `${error.name}: ${error.message}`;
        const message = 'Uncaught Exception:\n' + stack;
        dialog.showErrorBox('A JavaScript error occurred in the main process', message);
    });
});
// Emit 'exit' event on quit.
const { app } = require('electron');
app.on('quit', function (event, exitCode) {
    process.emit('exit', exitCode);
});
if (process.platform === 'win32') {
    // If we are a Squirrel.Windows-installed app, set app user model ID
    // so that users don't have to do this.
    //
    // Squirrel packages are always of the form:
    //
    // PACKAGE-NAME
    // - Update.exe
    // - app-VERSION
    //   - OUREXE.exe
    //
    // Squirrel itself will always set the shortcut's App User Model ID to the
    // form `com.squirrel.PACKAGE-NAME.OUREXE`. We need to call
    // app.setAppUserModelId with a matching identifier so that renderer processes
    // will inherit this value.
    const updateDotExe = path.join(path.dirname(process.execPath), '..', 'update.exe');
    if (fs.existsSync(updateDotExe)) {
        const packageDir = path.dirname(path.resolve(updateDotExe));
        const packageName = path.basename(packageDir).replace(/\s/g, '');
        const exeName = path.basename(process.execPath).replace(/\.exe$/i, '').replace(/\s/g, '');
        app.setAppUserModelId(`com.squirrel.${packageName}.${exeName}`);
    }
}
// Map process.exit to app.exit, which quits gracefully.
process.exit = app.exit;
// Load the RPC server.
require('@electron/internal/browser/rpc-server');
// Load the guest view manager.
require('@electron/internal/browser/guest-view-manager');
require('@electron/internal/browser/guest-window-manager');
// Now we try to load app's package.json.
let packagePath = null;
let packageJson = null;
const searchPaths = ['app', 'app.asar', 'default_app.asar'];
if (process.resourcesPath) {
    for (packagePath of searchPaths) {
        try {
            packagePath = path.join(process.resourcesPath, packagePath);
            packageJson = require(path.join(packagePath, 'package.json'));
            break;
        }
        catch (_a) {
            continue;
        }
    }
}
if (packageJson == null) {
    process.nextTick(function () {
        return process.exit(1);
    });
    throw new Error('Unable to find a valid app');
}
// Set application's version.
if (packageJson.version != null) {
    app.setVersion(packageJson.version);
}
// Set application's name.
if (packageJson.productName != null) {
    app.setName(`${packageJson.productName}`.trim());
}
else if (packageJson.name != null) {
    app.setName(`${packageJson.name}`.trim());
}
// Set application's desktop name.
if (packageJson.desktopName != null) {
    app.setDesktopName(packageJson.desktopName);
}
else {
    app.setDesktopName((app.getName()) + '.desktop');
}
// Set v8 flags
if (packageJson.v8Flags != null) {
    v8.setFlagsFromString(packageJson.v8Flags);
}
app._setDefaultAppPaths(packagePath);
// Load the chrome devtools support.
require('@electron/internal/browser/devtools');
// Load the chrome extension support.
require('@electron/internal/browser/chrome-extension');
const features = process.electronBinding('features');
if (features.isDesktopCapturerEnabled()) {
    // Load internal desktop-capturer module.
    require('@electron/internal/browser/desktop-capturer');
}
// Load protocol module to ensure it is populated on app ready
require('@electron/internal/browser/api/protocol');
// Set main startup script of the app.
const mainStartupScript = packageJson.main || 'index.js';
const KNOWN_XDG_DESKTOP_VALUES = ['Pantheon', 'Unity:Unity7', 'pop:GNOME'];
function currentPlatformSupportsAppIndicator() {
    if (process.platform !== 'linux')
        return false;
    const currentDesktop = process.env.XDG_CURRENT_DESKTOP;
    if (!currentDesktop)
        return false;
    if (KNOWN_XDG_DESKTOP_VALUES.includes(currentDesktop))
        return true;
    // ubuntu based or derived session (default ubuntu one, communitheme…) supports
    // indicator too.
    if (/ubuntu/ig.test(currentDesktop))
        return true;
    return false;
}
// Workaround for electron/electron#5050 and electron/electron#9046
if (currentPlatformSupportsAppIndicator()) {
    process.env.XDG_CURRENT_DESKTOP = 'Unity';
}
// Quit when all windows are closed and no other one is listening to this.
app.on('window-all-closed', () => {
    if (app.listenerCount('window-all-closed') === 1) {
        app.quit();
    }
});
Promise.all([
    Promise.resolve().then(() => require('@electron/internal/browser/default-menu')),
    app.whenReady
]).then(([{ setDefaultApplicationMenu }]) => {
    // Create default menu
    setDefaultApplicationMenu();
});
if (packagePath) {
    // Finally load app's main.js and transfer control to C++.
    Module._load(path.join(packagePath, mainStartupScript), Module, true);
}
else {
    console.error('Failed to locate a valid package to load (app, app.asar or default_app.asar)');
    console.error('This normally means you\'ve damaged the Electron package somehow');
}
//# sourceMappingURL=init.js.map
@milolav

This comment has been minimized.

Copy link
Owner Author

@milolav milolav commented Apr 1, 2020

@efraim-il those lines are now in app.js and not init.js.
I've updated tutorial to reflect the change.

@efraim-il

This comment has been minimized.

Copy link

@efraim-il efraim-il commented Apr 1, 2020

@efraim-il those lines are now in app.js and not init.js.
I've updated tutorial to reflect the change.

Thanks. But it doesn't work for me.
WhatsApp.exe doesn't open. I am able to run Whatsapp twice only if I use Sandboxie for the second one.

@mimorama

This comment has been minimized.

Copy link

@mimorama mimorama commented May 10, 2020

@milolav please can u check the latest update of whatsapp (WhatsApp-2.2019.6-full.nupkg) because it doesn't include the electron.aser file inside resources and we can't make a portable WhatsApp.
Thanks

@milolav

This comment has been minimized.

Copy link
Owner Author

@milolav milolav commented May 10, 2020

@mimorama looks like this tutorial is no longer applicable. But on the bright side, --user-data-dir flag with full path works. You can create multiple shortcuts like:

E:\Temp\Whatsapp\WhatsApp.exe --user-data-dir=E:\Temp\Whatsapp\number1
E:\Temp\Whatsapp\WhatsApp.exe --user-data-dir=E:\Temp\Whatsapp\number2

etc

And each will start WhatsApp with the specified profile, all working in parallel. At least on Windows.

@mimorama

This comment has been minimized.

Copy link

@mimorama mimorama commented May 17, 2020

@milolav
i don't what i can say but many many thanks for u it's worked well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.