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