001/*
002 * (C) Copyright 2017 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 java.io.File;
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025import org.nuxeo.connect.connector.ConnectServerError;
026import org.nuxeo.connect.data.DownloadingPackage;
027import org.nuxeo.connect.packages.PackageManager;
028import org.nuxeo.connect.update.LocalPackage;
029import org.nuxeo.connect.update.PackageException;
030import org.nuxeo.connect.update.PackageState;
031import org.nuxeo.connect.update.PackageUpdateService;
032import org.nuxeo.connect.update.Version;
033import org.nuxeo.connect.update.task.standalone.InstallTask;
034import org.nuxeo.connect.update.task.standalone.UninstallTask;
035import org.nuxeo.connect.update.task.update.Rollback;
036import org.nuxeo.connect.update.task.update.RollbackOptions;
037import org.nuxeo.connect.update.task.update.Update;
038import org.nuxeo.connect.update.task.update.UpdateOptions;
039import org.nuxeo.ecm.core.api.NuxeoException;
040import org.nuxeo.runtime.api.Framework;
041import org.nuxeo.runtime.reload.ReloadContext;
042import org.nuxeo.runtime.reload.ReloadResult;
043import org.nuxeo.runtime.reload.ReloadService;
044import org.osgi.framework.BundleException;
045
046/**
047 * Helper to hot reload studio bundles.
048 *
049 * @since 9.3
050 */
051public class ReloadHelper {
052
053    private static final Log log = LogFactory.getLog(ReloadHelper.class);
054
055    public static synchronized void hotReloadPackage(String packageId) {
056        log.info("Reload Studio package with id=" + packageId);
057        LocalPackage pkg = null;
058        InstallTask installTask = null;
059        try {
060            ReloadService reloadService = Framework.getService(ReloadService.class);
061            ReloadContext reloadContext = new ReloadContext();
062
063            PackageManager pm = Framework.getService(PackageManager.class);
064
065            PackageUpdateService pus = Framework.getService(PackageUpdateService.class);
066            pkg = pus.getPackage(packageId);
067
068            // Remove package from PackageUpdateService and get its bundleName to hot reload it
069            if (pkg != null) {
070                if (pkg.getPackageState().isInstalled()) {
071                    // get the bundle symbolic names to hot reload
072                    UninstallTask uninstallTask = (UninstallTask) pkg.getUninstallTask();
073                    // in our hot reload case, we just care about the bundle
074                    // so get the rollback commands and then the target
075                    uninstallTask.getCommands()
076                                 .stream()
077                                 .filter(Rollback.class::isInstance)
078                                 .map(Rollback.class::cast)
079                                 .map(Rollback::getRollbackOptions)
080                                 .map(uninstallTask.getUpdateManager()::getRollbackTarget)
081                                 .map(reloadService::getOSGIBundleName)
082                                 .forEachOrdered(reloadContext::undeploy);
083                }
084                // remove the package from package update service, unless download will fail
085                pus.removePackage(pkg.getId());
086            }
087
088            // Download
089            DownloadingPackage downloadingPkg = pm.download(packageId);
090            while (!downloadingPkg.isCompleted()) {
091                if (log.isTraceEnabled()) {
092                    log.trace("Downloading studio snapshot package: " + packageId);
093                }
094                Thread.sleep(100);
095            }
096
097            log.info("Installing " + packageId);
098            pkg = pus.getPackage(packageId);
099            if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) {
100                throw new NuxeoException("Error while downloading studio snapshot " + pkg);
101            }
102
103            // get bundles to deploy
104            installTask = (InstallTask) pkg.getInstallTask();
105            pus.setPackageState(pkg, PackageState.INSTALLING);
106
107            // in our hot reload case, we just care about the bundle
108            // so get the rollback commands and then the target
109            installTask.getCommands()
110                       .stream()
111                       .filter(Update.class::isInstance)
112                       .map(Update.class::cast)
113                       .map(Update::getFile)
114                       .forEachOrdered(reloadContext::deploy);
115
116            // Reload
117            ReloadResult result = reloadService.reloadBundles(reloadContext);
118
119            // set package as started
120            pus.setPackageState(pkg, PackageState.STARTED);
121            // we need to write uninstall.xml otherwise next hot reload will fail :/
122            // as we don't use the install task, commandLogs is empty
123            // fill it with deployed bundles
124            String id = pkg.getId();
125            Version version = pkg.getVersion();
126            result.deployedFilesAsStream()
127                  // first convert it to UpdateOptions
128                  .map(f -> UpdateOptions.newInstance(id, f, f.getParentFile()))
129                  // then get key
130                  .map(installTask.getUpdateManager()::getKey)
131                  // then build the Rollback command to append to commandLogs
132                  .map(key -> new RollbackOptions(id, key, version.toString()))
133                  .map(Rollback::new)
134                  .forEachOrdered(installTask.getCommandLog()::add);
135        } catch (BundleException | PackageException | ConnectServerError e) {
136            throw new NuxeoException("Error while updating studio snapshot", e);
137        } catch (InterruptedException e) {
138            Thread.currentThread().interrupt();
139            throw new NuxeoException("Error while downloading studio snapshot", e);
140        } finally {
141            if (pkg != null && installTask != null) {
142                // write the log
143                File file = pkg.getData().getEntry(LocalPackage.UNINSTALL);
144                try {
145                    installTask.writeLog(file);
146                } catch (PackageException e) {
147                    // don't rethrow inside finally
148                    log.error("Exception when writing uninstall.xml", e);
149                }
150            }
151        }
152    }
153
154}