1 /*******************************************************************************
2  * Copyright (c) 2007, 2019 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Rapicorp, Inc - prompt to install debian package
14  *******************************************************************************/
15 package org.eclipse.equinox.internal.p2.touchpoint.natives;
16 
17 import java.io.*;
18 import java.net.URISyntaxException;
19 import java.net.URL;
20 import java.util.*;
21 import java.util.stream.Collectors;
22 import org.eclipse.core.runtime.*;
23 import org.eclipse.equinox.internal.p2.touchpoint.natives.actions.ActionConstants;
24 import org.eclipse.equinox.p2.core.*;
25 import org.eclipse.equinox.p2.engine.IProfile;
26 import org.eclipse.equinox.p2.engine.spi.Touchpoint;
27 import org.eclipse.equinox.p2.metadata.IArtifactKey;
28 import org.eclipse.equinox.p2.metadata.IInstallableUnit;
29 import org.eclipse.equinox.p2.repository.artifact.IFileArtifactRepository;
30 import org.eclipse.osgi.util.NLS;
31 
32 public class NativeTouchpoint extends Touchpoint {
33 	public static final String PARM_BACKUP = "backup"; //$NON-NLS-1$
34 	public static final String PARM_ARTIFACT = "artifact"; //$NON-NLS-1$
35 	public static final String PARM_ARTIFACT_LOCATION = "artifact.location"; //$NON-NLS-1$
36 
37 	private static final String FOLDER = "nativePackageScripts"; //$NON-NLS-1$
38 	private static final String INSTALL_COMMANDS = "installCommands.txt"; //$NON-NLS-1$
39 	private static final String INSTALL_PREFIX = "installPrefix"; //$NON-NLS-1$
40 
41 	private static Map<IProfile, IBackupStore> backups = new WeakHashMap<>();
42 
43 	private static class NativePackageToInstallInfo {
44 		NativePackageEntry entry;
45 		IInstallableUnit iu;
46 
NativePackageToInstallInfo(NativePackageEntry entry, IInstallableUnit iu)47 		public NativePackageToInstallInfo(NativePackageEntry entry, IInstallableUnit iu) {
48 			this.entry = entry;
49 			this.iu = iu;
50 		}
51 	}
52 
53 	private List<NativePackageToInstallInfo> packagesToInstall = new ArrayList<>();
54 	private Properties installCommandsProperties = new Properties();
55 
56 	private IProvisioningAgent agent;
57 	private String distro;
58 
59 	@Override
initializeOperand(IProfile profile, Map<String, Object> parameters)60 	public IStatus initializeOperand(IProfile profile, Map<String, Object> parameters) {
61 		agent = (IProvisioningAgent) parameters.get(ActionConstants.PARM_AGENT);
62 		IArtifactKey artifactKey = (IArtifactKey) parameters.get(PARM_ARTIFACT);
63 		if (!parameters.containsKey(PARM_ARTIFACT_LOCATION) && artifactKey != null) {
64 			try {
65 				IFileArtifactRepository downloadCache = Util.getDownloadCacheRepo(agent);
66 				File fileLocation = downloadCache.getArtifactFile(artifactKey);
67 				if (fileLocation != null && fileLocation.exists())
68 					parameters.put(PARM_ARTIFACT_LOCATION, fileLocation.getAbsolutePath());
69 			} catch (ProvisionException e) {
70 				return e.getStatus();
71 			}
72 		}
73 		return Status.OK_STATUS;
74 	}
75 
76 	@Override
initializePhase(IProgressMonitor monitor, IProfile profile, String phaseId, Map<String, Object> touchpointParameters)77 	public IStatus initializePhase(IProgressMonitor monitor, IProfile profile, String phaseId,
78 			Map<String, Object> touchpointParameters) {
79 		touchpointParameters.put(PARM_BACKUP, getBackupStore(profile));
80 		return null;
81 	}
82 
83 	@Override
qualifyAction(String actionId)84 	public String qualifyAction(String actionId) {
85 		return Activator.ID + "." + actionId; //$NON-NLS-1$
86 	}
87 
88 	@Override
prepare(IProfile profile)89 	public IStatus prepare(IProfile profile) {
90 		// does not have to do anything - everything is already in the correct place
91 		// the commit means that the backup is discarded - if that fails it is not a
92 		// terrible problem.
93 		return super.prepare(profile);
94 	}
95 
96 	@Override
commit(IProfile profile)97 	public IStatus commit(IProfile profile) {
98 		promptForNativePackage();
99 		IBackupStore store = getBackupStore(profile);
100 		store.discard();
101 		clearProfileState(profile);
102 		return Status.OK_STATUS;
103 	}
104 
promptForNativePackage()105 	private void promptForNativePackage() {
106 		if (packagesToInstall.size() == 0)
107 			return;
108 		loadInstallCommandsProperties(installCommandsProperties, distro);
109 		UIServices serviceUI = agent.getService(UIServices.class);
110 		String text = Messages.PromptForNative_IntroText;
111 		String downloadLinks = ""; //$NON-NLS-1$
112 		List<NativePackageEntry> entriesWithoutDownloadLink = new ArrayList<>(packagesToInstall.size());
113 		for (NativePackageToInstallInfo nativePackageEntry : packagesToInstall) {
114 			text += '\t' + nativePackageEntry.entry.name + ' ' + formatVersion(nativePackageEntry.entry);
115 			if (nativePackageEntry.iu != null) {
116 				String name = nativePackageEntry.iu.getProperty(IInstallableUnit.PROP_NAME, null);
117 				if (name != null && !name.isEmpty()) {
118 					text += ' ';
119 					text += NLS.bind(Messages.NativeTouchpoint_PromptForNative_RequiredBy, name);
120 				}
121 			}
122 
123 			text += '\n';
124 			if (nativePackageEntry.entry.getDownloadLink() != null) {
125 				downloadLinks += "    <a>" + nativePackageEntry.entry.getDownloadLink() + "</a>\n"; //$NON-NLS-1$ //$NON-NLS-2$
126 			} else {
127 				entriesWithoutDownloadLink.add(nativePackageEntry.entry);
128 			}
129 		}
130 
131 		String installCommands = createCommand(entriesWithoutDownloadLink);
132 		if (installCommands != null) {
133 			text += Messages.PromptForNative_InstallText + installCommands;
134 		}
135 
136 		String downloadText = null;
137 		if (downloadLinks.length() > 0) {
138 			downloadText = Messages.NativeTouchpoint_PromptForNative_YouCanDownloadFrom + downloadLinks;
139 		}
140 
141 		serviceUI.showInformationMessage(Messages.PromptForNative_DialogTitle, text, downloadText);
142 	}
143 
formatVersion(NativePackageEntry entry)144 	private String formatVersion(NativePackageEntry entry) {
145 		if (entry.getVersion() == null)
146 			return null;
147 		return getUserFriendlyComparator(entry.comparator) + ' ' + entry.version + ' ';
148 	}
149 
getUserFriendlyComparator(String comparator)150 	private String getUserFriendlyComparator(String comparator) {
151 		if (comparator == null)
152 			return ""; //$NON-NLS-1$
153 		return installCommandsProperties.getProperty(comparator, ""); //$NON-NLS-1$
154 	}
155 
loadInstallCommandsProperties(Properties properties, String distro)156 	public static void loadInstallCommandsProperties(Properties properties, String distro) {
157 		File f = getFileFromBundle(distro, INSTALL_COMMANDS);
158 		if (f == null)
159 			return;
160 
161 		try (InputStream is = new BufferedInputStream(new FileInputStream(f))) {
162 			properties.load(is);
163 		} catch (IOException e) {
164 			// fallthrough to return empty string
165 		}
166 	}
167 
getInstallCommad()168 	private String getInstallCommad() {
169 		return installCommandsProperties.getProperty(INSTALL_PREFIX, ""); //$NON-NLS-1$
170 	}
171 
createCommand(List<NativePackageEntry> packageEntries)172 	private String createCommand(List<NativePackageEntry> packageEntries) {
173 		if (packageEntries.isEmpty())
174 			return null;
175 		String text = getInstallCommad() + ' ';
176 		for (NativePackageEntry nativePackageEntry : packageEntries) {
177 			text += nativePackageEntry.name + " "; //$NON-NLS-1$
178 		}
179 		return text;
180 	}
181 
182 	/**
183 	 * Add the given entry as a new native package that needs to be installed.
184 	 *
185 	 * @param entry Package information about the native
186 	 * @param iu    optional IU that has this requirement
187 	 */
addPackageToInstall(NativePackageEntry entry, IInstallableUnit iu)188 	public void addPackageToInstall(NativePackageEntry entry, IInstallableUnit iu) {
189 		packagesToInstall.add(new NativePackageToInstallInfo(entry, iu));
190 	}
191 
getPackagesToInstall()192 	public List<NativePackageEntry> getPackagesToInstall() {
193 		return Collections.unmodifiableList(packagesToInstall.stream().map(e -> e.entry).collect(Collectors.toList()));
194 	}
195 
setDistro(String distro)196 	public void setDistro(String distro) {
197 		this.distro = distro;
198 	}
199 
200 	/**
201 	 * Converts a profile id into a string that can be used as a file name in any
202 	 * file system.
203 	 */
escape(String toEscape)204 	public static String escape(String toEscape) {
205 		StringBuffer buffer = new StringBuffer();
206 		int length = toEscape.length();
207 		for (int i = 0; i < length; ++i) {
208 			char ch = toEscape.charAt(i);
209 			switch (ch) {
210 			case '\\':
211 			case '/':
212 			case ':':
213 			case '*':
214 			case '?':
215 			case '"':
216 			case '<':
217 			case '>':
218 			case '|':
219 			case '%':
220 				buffer.append("%" + (int) ch + ";"); //$NON-NLS-1$ //$NON-NLS-2$
221 				break;
222 			default:
223 				buffer.append(ch);
224 			}
225 		}
226 		return buffer.toString();
227 	}
228 
229 	@Override
rollback(IProfile profile)230 	public IStatus rollback(IProfile profile) {
231 		IStatus returnStatus = Status.OK_STATUS;
232 		IBackupStore store = getBackupStore(profile);
233 		try {
234 			store.restore();
235 		} catch (IOException e) {
236 			returnStatus = new Status(IStatus.ERROR, Activator.ID,
237 					NLS.bind(Messages.failed_backup_restore, store.getBackupName()), e);
238 		} catch (ClosedBackupStoreException e) {
239 			returnStatus = new Status(IStatus.ERROR, Activator.ID,
240 					NLS.bind(Messages.failed_backup_restore, store.getBackupName()), e);
241 		}
242 		clearProfileState(profile);
243 		return returnStatus;
244 	}
245 
getFileFromBundle(String distro, String file)246 	public static File getFileFromBundle(String distro, String file) {
247 		URL[] installScripts = FileLocator.findEntries(Activator.getContext().getBundle(),
248 				new Path(NativeTouchpoint.FOLDER + '/' + distro + '/' + file));
249 		if (installScripts.length == 0)
250 			return null;
251 
252 		try {
253 			return URIUtil.toFile(URIUtil.toURI(FileLocator.toFileURL(installScripts[0])));
254 		} catch (URISyntaxException e) {
255 			// Can't happen, the URI is returned by OSGi
256 		} catch (IOException e) {
257 			// continue to return null
258 		}
259 		return null;
260 	}
261 
262 	/**
263 	 * Cleans up the transactional state associated with a profile.
264 	 */
clearProfileState(IProfile profile)265 	private static synchronized void clearProfileState(IProfile profile) {
266 		backups.remove(profile);
267 	}
268 
269 	/**
270 	 * Gets the transactional state associated with a profile. A transactional state
271 	 * is created if it did not exist.
272 	 *
273 	 * @param profile
274 	 * @return a lazily initialized backup store
275 	 */
getBackupStore(IProfile profile)276 	private static synchronized IBackupStore getBackupStore(IProfile profile) {
277 		IBackupStore store = backups.get(profile);
278 		if (store == null) {
279 			store = new LazyBackupStore(escape(profile.getProfileId()));
280 			backups.put(profile, store);
281 		}
282 		return store;
283 	}
284 }
285