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}