Line data Source code
1 : // Copyright (C) 2012 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.plugins;
16 :
17 : import com.google.common.base.CharMatcher;
18 : import com.google.common.base.Joiner;
19 : import com.google.common.base.MoreObjects;
20 : import com.google.common.base.Strings;
21 : import com.google.common.collect.ComparisonChain;
22 : import com.google.common.collect.ImmutableList;
23 : import com.google.common.collect.Iterables;
24 : import com.google.common.collect.LinkedHashMultimap;
25 : import com.google.common.collect.Lists;
26 : import com.google.common.collect.Maps;
27 : import com.google.common.collect.SetMultimap;
28 : import com.google.common.collect.Sets;
29 : import com.google.common.flogger.FluentLogger;
30 : import com.google.gerrit.common.Nullable;
31 : import com.google.gerrit.extensions.events.LifecycleListener;
32 : import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
33 : import com.google.gerrit.extensions.systemstatus.ServerInformation;
34 : import com.google.gerrit.server.PluginUser;
35 : import com.google.gerrit.server.cache.PersistentCacheFactory;
36 : import com.google.gerrit.server.config.CanonicalWebUrl;
37 : import com.google.gerrit.server.config.ConfigUtil;
38 : import com.google.gerrit.server.config.GerritRuntime;
39 : import com.google.gerrit.server.config.GerritServerConfig;
40 : import com.google.gerrit.server.config.SitePaths;
41 : import com.google.gerrit.server.plugins.ServerPluginProvider.PluginDescription;
42 : import com.google.inject.Inject;
43 : import com.google.inject.Provider;
44 : import com.google.inject.Singleton;
45 : import java.io.IOException;
46 : import java.io.InputStream;
47 : import java.nio.file.DirectoryStream;
48 : import java.nio.file.Files;
49 : import java.nio.file.Path;
50 : import java.nio.file.Paths;
51 : import java.util.AbstractMap;
52 : import java.util.ArrayDeque;
53 : import java.util.ArrayList;
54 : import java.util.Collection;
55 : import java.util.Comparator;
56 : import java.util.HashSet;
57 : import java.util.Iterator;
58 : import java.util.List;
59 : import java.util.Map;
60 : import java.util.Queue;
61 : import java.util.Set;
62 : import java.util.TreeSet;
63 : import java.util.concurrent.ConcurrentMap;
64 : import java.util.concurrent.TimeUnit;
65 : import org.eclipse.jgit.internal.storage.file.FileSnapshot;
66 : import org.eclipse.jgit.lib.Config;
67 :
68 149 : @Singleton
69 : public class PluginLoader implements LifecycleListener {
70 149 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
71 :
72 : public String getPluginName(Path srcPath) {
73 0 : return MoreObjects.firstNonNull(getGerritPluginName(srcPath), PluginUtil.nameOf(srcPath));
74 : }
75 :
76 : private final Path pluginsDir;
77 : private final Path dataDir;
78 : private final Path tempDir;
79 : private final PluginGuiceEnvironment env;
80 : private final ServerInformationImpl srvInfoImpl;
81 : private final PluginUser.Factory pluginUserFactory;
82 149 : private final ConcurrentMap<String, Plugin> running = Maps.newConcurrentMap();
83 149 : private final ConcurrentMap<String, Plugin> disabled = Maps.newConcurrentMap();
84 149 : private final Map<String, FileSnapshot> broken = Maps.newHashMap();
85 149 : private final Map<Plugin, CleanupHandle> cleanupHandles = Maps.newConcurrentMap();
86 149 : private final Queue<Plugin> toCleanup = new ArrayDeque<>();
87 : private final Provider<PluginCleanerTask> cleaner;
88 : private final PluginScannerThread scanner;
89 : private final Provider<String> urlProvider;
90 : private final PersistentCacheFactory persistentCacheFactory;
91 : private final boolean remoteAdmin;
92 : private final MandatoryPluginsCollection mandatoryPlugins;
93 : private final UniversalServerPluginProvider serverPluginFactory;
94 : private final GerritRuntime gerritRuntime;
95 :
96 : @Inject
97 : public PluginLoader(
98 : SitePaths sitePaths,
99 : PluginGuiceEnvironment pe,
100 : ServerInformationImpl sii,
101 : PluginUser.Factory puf,
102 : Provider<PluginCleanerTask> pct,
103 : @GerritServerConfig Config cfg,
104 : @CanonicalWebUrl Provider<String> provider,
105 : PersistentCacheFactory cacheFactory,
106 : UniversalServerPluginProvider pluginFactory,
107 : MandatoryPluginsCollection mpc,
108 149 : GerritRuntime gerritRuntime) {
109 149 : pluginsDir = sitePaths.plugins_dir;
110 149 : dataDir = sitePaths.data_dir;
111 149 : tempDir = sitePaths.tmp_dir;
112 149 : env = pe;
113 149 : srvInfoImpl = sii;
114 149 : pluginUserFactory = puf;
115 149 : cleaner = pct;
116 149 : urlProvider = provider;
117 149 : persistentCacheFactory = cacheFactory;
118 149 : serverPluginFactory = pluginFactory;
119 :
120 149 : remoteAdmin = cfg.getBoolean("plugins", null, "allowRemoteAdmin", false);
121 149 : mandatoryPlugins = mpc;
122 149 : this.gerritRuntime = gerritRuntime;
123 :
124 149 : long checkFrequency =
125 149 : ConfigUtil.getTimeUnit(
126 : cfg,
127 : "plugins",
128 : null,
129 : "checkFrequency",
130 149 : TimeUnit.MINUTES.toMillis(1),
131 : TimeUnit.MILLISECONDS);
132 149 : if (checkFrequency > 0) {
133 13 : scanner = new PluginScannerThread(this, checkFrequency);
134 : } else {
135 138 : scanner = null;
136 : }
137 149 : }
138 :
139 : public boolean isRemoteAdminEnabled() {
140 3 : return remoteAdmin;
141 : }
142 :
143 : public void checkRemoteAdminEnabled() throws MethodNotAllowedException {
144 3 : if (!remoteAdmin) {
145 1 : throw new MethodNotAllowedException("remote plugin administration is disabled");
146 : }
147 3 : }
148 :
149 : public Plugin get(String name) {
150 3 : Plugin p = running.get(name);
151 3 : if (p != null) {
152 3 : return p;
153 : }
154 2 : return disabled.get(name);
155 : }
156 :
157 : public Iterable<Plugin> getPlugins(boolean all) {
158 2 : if (!all) {
159 2 : return running.values();
160 : }
161 1 : List<Plugin> plugins = new ArrayList<>(running.values());
162 1 : plugins.addAll(disabled.values());
163 1 : return plugins;
164 : }
165 :
166 : public String installPluginFromStream(String originalName, InputStream in)
167 : throws IOException, PluginInstallException {
168 3 : checkRemoteInstall();
169 :
170 3 : String fileName = originalName;
171 3 : Path tmp = PluginUtil.asTemp(in, ".next_" + fileName + "_", ".tmp", pluginsDir);
172 3 : String name = MoreObjects.firstNonNull(getGerritPluginName(tmp), PluginUtil.nameOf(fileName));
173 3 : if (!originalName.equals(name)) {
174 3 : logger.atWarning().log(
175 : "Plugin provides its own name: <%s>, use it instead of the input name: <%s>",
176 : name, originalName);
177 : }
178 :
179 3 : String fileExtension = getExtension(fileName);
180 3 : Path dst = pluginsDir.resolve(name + fileExtension);
181 3 : synchronized (this) {
182 3 : Plugin active = running.get(name);
183 3 : if (active != null) {
184 1 : fileName = active.getSrcFile().getFileName().toString();
185 1 : logger.atInfo().log("Replacing plugin %s", active.getName());
186 1 : Path old = pluginsDir.resolve(".last_" + fileName);
187 1 : Files.deleteIfExists(old);
188 1 : Files.move(active.getSrcFile(), old);
189 : }
190 :
191 3 : Files.deleteIfExists(pluginsDir.resolve(fileName + ".disabled"));
192 3 : Files.move(tmp, dst);
193 : try {
194 3 : Plugin plugin = runPlugin(name, dst, active);
195 3 : if (active == null) {
196 3 : logger.atInfo().log("Installed plugin %s", plugin.getName());
197 : }
198 1 : } catch (PluginInstallException e) {
199 1 : Files.deleteIfExists(dst);
200 1 : throw e;
201 3 : }
202 :
203 3 : cleanInBackground();
204 3 : }
205 :
206 3 : return name;
207 : }
208 :
209 : private synchronized void unloadPlugin(Plugin plugin) {
210 3 : persistentCacheFactory.onStop(plugin.getName());
211 3 : String name = plugin.getName();
212 3 : logger.atInfo().log("Unloading plugin %s, version %s", name, plugin.getVersion());
213 3 : plugin.stop(env);
214 3 : env.onStopPlugin(plugin);
215 3 : running.remove(name);
216 3 : disabled.remove(name);
217 3 : toCleanup.add(plugin);
218 3 : }
219 :
220 : public void disablePlugins(Set<String> names) {
221 2 : if (!isRemoteAdminEnabled()) {
222 0 : logger.atWarning().log(
223 : "Remote plugin administration is disabled, ignoring disablePlugins(%s)", names);
224 0 : return;
225 : }
226 :
227 2 : synchronized (this) {
228 2 : for (String name : names) {
229 2 : Plugin active = running.get(name);
230 2 : if (active == null) {
231 0 : continue;
232 : }
233 :
234 2 : if (mandatoryPlugins.contains(name)) {
235 0 : logger.atWarning().log("Mandatory plugin %s cannot be disabled", name);
236 0 : continue;
237 : }
238 :
239 2 : logger.atInfo().log("Disabling plugin %s", active.getName());
240 2 : Path off =
241 2 : active.getSrcFile().resolveSibling(active.getSrcFile().getFileName() + ".disabled");
242 : try {
243 1 : Files.move(active.getSrcFile(), off);
244 1 : } catch (IOException e) {
245 1 : logger.atSevere().withCause(e).log("Failed to disable plugin");
246 : // In theory we could still unload the plugin even if the rename
247 : // failed. However, it would be reloaded on the next server startup,
248 : // which is probably not what the user expects.
249 1 : continue;
250 1 : }
251 :
252 1 : unloadPlugin(active);
253 : try {
254 1 : FileSnapshot snapshot = FileSnapshot.save(off.toFile());
255 1 : Plugin offPlugin = loadPlugin(name, off, snapshot);
256 1 : disabled.put(name, offPlugin);
257 0 : } catch (Exception e) {
258 : // This shouldn't happen, as the plugin was loaded earlier.
259 0 : logger.atWarning().withCause(e.getCause()).log(
260 0 : "Cannot load disabled plugin %s", active.getName());
261 1 : }
262 1 : }
263 2 : cleanInBackground();
264 2 : }
265 2 : }
266 :
267 : public void enablePlugins(Set<String> names) throws PluginInstallException {
268 2 : if (!isRemoteAdminEnabled()) {
269 0 : logger.atWarning().log(
270 : "Remote plugin administration is disabled, ignoring enablePlugins(%s)", names);
271 0 : return;
272 : }
273 :
274 2 : synchronized (this) {
275 2 : for (String name : names) {
276 2 : Plugin off = disabled.get(name);
277 2 : if (off == null) {
278 1 : continue;
279 : }
280 :
281 1 : logger.atInfo().log("Enabling plugin %s", name);
282 1 : String n = off.getSrcFile().toFile().getName();
283 1 : if (n.endsWith(".disabled")) {
284 1 : n = n.substring(0, n.lastIndexOf('.'));
285 : }
286 1 : Path on = pluginsDir.resolve(n);
287 : try {
288 1 : Files.move(off.getSrcFile(), on);
289 0 : } catch (IOException e) {
290 0 : logger.atSevere().withCause(e).log("Failed to move plugin %s into place", name);
291 0 : continue;
292 1 : }
293 1 : disabled.remove(name);
294 1 : runPlugin(name, on, null);
295 1 : }
296 2 : cleanInBackground();
297 2 : }
298 2 : }
299 :
300 : private void removeStalePluginFiles() {
301 138 : DirectoryStream.Filter<Path> filter =
302 0 : entry -> entry.getFileName().toString().startsWith("plugin_");
303 15 : try (DirectoryStream<Path> files = Files.newDirectoryStream(tempDir, filter)) {
304 15 : for (Path file : files) {
305 0 : logger.atInfo().log("Removing stale plugin file: %s", file.toFile().getName());
306 : try {
307 0 : Files.delete(file);
308 0 : } catch (IOException e) {
309 0 : logger.atSevere().log(
310 0 : "Failed to remove stale plugin file %s: %s", file.toFile().getName(), e.getMessage());
311 0 : }
312 0 : }
313 132 : } catch (IOException e) {
314 132 : logger.atWarning().log("Unable to discover stale plugin files: %s", e.getMessage());
315 15 : }
316 138 : }
317 :
318 : @Override
319 : public synchronized void start() {
320 138 : removeStalePluginFiles();
321 138 : Path absolutePath = pluginsDir.toAbsolutePath();
322 138 : if (!Files.exists(absolutePath)) {
323 132 : logger.atInfo().log("%s does not exist; creating", absolutePath);
324 : try {
325 132 : Files.createDirectories(absolutePath);
326 0 : } catch (IOException e) {
327 0 : logger.atSevere().log("Failed to create %s: %s", absolutePath, e.getMessage());
328 132 : }
329 : }
330 138 : logger.atInfo().log("Loading plugins from %s", absolutePath);
331 138 : srvInfoImpl.state = ServerInformation.State.STARTUP;
332 138 : rescan();
333 138 : srvInfoImpl.state = ServerInformation.State.RUNNING;
334 138 : if (scanner != null) {
335 0 : scanner.start();
336 : }
337 138 : }
338 :
339 : @Override
340 : public void stop() {
341 138 : if (scanner != null) {
342 0 : scanner.end();
343 : }
344 138 : srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
345 138 : synchronized (this) {
346 138 : for (Plugin p : running.values()) {
347 3 : unloadPlugin(p);
348 3 : }
349 138 : running.clear();
350 138 : disabled.clear();
351 138 : broken.clear();
352 138 : if (!toCleanup.isEmpty()) {
353 3 : System.gc();
354 3 : processPendingCleanups();
355 : }
356 138 : }
357 138 : }
358 :
359 : public void reload(List<String> names) throws InvalidPluginException, PluginInstallException {
360 1 : synchronized (this) {
361 1 : List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
362 1 : List<String> bad = Lists.newArrayListWithExpectedSize(4);
363 1 : for (String name : names) {
364 1 : Plugin active = running.get(name);
365 1 : if (active != null) {
366 1 : reload.add(active);
367 : } else {
368 0 : bad.add(name);
369 : }
370 1 : }
371 1 : if (!bad.isEmpty()) {
372 0 : throw new InvalidPluginException(
373 0 : String.format("Plugin(s) \"%s\" not running", Joiner.on("\", \"").join(bad)));
374 : }
375 :
376 1 : for (Plugin active : reload) {
377 1 : String name = active.getName();
378 : try {
379 1 : logger.atInfo().log("Reloading plugin %s", name);
380 1 : Plugin newPlugin = runPlugin(name, active.getSrcFile(), active);
381 1 : logger.atInfo().log(
382 1 : "Reloaded plugin %s, version %s", newPlugin.getName(), newPlugin.getVersion());
383 0 : } catch (PluginInstallException e) {
384 0 : logger.atWarning().withCause(e.getCause()).log("Cannot reload plugin %s", name);
385 0 : throw e;
386 1 : }
387 1 : }
388 :
389 1 : cleanInBackground();
390 1 : }
391 1 : }
392 :
393 : public synchronized void rescan() {
394 138 : Set<String> loadedPlugins = new HashSet<>();
395 138 : SetMultimap<String, Path> pluginsFiles = prunePlugins(pluginsDir);
396 :
397 138 : if (!pluginsFiles.isEmpty()) {
398 0 : syncDisabledPlugins(pluginsFiles);
399 :
400 0 : Map<String, Path> activePlugins = filterDisabled(pluginsFiles);
401 0 : for (Map.Entry<String, Path> entry : jarsFirstSortedPluginsSet(activePlugins)) {
402 0 : String name = entry.getKey();
403 0 : Path path = entry.getValue();
404 0 : String fileName = path.getFileName().toString();
405 0 : if (!isUiPlugin(fileName) && !serverPluginFactory.handles(path)) {
406 0 : logger.atWarning().log(
407 : "No Plugin provider was found that handles this file format: %s", fileName);
408 0 : continue;
409 : }
410 :
411 0 : FileSnapshot brokenTime = broken.get(name);
412 0 : if (brokenTime != null && !brokenTime.isModified(path.toFile())) {
413 0 : continue;
414 : }
415 :
416 0 : Plugin active = running.get(name);
417 0 : if (active != null && !active.isModified(path)) {
418 0 : loadedPlugins.add(name);
419 0 : continue;
420 : }
421 :
422 0 : if (active != null) {
423 0 : logger.atInfo().log("Reloading plugin %s", active.getName());
424 : }
425 :
426 : try {
427 0 : Plugin loadedPlugin = runPlugin(name, path, active);
428 0 : if (!loadedPlugin.isDisabled()) {
429 0 : loadedPlugins.add(name);
430 0 : logger.atInfo().log(
431 : "%s plugin %s, version %s",
432 0 : active == null ? "Loaded" : "Reloaded",
433 0 : loadedPlugin.getName(),
434 0 : loadedPlugin.getVersion());
435 : }
436 0 : } catch (PluginInstallException e) {
437 0 : logger.atWarning().withCause(e.getCause()).log("Cannot load plugin %s", name);
438 0 : }
439 0 : }
440 : }
441 :
442 138 : Set<String> missingMandatory = Sets.difference(mandatoryPlugins.asSet(), loadedPlugins);
443 138 : if (!missingMandatory.isEmpty()) {
444 1 : throw new MissingMandatoryPluginsException(missingMandatory);
445 : }
446 :
447 138 : cleanInBackground();
448 138 : }
449 :
450 : private void addAllEntries(Map<String, Path> from, TreeSet<Map.Entry<String, Path>> to) {
451 0 : Iterator<Map.Entry<String, Path>> it = from.entrySet().iterator();
452 0 : while (it.hasNext()) {
453 0 : Map.Entry<String, Path> entry = it.next();
454 0 : to.add(new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue()));
455 0 : }
456 0 : }
457 :
458 : private TreeSet<Map.Entry<String, Path>> jarsFirstSortedPluginsSet(
459 : Map<String, Path> activePlugins) {
460 0 : TreeSet<Map.Entry<String, Path>> sortedPlugins =
461 0 : Sets.newTreeSet(
462 0 : new Comparator<Map.Entry<String, Path>>() {
463 : @Override
464 : public int compare(Map.Entry<String, Path> e1, Map.Entry<String, Path> e2) {
465 0 : Path n1 = e1.getValue().getFileName();
466 0 : Path n2 = e2.getValue().getFileName();
467 0 : return ComparisonChain.start()
468 0 : .compareTrueFirst(isJar(n1), isJar(n2))
469 0 : .compare(n1, n2)
470 0 : .result();
471 : }
472 :
473 : private boolean isJar(Path n1) {
474 0 : return n1.toString().endsWith(".jar");
475 : }
476 : });
477 :
478 0 : addAllEntries(activePlugins, sortedPlugins);
479 0 : return sortedPlugins;
480 : }
481 :
482 : private void syncDisabledPlugins(SetMultimap<String, Path> jars) {
483 0 : stopRemovedPlugins(jars);
484 0 : dropRemovedDisabledPlugins(jars);
485 0 : }
486 :
487 : private Plugin runPlugin(String name, Path plugin, Plugin oldPlugin)
488 : throws PluginInstallException {
489 3 : FileSnapshot snapshot = FileSnapshot.save(plugin.toFile());
490 : try {
491 3 : boolean restartRequired = oldPlugin != null && !oldPlugin.canReload();
492 3 : if (restartRequired && mandatoryPlugins.contains(name)) {
493 0 : logger.atWarning().log("Restarting mandatory plugin %s not allowed", name);
494 0 : return oldPlugin;
495 : }
496 :
497 3 : Plugin newPlugin = loadPlugin(name, plugin, snapshot);
498 3 : if (newPlugin.getCleanupHandle() != null) {
499 1 : cleanupHandles.put(newPlugin, newPlugin.getCleanupHandle());
500 : }
501 : /*
502 : * Pluggable plugin provider may have assigned a plugin name that could be
503 : * actually different from the initial one assigned during scan. It is
504 : * safer then to reassign it.
505 : */
506 3 : name = newPlugin.getName();
507 3 : boolean reload = oldPlugin != null && oldPlugin.canReload() && newPlugin.canReload();
508 3 : if (!reload && oldPlugin != null) {
509 0 : unloadPlugin(oldPlugin);
510 : }
511 3 : if (!newPlugin.isDisabled()) {
512 : try {
513 3 : newPlugin.start(env);
514 0 : } catch (Exception e) {
515 0 : newPlugin.stop(env);
516 0 : throw e;
517 3 : }
518 : }
519 3 : if (reload) {
520 1 : env.onReloadPlugin(oldPlugin, newPlugin);
521 1 : unloadPlugin(oldPlugin);
522 3 : } else if (!newPlugin.isDisabled()) {
523 3 : env.onStartPlugin(newPlugin);
524 : }
525 3 : if (!newPlugin.isDisabled()) {
526 3 : running.put(name, newPlugin);
527 : } else {
528 0 : disabled.put(name, newPlugin);
529 : }
530 3 : broken.remove(name);
531 3 : return newPlugin;
532 1 : } catch (Exception err) {
533 1 : broken.put(name, snapshot);
534 1 : throw new PluginInstallException(err);
535 : }
536 : }
537 :
538 : private void stopRemovedPlugins(SetMultimap<String, Path> jars) {
539 0 : Set<String> unload = Sets.newHashSet(running.keySet());
540 0 : for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
541 0 : for (Path path : entry.getValue()) {
542 0 : if (!path.getFileName().toString().endsWith(".disabled")) {
543 0 : unload.remove(entry.getKey());
544 : }
545 0 : }
546 0 : }
547 0 : for (String name : unload) {
548 0 : unloadPlugin(running.get(name));
549 0 : }
550 0 : }
551 :
552 : private void dropRemovedDisabledPlugins(SetMultimap<String, Path> jars) {
553 0 : Set<String> unload = Sets.newHashSet(disabled.keySet());
554 0 : for (Map.Entry<String, Collection<Path>> entry : jars.asMap().entrySet()) {
555 0 : for (Path path : entry.getValue()) {
556 0 : if (path.getFileName().toString().endsWith(".disabled")) {
557 0 : unload.remove(entry.getKey());
558 : }
559 0 : }
560 0 : }
561 0 : for (String name : unload) {
562 0 : disabled.remove(name);
563 0 : }
564 0 : }
565 :
566 : synchronized int processPendingCleanups() {
567 3 : Iterator<Plugin> iterator = toCleanup.iterator();
568 3 : while (iterator.hasNext()) {
569 3 : Plugin plugin = iterator.next();
570 3 : iterator.remove();
571 :
572 3 : CleanupHandle cleanupHandle = cleanupHandles.remove(plugin);
573 3 : if (cleanupHandle != null) {
574 1 : cleanupHandle.cleanup();
575 : }
576 3 : }
577 3 : return toCleanup.size();
578 : }
579 :
580 : private void cleanInBackground() {
581 138 : int cnt = toCleanup.size();
582 138 : if (0 < cnt) {
583 2 : cleaner.get().clean(cnt);
584 : }
585 138 : }
586 :
587 : private String getExtension(String name) {
588 3 : int ext = name.lastIndexOf('.');
589 3 : return 0 < ext ? name.substring(ext) : "";
590 : }
591 :
592 : private Plugin loadPlugin(String name, Path srcPlugin, FileSnapshot snapshot)
593 : throws InvalidPluginException {
594 3 : String pluginName = srcPlugin.getFileName().toString();
595 3 : if (isUiPlugin(pluginName)) {
596 3 : return loadJsPlugin(name, srcPlugin, snapshot);
597 2 : } else if (serverPluginFactory.handles(srcPlugin)) {
598 1 : return loadServerPlugin(srcPlugin, snapshot);
599 : } else {
600 1 : throw new InvalidPluginException(
601 1 : String.format("Unsupported plugin type: %s", srcPlugin.getFileName()));
602 : }
603 : }
604 :
605 : private Path getPluginDataDir(String name) {
606 1 : return dataDir.resolve(name);
607 : }
608 :
609 : private String getPluginCanonicalWebUrl(String name) {
610 1 : String canonicalWebUrl = urlProvider.get();
611 1 : if (Strings.isNullOrEmpty(canonicalWebUrl)) {
612 0 : return "/plugins/" + name;
613 : }
614 :
615 1 : String url =
616 1 : String.format(
617 1 : "%s/plugins/%s/", CharMatcher.is('/').trimTrailingFrom(canonicalWebUrl), name);
618 1 : return url;
619 : }
620 :
621 : private Plugin loadJsPlugin(String name, Path srcJar, FileSnapshot snapshot) {
622 3 : return new JsPlugin(name, srcJar, pluginUserFactory.create(name), snapshot);
623 : }
624 :
625 : private ServerPlugin loadServerPlugin(Path scriptFile, FileSnapshot snapshot)
626 : throws InvalidPluginException {
627 1 : String name = serverPluginFactory.getPluginName(scriptFile);
628 1 : return serverPluginFactory.get(
629 : scriptFile,
630 : snapshot,
631 : new PluginDescription(
632 1 : pluginUserFactory.create(name),
633 1 : getPluginCanonicalWebUrl(name),
634 1 : getPluginDataDir(name),
635 : gerritRuntime));
636 : }
637 :
638 : // Only one active plugin per plugin name can exist for each plugin name.
639 : // Filter out disabled plugins and transform the multimap to a map
640 : private Map<String, Path> filterDisabled(SetMultimap<String, Path> pluginPaths) {
641 0 : Map<String, Path> activePlugins = Maps.newHashMapWithExpectedSize(pluginPaths.keys().size());
642 0 : for (String name : pluginPaths.keys()) {
643 0 : for (Path pluginPath : pluginPaths.asMap().get(name)) {
644 0 : if (!pluginPath.getFileName().toString().endsWith(".disabled")) {
645 0 : assert !activePlugins.containsKey(name);
646 0 : activePlugins.put(name, pluginPath);
647 : }
648 0 : }
649 0 : }
650 0 : return activePlugins;
651 : }
652 :
653 : // Scan the $site_path/plugins directory and fetch all files and directories.
654 : // The Key in returned multimap is the plugin name initially assigned from its filename.
655 : // Values are the files. Plugins can optionally provide their name in MANIFEST file.
656 : // If multiple plugin files provide the same plugin name, then only
657 : // the first plugin remains active and all other plugins with the same
658 : // name are disabled.
659 : //
660 : // NOTE: Bear in mind that the plugin name can be reassigned after load by the
661 : // Server plugin provider.
662 : public SetMultimap<String, Path> prunePlugins(Path pluginsDir) {
663 138 : List<Path> pluginPaths = scanPathsInPluginsDirectory(pluginsDir);
664 : SetMultimap<String, Path> map;
665 138 : map = asMultimap(pluginPaths);
666 138 : for (String plugin : map.keySet()) {
667 0 : Collection<Path> files = map.asMap().get(plugin);
668 0 : if (files.size() == 1) {
669 0 : continue;
670 : }
671 : // retrieve enabled plugins
672 0 : Iterable<Path> enabled = filterDisabledPlugins(files);
673 : // If we have only one (the winner) plugin, nothing to do
674 0 : if (!Iterables.skip(enabled, 1).iterator().hasNext()) {
675 0 : continue;
676 : }
677 0 : Path winner = Iterables.getFirst(enabled, null);
678 0 : assert winner != null;
679 : // Disable all loser plugins by renaming their file names to
680 : // "file.disabled" and replace the disabled files in the multimap.
681 0 : Collection<Path> elementsToRemove = new ArrayList<>();
682 0 : Collection<Path> elementsToAdd = new ArrayList<>();
683 0 : for (Path loser : Iterables.skip(enabled, 1)) {
684 0 : logger.atWarning().log(
685 : "Plugin <%s> was disabled, because"
686 : + " another plugin <%s>"
687 : + " with the same name <%s> already exists",
688 : loser, winner, plugin);
689 0 : Path disabledPlugin = Paths.get(loser + ".disabled");
690 0 : elementsToAdd.add(disabledPlugin);
691 0 : elementsToRemove.add(loser);
692 : try {
693 0 : Files.move(loser, disabledPlugin);
694 0 : } catch (IOException e) {
695 0 : logger.atWarning().withCause(e).log("Failed to fully disable plugin %s", loser);
696 0 : }
697 0 : }
698 0 : Iterables.removeAll(files, elementsToRemove);
699 0 : Iterables.addAll(files, elementsToAdd);
700 0 : }
701 138 : return map;
702 : }
703 :
704 : private List<Path> scanPathsInPluginsDirectory(Path pluginsDir) {
705 : try {
706 138 : return PluginUtil.listPlugins(pluginsDir);
707 0 : } catch (IOException e) {
708 0 : logger.atSevere().withCause(e).log("Cannot list %s", pluginsDir.toAbsolutePath());
709 0 : return ImmutableList.of();
710 : }
711 : }
712 :
713 : private Iterable<Path> filterDisabledPlugins(Collection<Path> paths) {
714 0 : return Iterables.filter(paths, p -> !p.getFileName().toString().endsWith(".disabled"));
715 : }
716 :
717 : @Nullable
718 : public String getGerritPluginName(Path srcPath) {
719 3 : String fileName = srcPath.getFileName().toString();
720 3 : if (isUiPlugin(fileName)) {
721 0 : return fileName.substring(0, fileName.lastIndexOf('.'));
722 : }
723 3 : if (serverPluginFactory.handles(srcPath)) {
724 0 : return serverPluginFactory.getPluginName(srcPath);
725 : }
726 3 : return null;
727 : }
728 :
729 : private SetMultimap<String, Path> asMultimap(List<Path> plugins) {
730 138 : SetMultimap<String, Path> map = LinkedHashMultimap.create();
731 138 : for (Path srcPath : plugins) {
732 0 : map.put(getPluginName(srcPath), srcPath);
733 0 : }
734 138 : return map;
735 : }
736 :
737 : private boolean isUiPlugin(String name) {
738 3 : return isPlugin(name, "js");
739 : }
740 :
741 : private boolean isPlugin(String fileName, String ext) {
742 3 : String fullExt = "." + ext;
743 3 : return fileName.endsWith(fullExt) || fileName.endsWith(fullExt + ".disabled");
744 : }
745 :
746 : private void checkRemoteInstall() throws PluginInstallException {
747 3 : if (!isRemoteAdminEnabled()) {
748 0 : throw new PluginInstallException("remote installation is disabled");
749 : }
750 3 : }
751 : }
|