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.util.ArrayList; 023import java.util.List; 024 025import net.sf.json.JSONArray; 026import net.sf.json.JSONObject; 027 028import org.apache.commons.lang3.ArrayUtils; 029import org.apache.commons.logging.Log; 030import org.apache.commons.logging.LogFactory; 031import org.nuxeo.common.utils.ExceptionUtils; 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.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 boolean updateInProgress = false; 080 081 private static final Log log = LogFactory.getLog(HotReloadStudioSnapshot.class); 082 083 @Context 084 protected CoreSession session; 085 086 @Context 087 protected PackageManager pm; 088 089 @Param(name = "validate", required = false) 090 protected boolean validate = true; 091 092 @OperationMethod 093 public Blob run() { 094 095 if (updateInProgress) { 096 return jsonHelper(inProgress, "Update in progress.", null); 097 } 098 099 if (!((NuxeoPrincipal) session.getPrincipal()).isAdministrator()) { 100 return jsonHelper(error, "Must be Administrator to use this function.", null); 101 } 102 103 if (!Framework.isDevModeSet()) { 104 return jsonHelper(error, "You must enable Dev mode to Hot reload your Studio Snapshot package.", null); 105 } 106 107 List<DownloadablePackage> pkgs = pm.listRemoteAssociatedStudioPackages(); 108 DownloadablePackage snapshotPkg = StudioSnapshotHelper.getSnapshot(pkgs); 109 110 if (snapshotPkg == null) { 111 return jsonHelper(error, "No Snapshot Package was found.", null); 112 } 113 114 try { 115 updateInProgress = true; 116 return hotReloadPackage(snapshotPkg); 117 } finally { 118 updateInProgress = false; 119 } 120 } 121 122 public Blob hotReloadPackage(DownloadablePackage remotePkg) { 123 124 if (validate) { 125 pm.flushCache(); 126 127 String targetPlatform = PlatformVersionHelper.getPlatformFilter(); 128 if (!TargetPlatformFilterHelper.isCompatibleWithTargetPlatform(remotePkg, targetPlatform)) { 129 return jsonHelper(error, 130 String.format("This package is not validated for your current platform: %s", targetPlatform), 131 null); 132 } 133 134 PackageDependency[] pkgDeps = remotePkg.getDependencies(); 135 if (log.isDebugEnabled()) { 136 log.debug(String.format("%s target platforms: %s", remotePkg, 137 ArrayUtils.toString(remotePkg.getTargetPlatforms()))); 138 log.debug(String.format("%s dependencies: %s", remotePkg, ArrayUtils.toString(pkgDeps))); 139 } 140 141 String packageId = remotePkg.getId(); 142 143 // check deps requirements 144 if (pkgDeps != null && pkgDeps.length > 0) { 145 DependencyResolution resolution = pm.resolveDependencies(packageId, targetPlatform); 146 if (resolution.isFailed() && targetPlatform != null) { 147 // retry without PF filter in case it gives more information 148 resolution = pm.resolveDependencies(packageId, null); 149 } 150 if (resolution.isFailed()) { 151 return jsonHelper(dependencyMismatch, 152 String.format("Dependency check has failed for package '%s' (%s)", packageId, resolution), 153 null); 154 } else { 155 List<String> pkgToInstall = resolution.getInstallPackageIds(); 156 if (pkgToInstall != null && pkgToInstall.size() == 1 && packageId.equals(pkgToInstall.get(0))) { 157 // ignore 158 } else if (resolution.requireChanges()) { 159 // do not install needed deps: they may not be hot-reloadable and that's not what the 160 // "update snapshot" button is for. 161 // Returns missing dependencies in message instead of status 162 List<String> dependencies = new ArrayList<>(); 163 for (String dependency : resolution.getInstallPackageNames()) { 164 if (!dependency.contains(remotePkg.getName())) { 165 dependencies.add(dependency); 166 } 167 } 168 return jsonHelper(dependencyMismatch, 169 "A dependency mismatch has been detected. Please check your Studio project settings and your server configuration.", 170 dependencies); 171 } 172 } 173 } 174 } 175 176 boolean useCompatReload = Framework.isBooleanPropertyTrue(ReloadService.USE_COMPAT_HOT_RELOAD); 177 if (!useCompatReload) { 178 log.info("Use hot reload update mechanism"); 179 ReloadHelper.hotReloadPackage(remotePkg.getId()); 180 return jsonHelper(success, "Studio package installed.", null); 181 } 182 // Install 183 try { 184 PackageUpdateService pus = Framework.getService(PackageUpdateService.class); 185 String packageId = remotePkg.getId(); 186 LocalPackage pkg = pus.getPackage(packageId); 187 188 // Uninstall and/or remove if needed 189 if (pkg != null) { 190 removePackage(pus, pkg); 191 } 192 193 // Download 194 DownloadingPackage downloadingPkg = pm.download(packageId); 195 while (!downloadingPkg.isCompleted()) { 196 log.debug("Downloading studio snapshot package: " + packageId); 197 Thread.sleep(100); 198 } 199 200 log.info("Installing " + packageId); 201 pkg = pus.getPackage(packageId); 202 if (pkg == null || PackageState.DOWNLOADED != pkg.getPackageState()) { 203 throw new NuxeoException("Error while downloading studio snapshot " + pkg); 204 } 205 Task installTask = pkg.getInstallTask(); 206 try { 207 performTask(installTask); 208 return jsonHelper(success, "Studio package installed.", null); 209 } catch (PackageException e) { 210 installTask.rollback(); 211 throw e; 212 } 213 } catch (InterruptedException e) { 214 ExceptionUtils.checkInterrupt(e); 215 throw new NuxeoException("Error while downloading studio snapshot", e); 216 } catch (PackageException | ConnectServerError e) { 217 throw new NuxeoException("Error while installing studio snapshot", e); 218 } 219 220 } 221 222 protected static void removePackage(PackageUpdateService pus, LocalPackage pkg) throws PackageException { 223 log.info(String.format("Removing package %s before update...", pkg.getId())); 224 if (pkg.getPackageState().isInstalled()) { 225 // First remove it to allow SNAPSHOT upgrade 226 log.info("Uninstalling " + pkg.getId()); 227 Task uninstallTask = pkg.getUninstallTask(); 228 try { 229 performTask(uninstallTask); 230 } catch (PackageException e) { 231 uninstallTask.rollback(); 232 throw e; 233 } 234 } 235 pus.removePackage(pkg.getId()); 236 } 237 238 protected static void performTask(Task task) throws PackageException { 239 ValidationStatus validationStatus = task.validate(); 240 if (validationStatus.hasErrors()) { 241 throw new PackageException( 242 "Failed to validate package " + task.getPackage().getId() + " -> " + validationStatus.getErrors()); 243 } 244 if (validationStatus.hasWarnings()) { 245 log.warn("Got warnings on package validation " + task.getPackage().getId() + " -> " 246 + validationStatus.getWarnings()); 247 } 248 task.run(null); 249 } 250 251 protected static Blob jsonHelper(String status, String message, List<String> dependencies) { 252 JSONObject resultJSON = new JSONObject(); 253 JSONArray result = new JSONArray(); 254 resultJSON.put("status", status); 255 resultJSON.put("message", message); 256 if (dependencies != null) { 257 resultJSON.put("deps", dependencies); 258 } 259 result.add(resultJSON); 260 return Blobs.createJSONBlob(result.toString()); 261 } 262}