diff --git a/CHANGELOG.md b/CHANGELOG.md index ee44ce21..f9a2aa0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,16 +41,27 @@ * The "Slicing done" notification is now colored green ([#558](https://github.com/foosel/OctoPrint/issues/558)). * File management now supports STL files as first class citizens (including UI adjustments to allow management of uploaded STL files including removal and reslicing) and also allows folders (not yet supported by UI) +* Also interpret lines starting with "!!" as errors +* Added deletion of pyc files to the `python setup.py clean` command ### Bug Fixes * [#435](https://github.com/foosel/OctoPrint/issues/435) - Always interpret negative duration (e.g. for print time left) as 0 +* [#633](https://github.com/foosel/OctoPrint/issues/633) - Correctly interpret temperature lines from multi extruder + setups under Smoothieware * Various fixes of bugs in newly introduced features and improvements: * [#625](https://github.com/foosel/OctoPrint/pull/625) - Newly added GCODE files were not being added to the analysis queue -## 1.1.1 (Unreleased) +## 1.1.2 (Unreleased) + +### Bug Fixes + +* [#634](https://github.com/foosel/OctoPrint/pull/634) - Fixed missing `branch` fields in version dicts generated + by versioneer + +## 1.1.1 (2014-10-27) ### Improvements @@ -58,6 +69,7 @@ server start and written back into ``config.yaml`` * Event subscriptions are now enabled by default (it was an accident that they weren't) * Generate the key used for session hashing individually for each server instance +* Generate the salt used for hashing user passwords individually for each server instance ### Bug Fixes diff --git a/README.md b/README.md index f219d122..ac80de2e 100644 --- a/README.md +++ b/README.md @@ -29,43 +29,45 @@ OctoPrint via `setup.py`: python setup.py install -You should also do this after pulling from the repository, since the dependencies might have changed. +You should also do this every time after pulling from the repository, since the dependencies might have changed. OctoPrint currently only supports Python 2.7. Usage ----- -From the source directory you can start the server via +Running the `setup.py` script installs the `octoprint` script in your Python installation's scripts folder +(which depending on whether you installed OctoPrint globally or into a virtual env will be on your `PATH` or not). The +following usage examples assume that said `octoprint` script is on your `PATH`. - ./run +You can start the server via + + octoprint By default it binds to all interfaces on port 5000 (so pointing your browser to `http://127.0.0.1:5000` will do the trick). If you want to change that, use the additional command line parameters `host` and `port`, which accept the host ip to bind to and the numeric port number respectively. If for example you want the server to only listen on the local interface on port 8080, the command line would be - ./run --host=127.0.0.1 --port=8080 + octoprint --host=127.0.0.1 --port=8080 Alternatively, the host and port on which to bind can be defined via the configuration. If you want to run OctoPrint as a daemon (only supported on Linux), use - ./run --daemon {start|stop|restart} [--pid PIDFILE] + octoprint --daemon {start|stop|restart} [--pid PIDFILE] If you do not supply a custom pidfile location via `--pid PIDFILE`, it will be created at `/tmp/octoprint.pid`. You can also specify the configfile or the base directory (for basing off the `uploads`, `timelapse` and `logs` folders), e.g.: - ./run --config /path/to/another/config.yaml --basedir /path/to/my/basedir + octoprint --config /path/to/another/config.yaml --basedir /path/to/my/basedir -See `run --help` for further information. - -Running the `setup.py` script also installs the `octoprint` startup script in your Python installation's scripts folder -(which depending on whether you installed OctoPrint globally or into a virtual env will be on your `PATH` or not). The -examples above also work with that startup script as it excepts the same parameters as `run`. +See `octoprint --help` for further information. +OctoPrint also ships with a `run` script in its source directory. You can also invoke that to start up the server, it +takes the same command line arguments as the `octoprint` script. Configuration ------------- @@ -75,4 +77,5 @@ which is located at `~/.octoprint` on Linux, at `%APPDATA%/OctoPrint` on Windows at `~/Library/Application Support/OctoPrint` on MacOS. A comprehensive overview of all available configuration settings can be found -[on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration). +[on the wiki](https://github.com/foosel/OctoPrint/wiki/Configuration). Please note that the most commonly used +configuration settings can also easily be edited from OctoPrint's settings dialog. diff --git a/docs/events/index.rst b/docs/events/index.rst index 9a1d5310..f0a55169 100644 --- a/docs/events/index.rst +++ b/docs/events/index.rst @@ -328,6 +328,7 @@ SlicingStarted * ``stl``: the STL's filename * ``gcode``: the sliced GCODE's filename + * ``progressAvailable``: true if progress information via the ``slicingProgress`` push update will be available, false if not SlicingDone The slicing of a file has completed. diff --git a/setup.py b/setup.py index d6b6cb4b..26876883 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,26 @@ def package_data_dirs(source, sub_folders): return dirs +def _recursively_handle_files(directory, file_matcher, folder_handler=None, file_handler=None): + applied_handler = False + + for filename in os.listdir(directory): + path = os.path.join(directory, filename) + + if file_handler is not None and file_matcher(filename): + file_handler(path) + applied_handler = True + + elif os.path.isdir(path): + sub_applied_handler = _recursively_handle_files(path, file_matcher, folder_handler=folder_handler, file_handler=file_handler) + if sub_applied_handler: + applied_handler = True + + if folder_handler is not None: + folder_handler(path, sub_applied_handler) + + return applied_handler + class CleanCommand(Command): description = "clean build artifacts" user_options = [] @@ -45,14 +65,37 @@ class CleanCommand(Command): pass def run(self): + # build folder if os.path.exists('build'): print "Deleting build directory" shutil.rmtree('build') + + # eggs eggs = glob.glob('OctoPrint*.egg-info') for egg in eggs: print "Deleting %s directory" % egg shutil.rmtree(egg) + # pyc files + def delete_folder_if_empty(path, applied_handler): + if not applied_handler: + return + if len(os.listdir(path)) == 0: + shutil.rmtree(path) + print "Deleted %s since it was empty" % path + + def delete_file(path): + os.remove(path) + print "Deleted %s" % path + + import fnmatch + _recursively_handle_files( + os.path.abspath("src"), + lambda name: fnmatch.fnmatch(name.lower(), "*.pyc"), + folder_handler=delete_folder_if_empty, + file_handler=delete_file + ) + class NewTranslation(Command): description = "create a new translation" diff --git a/src/octoprint/_version.py b/src/octoprint/_version.py index 3808a4f8..35090f5b 100644 --- a/src/octoprint/_version.py +++ b/src/octoprint/_version.py @@ -214,7 +214,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose=False): print("guessing rootdir is '%s', but '%s' doesn't start with prefix '%s'" % (root, dirname, parentdir_prefix)) return None - return {"version": dirname[len(parentdir_prefix):], "full": ""} + return {"version": dirname[len(parentdir_prefix):], "full": "", "branch": ""} tag_prefix = "" parentdir_prefix = "" @@ -249,7 +249,7 @@ def parse_lookup_file(root, lookup_path=None): break return lookup -def get_versions(default={"version": "unknown", "full": ""}, lookup_path=None, verbose=False): +def get_versions(default={"version": "unknown", "full": "", "branch": "unknown"}, lookup_path=None, verbose=False): # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which diff --git a/src/octoprint/filemanager/__init__.py b/src/octoprint/filemanager/__init__.py index ce0c98b0..956f7cda 100644 --- a/src/octoprint/filemanager/__init__.py +++ b/src/octoprint/filemanager/__init__.py @@ -200,9 +200,11 @@ class FileManager(object): if dest_job_key in self._slicing_jobs: del self._slicing_jobs[dest_job_key] + slicer = self._slicing_manager.get_slicer(slicer_name) + import time start_time = time.time() - eventManager().fire(Events.SLICING_STARTED, {"stl": source_path, "gcode": dest_path}) + eventManager().fire(Events.SLICING_STARTED, {"stl": source_path, "gcode": dest_path, "progressAvailable": slicer.get_slicer_properties()["progress_report"] if slicer else False}) import tempfile f = tempfile.NamedTemporaryFile(suffix=".gco", delete=False) @@ -266,10 +268,11 @@ class FileManager(object): def add_file(self, destination, path, file_object, links=None, allow_overwrite=False): file_path = self._storage(destination).add_file(path, file_object, links=links, allow_overwrite=allow_overwrite) absolute_path = self._storage(destination).get_absolute_path(file_path) - file_type = get_file_type(file_path)[-1] - queue_entry = QueueEntry(file_path, file_type, destination, absolute_path) - self._analysis_queue.enqueue(queue_entry, high_priority=True) + file_type = get_file_type(absolute_path) + if file_type: + queue_entry = QueueEntry(file_path, file_type[-1], destination, absolute_path) + self._analysis_queue.enqueue(queue_entry, high_priority=True) eventManager().fire(Events.UPDATED_FILES, dict(type="printables")) return file_path diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 12e41dab..28747317 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -199,6 +199,7 @@ class SlicerPlugin(Plugin): type=None, name=None, same_device=True, + progress_report=False ) def get_slicer_profile_options(self): diff --git a/src/octoprint/plugins/cura/__init__.py b/src/octoprint/plugins/cura/__init__.py index 9e963919..9cebe18e 100644 --- a/src/octoprint/plugins/cura/__init__.py +++ b/src/octoprint/plugins/cura/__init__.py @@ -194,7 +194,8 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin, return dict( type="cura", name="CuraEngine", - same_device=True + same_device=True, + progress_report=True ) def get_slicer_default_profile(self): diff --git a/src/octoprint/plugins/cura/profile.py b/src/octoprint/plugins/cura/profile.py index 60ff045f..04b42c53 100644 --- a/src/octoprint/plugins/cura/profile.py +++ b/src/octoprint/plugins/cura/profile.py @@ -457,55 +457,70 @@ class Profile(object): @classmethod def merge_profile(cls, profile, overrides=None): - import copy - - result = copy.deepcopy(defaults) - for k in result.keys(): - profile_value = None - override_value = None - - if k in profile: - profile_value = profile[k] - if overrides and k in overrides: - override_value = overrides[k] - - if profile_value is None and override_value is None: - # neither override nor profile, no need to handle this key further - continue - - if k in ("filament_diameter", "print_temperature", "start_gcode", "end_gcode"): - # the array fields need some special treatment. Basically something like this: - # - # override_value: [None, "b"] - # profile_value : ["a" , None, "c"] - # default_value : ["d" , "e" , "f", "g"] - # - # should merge to something like this: - # - # ["a" , "b" , "c", "g"] - # - # So override > profile > default, if neither override nor profile value are available - # the default value should just be left as is - - for x in xrange(len(result[k])): - if override_value is not None and x < len(override_value) and override_value[x] is not None: - # we have an override value for this location, so we use it - result[k][x] = override_value[x] - elif profile_value is not None and x < len(profile_value) and profile_value[x] is not None: - # we have a profile value for this location, so we use it - result[k][x] = profile_value[x] - - else: - # just change the result value to the override_value if available, otherwise to the profile_value if - # that is given, else just leave as is - if override_value is not None: - result[k] = override_value - elif profile_value is not None: - result[k] = profile_value + result = dict() + for key in defaults.keys(): + r = cls.merge_profile_key(key, profile, overrides=overrides) + if r is not None: + result[key] = r return result - def __init__(self, profile): - self.profile = profile + @classmethod + def merge_profile_key(cls, key, profile, overrides=None): + profile_value = None + override_value = None + + if not key in defaults: + return None + import copy + result = copy.deepcopy(defaults[key]) + + if key in profile: + profile_value = profile[key] + if overrides and key in overrides: + override_value = overrides[key] + + if profile_value is None and override_value is None: + # neither override nor profile, no need to handle this key further + return None + + if key in ("filament_diameter", "print_temperature", "start_gcode", "end_gcode"): + # the array fields need some special treatment. Basically something like this: + # + # override_value: [None, "b"] + # profile_value : ["a" , None, "c"] + # default_value : ["d" , "e" , "f", "g"] + # + # should merge to something like this: + # + # ["a" , "b" , "c", "g"] + # + # So override > profile > default, if neither override nor profile value are available + # the default value should just be left as is + + for x in xrange(len(result)): + if override_value is not None and x < len(override_value) and override_value[x] is not None: + # we have an override value for this location, so we use it + result[x] = override_value[x] + elif profile_value is not None and x < len(profile_value) and profile_value[x] is not None: + # we have a profile value for this location, so we use it + result[x] = profile_value[x] + + else: + # just change the result value to the override_value if available, otherwise to the profile_value if + # that is given, else just leave as is + if override_value is not None: + result = override_value + elif profile_value is not None: + result = profile_value + + return result + + def __init__(self, profile, overrides=None): + self._profile = self.__class__.merge_profile(profile, overrides=overrides) + + def profile(self): + import copy + return copy.deepcopy(self._profile) def get(self, key): if key in ("machine_width", "machine_depth", "machine_center_is_zero"): @@ -547,7 +562,7 @@ class Profile(object): if not match: return 0.0 - diameters = defaults["filament_diameter"] + diameters = self._get("filament_diameter") if not match.group(1): return diameters[0] index = int(match.group(1)) @@ -560,7 +575,7 @@ class Profile(object): if not match: return 0.0 - temperatures = defaults["print_temperature"] + temperatures = self._get("print_temperature") if not match.group(1): return temperatures[0] index = int(match.group(1)) @@ -569,12 +584,15 @@ class Profile(object): return temperatures[index] else: - if key in self.profile: - return self.profile[key] - elif key in defaults: - return defaults[key] - else: - return None + return self._get(key) + + def _get(self, key): + if key in self._profile: + return self._profile[key] + elif key in defaults: + return defaults[key] + else: + return None def get_int(self, key, default=None): value = self.get(key) @@ -622,8 +640,8 @@ class Profile(object): def get_gcode_template(self, key): extruder_count = s.globalGetInt(["printerParameters", "numExtruders"]) - if key in self.profile: - gcode = self.profile[key] + if key in self._profile: + gcode = self._profile[key] else: gcode = defaults[key] @@ -646,7 +664,7 @@ class Profile(object): import copy profile = copy.deepcopy(defaults) - profile.update(self.profile) + profile.update(self._profile) for key in ("print_temperature", "print_temperature2", "print_temperature3", "print_temperature4", "filament_diameter", "filament_diameter2", "filament_diameter3", "filament_diameter4"): profile[key] = self.get(key) diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index efc56ecf..754a682c 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -413,6 +413,9 @@ class Server(): "level": "CRITICAL", "handlers": ["serialFile"], "propagate": False + }, + "tornado.application": { + "level": "ERROR" } }, "root": { diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 603b4cd2..0f339b7a 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -249,7 +249,7 @@ def login(): user = octoprint.server.userManager.findUser(username) if user is not None: - if user.check_password(octoprint.users.UserManager.createPasswordHash(password)): + if octoprint.server.userManager.checkPassword(username, password): if octoprint.server.userManager is not None: user = octoprint.server.userManager.login_user(user) session["usersession.id"] = user.get_session() diff --git a/src/octoprint/server/util/tornado.py b/src/octoprint/server/util/tornado.py index b88fe27e..cecc8f69 100644 --- a/src/octoprint/server/util/tornado.py +++ b/src/octoprint/server/util/tornado.py @@ -24,6 +24,7 @@ import tornado.httpclient import tornado.http1connection import tornado.iostream import tornado.tcpserver +import tornado.util import octoprint.util @@ -707,8 +708,6 @@ class CustomHTTP1ConnectionParameters(tornado.http1connection.HTTP1ConnectionPar class LargeResponseHandler(tornado.web.StaticFileHandler): - CHUNK_SIZE = 16 * 1024 - def initialize(self, path, default_filename=None, as_attachment=False, access_validation=None): tornado.web.StaticFileHandler.initialize(self, os.path.abspath(path), default_filename) self._as_attachment = as_attachment @@ -717,70 +716,18 @@ class LargeResponseHandler(tornado.web.StaticFileHandler): def get(self, path, include_body=True): if self._access_validation is not None: self._access_validation(self.request) - - path = self.parse_url_path(path) - abspath = os.path.abspath(os.path.join(self.root, path)) - # os.path.abspath strips a trailing / - # it needs to be temporarily added back for requests to root/ - if not (abspath + os.path.sep).startswith(self.root): - raise tornado.web.HTTPError(403, "%s is not in root static directory", path) - if os.path.isdir(abspath) and self.default_filename is not None: - # need to look at the request.path here for when path is empty - # but there is some prefix to the path that was already - # trimmed by the routing - if not self.request.path.endswith("/"): - self.redirect(self.request.path + "/") - return - abspath = os.path.join(abspath, self.default_filename) - if not os.path.exists(abspath): - raise tornado.web.HTTPError(404) - if not os.path.isfile(abspath): - raise tornado.web.HTTPError(403, "%s is not a file", path) - - stat_result = os.stat(abspath) - modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME]) - - self.set_header("Last-Modified", modified) - - mime_type, encoding = mimetypes.guess_type(abspath) - if mime_type: - self.set_header("Content-Type", mime_type) - - cache_time = self.get_cache_time(path, modified, mime_type) - - if cache_time > 0: - self.set_header("Expires", datetime.datetime.utcnow() + - datetime.timedelta(seconds=cache_time)) - self.set_header("Cache-Control", "max-age=" + str(cache_time)) - - self.set_extra_headers(path) - - # Check the If-Modified-Since, and don't send the result if the - # content has not been modified - ims_value = self.request.headers.get("If-Modified-Since") - if ims_value is not None: - date_tuple = email.utils.parsedate(ims_value) - if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple)) - if if_since >= modified: - self.set_status(304) - return - - if not include_body: - assert self.request.method == "HEAD" - self.set_header("Content-Length", stat_result[stat.ST_SIZE]) - else: - with open(abspath, "rb") as file: - while True: - data = file.read(LargeResponseHandler.CHUNK_SIZE) - if not data: - break - self.write(data) - self.flush() + result = tornado.web.StaticFileHandler.get(self, path, include_body=include_body) + return result def set_extra_headers(self, path): if self._as_attachment: self.set_header("Content-Disposition", "attachment") + @classmethod + def get_content_version(cls, abspath): + import os + import stat + return os.stat(abspath)[stat.ST_MTIME] ##~~ URL Forward Handler for forwarding requests to a preconfigured static URL diff --git a/src/octoprint/settings.py b/src/octoprint/settings.py index 6cc16d25..5691d502 100644 --- a/src/octoprint/settings.py +++ b/src/octoprint/settings.py @@ -131,6 +131,7 @@ default_settings = { }, "accessControl": { "enabled": True, + "salt": None, "userManager": "octoprint.users.FilebasedUserManager", "userfile": None, "autologinLocal": False, @@ -172,6 +173,7 @@ default_settings = { "includeCurrentToolInTemps": True, "hasBed": True, "repetierStyleTargetTemperature": False, + "smoothieTemperatureReporting": False, "extendedSdFileList": False } } diff --git a/src/octoprint/slicing/__init__.py b/src/octoprint/slicing/__init__.py index 0cd4ae17..fa44a8b7 100644 --- a/src/octoprint/slicing/__init__.py +++ b/src/octoprint/slicing/__init__.py @@ -73,22 +73,26 @@ class SlicingManager(object): @property def slicing_enabled(self): - return len(self.registered_slicers) > 0 + return len(self.configured_slicers) > 0 @property def registered_slicers(self): return self._slicers.keys() + @property + def configured_slicers(self): + return map(lambda slicer: slicer.get_slicer_properties()["type"], filter(lambda slicer: slicer.is_slicer_configured(), self._slicers.values())) + @property def default_slicer(self): slicer_name = settings().get(["slicing", "defaultSlicer"]) - if slicer_name in self.registered_slicers: + if slicer_name in self.configured_slicers: return slicer_name else: return None - def get_slicer(self, slicer): - return self._slicers[slicer] if slicer in self._slicers else None + def get_slicer(self, slicer, require_configured=True): + return self._slicers[slicer] if slicer in self._slicers and (not require_configured or self._slicers[slicer].is_slicer_configured()) else None def slice(self, slicer_name, source_path, dest_path, profile_name, callback, callback_args=None, callback_kwargs=None, overrides=None, on_progress=None, on_progress_args=None, on_progress_kwargs=None): if callback_args is None: @@ -96,8 +100,11 @@ class SlicingManager(object): if callback_kwargs is None: callback_kwargs = dict() - if not slicer_name in self.registered_slicers: - error = "No such slicer: {slicer_name}".format(**locals()) + if not slicer_name in self.configured_slicers: + if not slicer_name in self.registered_slicers: + error = "No such slicer: {slicer_name}".format(**locals()) + else: + error = "Slicer not configured: {slicer_name}".format(**locals()) callback_kwargs.update(dict(_error=error)) callback(*callback_args, **callback_kwargs) return False, error diff --git a/src/octoprint/static/js/app/dataupdater.js b/src/octoprint/static/js/app/dataupdater.js index f134fe86..9a709017 100644 --- a/src/octoprint/static/js/app/dataupdater.js +++ b/src/octoprint/static/js/app/dataupdater.js @@ -181,7 +181,11 @@ function DataUpdater(allViewModels) { } else if (type == "SlicingStarted") { gcodeUploadProgress.addClass("progress-striped").addClass("active"); gcodeUploadProgressBar.css("width", "100%"); - gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0})); + if (payload.progressAvailable) { + gcodeUploadProgressBar.text(_.sprintf(gettext("Slicing ... (%(percentage)d%%)"), {percentage: 0})); + } else { + gcodeUploadProgressBar.text(gettext("Slicing ...")); + } } else if (type == "SlicingDone") { gcodeUploadProgress.removeClass("progress-striped").removeClass("active"); gcodeUploadProgressBar.css("width", "0%"); diff --git a/src/octoprint/templates/index.jinja2 b/src/octoprint/templates/index.jinja2 index dfe555a6..d43bb00f 100644 --- a/src/octoprint/templates/index.jinja2 +++ b/src/octoprint/templates/index.jinja2 @@ -100,7 +100,7 @@