001/* 002 * (C) Copyright 2016 Nuxeo SA (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 * Nuxeo 018 */ 019 020package org.nuxeo.ecm.admin.operation; 021 022import java.io.IOException; 023import java.util.ArrayList; 024import java.util.LinkedHashMap; 025import java.util.List; 026import java.util.Map; 027 028import org.apache.commons.lang3.ArrayUtils; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.nuxeo.connect.client.we.StudioSnapshotHelper; 032import org.nuxeo.connect.connector.ConnectServerError; 033import org.nuxeo.connect.data.DownloadablePackage; 034import org.nuxeo.connect.data.DownloadingPackage; 035import org.nuxeo.connect.packages.PackageManager; 036import org.nuxeo.connect.packages.dependencies.DependencyResolution; 037import org.nuxeo.connect.packages.dependencies.TargetPlatformFilterHelper; 038import org.nuxeo.connect.update.LocalPackage; 039import org.nuxeo.connect.update.PackageDependency; 040import org.nuxeo.connect.update.PackageException; 041import org.nuxeo.connect.update.PackageState; 042import org.nuxeo.connect.update.PackageUpdateService; 043import org.nuxeo.connect.update.ValidationStatus; 044import org.nuxeo.connect.update.task.Task; 045import org.nuxeo.ecm.admin.runtime.PlatformVersionHelper; 046import org.nuxeo.ecm.admin.runtime.ReloadHelper; 047import org.nuxeo.ecm.automation.OperationException; 048import org.nuxeo.ecm.automation.core.Constants; 049import org.nuxeo.ecm.automation.core.annotations.Context; 050import org.nuxeo.ecm.automation.core.annotations.Operation; 051import org.nuxeo.ecm.automation.core.annotations.OperationMethod; 052import org.nuxeo.ecm.automation.core.annotations.Param; 053import org.nuxeo.ecm.core.api.Blob; 054import org.nuxeo.ecm.core.api.Blobs; 055import org.nuxeo.ecm.core.api.CoreSession; 056import org.nuxeo.ecm.core.api.NuxeoException; 057import org.nuxeo.ecm.core.api.NuxeoPrincipal; 058import org.nuxeo.runtime.api.Framework; 059import org.nuxeo.runtime.reload.ReloadService; 060 061/** 062 * Operation to trigger a Hot reload of the Studio Snapshot package. You must be an administrator to trigger it. 063 * 064 * @since 8.2 065 */ 066@Operation(id = HotReloadStudioSnapshot.ID, category = Constants.CAT_SERVICES, label = "Hot Reload Studio Snapshot Package", description = "Updates Studio project with latest snapshot.") 067public class HotReloadStudioSnapshot { 068 069 protected static final String inProgress = "updateInProgress"; 070 071 protected static final String success = "success"; 072 073 protected static final String error = "error"; 074 075 protected static final String dependencyMismatch = "dependencyMismatch"; 076 077 public static final String ID = "Service.HotReloadStudioSnapshot"; 078 079 protected static volatile boolean updateInProgress = false; 080 081 protected static synchronized boolean setInProgress(boolean inProgress) { 082 if (updateInProgress == inProgress) { 083 return false; 084 } 085 updateInProgress = inProgress; 086 return true; 087 } 088 089 private static final Log log = LogFactory.getLog(HotReloadStudioSnapshot.class); 090 091 @Context 092 protected CoreSession session; 093 094 @Context 095 protected PackageManager pm; 096 097 @Param(name = "validate", required = false) 098 protected boolean validate = true; 099 100 @OperationMethod 101 public Blob run() throws Exception { 102 try { 103 if (!setInProgress(true)) { 104 return jsonHelper(inProgress, "Update in progress.", null); 105 } 106 107 if (!((NuxeoPrincipal) session.getPrincipal()).isAdministrator()) { 108 return jsonHelper(error, "Must be Administrator to use this function.", null); 109 } 110 111 if (!Framework.isDevModeSet()) { 112 return jsonHelper(error, "You must enable Dev mode to Hot reload your Studio Snapshot package.", null); 113 } 114 115 List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages(); 116 DownloadablePackage snapshotPkg = StudioSnapshotHelper.getSnapshot(pkgs); 117 118 if (snapshotPkg == null) { 119 return jsonHelper(error, "No Snapshot Package was found.", null); 120 } 121 122 return hotReloadPackage(snapshotPkg); 123 } catch (RuntimeException e) { 124 throw new OperationException(e); 125 } finally { 126 setInProgress(false); 127 } 128 } 129 130 public Blob hotReloadPackage(DownloadablePackage remotePkg) { 131 132 if (validate) { 133 pm.flushCache(); 134 135 String targetPlatform = PlatformVersionHelper.getPlatformFilter(); 136 if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(remotePkg, targetPlatform)) { 137 return jsonHelper(error, 138 String.format("This package is not validated for your current platform: %s", targetPlatform), 139 null); 140 } 141 142 PackageDependency[] pkgDeps = remotePkg.getDependencies(); 143 if (log.isDebugEnabled()) { 144 log.debug(String.format("%s target platforms: %s", remotePkg, 145 ArrayUtils.toString(remotePkg.getTargetPlatforms()))); 146 log.debug(String.format("%s dependencies: %s", remotePkg, ArrayUtils.toString(pkgDeps))); 147 } 148 149 String packageId = remotePkg.getId(); 150 151 // check deps requirements 152 if (pkgDeps != null && pkgDeps.length > 0) { 153 DependencyResolution resolution = pm.resolveDependencies(packageId, targetPlatform); 154 if (resolution.isFailed() && targetPlatform != null) { 155 // retry without PF filter in case it gives more information 156 resolution = pm.resolveDependencies(packageId, null); 157 } 158 if (resolution.isFailed()) { 159 return jsonHelper(dependencyMismatch, 160 String.format("Dependency check has failed for package '%s' (%s)", packageId, resolution), 161 null); 162 } else { 163 List<String> pkgToInstall = resolution.getInstallPackageIds(); 164 if (pkgToInstall != null && pkgToInstall.size() == 1 && packageId.equals(pkgToInstall.get(0))) { 165 // ignore 166 } else if (resolution.requireChanges()) { 167 // do not install needed deps: they may not be hot-reloadable and that's not what the 168 // "update snapshot" button is for. 169 // Returns missing dependencies in message instead of status 170 List<String> dependencies = new ArrayList<>(); 171 for (String dependency : resolution.getInstallPackageNames()) { 172 if (!dependency.contains(remotePkg.getName())) { 173 dependencies.add(dependency); 174 } 175 } 176 return jsonHelper(dependencyMismatch, 177 "A dependency mismatch has been detected. Please check your Studio project settings and your server configuration.", 178 dependencies); 179 } 180 } 181 } 182 } 183 184 boolean useCompatReload = Framework.isBooleanPropertyTrue(ReloadService.USE_COMPAT_HOT_RELOAD); 185 if (!useCompatReload) { 186 log.info("Use hot reload update mechanism"); 187 ReloadHelper.hotReloadPackage(remotePkg.getId()); 188 return jsonHelper(success, "Studio package installed.", null); 189 } 190 // Install 191 try { 192 PackageUpdateService pus = Framework.getService(PackageUpdateService.class); 193 String packageId = remotePkg.getId(); 194 LocalPackage pkg = pus.getPackage(packageId); 195 196 // Uninstall and/or remove if needed 197 if (pkg != null) { 198 removePackage(pus, pkg); 199 } 200 201 // Download 202 DownloadingPackage downloadingPkg = pm.download(packageId); 203 while (!downloadingPkg.isCompleted()) { 204 log.debug("Downloading studio snapshot package: " + packageId); 205 Thread.sleep(100); 206 } 207 208 log.info("Installing " + packageId); 209 pkg = pus.getPackage(packageId); 210 if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) { 211 throw new NuxeoException("Error while downloading studio snapshot " + pkg); 212 } 213 Task installTask = pkg.getInstallTask(); 214 try { 215 performTask(installTask); 216 return jsonHelper(success, "Studio package installed.", null); 217 } catch (PackageException e) { 218 installTask.rollback(); 219 throw e; 220 } 221 } catch (InterruptedException e) { 222 Thread.currentThread().interrupt(); 223 throw new NuxeoException(e); 224 } catch (PackageException | ConnectServerError e) { 225 throw new NuxeoException("Error while installing studio snapshot", e); 226 } 227 228 } 229 230 protected static void removePackage(PackageUpdateService pus, LocalPackage pkg) throws PackageException { 231 log.info(String.format("Removing package %s before update...", pkg.getId())); 232 if (pkg.getPackageState().isInstalled()) { 233 // First remove it to allow SNAPSHOT upgrade 234 log.info("Uninstalling " + pkg.getId()); 235 Task uninstallTask = pkg.getUninstallTask(); 236 try { 237 performTask(uninstallTask); 238 } catch (PackageException e) { 239 uninstallTask.rollback(); 240 throw e; 241 } 242 } 243 pus.removePackage(pkg.getId()); 244 } 245 246 protected static void performTask(Task task) throws PackageException { 247 ValidationStatus validationStatus = task.validate(); 248 if (validationStatus.hasErrors()) { 249 throw new PackageException( 250 "Failed to validate package " + task.getPackage().getId() + " -> " + validationStatus.getErrors()); 251 } 252 if (validationStatus.hasWarnings()) { 253 log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> " 254 + validationStatus.getWarnings()); 255 } 256 task.run(null); 257 } 258 259 protected static Blob jsonHelper(String status, String message, List<String> dependencies) { 260 List<Map<String, Object>> result = new ArrayList<>(); 261 Map<String, Object> resultJSON = new LinkedHashMap<>(); 262 resultJSON.put("status", status); 263 resultJSON.put("message", message); 264 if (dependencies != null) { 265 resultJSON.put("deps", dependencies); 266 } 267 result.add(resultJSON); 268 try { 269 return Blobs.createJSONBlobFromValue(result); 270 } catch (IOException e) { 271 throw new NuxeoException("Unable to create json response", e); 272 } 273 } 274}