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}