com.playonlinux.framework.Wine.java Source code

Java tutorial

Introduction

Here is the source code for com.playonlinux.framework.Wine.java

Source

/*
 * Copyright (C) 2015 PRIS Quentin
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package com.playonlinux.framework;

import static com.playonlinux.core.lang.Localisation.translate;
import static java.lang.String.format;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.input.NullInputStream;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.log4j.Logger;

import com.playonlinux.app.PlayOnLinuxContext;
import com.playonlinux.core.config.ConfigFile;
import com.playonlinux.core.scripts.CancelException;
import com.playonlinux.core.scripts.ScriptClass;
import com.playonlinux.core.scripts.ScriptFailureException;
import com.playonlinux.core.services.manager.Service;
import com.playonlinux.core.services.manager.ServiceManager;
import com.playonlinux.core.streams.ProcessPipe;
import com.playonlinux.core.streams.TeeOutputStream;
import com.playonlinux.core.utils.Architecture;
import com.playonlinux.core.utils.ExeAnalyser;
import com.playonlinux.core.utils.OperatingSystem;
import com.playonlinux.core.version.Version;
import com.playonlinux.engines.wine.WineDistribution;
import com.playonlinux.filesystem.DirectoryWatcherSize;
import com.playonlinux.framework.wizard.SetupWizardComponent;
import com.playonlinux.framework.wizard.WineWizard;
import com.playonlinux.injection.Inject;
import com.playonlinux.injection.Scan;
import com.playonlinux.ui.api.ProgressControl;
import com.playonlinux.wine.WineException;
import com.playonlinux.wine.registry.AbstractRegistryNode;
import com.playonlinux.wine.registry.RegistryKey;
import com.playonlinux.wine.registry.RegistryValue;
import com.playonlinux.wine.registry.RegistryWriter;
import com.playonlinux.wine.registry.StringValueType;

@Scan
@ScriptClass
public class Wine implements SetupWizardComponent {
    private static final Logger LOGGER = Logger.getLogger(Wine.class);
    private static final Architecture DEFAULT_ARCHITECTURE = Architecture.I386;
    private static final long NEWPREFIXSIZE = 320_000_000L;
    private static final String DEFAULT_DISTRIBUTION_NAME = "staging";
    private static final String OVERWRITE = "Overwrite (usually works, no guarantee)";
    private static final String ERASE = "Erase (virtual drive content will be lost)";
    private static final String ABORT = "Abort installation";

    @Inject
    static PlayOnLinuxContext playOnLinuxContext;

    @Inject
    static ServiceManager backgroundServicesManager;

    @Inject
    static ExecutorService executorService;
    private final WineWizard setupWizard;

    private com.playonlinux.wine.WinePrefix prefix;
    private String prefixName;
    private WineVersion wineVersion;
    private int lastReturnCode = -1;

    private OutputStream outputStream = new NullOutputStream();
    private OutputStream errorStream = new NullOutputStream();
    private InputStream inputStream = new NullInputStream(0);

    private Wine(WineWizard setupWizard) {
        this.setupWizard = setupWizard;
    }

    public static Wine wizard(WineWizard setupWizard) {
        final SetupWizardComponent wineInstance = new Wine(setupWizard);
        setupWizard.registerComponent(wineInstance);
        return new Wine(setupWizard);
    }

    /**
     * Specifies the input stream
     *
     * @param inputStream
     *            the input stream
     * @return the same object
     */
    public Wine withInputStream(InputStream inputStream) {
        this.inputStream = inputStream;
        return this;
    }

    /**
     * Specifies the output stream
     *
     * @param outputStream
     *            the output stream
     * @return the same object
     */
    public Wine withOutputStream(OutputStream outputStream) {
        this.outputStream = outputStream;
        return this;
    }

    /**
     * Specifies the error stream
     *
     * @param errorStream
     *            the error stream
     * @return the same object
     */
    public Wine withErrorStream(OutputStream errorStream) {
        this.errorStream = errorStream;
        return this;
    }

    /**
     * Select the prefix. If the prefix already exists, this method load its
     * parameters and the prefix will be set as initialized.
     *
     * @param prefixName
     *            the name of the prefix
     * @return the same object
     */
    public Wine selectPrefix(String prefixName) throws CancelException {
        this.prefixName = prefixName;
        this.prefix = new com.playonlinux.wine.WinePrefix(playOnLinuxContext.makePrefixPathFromName(prefixName));

        if (prefix.initialized()) {
            wineVersion = new WineVersion(prefix.fetchVersion(), prefix.fetchDistribution(), setupWizard);
            if (!wineVersion.isInstalled()) {
                wineVersion.install();
            }
        }

        return this;
    }

    public Wine createPrefix(String version) throws CancelException {
        return this.createPrefix(version, DEFAULT_DISTRIBUTION_NAME);
    }

    /**
     * Create the prefix and load its parameters. The prefix will be set as
     * initialized
     *
     * @param version
     *            version of wine
     * @return the same object
     * @throws CancelException
     *             if the prefix cannot be created or if the user cancels the
     *             operation
     */
    public Wine createPrefix(String version, String distribution) throws CancelException {
        return this.createPrefix(version, distribution, DEFAULT_ARCHITECTURE.name());
    }

    /**
     * Create the prefix and load its parameters. The prefix will be set as
     * initialized
     *
     * @param version
     *            version of wine
     * @param architecture
     *            architecture of wine
     * @return the same object
     * @throws CancelException
     *             if the prefix cannot be created or if the user cancels the
     *             operation
     */
    public Wine createPrefix(String version, String distribution, String architecture) throws CancelException {
        if (prefix == null) {
            throw new ScriptFailureException("Prefix must be selected!");
        }

        if (prefix.exists() && userWantsToOverWritePrefix()) {
            return this;
        }

        wineVersion = new WineVersion(new Version(version),
                new WineDistribution(OperatingSystem.fetchCurrentOperationSystem(),
                        Architecture.valueOf(architecture), distribution),
                setupWizard);

        if (!wineVersion.isInstalled()) {
            wineVersion.install();
        }

        final ProgressControl progressControl = this.setupWizard.progressBar(
                format(translate("Please wait while the virtual drive is being created..."), prefixName));

        try (DirectoryWatcherSize observableDirectorySize = new DirectoryWatcherSize(executorService,
                prefix.getWinePrefixDirectory().toPath())) {

            observableDirectorySize
                    .setOnChange(newSize -> progressControl.setProgressPercentage(newSize * 100 / NEWPREFIXSIZE));
            Process process = wineVersion.getInstallation().createPrefix(this.prefix);
            waitWineProcess(process);
        } catch (WineException e) {
            throw new ScriptFailureException(e);
        }

        return this;
    }

    private void waitWineProcess(Process process) throws CancelException {
        try {
            process.waitFor();
        } catch (InterruptedException e) {
            process.destroy();
            killall();
            throw new CancelException(e);
        }
    }

    private boolean userWantsToOverWritePrefix() throws CancelException {
        try {
            log("Prefix already exists");

            switch (setupWizard.menu(translate(format("The target virtual drive %s already exists:", prefixName)),
                    Arrays.asList(OVERWRITE, ERASE, ABORT))) {
            case OVERWRITE:
                log("User choice: OVERWRITE");
                return true;
            case ERASE:
                log("User choice: ERASE");
                prefix.delete();
                return false;
            case ABORT:
            default:
                log("User choice: ABORT");
                throw new CancelException("The script was aborted");
            }
        } catch (IOException e) {
            throw new ScriptFailureException(e);
        }
    }

    /**
     * Killall the processes in the prefix
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine killall() throws ScriptFailureException {
        validateWineInstallationInitialized();
        try {
            wineVersion.getInstallation().killAllProcess(this.prefix);
        } catch (IOException logged) {
            LOGGER.warn("Unable to kill wine processes", logged);
        }

        return this;
    }

    /**
     * Run wine in the prefix
     *
     * @return the process object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    private Process runAndGetProcess(File workingDirectory, String executableToRun, List<String> arguments,
            Map<String, String> environment) throws ScriptFailureException {
        validateWineInstallationInitialized();
        validateArchitecture(workingDirectory, executableToRun);

        if ("regedit".equalsIgnoreCase(executableToRun)) {
            logRegFile(workingDirectory, arguments);
        }

        try {
            final Process process = wineVersion.getInstallation().run(prefix, workingDirectory, executableToRun,
                    environment, arguments);

            if (this.setupWizard.getLogContext() != null) {
                final Service processPipe = new ProcessPipe(process,
                        new TeeOutputStream(this.setupWizard.getLogContext(), outputStream),
                        new TeeOutputStream(this.setupWizard.getLogContext(), errorStream), inputStream);
                backgroundServicesManager.register(processPipe);
            }
            return process;
        } catch (WineException e) {
            throw new ScriptFailureException("Error while running wine:", e);
        }
    }

    private void logRegFile(File workingDirectory, List<String> pathToRegFile) throws ScriptFailureException {
        if (!pathToRegFile.isEmpty()) {
            final File regFile = findFile(workingDirectory, pathToRegFile.get(0));
            if (regFile.exists() && regFile.isFile()) {
                this.log("Content of " + regFile);
                this.log("-----------");
                try {
                    this.log(FileUtils.readFileToString(regFile));
                } catch (IOException e) {
                    LOGGER.warn(e);
                }
                this.log("-----------");
            } else {
                this.log(regFile + " does not seem to be a file. Not logging");
            }
        } else {
            this.log("User manually modified the registry");
        }
    }

    private void log(String message) throws ScriptFailureException {
        setupWizard.log(message);

        if (prefix != null) {
            try {
                prefix.log(message);
            } catch (IOException e) {
                setupWizard.log("Unable to log to the wineprefix", e);
            }
        }
    }

    private void validateArchitecture(File workingDirectory, String executableToRun) {
        try {
            if (wineVersion.getWineDistribution().getArchitecture() == Architecture.I386) {
                final File executedFile = findFile(workingDirectory, executableToRun);
                if (executedFile.exists() && ExeAnalyser.is64Bits(executedFile)) {
                    throw new IllegalStateException("A 32bit wineprefix cannot execute 64bits executables!");
                }
            }
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    private File findFile(File workingDirectory, String executableToRun) {
        if (new File(workingDirectory, executableToRun).exists()) {
            return new File(workingDirectory, executableToRun);
        } else {
            return new File(executableToRun);
        }
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(File workingDirectory, String executableToRun, List<String> arguments,
            Map<String, String> environment) throws ScriptFailureException {

        runAndGetProcess(workingDirectory, executableToRun, arguments, environment);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(File executableToRun, List<String> arguments, Map<String, String> environment)
            throws ScriptFailureException {
        File workingDirectory = executableToRun.getParentFile();
        runBackground(workingDirectory, executableToRun.getAbsolutePath(), arguments, environment);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(String executableToRun, List<String> arguments, Map<String, String> environment)
            throws ScriptFailureException {
        runBackground(this.prefix.getWinePrefixDirectory(), executableToRun, arguments, environment);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(String executableToRun, List<String> arguments) throws ScriptFailureException {
        runBackground(new File(executableToRun), arguments, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(File executableToRun, List<String> arguments) throws ScriptFailureException {
        runBackground(executableToRun, arguments, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @param executableToRun
     *            executable to run (file parameter)
     * @return the same object
     * @throws ScriptFailureException
     */
    public Wine runBackground(File executableToRun) throws ScriptFailureException {
        runBackground(executableToRun, (List<String>) null, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @param executableToRun
     *            executable to run (string parameter)
     * @return the same object
     * @throws ScriptFailureException
     */
    public Wine runBackground(String executableToRun) throws ScriptFailureException {
        runBackground(executableToRun, null, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(File workingDirectory, String executableToRun, List<String> arguments)
            throws ScriptFailureException {
        runBackground(workingDirectory, executableToRun, arguments, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runBackground(File workingDirectory, String executableToRun) throws ScriptFailureException {
        runBackground(workingDirectory, executableToRun, null, null);
        return this;
    }

    /**
     * Run wine in the prefix in foreground
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(File workingDirectory, String executableToRun, List<String> arguments,
            Map<String, String> environment) throws CancelException {
        Process process = runAndGetProcess(workingDirectory, executableToRun, arguments, environment);
        try {
            lastReturnCode = process.waitFor();
        } catch (InterruptedException e) {
            throw new CancelException(e);
        }
        return this;
    }

    /**
     * Run wine in the prefix in foreground
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(String workingDirectory, String executableToRun, List<String> arguments,
            Map<String, String> environment) throws CancelException {
        return runForeground(new File(workingDirectory), executableToRun, arguments, environment);
    }

    /**
     * Run wine in the prefix in foreground
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(File executableToRun, List<String> arguments, Map<String, String> environment)
            throws CancelException {
        File workingDirectory = executableToRun.getParentFile();
        runForeground(workingDirectory, executableToRun.getAbsolutePath(), arguments, environment);
        return this;
    }

    /**
     * Run wine in the prefix in foreground
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(String executableToRun, List<String> arguments, Map<String, String> environment)
            throws CancelException {
        runForeground(this.prefix.getWinePrefixDirectory(), executableToRun, arguments, environment);
        return this;
    }

    /**
     * Run wine in the prefix in foreground
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(String executableToRun, List<String> arguments) throws CancelException {
        runForeground(new File(executableToRun), arguments, null);
        return this;
    }

    /**
     * Run wine in the prefix in foreground
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(File executableToRun, List<String> arguments) throws CancelException {
        runForeground(executableToRun, arguments, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @param executableToRun
     *            executable to run (file parameter)
     * @return the same object
     * @throws ScriptFailureException
     */
    public Wine runForeground(File executableToRun) throws CancelException {
        runForeground(executableToRun, (List<String>) null, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @param executableToRun
     *            executable to run (string parameter)
     * @return the same object
     * @throws ScriptFailureException
     */
    public Wine runForeground(String executableToRun) throws CancelException {
        runForeground(executableToRun, null, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(File workingDirectory, String executableToRun, List<String> arguments)
            throws CancelException {
        runForeground(workingDirectory, executableToRun, arguments, null);
        return this;
    }

    /**
     * Run wine in the prefix in background
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine runForeground(File workingDirectory, String executableToRun) throws CancelException {
        runForeground(workingDirectory, executableToRun, null, null);
        return this;
    }

    /**
     * Wait for all wine application to be terminated
     *
     * @return the same object
     * @throws ScriptFailureException
     *             if the wine prefix is not initialized
     */
    public Wine waitExit() throws ScriptFailureException {
        validateWineInstallationInitialized();
        try {
            wineVersion.getInstallation().waitAllProcesses(this.prefix);
        } catch (IOException logged) {
            LOGGER.warn("Unable to wait for wine processes", logged);
        }

        return this;
    }

    /**
     * Wait for all wine application to be terminated and createPrefix a
     * progress bar watching for the size of a directory
     *
     * @param directory
     *            Directory to watch
     * @param endSize
     *            Expected size of the directory when the installation is
     *            terminated
     * @return the same object
     * @throws CancelException
     *             if the users cancels or if there is any error
     */
    public Wine waitAllWatchDirectory(File directory, long endSize) throws CancelException {
        ProgressControl progressControl = this.setupWizard
                .progressBar(format(translate("Please wait while the program is being installed..."), prefixName));

        final long startSize = FileUtils.sizeOfDirectory(directory);

        try (DirectoryWatcherSize observableDirectorySize = new DirectoryWatcherSize(executorService,
                prefix.getWinePrefixDirectory().toPath())) {

            observableDirectorySize.setOnChange(newSize -> {
                final double percentage = 100. * (newSize - startSize) / (endSize - startSize);
                progressControl.setProgressPercentage(percentage);
            });

            waitExit();
        } catch (IllegalStateException e) {
            throw new ScriptFailureException(e);
        }

        return this;
    }

    /**
     * Delete the wineprefix
     *
     * @return the same object
     * @throws CancelException
     *             if the users cancels or if there is any error
     */
    public Wine deletePrefix() throws CancelException {
        if (prefix.getWinePrefixDirectory().exists()) {
            ProgressControl progressControl = this.setupWizard.progressBar(
                    format(translate("Please wait while the virtual drive is being deleted..."), prefixName));
            final long startSize = prefix.getSize();
            final long endSize = 0L;

            try (DirectoryWatcherSize observableDirectorySize = new DirectoryWatcherSize(executorService,
                    prefix.getWinePrefixDirectory().toPath())) {

                observableDirectorySize.setOnChange(newSize -> {
                    final double percentage = 100. * (newSize - startSize) / (endSize - startSize);
                    progressControl.setProgressPercentage(percentage);
                });

                prefix.delete();
            } catch (IOException | IllegalStateException e) {
                throw new ScriptFailureException(e);

            }
        }

        return this;
    }

    /**
     * Overides DLLs parameters
     *
     * @param dllsToOverride
     *            a Map containing the dlls to overide (key = name of the dll,
     *            value = [disabled, builtin, native])
     * @return the same object
     */
    public Wine overrideDlls(Map<String, String> dllsToOverride) throws ScriptFailureException {
        validateWineInstallationInitialized();
        final RegistryKey hkeyCurrentUser = new RegistryKey("HKEY_CURRENT_USER");
        final RegistryKey software = new RegistryKey("Software");
        final RegistryKey wine = new RegistryKey("Wine");
        final RegistryKey dllOverrides = new RegistryKey("DllOverrides");

        hkeyCurrentUser.addChild(software);
        software.addChild(wine);
        wine.addChild(dllOverrides);

        for (String dll : dllsToOverride.keySet()) {
            final RegistryValue<StringValueType> dllNode = new RegistryValue<>("*" + dll,
                    new StringValueType(dllsToOverride.get(dll)));
            dllOverrides.addChild(dllNode);
        }

        writeToRegistry(hkeyCurrentUser);

        return this;
    }

    private void validateWineInstallationInitialized() throws ScriptFailureException {
        if (wineVersion == null) {
            throw new ScriptFailureException("The prefix must be initialized before running wine");
        }
    }

    private void writeToRegistry(AbstractRegistryNode node) throws ScriptFailureException {
        final RegistryWriter registryWriter = new RegistryWriter();
        final File temporaryRegFile;
        try {
            temporaryRegFile = File.createTempFile("registry", "pol");
            temporaryRegFile.deleteOnExit();
            com.google.common.io.Files.write(registryWriter.generateRegFileContent(node).getBytes(),
                    temporaryRegFile);
        } catch (IOException e) {
            throw new ScriptFailureException(e);
        }

        final List<String> arguments = new ArrayList<>();
        arguments.add(temporaryRegFile.getAbsolutePath());

        this.runAndGetProcess(prefix.getDriveCPath(), "regedit", arguments, new HashMap<>());
    }

    public ConfigFile config() {
        return prefix.getPrefixConfigFile();
    }

    public int getLastReturnCode() {
        return lastReturnCode;
    }

    @Override
    public void close() {
        if (prefix != null) {
            prefix.close();
        }
    }

    public Collection<File> findExecutables() {
        return prefix.findAllExecutables();
    }
}