1 /** 2 Copyright: Copyright (c) 2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 This module contains the functions that realize the plugin architecture. 11 12 All logger.XXX calls shall be dependent on the DebugLogging enum. 13 This is because this module otherwise produces a lot of junk logging that is 14 almost never relevant besides when making changes to this module. 15 */ 16 module application.plugin; 17 18 import my.path : AbsolutePath, Path; 19 20 import logger = std.experimental.logger; 21 22 /// Kind of plugin, part of the primary installation or found in DEXTOOL_PLUGINS. 23 enum Kind { 24 /// A plugin that is found in the same directory as _this_ executable 25 primary, 26 /// A plugin found in the PATH 27 secondary 28 } 29 30 /// Validated plugin with kind separating primary and secondar plugins. 31 struct Validated { 32 Path path; 33 Kind kind; 34 } 35 36 version (unittest) { 37 import unit_threaded : shouldEqual; 38 } 39 40 // change this to true to activate debug logging for this module. 41 private enum DebugLogging = false; 42 43 private void nothrowTrace(T...)(auto ref T args) @safe nothrow { 44 if (DebugLogging) { 45 try { 46 logger.trace(args); 47 } catch (Exception ex) { 48 } 49 } 50 } 51 52 private void nothrowTracef(T...)(auto ref T args) @safe nothrow { 53 if (DebugLogging) { 54 try { 55 logger.tracef(args); 56 } catch (Exception ex) { 57 } 58 } 59 } 60 61 /// Scan for files in the same directory as the executable. 62 Validated[] scanForExecutables() { 63 import std.algorithm : filter, map; 64 import std.array : array; 65 import std.file : thisExePath; 66 import std.path : dirName, stripExtension, baseName; 67 import std.range : tee; 68 import my.file : which, whichFromEnv; 69 70 auto pluginPattern = thisExePath.baseName.stripExtension ~ "*"; 71 72 auto primaryPlugins() { 73 return which([AbsolutePath(thisExePath.dirName)], pluginPattern).map!( 74 a => Validated(a, Kind.primary)); 75 } 76 77 auto secondaryPlugins() { 78 return whichFromEnv("DEXTOOL_PLUGINS", pluginPattern).map!(a => Validated(a, 79 Kind.secondary)); 80 } 81 82 static auto merge(T0, T1)(T0 primary, T1 secondary) { 83 import std.range : chain; 84 import my.set; 85 86 // remove secondary that clash with primary. 87 // secondaries may never override a primary. 88 auto prim = toSet(primary.save.map!(a => cast(string) a.path)); 89 90 return chain(primary, secondary.filter!(a => a.path.baseName !in prim)).array; 91 } 92 93 return merge(primaryPlugins, secondaryPlugins).tee!( 94 a => nothrowTrace("Found executable: ", a)).array; 95 } 96 97 /** Filter the filenames for those that fulfill the requirement for a plugin. 98 * 99 * Binaries that begin with <this binary>-* are plugins. 100 */ 101 auto filterValidPluginsThisExecutable(Validated[] fnames) @safe { 102 import std.file : thisExePath; 103 import std.path : baseName; 104 105 immutable base_name = thisExePath.baseName ~ "-"; 106 return filterValidPlugins(fnames, base_name); 107 } 108 109 /** Filter the filenames for those that fulfill the requirement for a plugin. 110 * 111 * Binaries that begin with basename are plugins. 112 */ 113 auto filterValidPlugins(Validated[] fnames, string base_name) @safe { 114 import std.algorithm : filter, startsWith, map; 115 import std.range : tee; 116 import std.path : baseName, absolutePath; 117 118 // dfmt off 119 return fnames 120 .filter!(a => a.path.baseName.startsWith(base_name)) 121 .tee!(a => nothrowTrace("Valid plugin prefix: ", a)); 122 // dfmt on 123 } 124 125 /// Holds information for a discovered plugin. 126 struct Plugin { 127 string name; 128 string help; 129 Path path; 130 Kind kind; 131 } 132 133 private struct ExecuteResult { 134 string output; 135 bool isValid; 136 Validated data; 137 } 138 139 ExecuteResult executePluginForShortHelp(Validated plugin) @safe nothrow { 140 import std.process : execute; 141 142 auto res = ExecuteResult("", false, plugin); 143 144 try { 145 res = ExecuteResult(execute([plugin.path, "--short-plugin-help"]).output, true, plugin); 146 } catch (Exception ex) { 147 nothrowTrace("No --short-plugin-help for: ", plugin); 148 } 149 150 nothrowTrace("Plugin --short-plugin-help: ", res); 151 152 return res; 153 } 154 155 Plugin[] toPlugins(alias execFunc, T)(T plugins) @safe { 156 import std.algorithm : filter, map, splitter, each, cache; 157 import std.array : array; 158 import std.ascii : newline; 159 import std.range : tee; 160 161 static struct Temp { 162 string[] output; 163 Validated data; 164 } 165 166 // dfmt off 167 auto res = plugins 168 .map!(a => execFunc(a)) 169 .cache 170 // plugins that do not implement the required parameter are ignored 171 .filter!(a => a.isValid) 172 // the shorthelp must be two lines, the plugins name and a help text 173 .map!(a => Temp(a.output.splitter(newline).array(), a.data)) 174 .filter!(a => a.output.length >= 2) 175 // convert 176 .map!(a => Plugin(a.output[0], a.output[1], a.data.path, a.data.kind)) 177 .array(); 178 // dfmt on 179 180 try { 181 res.each!(a => nothrowTracef("Found plugin '%s' (%s) (%s): %s", a.name, 182 a.path, a.kind, a.help)); 183 } catch (Exception ex) { 184 } 185 186 return res; 187 } 188 189 string toShortHelp(Plugin[] plugins) @safe { 190 import std.algorithm : map, joiner, reduce, max, copy, filter; 191 import std.array : appender; 192 import std.ascii : newline; 193 import std.conv : text; 194 import std.range : chain, only; 195 import std..string : leftJustifier; 196 197 // dfmt off 198 // +1 so there is a space left between category and info 199 auto max_length = 1 + reduce!((a,b) => max(a,b))(0UL, plugins.map!(a => a.name.length)); 200 201 auto app = appender!string(); 202 203 // dfmt off 204 plugins 205 .filter!(a => a.kind == Kind.primary) 206 .map!(a => 207 chain(only(" "), 208 only(leftJustifier(a.name, max_length).text), 209 only(a.help)) 210 .joiner() 211 ) 212 .joiner(newline) 213 .text() 214 .copy(app); 215 216 app.put(newline); 217 218 plugins 219 .filter!(a => a.kind == Kind.secondary) 220 .map!(a => 221 chain(only(" "), 222 only(leftJustifier(a.name, max_length).text), 223 only(a.help)) 224 .joiner() 225 ) 226 .joiner(newline) 227 .text() 228 .copy(app); 229 // dfmt on 230 231 return app.data; 232 } 233 234 @("Shall only keep those files prefixed with basename") 235 unittest { 236 import std.algorithm; 237 import std.array; 238 239 auto fnames = ["/ignore", "/usr/bin/dextool", "/usr/bin/dextool-ctest"].map!( 240 a => Validated(Path(a), Kind.primary)).array(); 241 242 filterValidPlugins(fnames, "dextool-").shouldEqual( 243 [Validated(Path("/usr/bin/dextool-ctest"), Kind.primary)]); 244 } 245 246 @("Shall get the short text for the plugins") 247 @safe unittest { 248 import std.algorithm; 249 import std.array; 250 251 auto fakeExec(Path plugin) { 252 if (plugin == "dextool-ctest") { 253 return ExecuteResult("ctest\nc test text", true, 254 Validated(Path("/a/dextool-ctest"), Kind.primary)); 255 } else if (plugin == "dextool-cpp") { 256 return ExecuteResult("cpp\ncpp test text", true, 257 Validated(Path("/b/dextool-cpp"), Kind.primary)); 258 } else if (plugin == "dextool-too_many_lines") { 259 return ExecuteResult("too_many_lines\n\nfoo", true); 260 } else if (plugin == "dextool-fail_run") { 261 return ExecuteResult("fail\nfoo", false); 262 } 263 264 assert(false); // should not happen 265 } 266 267 auto fake_plugins = [ 268 "dextool-ctest", "dextool-cpp", "dextool-too_many_lines", 269 "dextool-fail_run" 270 ].map!(a => Path(a)).array(); 271 272 toPlugins!fakeExec(fake_plugins).shouldEqual([ 273 Plugin("ctest", "c test text", Path("/a/dextool-ctest")), 274 Plugin("cpp", "cpp test text", Path("/b/dextool-cpp")), 275 Plugin("too_many_lines", "", Path("")) 276 ]); 277 } 278 279 @("A short help text with two plugins") 280 @safe unittest { 281 auto plugins = [ 282 Plugin("ctest", "c help text", Path("dummy"), Kind.primary), 283 Plugin("cpp", "c++ help text", Path("dummy"), Kind.secondary) 284 ]; 285 plugins.toShortHelp.shouldEqual(" ctest c help text\n cpp c++ help text"); 286 }