001/*
002 * (C) Copyright 2017-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *    http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Kevin Leturc <kleturc@nuxeo.com>
018 */
019package org.nuxeo.ecm.admin.runtime;
020
021import static org.apache.commons.lang3.StringUtils.isNotEmpty;
022
023import java.io.File;
024import java.util.ArrayList;
025import java.util.List;
026
027import org.apache.logging.log4j.LogManager;
028import org.apache.logging.log4j.Logger;
029import org.nuxeo.connect.connector.ConnectServerError;
030import org.nuxeo.connect.data.DownloadingPackage;
031import org.nuxeo.connect.packages.PackageManager;
032import org.nuxeo.connect.update.LocalPackage;
033import org.nuxeo.connect.update.PackageException;
034import org.nuxeo.connect.update.PackageState;
035import org.nuxeo.connect.update.PackageUpdateService;
036import org.nuxeo.connect.update.Version;
037import org.nuxeo.connect.update.task.standalone.InstallTask;
038import org.nuxeo.connect.update.task.standalone.UninstallTask;
039import org.nuxeo.connect.update.task.update.Rollback;
040import org.nuxeo.connect.update.task.update.RollbackOptions;
041import org.nuxeo.connect.update.task.update.Update;
042import org.nuxeo.connect.update.task.update.UpdateOptions;
043import org.nuxeo.ecm.core.api.NuxeoException;
044import org.nuxeo.runtime.api.Framework;
045import org.nuxeo.runtime.reload.ReloadContext;
046import org.nuxeo.runtime.reload.ReloadResult;
047import org.nuxeo.runtime.reload.ReloadService;
048import org.osgi.framework.BundleException;
049
050/**
051 * Helper to hot reload studio bundles.
052 *
053 * @since 9.3
054 */
055public class ReloadHelper {
056
057    private static final Logger log = LogManager.getLogger(ReloadHelper.class);
058
059    public static synchronized void hotReloadPackage(String packageId) {
060        log.info("Reload Studio package with id={}", packageId);
061        LocalPackage pkg = null;
062        InstallTask installTask = null;
063        try {
064            ReloadService reloadService = Framework.getService(ReloadService.class);
065            ReloadContext reloadContext = new ReloadContext();
066
067            PackageManager pm = Framework.getService(PackageManager.class);
068
069            PackageUpdateService pus = Framework.getService(PackageUpdateService.class);
070            pkg = pus.getPackage(packageId);
071
072            // Remove package from PackageUpdateService and get its bundleName to hot reload it
073            if (pkg != null) {
074                if (pkg.getPackageState().isInstalled()) {
075                    if (pkg.getUninstallFile().exists()) {
076                        // get the bundle symbolic names to hot reload
077                        UninstallTask uninstallTask = (UninstallTask) pkg.getUninstallTask();
078                        // in our hot reload case, we just care about the bundle
079                        // so get the rollback commands and then the target
080                        uninstallTask.getCommands()
081                                     .stream()
082                                     .filter(Rollback.class::isInstance)
083                                     .map(Rollback.class::cast)
084                                     .map(Rollback::getRollbackOptions)
085                                     .map(uninstallTask.getUpdateManager()::getRollbackTarget)
086                                     .map(reloadService::getOSGIBundleName)
087                                     .forEachOrdered(reloadContext::undeploy);
088                    } else {
089                        log.warn("Unable to uninstall previous bundle because {} doesn't exist",
090                                pkg.getUninstallFile());
091                    }
092                }
093                // remove the package from package update service, unless download will fail
094                pus.removePackage(pkg.getId());
095            }
096
097            // Download
098            List<String> messages = new ArrayList<>();
099            DownloadingPackage downloadingPkg = pm.download(packageId);
100            while (!downloadingPkg.isCompleted()) {
101                log.trace("Downloading studio snapshot package: {}", packageId);
102                if (isNotEmpty(downloadingPkg.getErrorMessage())) {
103                    messages.add(downloadingPkg.getErrorMessage());
104                }
105                Thread.sleep(100); // NOSONAR (we want the whole hot-reload to be synchronized)
106            }
107
108            log.info("Installing {}", packageId);
109            pkg = pus.getPackage(packageId);
110            if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) {
111                NuxeoException nuxeoException = new NuxeoException(
112                        String.format("Error while downloading studio snapshot: %s, package Id: %s", pkg, packageId));
113                messages.forEach(nuxeoException::addInfo);
114                throw nuxeoException;
115            }
116
117            // get bundles to deploy
118            installTask = (InstallTask) pkg.getInstallTask();
119            pus.setPackageState(pkg, PackageState.INSTALLING);
120
121            // in our hot reload case, we just care about the bundle
122            // so get the update commands and then the file
123            installTask.getCommands()
124                       .stream()
125                       .filter(Update.class::isInstance)
126                       .map(Update.class::cast)
127                       .map(Update::getFile)
128                       .forEachOrdered(reloadContext::deploy);
129
130            // Reload
131            ReloadResult result = reloadService.reloadBundles(reloadContext);
132
133            // set package as started
134            pus.setPackageState(pkg, PackageState.STARTED);
135            // we need to write uninstall.xml otherwise next hot reload will fail :/
136            // as we don't use the install task, commandLogs is empty
137            // fill it with deployed bundles
138            String id = pkg.getId();
139            Version version = pkg.getVersion();
140            result.deployedFilesAsStream()
141                  // first convert it to UpdateOptions
142                  .map(f -> UpdateOptions.newInstance(id, f, f.getParentFile()))
143                  // then get key
144                  .map(installTask.getUpdateManager()::getKey)
145                  // then build the Rollback command to append to commandLogs
146                  .map(key -> new RollbackOptions(id, key, version.toString()))
147                  .map(Rollback::new)
148                  .forEachOrdered(installTask.getCommandLog()::add);
149        } catch (BundleException | PackageException | ConnectServerError e) {
150            throw new NuxeoException("Error while updating studio snapshot", e);
151        } catch (InterruptedException e) {
152            Thread.currentThread().interrupt();
153            throw new NuxeoException("Error while downloading studio snapshot", e);
154        } finally {
155            if (pkg != null && installTask != null) {
156                // write the log
157                File file = pkg.getData().getEntry(LocalPackage.UNINSTALL);
158                try {
159                    installTask.writeLog(file);
160                } catch (PackageException e) {
161                    // don't rethrow inside finally
162                    log.error("Exception when writing uninstall.xml", e);
163                }
164            }
165        }
166    }
167
168}