-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathPythonPluginLoader.java
More file actions
491 lines (419 loc) · 19.7 KB
/
Copy pathPythonPluginLoader.java
File metadata and controls
491 lines (419 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
/**
*
*/
package net.lahwran.bukkit.jython;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.Validate;
import org.bukkit.Server;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.Listener;
import org.bukkit.event.server.PluginDisableEvent;
import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.InvalidDescriptionException;
import org.bukkit.plugin.InvalidPluginException;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginLoader;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.TimedRegisteredListener;
import org.bukkit.plugin.UnknownDependencyException;
import org.bukkit.plugin.java.JavaPluginLoader;
import org.python.core.Py;
import org.python.core.PyList;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.util.PythonInterpreter;
import org.yaml.snakeyaml.error.YAMLException;
import com.master.bukkit.python.ReflectionHelper;
/**
* A jython plugin loader. depends on JavaPluginLoader and SimplePluginManager.
*
* @author masteroftime
* @author lahwran
*/
public class PythonPluginLoader implements PluginLoader {
private final Server server;
/**
* Filter - matches all of the following, for the regex illiterate:
* <pre>
* plugin_py_dir
* plugin.py.dir
* plugin.py.zip
* plugin.pyp
* </pre>
*/
public static final Pattern[] fileFilters = new Pattern[] {
Pattern.compile("^(.*)\\.py\\.dir$"),
Pattern.compile("^(.*)_py_dir$"),
Pattern.compile("^(.*)\\.py\\.zip$"),
Pattern.compile("^(.*)\\.pyp$"),
Pattern.compile("^(.*)\\.py$"),
};
private HashSet<String> loadedplugins = new HashSet<String>();
/**
* @param server server to initialize with
*/
public PythonPluginLoader(Server server) {
this.server = server;
}
public Plugin loadPlugin(File file) throws InvalidPluginException/*, UnknownDependencyException*/ {
return loadPlugin(file, false);
}
public Plugin loadPlugin(File file, boolean ignoreSoftDependencies)
throws InvalidPluginException/*, InvalidDescriptionException, UnknownDependencyException*/ {
if (!file.exists()) {
throw new InvalidPluginException(new FileNotFoundException(String.format("%s does not exist",
file.getPath())));
}
PluginDataFile data = null;
if (file.getName().endsWith(".py")) {
if (file.isDirectory())
throw new InvalidPluginException(new Exception("python files cannot be directories! try .py.dir instead."));
data = new PluginPythonFile(file);
} else if (file.getName().endsWith("dir")) {
if (!file.isDirectory())
throw new InvalidPluginException(new Exception("python directories cannot be normal files! try .py or .py.zip instead."));
data = new PluginPythonDirectory(file);
} else if (file.getName().endsWith("zip") || file.getName().endsWith("pyp")) {
if (file.isDirectory())
throw new InvalidPluginException(new Exception("python zips cannot be directories! try .py.dir instead."));
data = new PluginPythonZip(file);
} else {
throw new InvalidPluginException(new Exception("filename '"+file.getName()+"' does not end in py, dir, zip, or pyp! did you add a regex without altering loadPlugin()?"));
}
try {
return loadPlugin(file, ignoreSoftDependencies, data);
} finally {
try {
data.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@SuppressWarnings("unchecked")
private Plugin loadPlugin(File file, boolean ignoreSoftDependencies, PluginDataFile data) throws InvalidPluginException/*, InvalidDescriptionException, UnknownDependencyException*/ {
System.out.println("[PythonLoader] Loading Plugin " + file.getName());
PythonPlugin result = null;
PluginDescriptionFile description = null;
boolean hasyml = true;
boolean hassolidmeta = false; // whether we have coder-set metadata. true for have set metadata, false for inferred metadata.
try {
InputStream stream = data.getStream("plugin.yml");
if (stream == null)
hasyml = false;
if (hasyml) {
description = new PluginDescriptionFile(stream);
hassolidmeta = true;
} else {
String stripped = stripExtension(file.getName());
if (stripped == null)
//throw new BukkitScrewedUpException("This would only occur if bukkit called the loader on a plugin which does not match the loader's regex.");
throw new InvalidPluginException(new Exception("This shouldn't be happening; go tell whoever altered the plugin loading api in bukkit that they're whores."));
description = new PluginDescriptionFile(stripped, "dev", "plugin.py");
}
if (stream != null)
stream.close();
} catch (IOException ex) {
throw new InvalidPluginException(ex);
} catch (YAMLException ex) {
throw new InvalidPluginException(ex);
} catch (InvalidDescriptionException ex) {
throw new InvalidPluginException(ex);
}
File dataFolder = new File(file.getParentFile(), description.getName());
if (dataFolder.getAbsolutePath().equals(file.getAbsolutePath())) {
throw new InvalidPluginException(new Exception(String.format("Projected datafolder: '%s' for %s is the same file as the plugin itself (%s)",
dataFolder,
description.getName(),
file)));
}
if (dataFolder.exists() && !dataFolder.isDirectory()) {
throw new InvalidPluginException(new Exception(String.format("Projected datafolder: '%s' for %s (%s) exists and is not a directory",
dataFolder,
description.getName(),
file)));
}
ArrayList<String> depend;
try {
depend = new ArrayList<String>(description.getDepend());
} catch (ClassCastException ex) {
throw new InvalidPluginException(ex);
}
for (String pluginName : depend) {
if (!isPluginLoaded(pluginName)) {
throw new UnknownDependencyException(pluginName);
}
}
PyList pythonpath = Py.getSystemState().path;
PyString filepath = new PyString(file.getAbsolutePath());
if (data.shouldAddPathEntry()) {
if (pythonpath.__contains__(filepath)) {
throw new InvalidPluginException(new Exception("path " + filepath
+ " already on pythonpath!")); //can't imagine how this would happen, but better safe than sorry
}
pythonpath.append(filepath);
}
String mainfile = description.getMain();
InputStream instream = null;
try {
instream = data.getStream(mainfile);
if (instream == null) {
mainfile = "plugin.py";
instream = data.getStream(mainfile);
}
if (instream == null) {
mainfile = "main.py";
instream = data.getStream(mainfile);
}
} catch (IOException e) {
throw new InvalidPluginException(e);
}
if (instream == null) {
throw new InvalidPluginException(new FileNotFoundException("Data file does not contain "+mainfile));
}
try {
PythonHooks hook = new PythonHooks(description);
PythonInterpreter interp = new PythonInterpreter();
interp.set("hook", hook);
interp.set("info", description);
// Decorator Enhancements
interp.exec("import __builtin__");
interp.exec("__builtin__.hook = hook");
interp.exec("__builtin__.info = info");
// Hardcoded for now, may be worth thinking about generalizing it as sort of "addons" for the PythonPluginLoader
// Could be used to extend the capabilities of python plugins the same way the metaclass decorators do, without requiring any changes to the PythonPluginLoader itself
String[] pre_plugin_scripts = {"imports.py", "meta_decorators.py"};
String[] post_plugin_scripts = {"meta_loader.py"};
// Run scripts designed to be run before plugin creation
for (String script : pre_plugin_scripts) {
InputStream metastream = this.getClass().getClassLoader().getResourceAsStream("scripts/"+script);
interp.execfile(metastream);
metastream.close();
}
interp.execfile(instream);
instream.close();
try {
if (!hasyml) {
Object name = interp.get("__plugin_name__");
Object version = interp.get("__plugin_version__");
Object website = interp.get("__plugin_website__");
Object main = interp.get("__plugin_mainclass__");
hassolidmeta = name != null && version != null;
if (name != null)
ReflectionHelper.setPrivateValue(description, "name", name.toString());
if (version != null)
ReflectionHelper.setPrivateValue(description, "version", version.toString());
if (website != null)
ReflectionHelper.setPrivateValue(description, "website", website.toString());
if (main != null)
ReflectionHelper.setPrivateValue(description, "main", main.toString());
}
} catch (Throwable t) {
Logger.getLogger("Minecraft").log(Level.SEVERE, "Error while setting python-set description values", t);
}
String mainclass = description.getMain();
PyObject pyClass = interp.get(mainclass);
if (pyClass == null)
pyClass = interp.get("Plugin");
if (pyClass == null)
result = new PythonPlugin();
else
result = (PythonPlugin) pyClass.__call__().__tojava__(PythonPlugin.class);
interp.set("pyplugin", result);
result.hooks = hook;
result.interp = interp;
// Run scripts designed to be run after plugin creation
for (String script : post_plugin_scripts) {
InputStream metastream = this.getClass().getClassLoader().getResourceAsStream("scripts/"+script);
interp.execfile(metastream);
metastream.close();
}
result.initialize(this, server, description, dataFolder, file);
result.setDataFile(data);
result.onLoaded();
} catch (Throwable t) {
if (data.shouldAddPathEntry() && pythonpath.__contains__(filepath)) {
pythonpath.remove(filepath);
}
throw new InvalidPluginException(t);
}
if (data.getNeedsSolidMeta() && !hassolidmeta) {
throw new InvalidPluginException(new Exception("Released plugins require either a plugin.yml or both __plugin_name__ and __plugin_version__ set in the main python file!"));
}
if (!loadedplugins.contains(description.getName()))
loadedplugins.add(description.getName());
return result;
}
/**
* Remove the extension for use in an automatic plugin name
* @param toStrip filename to strip
* @return stripped file name or null if not changed
*/
private String stripExtension(String toStrip) {
for (Pattern p : fileFilters) {
Matcher m = p.matcher(toStrip);
if (m.matches()) {
return m.group(1);
}
}
return null;
}
private boolean isPluginLoaded(String name) {
if (loadedplugins.contains(name))
return true;
if (ReflectionHelper.isJavaPluginLoaded(server.getPluginManager(), name))
return true;
return false;
}
public Pattern[] getPluginFileFilters() {
return fileFilters;
}
/*public EventExecutor createExecutor(Type type, Listener listener) {
if (listener.getClass().equals(PythonListener.class)) { // 8 times faster than instanceof \o/
return new EventExecutor() {
public void execute(Listener listener, Event event) {
((PythonListener) listener).onEvent(event);
}
};
} else {
JavaPluginLoader jplugload = ReflectionHelper.getJavaPluginLoader(server.getPluginManager());
return jplugload.createExecutor(type, listener);
}
}*/
public void disablePlugin(Plugin plugin) {
if (!(plugin instanceof PythonPlugin)) {
throw new IllegalArgumentException("Plugin is not associated with this PluginLoader");
}
if (plugin.isEnabled()) {
PythonPlugin pyPlugin = (PythonPlugin) plugin;
//ClassLoader cloader = jPlugin.getClassLoader();
try {
pyPlugin.setEnabled(false);
} catch (Throwable ex) {
server.getLogger().log(Level.SEVERE,
"Error occurred while disabling " + plugin.getDescription().getFullName()
+ " (Is it up to date?): " + ex.getMessage(),
ex);
}
server.getPluginManager().callEvent(new PluginDisableEvent(plugin));
String pluginName = pyPlugin.getDescription().getName();
if (loadedplugins.contains(pluginName))
loadedplugins.remove(pluginName);
}
}
public void enablePlugin(Plugin plugin) {
if (!(plugin instanceof PythonPlugin)) {
throw new IllegalArgumentException("Plugin is not associated with this PluginLoader");
}
if (!plugin.isEnabled()) {
PythonPlugin pyPlugin = (PythonPlugin) plugin;
String pluginName = pyPlugin.getDescription().getName();
if (!loadedplugins.contains(pluginName))
loadedplugins.add(pluginName);
try {
pyPlugin.setEnabled(true);
} catch (Throwable ex) {
server.getLogger().log(Level.SEVERE,
"Error occurred while enabling " + plugin.getDescription().getFullName()
+ " (Is it up to date?): " + ex.getMessage(),
ex);
}
//finally register the listener for the hook events
server.getPluginManager().registerEvents(pyPlugin.listener, pyPlugin);
// Perhaps abort here, rather than continue going, but as it stands,
// an abort is not possible the way it's currently written
server.getPluginManager().callEvent(new PluginEnableEvent(plugin));
}
}
@Override
public Map<Class<? extends Event>, Set<RegisteredListener>> createRegisteredListeners(
Listener listener, Plugin plugin) {
boolean useTimings = server.getPluginManager().useTimings();
Map<Class<? extends Event>, Set<RegisteredListener>> ret = new HashMap<Class<? extends Event>, Set<RegisteredListener>>();
if(!listener.getClass().equals(PythonListener.class)) {
throw new IllegalArgumentException("Listener to register is not a PythonListener");
}
PythonListener pyListener = (PythonListener)listener;
for(Map.Entry<Class<? extends Event>, Set<PythonEventHandler>> entry : pyListener.handlers.entrySet()) {
Set<RegisteredListener> eventSet = new HashSet<RegisteredListener>();
for(final PythonEventHandler handler : entry.getValue()) {
EventExecutor executor = new EventExecutor() {
@Override
public void execute(Listener listener, Event event) throws EventException {
if(!listener.getClass().equals(PythonListener.class)) {
throw new IllegalArgumentException("No PythonListener passed to EventExecutor! If this happens someone really fucked up something");
}
((PythonListener)listener).fireEvent(event, handler);
}
};
if(useTimings) {
eventSet.add(new TimedRegisteredListener(pyListener, executor, handler.priority, plugin, false));
}
else {
eventSet.add(new RegisteredListener(pyListener, executor, handler.priority, plugin, false));
}
}
ret.put(entry.getKey(), eventSet);
}
return ret;
}
@Override
public PluginDescriptionFile getPluginDescription(File file)
throws InvalidDescriptionException {
Validate.notNull(file, "File cannot be null");
InputStream stream = null;
PluginDataFile data = null;
if (file.getName().endsWith(".py")) {
if (file.isDirectory())
//cause we can't throw InvalidPluginExceptions from here we throw an InvalidDescriptionExecption with the InvalidPlugin as cause
throw new InvalidDescriptionException(new InvalidPluginException(new Exception("python files cannot be directories! try .py.dir instead.")));
data = new PluginPythonFile(file);
} else if (file.getName().endsWith("dir")) {
if (!file.isDirectory())
throw new InvalidDescriptionException(new InvalidPluginException(new Exception("python directories cannot be normal files! try .py or .py.zip instead.")));
data = new PluginPythonDirectory(file);
} else if (file.getName().endsWith("zip") || file.getName().endsWith("pyp")) {
if (file.isDirectory())
throw new InvalidDescriptionException(new InvalidPluginException(new Exception("python zips cannot be directories! try .py.dir instead.")));
try {
data = new PluginPythonZip(file);
} catch (InvalidPluginException ex) {
throw new InvalidDescriptionException(ex);
}
} else {
throw new InvalidDescriptionException(new InvalidPluginException(new Exception("filename '"+file.getName()+"' does not end in py, dir, zip, or pyp! did you add a regex without altering loadPlugin()?")));
}
try {
stream = data.getStream("plugin.yml");
if(stream == null) {
//TODO Does this cause serious problems with plugins which have no plugin.yml file?
throw new InvalidDescriptionException(new InvalidPluginException(new FileNotFoundException("Jar does not contain plugin.yml")));
}
return new PluginDescriptionFile(stream);
} catch (IOException e) {
e.printStackTrace();
} finally {
if(stream != null) {
try {
stream.close();
} catch (IOException e) {}
}
}
return null;
}
}