Merge branch 'maintenance' into devel
Conflicts: src/octoprint/plugins/softwareupdate/updaters/pip.py src/octoprint/util/pip.py
This commit is contained in:
commit
8722cdadcd
11 changed files with 199 additions and 53 deletions
69
docs/features/accesscontrol.rst
Normal file
69
docs/features/accesscontrol.rst
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
.. _sec-features-access_control:
|
||||
|
||||
Access Control
|
||||
==============
|
||||
|
||||
When Access Control is enabled, anonymous users (not logged in) will only see
|
||||
the read-only parts of the UI which are the following:
|
||||
|
||||
* printer state
|
||||
* available gcode files and stats (upload is disabled)
|
||||
* temperature
|
||||
* webcam
|
||||
* gcode viewer
|
||||
* terminal output (sending commands is disabled)
|
||||
* available timelapse movies
|
||||
* any components provided through plugins which are enabled for anonymous
|
||||
users
|
||||
|
||||
Logged in users will get access to everything besides the Settings and System
|
||||
Commands, which are admin-only.
|
||||
|
||||
If Access Control is disabled, everything is directly accessible. **That also
|
||||
includes all administrative functionality as well as full control over the
|
||||
printer!**
|
||||
|
||||
Upon first start a configuration wizard is provided which allows configuration
|
||||
of the first administrator account or alternatively disabling Access Control
|
||||
(which is **NOT** recommended for systems that are directly accessible via the
|
||||
Internet!).
|
||||
|
||||
.. hint::
|
||||
|
||||
If you plan to have your OctoPrint instance accessible over the internet,
|
||||
**always enable Access Control**.
|
||||
|
||||
.. _sec-features-access_control-rerunning_wizard:
|
||||
|
||||
Rerunning the wizard
|
||||
--------------------
|
||||
|
||||
In case Access Control was disabled in the configuration wizard, it is
|
||||
possibly to re-run it by editing ``config.yaml`` [#f1]_ and setting ``firstRun``
|
||||
in the ``server`` section and ``enabled`` in the ``accessControl`` section to
|
||||
``true``:
|
||||
|
||||
.. code-block-ext:: yaml
|
||||
|
||||
accessControl:
|
||||
enabled: true
|
||||
# ...
|
||||
server:
|
||||
firstRun: true
|
||||
|
||||
Then restart the server and connect to the web interface - the wizard should
|
||||
be shown again.
|
||||
|
||||
.. note::
|
||||
|
||||
If user accounts were created prior to disabling Access Control and those
|
||||
user accounts are not to be used any more, remove ``.octoprint/users.yaml``.
|
||||
If you don't remove this file, the above changes won't lead to the
|
||||
configuration being shown again, instead Access Control will just be
|
||||
enabled using the already existing login data. This is to prevent you from
|
||||
resetting access control by accident.
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] For Linux that will be ``~/.octoprint/config.yaml``, for Windows it will be ``%APPDATA%/OctoPrint/config.yaml`` and for
|
||||
Mac ``~/Library/Application Support/OctoPrint/config.yaml``
|
||||
|
|
@ -10,7 +10,7 @@ buttons which trigger sending of one or more lines of GCODE to the printer over
|
|||
parameterization of these commands with values entered by the user to full blown GCODE script templates backed by
|
||||
`Jinja2 <http://jinja.pocoo.org/>`_.
|
||||
|
||||
Custom controls are configured within :ref:`config.yaml <sec-configuration-config_yaml>` in a ``controls`` section which
|
||||
Custom controls are configured within :ref:`config.yaml <sec-configuration-config_yaml>` [#f1]_ in a ``controls`` section which
|
||||
basically represents a hierarchical structure of all configured custom controls of various types.
|
||||
|
||||
.. note::
|
||||
|
|
@ -94,6 +94,13 @@ button that sends one or more commands to the printer when clicked, displaying o
|
|||
controls that just serve as *container* for other controls, the latter being identified by having a ``children``
|
||||
attribute wrapping more controls.
|
||||
|
||||
.. hint::
|
||||
|
||||
Take a look at the `Custom Control Editor plugin <http://plugins.octoprint.org/plugins/customControl/>`_
|
||||
which allows you configuring your Custom Controls through OctoPrint's
|
||||
settings interface without the need to manually edit the configuration
|
||||
file.
|
||||
|
||||
.. _sec-features-custom_controls-types:
|
||||
|
||||
Types
|
||||
|
|
@ -278,4 +285,9 @@ Parameterized GCODE Script
|
|||
G28 X0 Y0
|
||||
|
||||
Note the usage of the ``parameters.repetitions`` template variable in the GCODE script template, which will contain
|
||||
the value selected by the user for the "Go arounds" slider.
|
||||
the value selected by the user for the "Go arounds" slider.
|
||||
|
||||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] For Linux that will be ``~/.octoprint/config.yaml``, for Windows it will be ``%APPDATA%/OctoPrint/config.yaml`` and for
|
||||
Mac ``~/Library/Application Support/OctoPrint/config.yaml``
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ Features
|
|||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
accesscontrol.rst
|
||||
custom_controls.rst
|
||||
gcode_scripts.rst
|
||||
action_commands.rst
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ See :ref:`Developing Plugins <sec-plugins>`.
|
|||
.. rubric:: Footnotes
|
||||
|
||||
.. [#f1] For Linux that will be ``~/.octoprint/plugins``, for Windows it will be ``%APPDATA%/OctoPrint/plugins`` and for
|
||||
Mac ``~/Library/Application Support/OctoPrint``
|
||||
Mac ``~/Library/Application Support/OctoPrint/plugins``
|
||||
.. [#f2] Make sure to use the exact same Python installation for installing the plugin that you also used for
|
||||
installing & running OctoPrint. For OctoPi this means using ``~/oprint/bin/pip`` for installing plugins
|
||||
instead of just ``pip``.
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ Welcome to OctoPrint's documentation!
|
|||
:alt: The OctoPrint Logo
|
||||
:align: right
|
||||
|
||||
This documentation is still in the process of being migrated from
|
||||
`OctoPrint's wiki <https://github.com/foosel/OctoPrint/wiki>`_, so also take
|
||||
a look there!
|
||||
|
||||
Contents
|
||||
========
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
if not on_progress_kwargs:
|
||||
on_progress_kwargs = dict()
|
||||
|
||||
self._cura_logger.info("### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path))
|
||||
self._cura_logger.info(u"### Slicing %s to %s using profile stored at %s" % (model_path, machinecode_path, profile_path))
|
||||
|
||||
engine_settings = self._convert_to_engine(profile_path, printer_profile, posX, posY)
|
||||
|
||||
|
|
@ -227,16 +227,15 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
return False, "Path to CuraEngine is not configured "
|
||||
|
||||
working_dir, _ = os.path.split(executable)
|
||||
args = ['"%s"' % executable, '-v', '-p']
|
||||
args = [executable, '-v', '-p']
|
||||
for k, v in engine_settings.items():
|
||||
args += ["-s", '"%s=%s"' % (k, str(v))]
|
||||
args += ['-o', '"%s"' % machinecode_path, '"%s"' % model_path]
|
||||
args += ["-s", "%s=%s" % (k, str(v))]
|
||||
args += ["-o", machinecode_path, model_path]
|
||||
|
||||
self._logger.info(u"Running %r in %s" % (" ".join(args), working_dir))
|
||||
|
||||
import sarge
|
||||
command = " ".join(args)
|
||||
self._logger.info("Running %r in %s" % (command, working_dir))
|
||||
|
||||
p = sarge.run(command, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture())
|
||||
p = sarge.run(args, cwd=working_dir, async=True, stdout=sarge.Capture(), stderr=sarge.Capture())
|
||||
p.wait_events()
|
||||
self._slicing_commands[machinecode_path] = p.commands[0]
|
||||
|
||||
|
|
@ -254,6 +253,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
p.commands[0].poll()
|
||||
continue
|
||||
|
||||
line = octoprint.util.to_unicode(line, errors="replace")
|
||||
self._cura_logger.debug(line.strip())
|
||||
|
||||
if on_progress is not None:
|
||||
|
|
@ -282,14 +282,14 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
#
|
||||
# with <step_factor> being 0 for "inset", 1 for "skin" and 2 for "export".
|
||||
|
||||
if line.startswith("Layer count:") and layer_count is None:
|
||||
if line.startswith(u"Layer count:") and layer_count is None:
|
||||
try:
|
||||
layer_count = float(line[len("Layer count:"):].strip())
|
||||
layer_count = float(line[len(u"Layer count:"):].strip())
|
||||
except:
|
||||
pass
|
||||
|
||||
elif line.startswith("Progress:"):
|
||||
split_line = line[len("Progress:"):].strip().split(":")
|
||||
elif line.startswith(u"Progress:"):
|
||||
split_line = line[len(u"Progress:"):].strip().split(":")
|
||||
if len(split_line) == 3:
|
||||
step, current_layer, _ = split_line
|
||||
try:
|
||||
|
|
@ -302,21 +302,21 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
on_progress_kwargs["_progress"] = (step_factor[step] * layer_count + current_layer) / (layer_count * 3)
|
||||
on_progress(*on_progress_args, **on_progress_kwargs)
|
||||
|
||||
elif line.startswith("Print time:"):
|
||||
elif line.startswith(u"Print time:"):
|
||||
try:
|
||||
print_time = int(line[len("Print time:"):].strip())
|
||||
print_time = int(line[len(u"Print time:"):].strip())
|
||||
if analysis is None:
|
||||
analysis = dict()
|
||||
analysis["estimatedPrintTime"] = print_time
|
||||
except:
|
||||
pass
|
||||
|
||||
elif line.startswith("Filament:") or line.startswith("Filament2:"):
|
||||
if line.startswith("Filament:"):
|
||||
filament_str = line[len("Filament:"):].strip()
|
||||
elif line.startswith(u"Filament:") or line.startswith(u"Filament2:"):
|
||||
if line.startswith(u"Filament:"):
|
||||
filament_str = line[len(u"Filament:"):].strip()
|
||||
tool_key = "tool0"
|
||||
else:
|
||||
filament_str = line[len("Filament2:"):].strip()
|
||||
filament_str = line[len(u"Filament2:"):].strip()
|
||||
tool_key = "tool1"
|
||||
|
||||
try:
|
||||
|
|
@ -339,20 +339,20 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
|
||||
with self._job_mutex:
|
||||
if machinecode_path in self._cancelled_jobs:
|
||||
self._cura_logger.info("### Cancelled")
|
||||
self._cura_logger.info(u"### Cancelled")
|
||||
raise octoprint.slicing.SlicingCancelled()
|
||||
|
||||
self._cura_logger.info("### Finished, returncode %d" % p.returncode)
|
||||
self._cura_logger.info(u"### Finished, returncode %d" % p.returncode)
|
||||
if p.returncode == 0:
|
||||
return True, dict(analysis=analysis)
|
||||
else:
|
||||
self._logger.warn("Could not slice via Cura, got return code %r" % p.returncode)
|
||||
self._logger.warn(u"Could not slice via Cura, got return code %r" % p.returncode)
|
||||
return False, "Got returncode %r" % p.returncode
|
||||
|
||||
except octoprint.slicing.SlicingCancelled as e:
|
||||
raise e
|
||||
except:
|
||||
self._logger.exception("Could not slice via Cura, got an unknown error")
|
||||
self._logger.exception(u"Could not slice via Cura, got an unknown error")
|
||||
return False, "Unknown error, please consult the log file"
|
||||
|
||||
finally:
|
||||
|
|
@ -371,7 +371,7 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
|
|||
command = self._slicing_commands[machinecode_path]
|
||||
if command is not None:
|
||||
command.terminate()
|
||||
self._logger.info("Cancelled slicing of %s" % machinecode_path)
|
||||
self._logger.info(u"Cancelled slicing of %s" % machinecode_path)
|
||||
|
||||
def _load_profile(self, path):
|
||||
import yaml
|
||||
|
|
|
|||
|
|
@ -420,13 +420,13 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
|
|||
return self._pip_caller.execute(*args)
|
||||
|
||||
def _log_call(self, *lines):
|
||||
self._log(lines, prefix=" ", stream="call")
|
||||
self._log(lines, prefix=u" ", stream="call")
|
||||
|
||||
def _log_stdout(self, *lines):
|
||||
self._log(lines, prefix=">", stream="stdout")
|
||||
self._log(lines, prefix=u">", stream="stdout")
|
||||
|
||||
def _log_stderr(self, *lines):
|
||||
self._log(lines, prefix="!", stream="stderr")
|
||||
self._log(lines, prefix=u"!", stream="stderr")
|
||||
|
||||
def _log(self, lines, prefix=None, stream=None, strip=True):
|
||||
if strip:
|
||||
|
|
|
|||
|
|
@ -62,12 +62,12 @@ def perform_update(target, check, target_version, log_cb=None):
|
|||
|
||||
install_arg = check["pip"].format(target_version=target_version)
|
||||
|
||||
logger.debug("Target: %s, executing pip install %s" % (target, install_arg))
|
||||
logger.debug(u"Target: %s, executing pip install %s" % (target, install_arg))
|
||||
pip_args = ["install", check["pip"].format(target_version=target_version, target=target_version)]
|
||||
|
||||
pip_caller.execute(*pip_args)
|
||||
|
||||
logger.debug("Target: %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg))
|
||||
logger.debug(u"Target: %s, executing pip install %s --ignore-reinstalled --force-reinstall --no-deps" % (target, install_arg))
|
||||
pip_args += ["--ignore-installed", "--force-reinstall", "--no-deps"]
|
||||
|
||||
pip_caller.execute(*pip_args)
|
||||
|
|
|
|||
|
|
@ -44,35 +44,71 @@ def _get_latest_release(user, repo, include_prerelease=False):
|
|||
return latest["name"], latest["tag_name"]
|
||||
|
||||
|
||||
def _is_current(release_information, compare_type, custom=None):
|
||||
def _get_sanitized_version(version_string):
|
||||
if "-" in version_string:
|
||||
version_string = version_string[:version_string.find("-")]
|
||||
return version_string
|
||||
|
||||
|
||||
def _get_comparable_version_pkg_resources(version_string, force_base=True):
|
||||
import pkg_resources
|
||||
|
||||
version = pkg_resources.parse_version(version_string)
|
||||
|
||||
if force_base:
|
||||
if isinstance(version, tuple):
|
||||
# old setuptools
|
||||
base_version = []
|
||||
for part in version:
|
||||
if part.startswith("*"):
|
||||
break
|
||||
base_version.append(part)
|
||||
version = tuple(base_version)
|
||||
else:
|
||||
# new setuptools
|
||||
version = pkg_resources.parse_version(version.base_version)
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def _get_comparable_version_semantic(version_string, force_base=True):
|
||||
import semantic_version
|
||||
|
||||
version = semantic_version.Version.coerce(version_string, partial=False)
|
||||
|
||||
if force_base:
|
||||
version_string = "{}.{}.{}".format(version.major, version.minor, version.patch)
|
||||
version = semantic_version.Version.coerce(version_string, partial=False)
|
||||
|
||||
return version
|
||||
|
||||
|
||||
def _is_current(release_information, compare_type, custom=None, force_base=True):
|
||||
if release_information["remote"]["value"] is None:
|
||||
return True
|
||||
|
||||
if not compare_type in ("python", "semantic", "unequal", "custom") or compare_type == "custom" and custom is None:
|
||||
compare_type = "python"
|
||||
|
||||
sanitized_local = _get_sanitized_version(release_information["local"]["value"])
|
||||
sanitized_remote = _get_sanitized_version(release_information["remote"]["value"])
|
||||
|
||||
try:
|
||||
if compare_type == "python":
|
||||
import pkg_resources
|
||||
|
||||
local_version = pkg_resources.parse_version(release_information["local"]["value"])
|
||||
remote_version = pkg_resources.parse_version(release_information["remote"]["value"])
|
||||
|
||||
local_version = _get_comparable_version_pkg_resources(sanitized_local, force_base=force_base)
|
||||
remote_version = _get_comparable_version_pkg_resources(sanitized_remote, force_base=force_base)
|
||||
return local_version >= remote_version
|
||||
|
||||
elif compare_type == "semantic":
|
||||
import semantic_version
|
||||
|
||||
local_version = semantic_version.Version(release_information["local"]["value"])
|
||||
remote_version = semantic_version.Version(release_information["remote"]["value"])
|
||||
|
||||
local_version = _get_comparable_version_semantic(sanitized_local, force_base=force_base)
|
||||
remote_version = _get_comparable_version_semantic(sanitized_remote, force_base=force_base)
|
||||
return local_version >= remote_version
|
||||
|
||||
elif compare_type == "custom":
|
||||
return custom(release_information["local"], release_information["remote"])
|
||||
return custom(sanitized_local, sanitized_remote)
|
||||
|
||||
else:
|
||||
return release_information["local"]["value"] == release_information["remote"]["value"]
|
||||
return sanitized_local == sanitized_remote
|
||||
except:
|
||||
logger.exception("Could not check if version is current due to an error, assuming it is")
|
||||
return True
|
||||
|
|
@ -82,11 +118,13 @@ def get_latest(target, check, custom_compare=None):
|
|||
if not "user" in check or not "repo" in check:
|
||||
raise ConfigurationInvalid("github_release update configuration for %s needs user and repo set" % target)
|
||||
|
||||
current = None
|
||||
if "current" in check:
|
||||
current = check["current"]
|
||||
current = check.get("current", None)
|
||||
include_prerelease = check.get("prerelease", False)
|
||||
force_base = check.get("force_base", True)
|
||||
|
||||
remote_name, remote_tag = _get_latest_release(check["user"], check["repo"], include_prerelease=check["prerelease"] == True if "prerelease" in check else False)
|
||||
remote_name, remote_tag = _get_latest_release(check["user"],
|
||||
check["repo"],
|
||||
include_prerelease=include_prerelease)
|
||||
compare_type = check["release_compare"] if "release_compare" in check else "python"
|
||||
|
||||
information =dict(
|
||||
|
|
@ -96,4 +134,7 @@ def get_latest(target, check, custom_compare=None):
|
|||
|
||||
logger.debug("Target: %s, local: %s, remote: %s" % (target, current, remote_tag))
|
||||
|
||||
return information, _is_current(information, compare_type, custom=custom_compare)
|
||||
return information, _is_current(information,
|
||||
compare_type,
|
||||
custom=custom_compare,
|
||||
force_base=force_base)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
|
|||
file_wrapper.filename,
|
||||
file_wrapper,
|
||||
allow_overwrite=True)
|
||||
if os.path.exists(path):
|
||||
try:
|
||||
os.remove(path)
|
||||
except:
|
||||
self._logger.exception("Error while trying to clear a file from the watched folder")
|
||||
|
||||
def on_created(self, event):
|
||||
self._upload(event.src_path)
|
||||
|
|
|
|||
|
|
@ -352,9 +352,7 @@ def silent_remove(file):
|
|||
def sanitize_ascii(line):
|
||||
if not isinstance(line, basestring):
|
||||
raise ValueError("Expected either str or unicode but got {} instead".format(line.__class__.__name__ if line is not None else None))
|
||||
if isinstance(line, str):
|
||||
line = unicode(line, 'ascii', 'replace')
|
||||
return line.encode('ascii', 'replace').rstrip()
|
||||
return to_unicode(line, encoding="ascii", errors="replace").rstrip()
|
||||
|
||||
|
||||
def filter_non_ascii(line):
|
||||
|
|
@ -369,12 +367,28 @@ def filter_non_ascii(line):
|
|||
"""
|
||||
|
||||
try:
|
||||
unicode(line, 'ascii').encode('ascii')
|
||||
to_str(to_unicode(line, encoding="ascii"), encoding="ascii")
|
||||
return False
|
||||
except ValueError:
|
||||
return True
|
||||
|
||||
|
||||
def to_str(s_or_u, encoding="utf-8", errors="strict"):
|
||||
"""Make sure ``s_or_u`` is a str."""
|
||||
if isinstance(s_or_u, unicode):
|
||||
return s_or_u.encode(encoding, errors=errors)
|
||||
else:
|
||||
return s_or_u
|
||||
|
||||
|
||||
def to_unicode(s_or_u, encoding="utf-8", errors="strict"):
|
||||
"""Make sure ``s_or_u`` is a unicode string."""
|
||||
if isinstance(s_or_u, str):
|
||||
return s_or_u.decode(encoding, errors=errors)
|
||||
else:
|
||||
return s_or_u
|
||||
|
||||
|
||||
def dict_merge(a, b):
|
||||
"""
|
||||
Recursively deep-merges two dictionaries.
|
||||
|
|
|
|||
Loading…
Reference in a new issue