2015-05-29 14:31:43 +00:00
# coding=utf-8
2016-07-15 07:16:58 +00:00
from __future__ import absolute_import , division , print_function
2015-05-29 14:31:43 +00:00
__author__ = " Gina Häußge <osd@foosel.net> "
__license__ = ' GNU Affero General Public License http://www.gnu.org/licenses/agpl.html '
__copyright__ = " Copyright (C) 2015 The OctoPrint Project - Released under terms of the AGPLv3 License "
import octoprint . plugin
import octoprint . plugin . core
from octoprint . settings import valid_boolean_trues
2016-08-30 16:03:50 +00:00
from octoprint . server . util . flask import restricted_access , with_revalidation_checking , check_etag
2015-12-09 09:29:24 +00:00
from octoprint . server import admin_permission , VERSION
2016-02-04 12:55:10 +00:00
from octoprint . util . pip import LocalPipCaller , UnknownPip
2015-05-29 14:31:43 +00:00
from flask import jsonify , make_response
2015-06-08 06:22:40 +00:00
from flask . ext . babel import gettext
2017-03-31 14:52:48 +00:00
from collections import OrderedDict
2015-05-29 14:31:43 +00:00
import logging
import sarge
import sys
import requests
import re
2015-06-22 11:00:29 +00:00
import os
2015-06-23 09:36:27 +00:00
import pkg_resources
2017-03-31 14:52:48 +00:00
import copy
import dateutil . parser
import time
2015-05-29 14:31:43 +00:00
class PluginManagerPlugin ( octoprint . plugin . SimpleApiPlugin ,
octoprint . plugin . TemplatePlugin ,
octoprint . plugin . AssetPlugin ,
octoprint . plugin . SettingsPlugin ,
octoprint . plugin . StartupPlugin ,
octoprint . plugin . BlueprintPlugin ) :
2016-10-17 09:33:23 +00:00
ARCHIVE_EXTENSIONS = ( " .zip " , " .tar.gz " , " .tgz " , " .tar " )
2017-01-19 11:08:05 +00:00
OPERATING_SYSTEMS = dict ( windows = [ " win32 " ] ,
linux = [ " linux2 " ] ,
macos = [ " darwin " ] )
2016-11-07 09:50:14 +00:00
pip_inapplicable_arguments = dict ( uninstall = [ " --user " ] )
2015-05-29 14:31:43 +00:00
def __init__ ( self ) :
self . _pending_enable = set ( )
self . _pending_disable = set ( )
self . _pending_install = set ( )
self . _pending_uninstall = set ( )
2015-06-23 09:36:27 +00:00
self . _pip_caller = None
2015-05-29 14:31:43 +00:00
self . _repository_available = False
self . _repository_plugins = [ ]
2015-06-22 11:00:29 +00:00
self . _repository_cache_path = None
self . _repository_cache_ttl = 0
2015-05-29 14:31:43 +00:00
2017-03-31 14:52:48 +00:00
self . _notices = dict ( )
self . _notices_available = False
self . _notices_cache_path = None
self . _notices_cache_ttl = 0
2015-07-03 11:57:15 +00:00
self . _console_logger = None
2015-05-29 14:31:43 +00:00
def initialize ( self ) :
self . _console_logger = logging . getLogger ( " octoprint.plugins.pluginmanager.console " )
2015-06-24 12:33:33 +00:00
self . _repository_cache_path = os . path . join ( self . get_plugin_data_folder ( ) , " plugins.json " )
2015-06-22 11:00:29 +00:00
self . _repository_cache_ttl = self . _settings . get_int ( [ " repository_ttl " ] ) * 60
2017-03-31 14:52:48 +00:00
self . _notices_cache_path = os . path . join ( self . get_plugin_data_folder ( ) , " notices.json " )
self . _notices_cache_ttl = self . _settings . get_int ( [ " notices_ttl " ] ) * 60
2015-05-29 14:31:43 +00:00
2016-02-04 12:55:10 +00:00
self . _pip_caller = LocalPipCaller ( force_user = self . _settings . get_boolean ( [ " pip_force_user " ] ) )
2015-06-23 09:36:27 +00:00
self . _pip_caller . on_log_call = self . _log_call
self . _pip_caller . on_log_stdout = self . _log_stdout
self . _pip_caller . on_log_stderr = self . _log_stderr
2015-06-29 18:59:46 +00:00
##~~ Body size hook
def increase_upload_bodysize ( self , current_max_body_sizes , * args , * * kwargs ) :
# set a maximum body size of 50 MB for plugin archive uploads
return [ ( " POST " , r " /upload_archive " , 50 * 1024 * 1024 ) ]
2015-05-29 14:31:43 +00:00
##~~ StartupPlugin
def on_startup ( self , host , port ) :
2016-10-11 08:54:16 +00:00
from octoprint . logging . handlers import CleaningTimedRotatingFileHandler
console_logging_handler = CleaningTimedRotatingFileHandler ( self . _settings . get_plugin_logfile_path ( postfix = " console " ) , when = " D " , backupCount = 3 )
2015-05-29 14:31:43 +00:00
console_logging_handler . setFormatter ( logging . Formatter ( " %(asctime)s %(message)s " ) )
console_logging_handler . setLevel ( logging . DEBUG )
self . _console_logger . addHandler ( console_logging_handler )
self . _console_logger . setLevel ( logging . DEBUG )
self . _console_logger . propagate = False
2015-06-22 11:00:29 +00:00
self . _repository_available = self . _fetch_repository_from_disk ( )
2017-03-31 14:52:48 +00:00
self . _notices_available = self . _fetch_notices_from_disk ( )
2015-05-29 14:31:43 +00:00
##~~ SettingsPlugin
def get_settings_defaults ( self ) :
return dict (
repository = " http://plugins.octoprint.org/plugins.json " ,
2015-06-22 11:00:29 +00:00
repository_ttl = 24 * 60 ,
2017-03-31 14:52:48 +00:00
notices = " http://plugins.octoprint.org/notices.json " ,
notices_ttl = 6 * 60 ,
2015-09-28 17:53:30 +00:00
pip_args = None ,
2015-09-30 11:36:00 +00:00
pip_force_user = False ,
2015-07-22 14:57:32 +00:00
dependency_links = False ,
2015-07-07 13:33:00 +00:00
hidden = [ ]
2015-05-29 14:31:43 +00:00
)
2015-06-22 11:00:29 +00:00
def on_settings_save ( self , data ) :
octoprint . plugin . SettingsPlugin . on_settings_save ( self , data )
2015-09-22 09:36:57 +00:00
2015-06-22 11:00:29 +00:00
self . _repository_cache_ttl = self . _settings . get_int ( [ " repository_ttl " ] ) * 60
2017-03-31 14:52:48 +00:00
self . _notices_cache_ttl = self . _settings . get_int ( [ " notices_ttl " ] ) * 60
2015-09-30 11:36:00 +00:00
self . _pip_caller . force_user = self . _settings . get_boolean ( [ " pip_force_user " ] )
2015-06-22 11:00:29 +00:00
2015-05-29 14:31:43 +00:00
##~~ AssetPlugin
def get_assets ( self ) :
return dict (
js = [ " js/pluginmanager.js " ] ,
css = [ " css/pluginmanager.css " ] ,
less = [ " less/pluginmanager.less " ]
)
##~~ TemplatePlugin
def get_template_configs ( self ) :
return [
2015-07-09 13:43:11 +00:00
dict ( type = " settings " , name = gettext ( " Plugin Manager " ) , template = " pluginmanager_settings.jinja2 " , custom_bindings = True ) ,
2016-05-04 07:51:50 +00:00
dict ( type = " about " , name = " Plugin Licenses " , template = " pluginmanager_about.jinja2 " )
2015-07-09 13:43:11 +00:00
]
def get_template_vars ( self ) :
plugins = sorted ( self . _get_plugins ( ) , key = lambda x : x [ " name " ] . lower ( ) )
return dict (
all = plugins ,
2016-10-17 09:33:23 +00:00
thirdparty = filter ( lambda p : not p [ " bundled " ] , plugins ) ,
archive_extensions = self . __class__ . ARCHIVE_EXTENSIONS
2015-07-09 13:43:11 +00:00
)
def get_template_types ( self , template_sorting , template_rules , * args , * * kwargs ) :
return [
( " about_thirdparty " , dict ( ) , dict ( template = lambda x : x + " _about_thirdparty.jinja2 " ) )
2015-05-29 14:31:43 +00:00
]
##~~ BlueprintPlugin
@octoprint.plugin.BlueprintPlugin.route ( " /upload_archive " , methods = [ " POST " ] )
@restricted_access
@admin_permission.require ( 403 )
def upload_archive ( self ) :
import flask
input_name = " file "
input_upload_path = input_name + " . " + self . _settings . global_get ( [ " server " , " uploads " , " pathSuffix " ] )
input_upload_name = input_name + " . " + self . _settings . global_get ( [ " server " , " uploads " , " nameSuffix " ] )
if input_upload_path not in flask . request . values or input_upload_name not in flask . request . values :
return flask . make_response ( " No file included " , 400 )
upload_path = flask . request . values [ input_upload_path ]
upload_name = flask . request . values [ input_upload_name ]
2016-10-17 09:33:23 +00:00
exts = filter ( lambda x : upload_name . lower ( ) . endswith ( x ) , self . __class__ . ARCHIVE_EXTENSIONS )
2015-06-30 16:59:45 +00:00
if not len ( exts ) :
return flask . make_response ( " File doesn ' t have a valid extension for a plugin archive " , 400 )
ext = exts [ 0 ]
2015-05-29 14:31:43 +00:00
import tempfile
import shutil
import os
2015-06-30 16:59:45 +00:00
archive = tempfile . NamedTemporaryFile ( delete = False , suffix = " {ext} " . format ( * * locals ( ) ) )
2015-05-29 14:31:43 +00:00
try :
archive . close ( )
shutil . copy ( upload_path , archive . name )
return self . command_install ( path = archive . name , force = " force " in flask . request . values and flask . request . values [ " force " ] in valid_boolean_trues )
finally :
try :
os . remove ( archive . name )
except Exception as e :
self . _logger . warn ( " Could not remove temporary file {path} again: {message} " . format ( path = archive . name , message = str ( e ) ) )
##~~ SimpleApiPlugin
def get_api_commands ( self ) :
return {
" install " : [ " url " ] ,
" uninstall " : [ " plugin " ] ,
" enable " : [ " plugin " ] ,
" disable " : [ " plugin " ] ,
" refresh_repository " : [ ]
}
def on_api_get ( self , request ) :
if not admin_permission . can ( ) :
return make_response ( " Insufficient rights " , 403 )
2016-11-18 12:01:14 +00:00
from octoprint . server import safe_mode
2016-08-30 16:03:50 +00:00
refresh_repository = request . values . get ( " refresh_repository " , " false " ) in valid_boolean_trues
if refresh_repository :
2015-06-19 13:20:25 +00:00
self . _repository_available = self . _refresh_repository ( )
2015-05-29 14:31:43 +00:00
2017-03-31 14:52:48 +00:00
refresh_notices = request . values . get ( " refresh_notices " , " false " ) in valid_boolean_trues
if refresh_notices :
self . _notices_available = self . _refresh_notices ( )
2016-08-30 16:03:50 +00:00
def view ( ) :
return jsonify ( plugins = self . _get_plugins ( ) ,
repository = dict (
available = self . _repository_available ,
plugins = self . _repository_plugins
) ,
os = self . _get_os ( ) ,
octoprint = self . _get_octoprint_version_string ( ) ,
pip = dict (
available = self . _pip_caller . available ,
version = self . _pip_caller . version_string ,
install_dir = self . _pip_caller . install_dir ,
use_user = self . _pip_caller . use_user ,
virtual_env = self . _pip_caller . virtual_env ,
additional_args = self . _settings . get ( [ " pip_args " ] ) ,
python = sys . executable
2016-11-18 12:01:14 +00:00
) ,
safe_mode = safe_mode )
2015-05-29 14:31:43 +00:00
2016-08-30 16:03:50 +00:00
def etag ( ) :
import hashlib
hash = hashlib . sha1 ( )
hash . update ( repr ( self . _get_plugins ( ) ) )
hash . update ( str ( self . _repository_available ) )
hash . update ( repr ( self . _repository_plugins ) )
2017-03-31 14:52:48 +00:00
hash . update ( str ( self . _notices_available ) )
hash . update ( repr ( self . _notices ) )
2016-11-18 12:01:14 +00:00
hash . update ( repr ( safe_mode ) )
2016-08-30 16:03:50 +00:00
return hash . hexdigest ( )
def condition ( ) :
return check_etag ( etag ( ) )
return with_revalidation_checking ( etag_factory = lambda * args , * * kwargs : etag ( ) ,
condition = lambda * args , * * kwargs : condition ( ) ,
2017-03-31 14:52:48 +00:00
unless = lambda : refresh_repository or refresh_notices ) ( view ) ( )
2016-08-30 16:03:50 +00:00
2015-05-29 14:31:43 +00:00
def on_api_command ( self , command , data ) :
if not admin_permission . can ( ) :
return make_response ( " Insufficient rights " , 403 )
2015-06-18 09:37:08 +00:00
if self . _printer . is_printing ( ) or self . _printer . is_paused ( ) :
# do not update while a print job is running
return make_response ( " Printer is currently printing or paused " , 409 )
2015-05-29 14:31:43 +00:00
if command == " install " :
url = data [ " url " ]
plugin_name = data [ " plugin " ] if " plugin " in data else None
2015-06-10 17:34:33 +00:00
return self . command_install ( url = url ,
force = " force " in data and data [ " force " ] in valid_boolean_trues ,
2017-04-04 13:18:37 +00:00
dependency_links = " dependency_links " in data
and data [ " dependency_links " ] in valid_boolean_trues ,
2015-06-10 17:34:33 +00:00
reinstall = plugin_name )
2015-05-29 14:31:43 +00:00
elif command == " uninstall " :
plugin_name = data [ " plugin " ]
if not plugin_name in self . _plugin_manager . plugins :
return make_response ( " Unknown plugin: %s " % plugin_name , 404 )
plugin = self . _plugin_manager . plugins [ plugin_name ]
return self . command_uninstall ( plugin )
elif command == " enable " or command == " disable " :
plugin_name = data [ " plugin " ]
if not plugin_name in self . _plugin_manager . plugins :
return make_response ( " Unknown plugin: %s " % plugin_name , 404 )
plugin = self . _plugin_manager . plugins [ plugin_name ]
return self . command_toggle ( plugin , command )
2015-07-22 14:57:32 +00:00
def command_install ( self , url = None , path = None , force = False , reinstall = None , dependency_links = False ) :
2015-05-29 14:31:43 +00:00
if url is not None :
2017-04-04 13:18:37 +00:00
source = url
source_type = " url "
already_installed_check = lambda line : url in line
2015-05-29 14:31:43 +00:00
elif path is not None :
2017-04-04 13:18:37 +00:00
path = os . path . abspath ( path )
path_url = " file:// " + path
if os . sep != " / " :
# windows gets special handling
path = path . replace ( os . sep , " / " ) . lower ( )
path_url = " file:/// " + path
source = path
source_type = " path "
already_installed_check = lambda line : path_url in line . lower ( ) # lower case in case of windows
2015-05-29 14:31:43 +00:00
else :
2016-01-28 18:59:38 +00:00
raise ValueError ( " Either URL or path must be provided " )
2015-05-29 14:31:43 +00:00
2017-04-04 13:18:37 +00:00
self . _logger . info ( " Installing plugin from {} " . format ( source ) )
pip_args = [ " install " , sarge . shell_quote ( source ) ]
2015-07-22 14:57:32 +00:00
if dependency_links or self . _settings . get_boolean ( [ " dependency_links " ] ) :
pip_args . append ( " --process-dependency-links " )
2017-04-04 13:18:37 +00:00
all_plugins_before = self . _plugin_manager . find_plugins ( existing = dict ( ) )
2015-05-29 14:31:43 +00:00
2017-04-04 13:18:37 +00:00
already_installed_string = " Requirement already satisfied (use --upgrade to upgrade) "
2015-06-03 08:46:58 +00:00
success_string = " Successfully installed "
failure_string = " Could not install "
2017-04-04 13:18:37 +00:00
2015-05-29 14:31:43 +00:00
try :
returncode , stdout , stderr = self . _call_pip ( pip_args )
2017-04-04 13:18:37 +00:00
# pip's output for a package that is already installed looks something like any of these:
#
# Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \
# https://example.com/foobar.zip in <lib>
# Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin in <lib>
# Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \
# file:///tmp/foobar.zip in <lib>
# Requirement already satisfied (use --upgrade to upgrade): OctoPrint-Plugin==1.0 from \
# file:///C:/Temp/foobar.zip in <lib>
#
# If we detect any of these matching what we just tried to install, we'll need to trigger a second
# install with reinstall flags.
if not force and any ( map ( lambda x : x . strip ( ) . startswith ( already_installed_string ) and already_installed_check ( x ) ,
stdout ) ) :
self . _logger . info ( " Plugin to be installed from {} was already installed, forcing a reinstall " . format ( source ) )
self . _log_message ( " Looks like the plugin was already installed. Forcing a reinstall. " )
force = True
2015-05-29 14:31:43 +00:00
except :
self . _logger . exception ( " Could not install plugin from %s " % url )
2016-01-28 18:59:38 +00:00
return make_response ( " Could not install plugin from URL, see the log for more details " , 500 )
2015-05-29 14:31:43 +00:00
else :
if force :
2017-04-04 13:18:37 +00:00
# We don't use --upgrade here because that will also happily update all our dependencies - we'd rather
# do that in a controlled manner
2015-05-29 14:31:43 +00:00
pip_args + = [ " --ignore-installed " , " --force-reinstall " , " --no-deps " ]
try :
returncode , stdout , stderr = self . _call_pip ( pip_args )
except :
2017-04-04 13:18:37 +00:00
self . _logger . exception ( " Could not install plugin from {} " . format ( source ) )
return make_response ( " Could not install plugin from source {} , see the log for more details "
. format ( source ) , 500 )
2015-05-29 14:31:43 +00:00
try :
2017-04-04 13:18:37 +00:00
result_line = filter ( lambda x : x . startswith ( success_string ) or x . startswith ( failure_string ) ,
stdout ) [ - 1 ]
2015-05-29 14:31:43 +00:00
except IndexError :
2017-04-04 13:18:37 +00:00
self . _logger . error ( " Installing the plugin from {} failed, could not parse output from pip. "
" See plugin_pluginmanager_console.log for generated output " . format ( source ) )
result = dict ( result = False ,
source = source ,
source_type = source_type ,
reason = " Could not parse output from pip, see plugin_pluginmanager_console.log "
" for generated output " )
2015-05-29 14:31:43 +00:00
self . _send_result_notification ( " install " , result )
return jsonify ( result )
2015-06-03 08:46:58 +00:00
# The final output of a pip install command looks something like this:
2015-05-29 14:31:43 +00:00
#
# Successfully installed OctoPrint-Plugin-1.0 Dependency-One-0.1 Dependency-Two-9.3
#
2015-06-03 08:46:58 +00:00
# or this:
#
# Successfully installed OctoPrint-Plugin Dependency-One Dependency-Two
# Cleaning up...
#
2017-04-04 13:18:37 +00:00
# So we'll need to fetch the "Successfully installed" line, strip the "Successfully" part, then split
# by whitespace and strip to get all installed packages.
2015-06-03 08:46:58 +00:00
#
# We then need to iterate over all known plugins and see if either the package name or the package name plus
# version number matches one of our installed packages. If it does, that's our installed plugin.
#
# Known issue: This might return the wrong plugin if more than one plugin was installed through this
# command (e.g. due to pulling in another plugin as dependency). It should be safe for now though to
# consider this a rare corner case. Once it becomes a real problem we'll just extend the plugin manager
# so that it can report on more than one installed plugin.
2015-05-29 14:31:43 +00:00
2015-06-03 08:46:58 +00:00
result_line = result_line . strip ( )
if not result_line . startswith ( success_string ) :
2017-04-04 13:18:37 +00:00
self . _logger . error ( " Installing the plugin from {} failed, pip did not report successful installation "
. format ( source ) )
result = dict ( result = False ,
source = source ,
source_type = source_type ,
reason = " Pip did not report successful installation " )
2015-05-29 14:31:43 +00:00
self . _send_result_notification ( " install " , result )
return jsonify ( result )
2015-06-03 08:46:58 +00:00
installed = map ( lambda x : x . strip ( ) , result_line [ len ( success_string ) : ] . split ( " " ) )
2015-05-29 14:31:43 +00:00
all_plugins_after = self . _plugin_manager . find_plugins ( existing = dict ( ) , ignore_uninstalled = False )
2015-06-03 08:46:58 +00:00
2017-04-04 13:18:37 +00:00
new_plugin = self . _find_installed_plugin ( installed , plugins = all_plugins_after )
if new_plugin is None :
self . _logger . warn ( " The plugin was installed successfully, but couldn ' t be found afterwards to "
" initialize properly during runtime. Please restart OctoPrint. " )
result = dict ( result = True ,
source = source ,
source_type = source_type ,
needs_restart = True ,
needs_refresh = True ,
was_reinstalled = False ,
plugin = " unknown " )
2015-06-19 22:56:26 +00:00
self . _send_result_notification ( " install " , result )
return jsonify ( result )
2015-05-29 14:31:43 +00:00
self . _plugin_manager . reload_plugins ( )
2017-04-04 13:18:37 +00:00
needs_restart = self . _plugin_manager . is_restart_needing_plugin ( new_plugin ) \
or new_plugin . key in all_plugins_before \
or reinstall is not None
needs_refresh = new_plugin . implementation \
and isinstance ( new_plugin . implementation , octoprint . plugin . ReloadNeedingPlugin )
is_reinstall = self . _plugin_manager . is_plugin_marked ( new_plugin . key , " uninstalled " )
self . _plugin_manager . mark_plugin ( new_plugin . key ,
2015-09-30 11:37:27 +00:00
uninstalled = False ,
2015-09-30 12:30:10 +00:00
installed = not is_reinstall and needs_restart )
2015-05-29 14:31:43 +00:00
self . _plugin_manager . log_all_plugins ( )
2017-04-04 13:18:37 +00:00
self . _logger . info ( " The plugin was installed successfully: {} , version {} " . format ( new_plugin . name , new_plugin . version ) )
2017-03-31 14:52:48 +00:00
result = dict ( result = True ,
2017-04-04 13:18:37 +00:00
source = source ,
source_type = source_type ,
2017-03-31 14:52:48 +00:00
needs_restart = needs_restart ,
needs_refresh = needs_refresh ,
2017-04-04 13:18:37 +00:00
was_reinstalled = new_plugin . key in all_plugins_before or reinstall is not None ,
2017-03-31 14:52:48 +00:00
plugin = self . _to_external_plugin ( new_plugin ) )
2015-05-29 14:31:43 +00:00
self . _send_result_notification ( " install " , result )
return jsonify ( result )
def command_uninstall ( self , plugin ) :
if plugin . key == " pluginmanager " :
2015-09-30 11:37:27 +00:00
return make_response ( " Can ' t uninstall Plugin Manager " , 403 )
if not plugin . managable :
return make_response ( " Plugin is not managable and hence cannot be uninstalled " , 403 )
2015-05-29 14:31:43 +00:00
if plugin . bundled :
2015-09-30 11:37:27 +00:00
return make_response ( " Bundled plugins cannot be uninstalled " , 403 )
2015-05-29 14:31:43 +00:00
2015-06-03 08:46:58 +00:00
if plugin . origin is None :
self . _logger . warn ( u " Trying to uninstall plugin {plugin} but origin is unknown " . format ( * * locals ( ) ) )
return make_response ( " Could not uninstall plugin, its origin is unknown " )
if plugin . origin . type == " entry_point " :
2015-05-29 14:31:43 +00:00
# plugin is installed through entry point, need to use pip to uninstall it
origin = plugin . origin [ 3 ]
if origin is None :
origin = plugin . origin [ 2 ]
pip_args = [ " uninstall " , " --yes " , origin ]
try :
self . _call_pip ( pip_args )
except :
self . _logger . exception ( u " Could not uninstall plugin via pip " )
return make_response ( " Could not uninstall plugin via pip, see the log for more details " , 500 )
2015-06-03 08:46:58 +00:00
elif plugin . origin . type == " folder " :
2015-05-29 14:31:43 +00:00
import os
import shutil
full_path = os . path . realpath ( plugin . location )
if os . path . isdir ( full_path ) :
# plugin is installed via a plugin folder, need to use rmtree to get rid of it
self . _log_stdout ( u " Deleting plugin from {folder} " . format ( folder = plugin . location ) )
shutil . rmtree ( full_path )
elif os . path . isfile ( full_path ) :
self . _log_stdout ( u " Deleting plugin from {file} " . format ( file = plugin . location ) )
os . remove ( full_path )
if full_path . endswith ( " .py " ) :
pyc_file = " {full_path} c " . format ( * * locals ( ) )
if os . path . isfile ( pyc_file ) :
os . remove ( pyc_file )
2015-06-03 08:46:58 +00:00
else :
self . _logger . warn ( u " Trying to uninstall plugin {plugin} but origin is unknown ( {plugin.origin.type} ) " . format ( * * locals ( ) ) )
return make_response ( " Could not uninstall plugin, its origin is unknown " )
2015-05-29 14:31:43 +00:00
needs_restart = self . _plugin_manager . is_restart_needing_plugin ( plugin )
needs_refresh = plugin . implementation and isinstance ( plugin . implementation , octoprint . plugin . ReloadNeedingPlugin )
2015-09-30 11:37:27 +00:00
was_pending_install = self . _plugin_manager . is_plugin_marked ( plugin . key , " installed " )
2015-09-30 12:30:10 +00:00
self . _plugin_manager . mark_plugin ( plugin . key ,
uninstalled = not was_pending_install and needs_restart ,
installed = False )
2015-05-29 14:31:43 +00:00
if not needs_restart :
try :
self . _plugin_manager . disable_plugin ( plugin . key , plugin = plugin )
except octoprint . plugin . core . PluginLifecycleException as e :
self . _logger . exception ( u " Problem disabling plugin {name} " . format ( name = plugin . key ) )
result = dict ( result = False , uninstalled = True , disabled = False , unloaded = False , reason = e . reason )
self . _send_result_notification ( " uninstall " , result )
return jsonify ( result )
try :
self . _plugin_manager . unload_plugin ( plugin . key )
except octoprint . plugin . core . PluginLifecycleException as e :
self . _logger . exception ( u " Problem unloading plugin {name} " . format ( name = plugin . key ) )
result = dict ( result = False , uninstalled = True , disabled = True , unloaded = False , reason = e . reason )
self . _send_result_notification ( " uninstall " , result )
return jsonify ( result )
self . _plugin_manager . reload_plugins ( )
2017-03-31 14:52:48 +00:00
result = dict ( result = True , needs_restart = needs_restart , needs_refresh = needs_refresh , plugin = self . _to_external_plugin ( plugin ) )
2015-05-29 14:31:43 +00:00
self . _send_result_notification ( " uninstall " , result )
return jsonify ( result )
def command_toggle ( self , plugin , command ) :
if plugin . key == " pluginmanager " :
return make_response ( " Can ' t enable/disable Plugin Manager " , 400 )
needs_restart = self . _plugin_manager . is_restart_needing_plugin ( plugin )
needs_refresh = plugin . implementation and isinstance ( plugin . implementation , octoprint . plugin . ReloadNeedingPlugin )
pending = ( ( command == " disable " and plugin . key in self . _pending_enable ) or ( command == " enable " and plugin . key in self . _pending_disable ) )
2016-11-18 12:01:14 +00:00
safe_mode_victim = getattr ( plugin , " safe_mode_victim " , False )
needs_restart_api = ( needs_restart or safe_mode_victim ) and not pending
2015-05-29 14:31:43 +00:00
needs_refresh_api = needs_refresh and not pending
try :
if command == " disable " :
self . _mark_plugin_disabled ( plugin , needs_restart = needs_restart )
elif command == " enable " :
self . _mark_plugin_enabled ( plugin , needs_restart = needs_restart )
except octoprint . plugin . core . PluginLifecycleException as e :
self . _logger . exception ( u " Problem toggling enabled state of {name} : {reason} " . format ( name = plugin . key , reason = e . reason ) )
result = dict ( result = False , reason = e . reason )
except octoprint . plugin . core . PluginNeedsRestart :
2017-03-31 14:52:48 +00:00
result = dict ( result = True ,
needs_restart = True ,
needs_refresh = True ,
plugin = self . _to_external_plugin ( plugin ) )
2015-05-29 14:31:43 +00:00
else :
2017-03-31 14:52:48 +00:00
result = dict ( result = True ,
needs_restart = needs_restart_api ,
needs_refresh = needs_refresh_api ,
plugin = self . _to_external_plugin ( plugin ) )
2015-05-29 14:31:43 +00:00
self . _send_result_notification ( command , result )
return jsonify ( result )
2017-04-04 13:18:37 +00:00
def _find_installed_plugin ( self , packages , plugins = None ) :
if plugins is None :
plugins = self . _plugin_manager . find_plugins ( existing = dict ( ) , ignore_uninstalled = False )
for key , plugin in plugins . items ( ) :
if plugin . origin is None or plugin . origin . type != " entry_point " :
continue
package_name = plugin . origin . package_name
package_version = plugin . origin . package_version
versioned_package = " {package_name} - {package_version} " . format ( * * locals ( ) )
if package_name in packages or versioned_package in packages :
# exact match, we are done here
return plugin
else :
# it might still be a version that got stripped by python's package resources, e.g. 1.4.5a0 => 1.4.5a
found = False
for inst in packages :
if inst . startswith ( versioned_package ) :
found = True
break
if found :
return plugin
return None
2015-05-29 14:31:43 +00:00
def _send_result_notification ( self , action , result ) :
notification = dict ( type = " result " , action = action )
notification . update ( result )
self . _plugin_manager . send_plugin_message ( self . _identifier , notification )
def _call_pip ( self , args ) :
2015-06-23 09:36:27 +00:00
if self . _pip_caller is None or not self . _pip_caller . available :
raise RuntimeError ( u " No pip available, can ' t operate " . format ( * * locals ( ) ) )
2015-07-22 14:57:32 +00:00
2015-07-23 08:54:39 +00:00
if " --process-dependency-links " in args :
self . _log_message ( u " Installation needs to process external dependencies, that might make it take a bit longer than usual depending on the pip version " )
2015-07-22 14:57:32 +00:00
2015-09-28 17:53:30 +00:00
additional_args = self . _settings . get ( [ " pip_args " ] )
2016-11-07 09:50:14 +00:00
if additional_args is not None :
inapplicable_arguments = self . __class__ . pip_inapplicable_arguments . get ( args [ 0 ] , list ( ) )
for inapplicable_argument in inapplicable_arguments :
additional_args = re . sub ( " (^| \ s) " + re . escape ( inapplicable_argument ) + " \\ b " , " " , additional_args )
if additional_args :
args . append ( additional_args )
2015-09-28 17:53:30 +00:00
2015-06-23 09:36:27 +00:00
return self . _pip_caller . execute ( * args )
2015-05-29 14:31:43 +00:00
2015-07-23 08:54:39 +00:00
def _log_message ( self , * lines ) :
self . _log ( lines , prefix = u " * " , stream = " message " )
2015-06-23 09:36:27 +00:00
def _log_call ( self , * lines ) :
2015-07-02 06:36:03 +00:00
self . _log ( lines , prefix = u " " , stream = " call " )
2015-05-29 14:31:43 +00:00
def _log_stdout ( self , * lines ) :
2015-07-02 06:36:03 +00:00
self . _log ( lines , prefix = u " > " , stream = " stdout " )
2015-05-29 14:31:43 +00:00
def _log_stderr ( self , * lines ) :
2015-07-02 06:36:03 +00:00
self . _log ( lines , prefix = u " ! " , stream = " stderr " )
2015-05-29 14:31:43 +00:00
def _log ( self , lines , prefix = None , stream = None , strip = True ) :
if strip :
lines = map ( lambda x : x . strip ( ) , lines )
self . _plugin_manager . send_plugin_message ( self . _identifier , dict ( type = " loglines " , loglines = [ dict ( line = line , stream = stream ) for line in lines ] ) )
for line in lines :
self . _console_logger . debug ( u " {prefix} {line} " . format ( * * locals ( ) ) )
def _mark_plugin_enabled ( self , plugin , needs_restart = False ) :
disabled_list = list ( self . _settings . global_get ( [ " plugins " , " _disabled " ] ) )
if plugin . key in disabled_list :
disabled_list . remove ( plugin . key )
self . _settings . global_set ( [ " plugins " , " _disabled " ] , disabled_list )
self . _settings . save ( force = True )
2016-11-18 12:01:14 +00:00
if not needs_restart and not getattr ( plugin , " safe_mode_victim " , False ) :
2015-05-29 14:31:43 +00:00
self . _plugin_manager . enable_plugin ( plugin . key )
else :
if plugin . key in self . _pending_disable :
self . _pending_disable . remove ( plugin . key )
2016-11-18 12:01:14 +00:00
elif ( not plugin . enabled and not getattr ( plugin , " safe_mode_enabled " , False ) ) and plugin . key not in self . _pending_enable :
2015-05-29 14:31:43 +00:00
self . _pending_enable . add ( plugin . key )
def _mark_plugin_disabled ( self , plugin , needs_restart = False ) :
disabled_list = list ( self . _settings . global_get ( [ " plugins " , " _disabled " ] ) )
if not plugin . key in disabled_list :
disabled_list . append ( plugin . key )
self . _settings . global_set ( [ " plugins " , " _disabled " ] , disabled_list )
self . _settings . save ( force = True )
2016-11-18 12:01:14 +00:00
if not needs_restart and not getattr ( plugin , " safe_mode_victim " , False ) :
2015-05-29 14:31:43 +00:00
self . _plugin_manager . disable_plugin ( plugin . key )
else :
if plugin . key in self . _pending_enable :
self . _pending_enable . remove ( plugin . key )
2016-11-18 12:01:14 +00:00
elif ( plugin . enabled or getattr ( plugin , " safe_mode_enabled " , False ) ) and plugin . key not in self . _pending_disable :
2015-05-29 14:31:43 +00:00
self . _pending_disable . add ( plugin . key )
2015-06-22 11:00:29 +00:00
def _fetch_repository_from_disk ( self ) :
repo_data = None
if os . path . isfile ( self . _repository_cache_path ) :
import time
mtime = os . path . getmtime ( self . _repository_cache_path )
if mtime + self . _repository_cache_ttl > = time . time ( ) > mtime :
try :
import json
with open ( self . _repository_cache_path ) as f :
repo_data = json . load ( f )
self . _logger . info ( " Loaded plugin repository data from disk, was still valid " )
except :
self . _logger . exception ( " Error while loading repository data from {} " . format ( self . _repository_cache_path ) )
return self . _refresh_repository ( repo_data = repo_data )
def _fetch_repository_from_url ( self ) :
2015-05-29 14:31:43 +00:00
repository_url = self . _settings . get ( [ " repository " ] )
try :
r = requests . get ( repository_url )
2015-06-22 11:00:29 +00:00
self . _logger . info ( " Loaded plugin repository data from {} " . format ( repository_url ) )
2015-05-29 14:31:43 +00:00
except Exception as e :
2015-06-22 11:00:29 +00:00
self . _logger . exception ( " Could not fetch plugins from repository at {repository_url} : {message} " . format ( repository_url = repository_url , message = str ( e ) ) )
return None
repo_data = r . json ( )
try :
import json
2015-10-05 16:07:43 +00:00
with octoprint . util . atomic_write ( self . _repository_cache_path , " wb " ) as f :
2015-06-22 11:00:29 +00:00
json . dump ( repo_data , f )
except Exception as e :
self . _logger . exception ( " Error while saving repository data to {} : {} " . format ( self . _repository_cache_path , str ( e ) ) )
return repo_data
def _refresh_repository ( self , repo_data = None ) :
if repo_data is None :
repo_data = self . _fetch_repository_from_url ( )
if repo_data is None :
return False
2015-05-29 14:31:43 +00:00
current_os = self . _get_os ( )
2015-07-01 08:20:09 +00:00
octoprint_version = self . _get_octoprint_version ( base = True )
2015-05-29 14:31:43 +00:00
def map_repository_entry ( entry ) :
2017-03-31 14:52:48 +00:00
result = copy . deepcopy ( entry )
2015-06-10 17:34:33 +00:00
2015-07-22 14:57:32 +00:00
if not " follow_dependency_links " in result :
result [ " follow_dependency_links " ] = False
2015-05-29 14:31:43 +00:00
result [ " is_compatible " ] = dict (
octoprint = True ,
os = True
)
if " compatibility " in entry :
2017-01-19 11:08:05 +00:00
if " octoprint " in entry [ " compatibility " ] and entry [ " compatibility " ] [ " octoprint " ] is not None and isinstance ( entry [ " compatibility " ] [ " octoprint " ] , ( list , tuple ) ) and len ( entry [ " compatibility " ] [ " octoprint " ] ) :
2015-06-29 08:13:44 +00:00
result [ " is_compatible " ] [ " octoprint " ] = self . _is_octoprint_compatible ( octoprint_version , entry [ " compatibility " ] [ " octoprint " ] )
2015-05-29 14:31:43 +00:00
2017-01-19 11:08:05 +00:00
if " os " in entry [ " compatibility " ] and entry [ " compatibility " ] [ " os " ] is not None and isinstance ( entry [ " compatibility " ] [ " os " ] , ( list , tuple ) ) and len ( entry [ " compatibility " ] [ " os " ] ) :
2015-06-29 08:13:44 +00:00
result [ " is_compatible " ] [ " os " ] = self . _is_os_compatible ( current_os , entry [ " compatibility " ] [ " os " ] )
2015-05-29 14:31:43 +00:00
return result
2015-06-22 11:00:29 +00:00
self . _repository_plugins = map ( map_repository_entry , repo_data )
2015-05-29 14:31:43 +00:00
return True
2017-03-31 14:52:48 +00:00
def _fetch_notices_from_disk ( self ) :
notice_data = None
if os . path . isfile ( self . _notices_cache_path ) :
import time
mtime = os . path . getmtime ( self . _notices_cache_path )
if mtime + self . _notices_cache_ttl > = time . time ( ) > mtime :
try :
import json
with open ( self . _notices_cache_path ) as f :
notice_data = json . load ( f )
self . _logger . info ( " Loaded notices from disk, was still valid " )
except :
self . _logger . exception ( " Error while loading notices from {} " . format ( self . _notices_cache_path ) )
return self . _refresh_notices ( notice_data = notice_data )
def _fetch_notices_from_url ( self ) :
notices_url = self . _settings . get ( [ " notices " ] )
try :
r = requests . get ( notices_url )
self . _logger . info ( " Loaded plugin notices data from {} " . format ( notices_url ) )
except Exception as e :
self . _logger . exception ( " Could not fetch notices from {notices_url} : {message} " . format ( notices_url = notices_url , message = str ( e ) ) )
return None
notice_data = r . json ( )
try :
import json
with octoprint . util . atomic_write ( self . _notices_cache_path , " wb " ) as f :
json . dump ( notice_data , f )
except Exception as e :
self . _logger . exception ( " Error while saving notices to {} : {} " . format ( self . _notices_cache_path , str ( e ) ) )
return notice_data
def _refresh_notices ( self , notice_data = None ) :
if notice_data is None :
notice_data = self . _fetch_notices_from_url ( )
if notice_data is None :
return False
notices = dict ( )
for notice in notice_data :
if not " plugin " in notice or not " text " in notice or not " date " in notice :
continue
key = notice [ " plugin " ]
try :
parsed_date = dateutil . parser . parse ( notice [ " date " ] )
notice [ " timestamp " ] = parsed_date . timetuple ( )
except Exception as e :
self . _logger . warn ( " Error while parsing date {!r} for plugin notice "
" of plugin {} , ignoring notice: {} " . format ( notice [ " date " ] , key , str ( e ) ) )
continue
if not key in notices :
notices [ key ] = [ ]
notices [ key ] . append ( notice )
self . _notices = notices
return True
2015-07-01 08:20:09 +00:00
def _is_octoprint_compatible ( self , octoprint_version , compatibility_entries ) :
2015-06-29 08:13:44 +00:00
"""
Tests if the current ` ` octoprint_version ` ` is compatible to any of the provided ` ` compatibility_entries ` ` .
"""
for octo_compat in compatibility_entries :
2017-01-19 11:08:05 +00:00
try :
if not any ( octo_compat . startswith ( c ) for c in ( " < " , " <= " , " != " , " == " , " >= " , " > " , " ~= " , " === " ) ) :
octo_compat = " >= {} " . format ( octo_compat )
2015-06-29 16:08:34 +00:00
2017-01-19 11:08:05 +00:00
s = next ( pkg_resources . parse_requirements ( " OctoPrint " + octo_compat ) )
if octoprint_version in s :
break
except :
self . _logger . exception ( " Something is wrong with this compatibility string for OctoPrint: {} " . format ( octo_compat ) )
2015-06-29 08:13:44 +00:00
else :
return False
return True
def _is_os_compatible ( self , current_os , compatibility_entries ) :
"""
Tests if the ` ` current_os ` ` matches any of the provided ` ` compatibility_entries ` ` .
"""
2017-01-19 11:08:05 +00:00
return current_os in filter ( lambda x : x in self . __class__ . OPERATING_SYSTEMS . keys ( ) , compatibility_entries )
2015-06-29 08:13:44 +00:00
2015-05-29 14:31:43 +00:00
def _get_os ( self ) :
2017-01-19 11:08:05 +00:00
for identifier , platforms in self . __class__ . OPERATING_SYSTEMS . items ( ) :
if sys . platform in platforms :
return identifier
2015-05-29 14:31:43 +00:00
else :
return " unknown "
2015-07-01 08:20:09 +00:00
def _get_octoprint_version_string ( self ) :
2015-12-09 09:29:24 +00:00
return VERSION
2015-05-29 14:31:43 +00:00
2015-07-01 08:20:09 +00:00
def _get_octoprint_version ( self , base = False ) :
octoprint_version_string = self . _get_octoprint_version_string ( )
if " - " in octoprint_version_string :
octoprint_version_string = octoprint_version_string [ : octoprint_version_string . find ( " - " ) ]
octoprint_version = pkg_resources . parse_version ( octoprint_version_string )
2017-01-24 10:38:30 +00:00
# A leading v is common in github release tags and old setuptools doesn't remove it. While OctoPrint's
# versions should never contains such a prefix, we'll make sure to have stuff behave the same
# regardless of setuptools version anyhow.
2017-01-21 12:00:37 +00:00
if octoprint_version and isinstance ( octoprint_version , tuple ) and octoprint_version [ 0 ] . lower ( ) == " *v " :
octoprint_version = octoprint_version [ 1 : ]
2017-01-24 10:38:30 +00:00
2015-07-01 08:20:09 +00:00
if base :
2015-07-02 08:48:46 +00:00
if isinstance ( octoprint_version , tuple ) :
# old setuptools
base_version = [ ]
for part in octoprint_version :
if part . startswith ( " * " ) :
break
base_version . append ( part )
2015-12-14 13:14:44 +00:00
base_version . append ( " *final " )
2015-07-02 08:48:46 +00:00
octoprint_version = tuple ( base_version )
else :
# new setuptools
octoprint_version = pkg_resources . parse_version ( octoprint_version . base_version )
2015-07-01 08:20:09 +00:00
return octoprint_version
2015-07-09 13:43:11 +00:00
def _get_plugins ( self ) :
plugins = self . _plugin_manager . plugins
hidden = self . _settings . get ( [ " hidden " ] )
result = [ ]
2017-03-31 14:52:48 +00:00
for key , plugin in plugins . items ( ) :
if key in hidden :
2015-07-09 13:43:11 +00:00
continue
2017-03-31 14:52:48 +00:00
result . append ( self . _to_external_plugin ( plugin ) )
2015-07-09 13:43:11 +00:00
return result
2017-03-31 14:52:48 +00:00
def _to_external_plugin ( self , plugin ) :
2015-05-29 14:31:43 +00:00
return dict (
key = plugin . key ,
name = plugin . name ,
description = plugin . description ,
2017-03-30 12:08:47 +00:00
disabling_discouraged = gettext ( plugin . disabling_discouraged ) if plugin . disabling_discouraged else False ,
2015-05-29 14:31:43 +00:00
author = plugin . author ,
version = plugin . version ,
url = plugin . url ,
license = plugin . license ,
bundled = plugin . bundled ,
2015-09-30 11:37:27 +00:00
managable = plugin . managable ,
2015-05-29 14:31:43 +00:00
enabled = plugin . enabled ,
2016-11-18 12:01:14 +00:00
safe_mode_victim = getattr ( plugin , " safe_mode_victim " , False ) ,
safe_mode_enabled = getattr ( plugin , " safe_mode_enabled " , False ) ,
pending_enable = ( not plugin . enabled and not getattr ( plugin , " safe_mode_enabled " , False ) and plugin . key in self . _pending_enable ) ,
pending_disable = ( ( plugin . enabled or getattr ( plugin , " safe_mode_enabled " , False ) ) and plugin . key in self . _pending_disable ) ,
2015-09-30 11:37:27 +00:00
pending_install = ( self . _plugin_manager . is_plugin_marked ( plugin . key , " installed " ) ) ,
pending_uninstall = ( self . _plugin_manager . is_plugin_marked ( plugin . key , " uninstalled " ) ) ,
2017-03-31 14:52:48 +00:00
origin = plugin . origin . type ,
notifications = self . _get_notifications ( plugin )
2015-05-29 14:31:43 +00:00
)
2017-03-31 14:52:48 +00:00
def _get_notifications ( self , plugin ) :
key = plugin . key
2017-03-31 15:32:41 +00:00
if not plugin . enabled :
return
2017-03-31 14:52:48 +00:00
if key not in self . _notices :
return
2017-03-31 15:32:41 +00:00
octoprint_version = self . _get_octoprint_version ( base = True )
2017-03-31 14:52:48 +00:00
plugin_notifications = self . _notices . get ( key , [ ] )
2017-03-31 15:32:41 +00:00
def filter_relevant ( notification ) :
2017-03-31 14:52:48 +00:00
return " text " in notification and " date " in notification and \
2017-03-31 15:32:41 +00:00
( " versions " not in notification or plugin . version in notification [ " versions " ] ) and \
( " octoversions " not in notification or self . _is_octoprint_compatible ( octoprint_version , notification [ " octoversions " ] ) )
2017-03-31 14:52:48 +00:00
def map_notification ( notification ) :
return self . _to_external_notification ( key , notification )
return filter ( lambda x : x is not None ,
map ( map_notification ,
2017-03-31 15:32:41 +00:00
filter ( filter_relevant ,
2017-03-31 14:52:48 +00:00
plugin_notifications ) ) )
def _to_external_notification ( self , key , notification ) :
return dict ( key = key ,
date = time . mktime ( notification [ " timestamp " ] ) ,
text = notification [ " text " ] ,
link = notification . get ( " link " ) ,
versions = notification . get ( " versions " , [ ] ) ,
important = notification . get ( " important " , False ) )
2015-05-29 14:31:43 +00:00
__plugin_name__ = " Plugin Manager "
__plugin_author__ = " Gina Häußge "
2017-03-30 12:09:12 +00:00
__plugin_url__ = " http://docs.octoprint.org/en/master/bundledplugins/pluginmanager.html "
2015-05-29 14:31:43 +00:00
__plugin_description__ = " Allows installing and managing OctoPrint plugins "
__plugin_license__ = " AGPLv3 "
2015-06-29 18:59:46 +00:00
def __plugin_load__ ( ) :
global __plugin_implementation__
__plugin_implementation__ = PluginManagerPlugin ( )
global __plugin_hooks__
__plugin_hooks__ = {
2015-07-09 13:43:11 +00:00
" octoprint.server.http.bodysize " : __plugin_implementation__ . increase_upload_bodysize ,
" octoprint.ui.web.templatetypes " : __plugin_implementation__ . get_template_types
2015-06-29 18:59:46 +00:00
}