Files: 69dc20c646b477c874cb1f6eb145f9cd1016b04c / plugins / plugins.js
8657 bytesRaw
1 | var assert = require('assert') |
2 | var path = require('path') |
3 | var fs = require('fs') |
4 | var pull = require('pull-stream') |
5 | var cat = require('pull-cat') |
6 | var many = require('pull-many') |
7 | var pushable = require('pull-pushable') |
8 | var toPull = require('stream-to-pull-stream') |
9 | var spawn = require('cross-spawn') |
10 | var mkdirp = require('mkdirp') |
11 | var osenv = require('osenv') |
12 | var rimraf = require('rimraf') |
13 | var mv = require('mv') |
14 | var mdm = require('mdmanifest') |
15 | var explain = require('explain-error') |
16 | var valid = require('../lib/validators') |
17 | var apidoc = require('../lib/apidocs').plugins |
18 | |
19 | module.exports = { |
20 | name: 'plugins', |
21 | version: '1.0.0', |
22 | manifest: mdm.manifest(apidoc), |
23 | permissions: { |
24 | master: {allow: ['install', 'uninstall', 'enable', 'disable']} |
25 | }, |
26 | init: function (server, config) { |
27 | var installPath = config.path |
28 | config.plugins = config.plugins || {} |
29 | mkdirp.sync(path.join(installPath, 'node_modules')) |
30 | |
31 | // helper to enable/disable plugins |
32 | function configPluginEnabled (b) { |
33 | return function (pluginName, cb) { |
34 | checkInstalled(pluginName, function (err) { |
35 | if (err) return cb(err) |
36 | |
37 | config.plugins[pluginName] = b |
38 | writePluginConfig(pluginName, b) |
39 | if (b) |
40 | cb(null, '\''+pluginName+'\' has been enabled. Restart Scuttlebot server to use the plugin.') |
41 | else |
42 | cb(null, '\''+pluginName+'\' has been disabled. Restart Scuttlebot server to stop using the plugin.') |
43 | }) |
44 | } |
45 | } |
46 | |
47 | // helper to check if a plugin is installed |
48 | function checkInstalled (pluginName, cb) { |
49 | if (!pluginName || typeof pluginName !== 'string') |
50 | return cb(new Error('plugin name is required')) |
51 | var modulePath = path.join(installPath, 'node_modules', pluginName) |
52 | fs.stat(modulePath, function (err) { |
53 | if (err) |
54 | cb(new Error('Plugin "'+pluginName+'" is not installed.')) |
55 | else |
56 | cb() |
57 | }) |
58 | } |
59 | |
60 | // write the plugin config to ~/.ssb/config |
61 | function writePluginConfig (pluginName, value) { |
62 | var cfgPath = path.join(config.path, 'config') |
63 | // load ~/.ssb/config |
64 | let existingConfig |
65 | fs.readFile(cfgPath, 'utf-8', (err, data) => { |
66 | if (err) { |
67 | if (err.code === 'ENOENT') { |
68 | // only catch "file not found" |
69 | existingConfig = {} |
70 | } else { |
71 | throw err |
72 | } |
73 | } else { |
74 | existingConfig = JSON.parse(data) |
75 | } |
76 | |
77 | |
78 | // update the plugins config |
79 | existingConfig.plugins = existingConfig.plugins || {} |
80 | existingConfig.plugins[pluginName] = value |
81 | |
82 | // write to disc |
83 | fs.writeFileSync(cfgPath, JSON.stringify(existingConfig, null, 2), 'utf-8') |
84 | }) |
85 | |
86 | } |
87 | |
88 | return { |
89 | install: valid.source(function (pluginName, opts) { |
90 | var p = pushable() |
91 | var dryRun = opts && opts['dry-run'] |
92 | var from = opts && opts.from |
93 | |
94 | if (!pluginName || typeof pluginName !== 'string') |
95 | return pull.error(new Error('plugin name is required')) |
96 | |
97 | // pull out the version, if given |
98 | if (pluginName.indexOf('@') !== -1) { |
99 | var pluginNameSplitted = pluginName.split('@') |
100 | pluginName = pluginNameSplitted[0] |
101 | var version = pluginNameSplitted[1] |
102 | |
103 | if (version && !from) |
104 | from = pluginName + '@' + version |
105 | } |
106 | |
107 | if (!validatePluginName(pluginName)) |
108 | return pull.error(new Error('invalid plugin name: "'+pluginName+'"')) |
109 | |
110 | // create a tmp directory to install into |
111 | var tmpInstallPath = path.join(osenv.tmpdir(), pluginName) |
112 | rimraf.sync(tmpInstallPath); mkdirp.sync(tmpInstallPath) |
113 | |
114 | // build args |
115 | // --global-style: dont dedup at the top level, gives proper isolation between each plugin |
116 | // --loglevel error: dont output warnings, because npm just whines about the lack of a package.json in ~/.ssb |
117 | var args = ['install', from||pluginName, '--global-style', '--loglevel', 'error'] |
118 | if (dryRun) |
119 | args.push('--dry-run') |
120 | |
121 | // exec npm |
122 | var child = spawn('npm', args, { cwd: tmpInstallPath }) |
123 | .on('close', function (code) { |
124 | if (code == 0 && !dryRun) { |
125 | var tmpInstallNMPath = path.join(tmpInstallPath, 'node_modules') |
126 | var finalInstallNMPath = path.join(installPath, 'node_modules') |
127 | |
128 | // delete plugin, if it's already there |
129 | rimraf.sync(path.join(finalInstallNMPath, pluginName)) |
130 | |
131 | // move the plugin from the tmpdir into our install path |
132 | // ...using our given plugin name |
133 | var dirs = fs.readdirSync(tmpInstallNMPath) |
134 | .filter(function (name) { return name.charAt(0) !== '.' }) // filter out dot dirs, like '.bin' |
135 | mv( |
136 | path.join(tmpInstallNMPath, dirs[0]), |
137 | path.join(finalInstallNMPath, pluginName), |
138 | function (err) { |
139 | if (err) |
140 | return p.end(explain(err, '"'+pluginName+'" failed to install. See log output above.')) |
141 | |
142 | // enable the plugin |
143 | // - use basename(), because plugins can be installed from the FS, in which case pluginName is a path |
144 | var name = path.basename(pluginName) |
145 | config.plugins[name] = true |
146 | writePluginConfig(name, true) |
147 | p.push(Buffer.from('"'+pluginName+'" has been installed. Restart Scuttlebot server to enable the plugin.\n', 'utf-8')) |
148 | p.end() |
149 | } |
150 | ) |
151 | } else |
152 | p.end(new Error('"'+pluginName+'" failed to install. See log output above.')) |
153 | }) |
154 | return cat([ |
155 | pull.values([Buffer.from('Installing "'+pluginName+'"...\n', 'utf-8')]), |
156 | many([toPull(child.stdout), toPull(child.stderr)]), |
157 | p |
158 | ]) |
159 | }, 'string', 'object?'), |
160 | uninstall: valid.source(function (pluginName, opts) { |
161 | var p = pushable() |
162 | if (!pluginName || typeof pluginName !== 'string') |
163 | return pull.error(new Error('plugin name is required')) |
164 | |
165 | var modulePath = path.join(installPath, 'node_modules', pluginName) |
166 | |
167 | rimraf(modulePath, function (err) { |
168 | if (!err) { |
169 | writePluginConfig(pluginName, false) |
170 | p.push(Buffer.from('"'+pluginName+'" has been uninstalled. Restart Scuttlebot server to disable the plugin.\n', 'utf-8')) |
171 | p.end() |
172 | } else |
173 | p.end(err) |
174 | }) |
175 | return p |
176 | }, 'string', 'object?'), |
177 | enable: valid.async(configPluginEnabled(true), 'string'), |
178 | disable: valid.async(configPluginEnabled(false), 'string') |
179 | } |
180 | } |
181 | } |
182 | |
183 | module.exports.loadUserPlugins = function (createSbot, config) { |
184 | // iterate all modules |
185 | var nodeModulesPath = path.join(config.path, 'node_modules') |
186 | //instead of testing all plugins, only load things explicitly |
187 | //enabled in the config |
188 | for (var module_name in config.plugins) { |
189 | const configv = config.plugins[module_name] |
190 | if (configv) { |
191 | const name = /^ssb-/.test(module_name) ? module_name.substring(4) : module_name |
192 | |
193 | if (createSbot.plugins.some(plug => plug.name === name)) |
194 | throw new Error('already loaded plugin named:'+name) |
195 | |
196 | let plugin = null |
197 | if (typeof configv === 'object') { // out-of-process plugin |
198 | plugin = require('ssb-plugins2/load')(configv.location) |
199 | } else if (typeof configv === 'boolean') { |
200 | plugin = require(path.join(nodeModulesPath, module_name)) |
201 | } |
202 | |
203 | if (!plugin || plugin.name !== name) |
204 | throw new Error(`plugin at:${module_name} expected name:${name} but had:${(plugin||{}).name}`) |
205 | |
206 | assertSbotPlugin(plugin) |
207 | createSbot.use(plugin) |
208 | } |
209 | } |
210 | } |
211 | |
212 | // predictate to check if an object appears to be a sbot plugin |
213 | function assertSbotPlugin (obj) { |
214 | // function signature: |
215 | if (typeof obj == 'function') |
216 | return |
217 | |
218 | // object signature: |
219 | assert(obj && typeof obj == 'object', 'module.exports must be an object') |
220 | assert(typeof obj.name == 'string', 'module.exports.name must be a string') |
221 | assert(typeof obj.version == 'string', 'module.exports.version must be a string') |
222 | assert(obj.manifest && |
223 | typeof obj.manifest == 'object', 'module.exports.manifest must be an object') |
224 | assert(typeof obj.init == 'function', 'module.exports.init must be a function') |
225 | } |
226 | |
227 | function validatePluginName (name) { |
228 | if (/^[._]/.test(name)) |
229 | return false |
230 | // from npm-validate-package-name: |
231 | if (encodeURIComponent(name) !== name) |
232 | return false |
233 | return true |
234 | } |
235 | |
236 | |
237 |
Built with git-ssb-web