Merge remote-tracking branch 'origin/maintenance' into maintenance

This commit is contained in:
Gina Häußge 2016-01-19 11:51:15 +01:00
commit 660de61997
67 changed files with 3629 additions and 1268 deletions

View file

@ -10,13 +10,14 @@
# master shall not use the lookup table, only tags
master
# maintenance is currently the branch for preparation of maintenance release 1.2.8
# so are any fix/... branches
maintenance 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev
fix/.* 1.2.8 6c622f7c4332b71c6ece59552ffc87c146155c84 pep440-dev
# Special case disconnected checkouts, e.g. 'git checkout <tag>'
# neither should disconnected checkouts, e.g. 'git checkout <tag>'
HEAD
\(detached.*
# maintenance is currently the branch for preparation of maintenance release 1.2.9
# so are any fix/... branches
maintenance 1.2.9 dedadbc9ac0305799e94ae279d3bca131629c4c5 pep440-dev
fix/.* 1.2.9 dedadbc9ac0305799e94ae279d3bca131629c4c5 pep440-dev
# every other branch is a development branch and thus gets resolved to 1.3.0-dev for now
.* 1.3.0 198d3450d94be1a2 pep440-dev

View file

@ -53,7 +53,9 @@ date of first contribution):
* [Andrew Erickson](https://github.com/aerickson)
* [Nicanor Romero Venier](https://github.com/nicanor-romero)
* [Thomas Hou](https://github.com/masterhou)
* [Mark Bastiaans](https://github.com/markbastiaans)
* [Kevin Murphy](https://github.com/kevingelion)
* [Richard Joyce](https://github.com/richjoyce)
OctoPrint started off as a fork of [Cura](https://github.com/daid/Cura) by
[Daid Braam](https://github.com/daid). Parts of its communication layer and

View file

@ -1,5 +1,128 @@
# OctoPrint Changelog
## 1.2.8 (2015-12-07)
### Notes for Upgraders
#### A bug in 1.2.7 prevents directly updating to 1.2.8, here's what to do
A bug in OctoPrint 1.2.7 (fixed in 1.2.8) prevents updating OctoPrint to version
1.2.8. If you try to perform the update, you will simply be told that "the update
was successful", but the update won't actually have taken place. To solve this
hen-egg-problem, a plugin has been made available that fixes said bug (through
monkey patching).
The plugin is called "Updatefix 1.2.7" and can be found
[in the plugin repository](http://plugins.octoprint.org/plugins/updatefix127/)
and [on Github](https://github.com/OctoPrint/OctoPrint-Updatefix-1.2.7/).
Before attempting to update your installation from version 1.2.7 to version 1.2.8,
please install the plugin via your plugin manager and restart your server. Note that
you will only see it in the Plugin Manager if you need it, since it's only compatible with
OctoPrint version 1.2.7. After you installed the plugin and restarted your server
you can update as usual. The plugin will self-uninstall once it detects that it's
running under OctoPrint 1.2.8. After the self-uninstall another restart of your server
will be triggered (if you have setup your server's restart command, defaults to
`sudo service octoprint restart` on OctoPi) in order to really get rid of any
left-overs, so don't be alarmed when that happens, it is intentional.
**If you cannot or don't want to use the plugin**, alternatively you can switch
OctoPrint to "Commit" based tracking via the settings of the Software Update plugin,
update, then switch back to "Release" based tracking (see [this screenshot](https://i.imgur.com/wvkgiGJ.png)).
#### Bed temperatures are now only displayed if printer profile has a heated bed configured
This release fixes a [bug](https://github.com/foosel/OctoPrint/issues/1125)
that caused bed temperature display and controls to be available even if the
selected printer profile didn't have a heated bed configured.
If your printer does have a heated bed but you are not seeing its temperature
in the "Temperature" tab after updating to 1.2.8, please make sure to check
the "Heated Bed" option in your printer profile (under Settings > Printer Profiles)
as shown [in this short GIF](http://i.imgur.com/wp1j9bs.gif).
### Improvements
* Version numbering now follows [PEP440](https://www.python.org/dev/peps/pep-0440/).
* Prepared some things for publishing OctoPrint on [PyPi](https://pypi.python.org/pypi)
in the future.
* [BlueprintPlugin mixin](http://docs.octoprint.org/en/master/plugins/mixins.html#blueprintplugin)
now has an `errorhandler` decorator that serves the same purpose as
[Flask's](http://flask.pocoo.org/docs/0.10/patterns/errorpages/#error-handlers)
([#1059](https://github.com/foosel/OctoPrint/pull/1059))
* Interpret `M25` in a GCODE file that is being streamed from OctoPrint as
indication to pause, like `M0` and `M1`.
* Cache rendered page and translation files indefinitely. That should
significantly improve performance on reloads of the web interface.
* Added the string "unknown command" to the list of ignored printer errors.
This should help with general firmware compatibility in case a firmware
lacks features.
* Added the strings "cannot open" and "cannot enter" to the list of ignored
printer errors. Those are errors that Marlin may report if there is an issue
with the printer's SD card.
* The "CuraEngine" plugin now makes it more obvious that it only targets
CuraEngine versions up to and including 15.04 and also links to the plugin's
homepage with more information right within the settings dialog.
* Browser tab visibility is now tracked by the web interface, disabling the
webcam and the GCODE viewer if the tab containing OctoPrint is not active.
That should reduce the amount of resource utilized by the web interface on
the client when it is not actively monitored. Might also help to mitigate
[#1065](https://github.com/foosel/OctoPrint/issues/1065), the final verdict
on that one is still out though.
* The printer log in the terminal tab will now be cut off after 3000 lines
even if autoscroll is disabled. If the limit is reached, no more log lines
will be added to the client's buffer. That ensures that the log will not
scroll and the current log excerpt will stay put while also not causing
the browser to run into memory errors due to trying to buffer an endless
amount of log lines.
* Increased timeout of "waiting for restart" after an update from 20 to 60sec
(20sec turned out to be too little for OctoPi for whatever reason).
* Added a couple of unit tests
### Bug Fixes
* [#1120](https://github.com/foosel/OctoPrint/issues/1120) - Made the watchdog
that monitors and handles the `watched` folder more resilient towards errors.
* [#1125](https://github.com/foosel/OctoPrint/issues/1125) - Fixed OctoPrint
displaying bed temperature and controls and allowing the sending of GCODE
commands targeting the bed (`M140`, `M190`) if the printer profile doesn't
have a heated bed configured.
* Fixed an issue that stopped the software updater working for OctoPrint. The
updater reports success updating, but no update has actually taken place. A
fix can be applied for this issue to OctoPrint version 1.2.7 via
[the Updatefix 1.2.7 plugin](https://github.com/OctoPrint/OctoPrint-Updatefix-1.2.7).
For more information please refer to the [Important information for people updating from version 1.2.7](#important-information-for-people-updating-from-version-127)
above.
* Fix: Current filename in job data should never be prefixed with `/`
* Only persist plugin settings that differ from the defaults. This way the
`config.yaml` won't be filled with lots of redundant data. It's the
responsibility of the plugin authors to responsibly handle changes in default
settings of their plugins and add data migration where necessary.
* Fixed a documentation bug ([#1067](https://github.com/foosel/OctoPrint/pull/1067))
* Fixed a conflict with bootstrap-responsive, e.g. when using the
[ScreenSquish Plugin](http://plugins.octoprint.org/plugins/screensquish/)
([#1103](https://github.com/foosel/OctoPrint/pull/1067))
* Fixed OctoPrint still sending SD card related commands to the printer even
if SD card support is disabled (e.g. `M21`).
* Hidden files are no longer visible to the template engine, neither as (GCODE)
scripts nor as interface templates.
* The hostname and URL prefix via which the OctoPrint web interface is accessed
is now part of the cache key. Without that being the case the cache could
be created referring to something like `/octoprint/prefix/api/` for its API
endpoint (if accessed via `http://somehost:someport/octoprint/prefix/` first
time), which would then cause the interface to not work if accessed later
via another route (e.g. `http://someotherhost/`).
* Fixed a JavaScript error on finishing streaming of a file to SD.
* Fixed version reporting on detached HEADs (when the branch detection
reported "HEAD" instead of "(detached"
* Fixed some path checks for systems with symlinked paths
([#1051](https://github.com/foosel/OctoPrint/pull/1051))
* Fixed a bug causing the "Server Offline" overlay to pop _under_ the
"Please reload" overlay, which could lead to "Connection refused" browser
messages when clicking "Reload now" in the wrong moment.
([Commits](https://github.com/foosel/OctoPrint/compare/1.2.7...1.2.8))
## 1.2.7 (2015-10-20)
### Improvements
@ -386,7 +509,7 @@
changed under "Temperatures" in the Settings ([#343](https://github.com/foosel/OctoPrint/issues/343)).
* High-DPI support for the GCode viewer ([#837](https://github.com/foosel/OctoPrint/issues/837)).
* Stop websocket connections from multiplying ([#888](https://github.com/foosel/OctoPrint/pull/888)).
* New setting to rotate webcam by 90° counter clockwise ([#895](https://github.com/foosel/OctoPrint/issues/895) and
* New setting to rotate webcam by 90° counter clockwise ([#895](https://github.com/foosel/OctoPrint/issues/895) and
[#906](https://github.com/foosel/OctoPrint/pull/906))
* System commands now be set to a) run asynchronized by setting their `async` property to `true` and b) to ignore their
result by setting their `ignore` property to `true`.

View file

@ -1,51 +1,264 @@
Issues, Tickets, however you may call them
------------------------------------------
# Contribution Guidelines
Read the following short instructions **fully** and **follow them** if you want your ticket to be taken care of and not closed again directly! They are linked on top of every new issue form, so don't say nobody warned you afterwards.
This document outlines what you need to know before **[creating tickets](#issues-tickets-however-you-may-call-them)**
or **[creating pull requests](#pull-requests)**.
- **Read the [FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)**
- Always create **one ticket for one purpose**. So don't mix two or more feature requests, support requests, bugs etc into one ticket. If you do, your ticket will be closed!
- If you want to report a bug, **READ AND FOLLOW [How to file a bug report](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)!** Tickets will be automatically checked if they comply with the requirements outlined in that wiki node! Other then what's written in there (**and really EVERYTHING that is written in there!**) you don't have to do anything special with your ticket. Listen to what GitIssueBot might have to say to you!
- If you want to post a **request** of any kind (feature request, documentation request, ...), **add [Request] to your issue's title!**
- If you need **support** with a problem of your installation (e.g. if you have problems getting the webcam to work) or have a general **question**, the issue tracker is not the right place. Consult the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741) instead!
- If you are a developer that wants to brainstorm a pull request or possible changes to the plugin system, **add [Brainstorming] to your issue's title!** (see below).
- If you have another reason for creating a ticket that doesn't fit any of the above categories, it's something better suited for the [Mailinglist](https://groups.google.com/group/octoprint) or the [Google+ Community](https://plus.google.com/communities/102771308349328485741).
## Contents
Following these guidelines (**especially EVERYTHING mentioned in ["How to file a bug report"](https://github.com/foosel/OctoPrint/wiki/How-to-file-a-bug-report)**) is necessary so the tickets stay manageable - you are not the only one with an open issue, so please respect that you have to **play by the rules** so that your problem can be taken care of. Tickets not playing by the rules **will be closed without further investigation!**.
* [Issues, Tickets, however you may call them](#issues-tickets-however-you-may-call-them)
* [How to file a bug report](#how-to-file-a-bug-report)
* [What should I do before submitting a bug report?](#what-should-i-do-before-submitting-a-bug-report)
* [What should I include in a bug report?](#what-should-i-include-in-a-bug-report)
* [Where can I find which version and branch I'm on?](#where-can-i-find-which-version-and-branch-im-on)
* [Where can I find those log files you keep talking about?](#where-can-i-find-those-log-files-you-keep-talking-about)
* [Where can I find my browser's error console?](#where-can-i-find-my-browsers-error-console)
* [Pull requests](#pull-requests)
* [History](#history)
* [Footnotes](#footnotes)
Pull Requests
-------------
## Issues, Tickets, however you may call them
1. If you want to add a new feature to OctoPrint, **please always first consider if it wouldn't be better suited for a
plugin.** As a general rule of thumb, any feature that is only of interest to a small sub group should be moved into a
plugin. If the current plugin system doesn't allow you to implement your feature as a plugin, create a "Brainstorming"
ticket to get the discussion going how best to solve *this* in OctoPrint's plugin system - maybe that's the actual PR
you have been waiting for to contribute :)
2. If you plan to make **any large changes to the code or appearance, please open a "Brainstorming" ticket first** so that
we can determine if it's a good time for your specific pull request. It might be that I'm currently in the process of
making heavy changes to the code locations you'd target as well, or your approach doesn't fit the general "project
vision", and that would just cause unnecessary work and frustration for everyone or possibly get the PR rejected.
3. When adding code to OctoPrint, make sure you **follow the current coding style**. That means tabs instead of spaces in the
python files (yes, I know that this goes against PEP-8, I don't care) and space instead of tabs in the Javascript sources,
english language (that means code, variables, comments!), comments where necessary (tell why the code does something like
it does it, structure your code), following the general architecture. If your PR needs to make changes to the Stylesheets,
change the ``.less`` files from which the CSS is compiled. PRs that contain direct changes to the compiled
CSS will be closed.
4. **Test your changes thoroughly**. That also means testing with usage scenarios you don't normally use, e.g. if you only
use access control, test without and vice versa. If you only test with your printer, test with the virtual printer and
vice versa. State in your pull request how your tested your changes.
5. Please create all pull requests **against the `devel` branch**.
6. Create **one pull request per feature/bug fix**.
7. Create a **custom branch** for your feature/bug fix and use that as base for your pull request. Pull requests directly
against your version of `devel` will be closed.
8. In your pull request's description, **state what your pull request is doing**, as in, what feature does it implement, what
bug does it fix. The more thoroughly you explain your intent behind the PR here, the higher the chances it will get merged
fast.
9. Don't forget to **add yourself to the [AUTHORS](../AUTHORS.md) file** :)
Please read the following short instructions fully and follow them. You can
help the project tremendously this way: not only do you help the maintainers
to **address problems in a timely manner** but also keep it possible for them
to **fix bugs, add new and improve on existing functionality** instead of doing
nothing but ticket management.
History
-------
![Ticket flow chart](http://i.imgur.com/qYSZyuw.png)
* 2015-01-23: More guidelines for creating pull requests, support/questions redirected to Mailinglist/G+ community
- **[Read the FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)**
- If you want to report a **bug**, [read "How to file a bug report" below](#how-to-file-a-bug-report)
and *[use the provided template](#what-should-i-include-in-a-ticket)*.
You do not need to do anything else with your ticket.
- If you want to post a **request** of any kind (feature request, documentation
request, ...), add `[Request]` to your issue's title (e.g. `[Request] Awesome new feature`).
- If you are a **developer** that wants to brainstorm a pull request or possible
changes to the plugin system, add [Brainstorming] to your issue's title (e.g.
`[Brainstorming] New plugin hook for doing some cool stuff`).
- If you need **support**, have a **question** or some **other reason** that
doesn't fit any of the above categories, the issue tracker is not the right place.
Consult the [Mailinglist](https://groups.google.com/group/octoprint) or the
[Google+ Community](https://plus.google.com/communities/102771308349328485741) instead.
No matter what kind of ticket you create, never mix two or more "ticket reasons"
into one ticket: One ticket per bug, request, brainstorming thread please.
----
**Note**: A bot is in place that monitors new tickets, automatically
categorizes them and checks new bug reports for usage of the provided template.
That bot will only bother you if you open a ticket that appears to be a bug (no
`[Request]` or `[Brainstorming]` in the title) without the template, and it
will do that only to ensure that all information needed to solve the issue is
available for the maintainers to directly start tackling that problem.
----
## How to file a bug report
If you encounter an issue with OctoPrint, you are welcome to
[submit a bug report](https://goo.gl/GzkGv9).
Before you do that for the first time though please take a moment to read the
following section *completely*. Thank you! :)
### What should I do before submitting a bug report?
1. **Make sure you are at the right location**. This is the Github repository
of the official version of OctoPrint, which is the 3D print server and
corresponding web interface itself.
**This is not the Github respository of OctoPi**, which is the preconfigured
Raspberry Pi image including OctoPrint among other things - that one can be found
[here](https://github.com/guysoft/OctoPi). Please note that while we do have
some entries regarding OctoPi in the FAQ, any bugs should be reported in the
[proper bug tracker](https://github.com/guysoft/OctoPi/issues) which - again -
is not here.
**This is also not the Github repository of any OctoPrint Plugins you
might have installed**. Report any issues with those in their corresponding
bug tracker (probably linked to from the plugin's homepage).
Finally, **this is also not the right issue tracker if you are running an
forked version of OctoPrint**. Seek help for such unofficial versions from
the people maintaining them instead.
2. Please make sure to **test out the current version** of OctoPrint to see
whether the problem you are encountering still exists.
If you are feeling up to it you might also want to try the current development
version of OctoPrint (if you aren't already). Refer to the [FAQ](https://github.com/foosel/OctoPrint/wiki/FAQ)
for information on how to do this.
3. The problem still exists? Then please **look through the
[existing tickets](https://github.com/foosel/OctoPrint/issues?state=open)
(use the [search](https://github.com/foosel/OctoPrint/search?q=&ref=cmdform&type=Issues))**
to check if there already exists a report of the issue you are encountering.
Sorting through duplicates of the same issue sometimes causes more work than
fixing it. Take the time to filter through possible duplicates and be really
sure that your problem definitely is a new one. Try more than one search query
(e.g. do not only search for "webcam" if you happen to run into an issue
with your webcam, also search for "timelapse" etc).
### What should I include in a bug report?
Always use the following template (you can remove what's within `[...]`, that's
only provided here as some additional information for you):
#### What were you doing?
[Please be as specific as possible here. The maintainers will need to reproduce
your issue in order to fix it and that is not possible if they don't know
what you did to get it to happen in the first place. If you encountered
a problem with specific files of any sorts, make sure to also include a link to a file
with which to reproduce the problem.]
#### What did you expect to happen?
#### What happened instead?
#### Branch & Commit or Version of OctoPrint
[Can be found in the lower left corner of the web interface.]
#### Printer model & used firmware incl. version
[If applicable, always include if unsure.]
#### Browser and Version of Browser, Operating System running Browser
[If applicable, always include if unsure.]
#### Link to octoprint.log
[On gist.github.com or pastebin.com. Always include and never truncate.]
#### Link to contents of terminal tab or serial.log
[On gist.github.com or pastebin.com. If applicable, always include if unsure or
reporting communication issues. Never truncate.]
#### Link to contents of Javascript console in the browser
[On gist.github.com or pastebin.com or alternatively a screenshot. If applicable -
always include if unsure or reporting UI issues.]
#### Screenshot(s) showing the problem:
[If applicable. Always include if unsure or reporting UI issues.]
I have read the FAQ.
### Where can I find which version and branch I'm on?
You can find out all of them by taking a look into the lower left corner of the
OctoPrint UI:
![Current version and git branch info in OctoPrint's UI](http://i.imgur.com/HyHMlY2.png)
If you don't have access to the UI you can find out that information via the
command line as well. Either `octoprint --version` or `python setup.py version`
in OctoPrint's folder will tell you the version of OctoPrint you are running
(note: if it doesn't then you are running a version older than 1.1.0,
*upgrade now*). A `git branch` in your OctoPrint installation folder will mark
the branch you are on with a little *. `git rev-parse HEAD` will tell you the
current commit.
### Where can I find those log files you keep talking about?
OctoPrint by default provides two log outputs, a third one can be enabled if
more information is needed.
One is contained in the **"Terminal" tab** within OctoPrint's UI and is a log
of the last 300 lines of communication with the printer. Please copy-paste
this somewhere (disable auto scroll to make copying the contents easier) -
e.g. http://pastebin.com or http://gist.github.com - and include a link in
your bug report.
There is also **OctoPrint's application log file** or in short `octoprint.log`,
which is by default located at `~/.octoprint/logs/octoprint.log` on Linux,
`%APPDATA%\OctoPrint\logs\octoprint.log` on Windows and
`~/Library/Application Support/OctoPrint/logs/octoprint.log` on MacOS. Please
copy-paste this to pastebin or gist as well and include a link in your bug
report.
It might happen that you are asked to provide a more **thorough log of the
communication with the printer** if you haven't already done so, the `serial.log`.
This is not written by default due to performance reasons, but you can enable
it in the settings dialog. After enabling that log, please reproduce the problem
again (connect to the printer, do whatever triggers it), then copy-paste
`~/.octoprint/logs/serial.log` (Windows: `%APPDATA%\OctoPrint\logs\serial.log`,
MacOS: `~/Library/Application Support/OctoPrint/logs/serial.log`) to pastebin
or gist and include the link in the bug report.
You might also be asked to provide a log with an increased log level. You can
find information on how to do just that in the
[docs](http://docs.octoprint.org/en/master/configuration/logging_yaml.html).
### Where can I find my browser's error console?
See [How to open the Javascript Console in different browsers](https://webmasters.stackexchange.com/questions/8525/how-to-open-the-javascript-console-in-different-browsers)
## Pull requests
1. If you want to add a new feature to OctoPrint, **please always first
consider if it wouldn't be better suited for a plugin.** As a general rule
of thumb, any feature that is only of interest to a small sub group should
be moved into a plugin. If the current plugin system doesn't allow you to
implement your feature as a plugin, create a "Brainstorming" ticket to get
the discussion going on how best to solve *this* in OctoPrint's plugin
system - maybe that's the actual PR you have been waiting for to contribute :)
2. If you plan to make **any large changes to the code or appearance, please
open a "Brainstorming" ticket first** so that we can determine if it's a
good time for your specific pull request. It might be that we're currently
in the process of making heavy changes to the code locations you'd target
as well, or your approach doesn't fit the general "project vision", and
that would just cause unnecessary work and frustration for everyone or
possibly get the PR rejected.
3. Create your pull request **from a custom branch** on your end (e.g.
`dev/myNewFeature`)[1] **against the `devel` branch**. Create **one pull request
per feature/bug fix**. If your PR contains an important bug fix, we will
make sure to backport it to the `maintenance` branch to also include it in
the next release.
4. Make sure you **follow the current coding style**. This means:
* Tabs instead of spaces in the Python files[2]
* Spaces instead of tabs in the Javascript sources
* English language (code, variables, comments, ...)
* Comments where necessary: Tell why the code does something like it does
it, structure your code
* Following the general architecture
If your PR needs to make changes to the Stylesheets, change the ``.less`` files
from which the CSS is compiled.
5. **Test your changes thoroughly**. That also means testing with usage
scenarios you don't normally use, e.g. if you only use access control, test
without and vice versa. If you only test with your printer, test with the
virtual printer and vice versa. State in your pull request how your tested
your changes. Ideally **add unit tests** - OctoPrint severly lacks in that
department, but we are trying to change that, so any new code already covered
with a test suite helps a lot!
6. In your pull request's description, **state what your pull request is doing**,
as in, what feature does it implement, what bug does it fix. The more
thoroughly you explain your intent behind the PR here, the higher the
chances it will get merged fast.
7. Important: Don't forget to **add yourself to the [AUTHORS](./AUTHORS.md)
file** :)
## History
* 2015-01-23: More guidelines for creating pull requests, support/questions
redirected to Mailinglist/G+ community
* 2015-01-27: Added another explicit link to the FAQ
* 2015-07-07: Added step to add yourself to AUTHORS when creating a PR :)
* 2015-12-01: Heavily reworked to include examples, better structure and
all information in one document.
## Footnotes
* [1] - If you are wondering why, the problem is that anything that you add
to your PR's branch will also become part of your PR, so if you create a
PR from your version of `devel` chances are high you'll add changes to the
PR that do not belong to the PR.
* [2] - Yes, we know that this goes against PEP-8. OctoPrint started out as
a fork of Cura and hence stuck to the coding style found therein. Changing
it now would make the history and especially `git blame` completely
unusable, so for now we'll have to deal with it (this decision might be
revisited in the future).

View file

@ -47,11 +47,11 @@ which is a custom SD card image that includes OctoPrint plus dependencies.
The generic steps that should basically be done regardless of operating system
and runtime environment are the following (as *regular
user*, please keep your hands *off* of the `sudo` command here!) - this assumes
you already have Python 2.7, pip and virtualenv set up:
you already have Python 2.7, pip and virtualenv set up on your system:
1. Checkout OctoPrint: `git clone https://github.com/foosel/OctoPrint.git`
2. Change into the OctoPrint folder: `cd OctoPrint`
3. Create a user-owned virtual environment therein: `virtualenv --system-site-packages venv`
3. Create a user-owned virtual environment therein: `virtualenv venv`
4. Install OctoPrint *into that virtual environment*: `./venv/bin/python setup.py install`
You may then start the OctoPrint server via `/path/to/OctoPrint/venv/bin/octoprint`, see [Usage](#usage)

View file

@ -25,7 +25,8 @@ Tool
See :ref:`sec-api-printer-toolcommand`.
Bed
Bed commands allow setting the temperature and temperature offset for the printer's heated bed. Querying the
corresponding resource returns temperature information including an optional history.
corresponding resource returns temperature information including an optional history. Note that Bed commands
are only available if the currently selected printer profile has a heated bed.
See :ref:`sec-api-printer-bedcommand`.
SD card
SD commands allow initialization, refresh and release of the printer's SD card (if available). Querying the
@ -564,6 +565,9 @@ Issue a bed command
Upon success, a status code of :http:statuscode:`204` and an empty body is returned.
If no heated bed is configured for the currently selected printer profile, the resource will return
an :http:statuscode:`409`.
**Example Target Temperature Request**
Set the target temperature for the printer's heated bed to 75°C.
@ -610,7 +614,8 @@ Issue a bed command
:statuscode 204: No error
:statuscode 400: If ``target`` or ``offset`` is not a valid number or outside of the supported range, or if the
request is otherwise invalid.
:statuscode 409: If the printer is not operational.
:statuscode 409: If the printer is not operational or the selected printer profile
does not have a heated bed.
.. _sec-api-printer-bedstate:
@ -627,6 +632,9 @@ Retrieve the current bed state
Returns a :http:statuscode:`200` with a Temperature Response in the body upon success.
If no heated bed is configured for the currently selected printer profile, the resource will return
an :http:statuscode:`409`.
.. note::
If you want both tool and bed temperature information at the same time, take a look at
:ref:`Retrieve the current printer state <sec-api-printer-state>`.
@ -675,7 +683,8 @@ Retrieve the current bed state
:query limit: If set to an integer (``n``), only the last ``n`` data points from the printer's temperature history
will be returned. Will be ignored if ``history`` is not enabled.
:statuscode 200: No error
:statuscode 409: If the printer is not operational.
:statuscode 409: If the printer is not operational or the selected printer profile
does not have a heated bed.
.. _sec-api-printer-sdcommand:
@ -856,7 +865,7 @@ Send an arbitrary command to the printer
.. sourcecode:: http
HTTP/1.1 204 No Content
:json string command: Single command to send to the printer, mutually exclusive with ``commands``.
:json string commands: List of commands to send to the printer, mutually exclusive with ``command``.
:statuscode 204: No error
@ -913,7 +922,8 @@ Temperature State
* - ``bed``
- 0..1
- :ref:`Temperature Data <sec-api-datamodel-printer-tempdata>`
- Current temperature stats for the printer's heated bed. Not included if querying only tool state.
- Current temperature stats for the printer's heated bed. Not included if querying only tool state or if
the currently selected printer profile does not have a heated bed.
* - ``history``
- 0..1
- List of :ref:`Historic Temperature Datapoint <sec-api-datamodel-printer-temphistory>`

View file

@ -65,7 +65,7 @@ a GCODE script including user input.
children:
- name: Get Position
command: M114
regex: "X:([0-9.]+) Y:([0-9.]+) Z:([0-9.]+) E:([0-9.]+)"
regex: "X:([-+]?[0-9.]+) Y:([-+]?[0-9.]+) Z:([-+]?[0-9.]+) E:([-+]?[0-9.]+)"
template: "Position: X={0}, Y={1}, Z={2}, E={3}"
- name: Fun stuff
children:

View file

@ -86,7 +86,7 @@ Out of the box, OctoPrint defaults to the following script setup for ``afterPrin
;disable all heaters
{% snippet 'disable_hotends' %}
M140 S0
[% snippet 'disable_bed' %}
;disable fan
M106 S0
@ -100,8 +100,19 @@ The ``disable_hotends`` snippet is defined as follows:
M104 T{{ tool }} S0
{% endfor %}
As you can see, the ``disable_hotends`` snippet utilizes the ``printer_profile`` context variable in order to
iterate through all available extruders and set their temperature to 0.
The ``disable_bed`` snippet is defined as follows:
.. code-block:: jinja
:caption: Default ``disable_bed`` snippet
{% if printer_profile.heatedBed %}
M140 S0
{% endif %}
As you can see, the ``disable_hotends`` and ``disable_bed`` snippets utilize the
``printer_profile`` context variable in order to iterate through all available
extruders and set their temperature to 0, and to also set the bed temperature
to 0 if a heated bed is configured.
.. seealso::

View file

@ -32,7 +32,8 @@ INSTALL_REQUIRES = [
"pkginfo==1.2.1",
"requests==2.7.0",
"semantic_version==2.4.2",
"psutil==3.2.1"
"psutil==3.2.1",
"awesome-slugify>=1.6.5,<1.7"
]
# Additional requirements for optional install options

View file

@ -13,6 +13,8 @@ import tempfile
import octoprint.filemanager
from octoprint.util import is_hidden_path
class StorageInterface(object):
"""
Interface of storage adapters for OctoPrint.
@ -309,6 +311,10 @@ class LocalFileStorage(StorageInterface):
self._metadata_cache = pylru.lrucache(10)
from slugify import Slugify
self._slugify = Slugify()
self._slugify.safe_chars = "-_.() "
self._old_metadata = None
self._initialize_metadata()
@ -357,7 +363,7 @@ class LocalFileStorage(StorageInterface):
if not metadata:
metadata = dict()
for entry in os.listdir(path):
if entry.startswith(".") or not octoprint.filemanager.valid_file_type(entry):
if is_hidden_path(entry) or not octoprint.filemanager.valid_file_type(entry):
continue
absolute_path = os.path.join(path, entry)
@ -607,9 +613,9 @@ class LocalFileStorage(StorageInterface):
def sanitize_name(self, name):
"""
Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise strips any characters from the
given ``name`` that are not any of the ASCII characters, digits, ``-``, ``_``, ``.``, ``(``, ``)`` or space and
replaces and spaces with ``_``.
Raises a :class:`ValueError` for a ``name`` containing ``/`` or ``\``. Otherwise
slugifies the given ``name`` by converting it to ASCII, leaving ``-``, ``_``, ``.``,
``(``, and ``)`` as is.
"""
if name is None:
return None
@ -617,11 +623,7 @@ class LocalFileStorage(StorageInterface):
if "/" in name or "\\" in name:
raise ValueError("name must not contain / or \\")
import string
valid_chars = "-_.() {ascii}{digits}".format(ascii=string.ascii_letters, digits=string.digits)
sanitized_name = ''.join(c for c in name if c in valid_chars)
sanitized_name = sanitized_name.replace(" ", "_")
return sanitized_name
return self._slugify(name).replace(" ", "_")
def sanitize_path(self, path):
"""
@ -887,7 +889,7 @@ class LocalFileStorage(StorageInterface):
result = dict()
for entry in os.listdir(path):
if entry.startswith("."):
if is_hidden_path(entry):
# no hidden files and folders
continue

View file

@ -330,7 +330,7 @@ class PluginSettings(object):
set_int =("setInt", prefix_path_in_args, add_setter_kwargs),
set_float =("setFloat", prefix_path_in_args, add_setter_kwargs),
set_boolean=("setBoolean", prefix_path_in_args, add_setter_kwargs),
remove =("remove", prefix_path_in_args)
remove =("remove", prefix_path_in_args, lambda x: x)
)
self.deprecated_access_methods = dict(
getInt ="get_int",

View file

@ -37,11 +37,10 @@ class CuraPlugin(octoprint.plugin.SlicerPlugin,
##~~ TemplatePlugin API
def get_template_configs(self):
from flask.ext.babel import gettext
return [
dict(type="settings", name=gettext("CuraEngine"))
]
def get_template_vars(self):
return dict(
homepage=__plugin_url__
)
##~~ StartupPlugin API
@ -413,9 +412,9 @@ def _sanitize_name(name):
sanitized_name = sanitized_name.replace(" ", "_")
return sanitized_name.lower()
__plugin_name__ = "CuraEngine"
__plugin_name__ = "CuraEngine (<= 15.04)"
__plugin_author__ = "Gina Häußge"
__plugin_url__ = "https://github.com/foosel/OctoPrint/wiki/Plugin:-Cura"
__plugin_description__ = "Adds support for slicing via CuraEngine from within OctoPrint"
__plugin_description__ = "Adds support for slicing via CuraEngine versions up to and including version 15.04 from within OctoPrint"
__plugin_license__ = "AGPLv3"
__plugin_implementation__ = CuraPlugin()

View file

@ -1,5 +1,12 @@
<h4>{{ _('General') }}</h4>
<p>{% trans %}
Specify the path to the CuraEngine binary. Note that only
<strong>versions up to and including 15.04</strong> are supported.
CuraEngine version 15.06 or newer is <strong>not</strong>
compatible with this plugin.
{% endtrans %}</p>
<form class="form-horizontal">
<div class="control-group">
<label class="control-label" for="settings-cura-path">{{ _('Path to CuraEngine') }}</label>
@ -10,7 +17,7 @@
<div class="control-group">
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.cura.debug_logging"> {{ _('Log the output of CuraEngine to plugin_cura_engine.log') }}
<input type="checkbox" data-bind="checked: settings.plugins.cura.debug_logging"> {{ _('Log the output of CuraEngine to <code>plugin_cura_engine.log</code>') }}
</label>
</div>
</div>
@ -53,6 +60,10 @@
<button class="btn pull-right" data-bind="click: function() { $root.showImportProfileDialog() }">{{ _('Import Profile...') }}</button>
<div style="clear: both">
<small>{% trans %}For more information on configuration and usage please <a href="{{ plugin_cura_homepage }}" target="_blank">see the Plugin's homepage</a>.{% endtrans %}</small>
</div>
<div id="settings_plugin_cura_import" class="modal hide fade">
<div class="modal-header">
<a href="#" class="close" data-dismiss="modal" aria-hidden="true">&times;</a>
@ -98,6 +109,13 @@
</div>
</div>
</form>
<small>{% trans %}
You can import your existing profile <code>.ini</code> files from Cura (version up to and
including 15.04) here. Please be aware that neither the <code>.json</code> profile format
from Cura versions starting with 15.06 is supported, nor are the custom Cura profile formats
that third party tools like e.g. Repetier create.
{% endtrans %}</small>
</div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">{{ _('Abort') }}</button>

View file

@ -11,7 +11,7 @@ import octoprint.plugin.core
from octoprint.settings import valid_boolean_trues
from octoprint.server.util.flask import restricted_access
from octoprint.server import admin_permission
from octoprint.server import admin_permission, VERSION
from octoprint.util.pip import PipCaller, UnknownPip
from flask import jsonify, make_response
@ -184,7 +184,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
plugins=self._repository_plugins
),
os=self._get_os(),
octoprint=self._get_octoprint_version(),
octoprint=VERSION,
pip=dict(
available=self._pip_caller.available,
command=self._pip_caller.command,
@ -550,9 +550,7 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
return False
current_os = self._get_os()
octoprint_version = self._get_octoprint_version()
if "-" in octoprint_version:
octoprint_version = octoprint_version[:octoprint_version.find("-")]
octoprint_version = self._get_octoprint_version(base=True)
def map_repository_entry(entry):
result = dict(entry)
@ -577,12 +575,11 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
self._repository_plugins = map(map_repository_entry, repo_data)
return True
def _is_octoprint_compatible(self, octoprint_version_string, compatibility_entries):
def _is_octoprint_compatible(self, octoprint_version, compatibility_entries):
"""
Tests if the current ``octoprint_version`` is compatible to any of the provided ``compatibility_entries``.
"""
octoprint_version = pkg_resources.parse_version(octoprint_version_string)
for octo_compat in compatibility_entries:
if not any(octo_compat.startswith(c) for c in ("<", "<=", "!=", "==", ">=", ">", "~=", "===")):
octo_compat = ">={}".format(octo_compat)
@ -611,9 +608,30 @@ class PluginManagerPlugin(octoprint.plugin.SimpleApiPlugin,
else:
return "unknown"
def _get_octoprint_version(self):
from octoprint._version import get_versions
return get_versions()["version"]
def _get_octoprint_version_string(self):
return VERSION
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)
if base:
if isinstance(octoprint_version, tuple):
# old setuptools
base_version = []
for part in octoprint_version:
if part.startswith("*"):
break
base_version.append(part)
base_version.append("*final")
octoprint_version = tuple(base_version)
else:
# new setuptools
octoprint_version = pkg_resources.parse_version(octoprint_version.base_version)
return octoprint_version
def _to_external_representation(self, plugin):
return dict(

View file

@ -18,7 +18,7 @@ from . import version_checks, updaters, exceptions, util
from octoprint.server.util.flask import restricted_access
from octoprint.server import admin_permission
from octoprint.server import admin_permission, VERSION, REVISION
from octoprint.util import dict_merge
import octoprint.settings
@ -430,8 +430,6 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
update_available = update_available or target_update_available
update_possible = update_possible or (target_update_possible and target_update_available)
from octoprint._version import get_versions
octoprint_version = get_versions()["version"]
local_name = target_information["local"]["name"]
local_value = target_information["local"]["value"]
@ -439,7 +437,7 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
updatePossible=target_update_possible,
information=target_information,
displayName=populated_check["displayName"],
displayVersion=populated_check["displayVersion"].format(octoprint_version=octoprint_version, local_name=local_name, local_value=local_value),
displayVersion=populated_check["displayVersion"].format(octoprint_version=VERSION, local_name=local_name, local_value=local_value),
check=populated_check)
if self._version_cache_dirty:
@ -503,19 +501,28 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
"""
checks = self._get_configured_checks()
populated_checks = dict()
for target, check in checks.items():
try:
populated_checks[target] = self._populated_check(target, check)
except exceptions.UnknownCheckType:
self._logger.debug("Ignoring unknown check type for target {}".format(target))
except:
self._logger.exception("Error while populating check prior to update for target {}".format(target))
if check_targets is None:
check_targets = checks.keys()
to_be_updated = sorted(set(check_targets) & set(checks.keys()))
check_targets = populated_checks.keys()
to_be_updated = sorted(set(check_targets) & set(populated_checks.keys()))
if "octoprint" in to_be_updated:
to_be_updated.remove("octoprint")
tmp = ["octoprint"] + to_be_updated
to_be_updated = tmp
updater_thread = threading.Thread(target=self._update_worker, args=(checks, to_be_updated, force))
updater_thread = threading.Thread(target=self._update_worker, args=(populated_checks, to_be_updated, force))
updater_thread.daemon = False
updater_thread.start()
return to_be_updated, dict((key, check["displayName"] if "displayName" in check else key) for key, check in checks.items() if key in to_be_updated)
return to_be_updated, dict((key, check["displayName"] if "displayName" in check else key) for key, check in populated_checks.items() if key in to_be_updated)
def _update_worker(self, checks, check_targets, force):
@ -680,12 +687,10 @@ class SoftwareUpdatePlugin(octoprint.plugin.BlueprintPlugin,
result["displayName"] = check.get("displayName", gettext("OctoPrint"))
result["displayVersion"] = check.get("displayVersion", "{octoprint_version}")
from octoprint._version import get_versions
versions = get_versions()
if check["type"] == "github_commit":
result["current"] = versions.get("full-revisionid", versions.get("full", "unknown"))
result["current"] = REVISION if REVISION else "unknown"
else:
result["current"] = versions["version"]
result["current"] = VERSION
else:
result["displayName"] = check.get("displayName", target)
result["displayVersion"] = check.get("displayVersion", check.get("current", "unknown"))

View file

@ -387,7 +387,7 @@ $(function() {
}
});
self.waitingForRestart = false;
}, 20000);
}, 60000);
break;
}

View file

@ -70,6 +70,8 @@ class VirtualPrinter():
self._sendWait = settings().getBoolean(["devel", "virtualPrinter", "sendWait"])
self._waitInterval = settings().getFloat(["devel", "virtualPrinter", "waitInterval"])
self._echoOnM117 = settings().getBoolean(["devel", "virtualPrinter", "echoOnM117"])
self.currentLine = 0
self.lastN = 0
@ -234,7 +236,8 @@ class VirtualPrinter():
continue
elif "M117" in data:
# we'll just use this to echo a message, to allow playing around with pause triggers
self.outgoing.put("echo:%s" % re.search("M117\s+(.*)", data).group(1))
if self._echoOnM117:
self.outgoing.put("echo:%s" % re.search("M117\s+(.*)", data).group(1))
elif "M999" in data:
# mirror Marlin behaviour
self.outgoing.put("Resend: 1")

View file

@ -12,7 +12,7 @@ import re
import logging
from octoprint.settings import settings
from octoprint.util import dict_merge, dict_sanitize, dict_contains_keys
from octoprint.util import dict_merge, dict_sanitize, dict_contains_keys, is_hidden_path
class SaveError(Exception):
pass
@ -151,7 +151,7 @@ class PrinterProfileManager(object):
formFactor = BedTypes.RECTANGULAR,
origin = BedOrigin.LOWERLEFT
),
heatedBed = False,
heatedBed = True,
extruder=dict(
count = 1,
offsets = [
@ -289,7 +289,7 @@ class PrinterProfileManager(object):
def _load_all_identifiers(self):
results = dict(_default=None)
for entry in os.listdir(self._folder):
if entry.startswith(".") or not entry.endswith(".profile") or entry == "_default.profile":
if is_hidden_path(entry) or not entry.endswith(".profile") or entry == "_default.profile":
continue
path = os.path.join(self._folder, entry)

View file

@ -22,9 +22,11 @@ import logging
import logging.config
import atexit
import signal
import base64
SUCCESS = {}
NO_CONTENT = ("", 204)
NOT_MODIFIED = ("Not Modified", 304)
app = Flask("octoprint")
assets = None
@ -42,6 +44,7 @@ loginManager = None
pluginManager = None
appSessionManager = None
pluginLifecycleManager = None
preemptiveCache = None
principals = Principal(app)
admin_permission = Permission(RoleNeed("admin"))
@ -61,6 +64,7 @@ import octoprint.util
import octoprint.filemanager.storage
import octoprint.filemanager.analysis
import octoprint.slicing
from octoprint.server.util.flask import PreemptiveCache
from . import util
@ -68,8 +72,9 @@ UI_API_KEY = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes)
versions = octoprint._version.get_versions()
VERSION = versions['version']
BRANCH = versions['branch'] if 'branch' in versions else None
BRANCH = versions.get('branch', None)
DISPLAY_VERSION = "%s (%s branch)" % (VERSION, BRANCH) if BRANCH else VERSION
REVISION = versions.get('full-revision-id', versions.get('full', None))
del versions
LOCALES = []
@ -96,7 +101,7 @@ def load_user(id):
else:
sessionid = None
if userManager is not None:
if userManager.enabled:
if sessionid:
return userManager.findUser(userid=id, session=sessionid)
else:
@ -124,6 +129,8 @@ class Server():
self._template_searchpaths = []
self._intermediary_server = None
def run(self):
if not self._allowRoot:
self._check_for_root()
@ -142,6 +149,7 @@ class Server():
global pluginManager
global appSessionManager
global pluginLifecycleManager
global preemptiveCache
global debug
from tornado.ioloop import IOLoop
@ -172,6 +180,9 @@ class Server():
sys.excepthook = exception_logger
self._logger.info("Starting OctoPrint %s" % DISPLAY_VERSION)
# start the intermediary server
self._start_intermediary_server(s)
# then initialize the plugin manager
pluginManager = octoprint.plugin.plugin_manager(init=True)
@ -185,6 +196,7 @@ class Server():
printer = Printer(fileManager, analysisQueue, printerProfileManager)
appSessionManager = util.flask.AppSessionManager()
pluginLifecycleManager = LifecycleManager(pluginManager)
preemptiveCache = PreemptiveCache(os.path.join(s.getBaseFolder("data"), "preemptive_cache_config.yaml"))
def octoprint_plugin_inject_factory(name, implementation):
if not isinstance(implementation, octoprint.plugin.OctoPrintPlugin):
@ -199,7 +211,8 @@ class Server():
printer=printer,
app_session_manager=appSessionManager,
plugin_lifecycle_manager=pluginLifecycleManager,
data_folder=os.path.join(settings().getBaseFolder("data"), name)
data_folder=os.path.join(settings().getBaseFolder("data"), name),
preemptive_cache=preemptiveCache
)
def settings_plugin_inject_factory(name, implementation):
@ -278,13 +291,15 @@ class Server():
events.DebugEventListener()
# setup access control
if s.getBoolean(["accessControl", "enabled"]):
userManagerName = s.get(["accessControl", "userManager"])
try:
clazz = octoprint.util.get_class(userManagerName)
userManager = clazz()
except AttributeError, e:
self._logger.exception("Could not instantiate user manager %s, will run with accessControl disabled!" % userManagerName)
userManagerName = s.get(["accessControl", "userManager"])
try:
clazz = octoprint.util.get_class(userManagerName)
userManager = clazz()
except AttributeError, e:
self._logger.exception("Could not instantiate user manager {}, falling back to FilebasedUserManager!".format(userManagerName))
userManager = octoprint.users.FilebasedUserManager()
finally:
userManager.enabled = s.getBoolean(["accessControl", "enabled"])
app.wsgi_app = util.ReverseProxied(
app.wsgi_app,
@ -308,7 +323,7 @@ class Server():
loginManager = LoginManager()
loginManager.session_protection = "strong"
loginManager.user_callback = load_user
if userManager is None:
if not userManager.enabled:
loginManager.anonymous_user = users.DummyUser
principals.identity_loaders.appendleft(users.dummy_identity_loader)
loginManager.init_app(app)
@ -342,7 +357,7 @@ class Server():
)
additional_mime_types=dict(mime_type_guesser=mime_type_guesser)
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not octoprint.util.is_hidden_path(path), status_code=404))
def joined_dict(*dicts):
if not len(dicts):
@ -361,7 +376,10 @@ class Server():
# camera snapshot
(r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
# generated webassets
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets")))
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets"))),
# online indicators - text file with "online" as content and a transparent gif
(r"/online.txt", util.tornado.StaticDataHandler, dict(data="online\n")),
(r"/online.gif", util.tornado.StaticDataHandler, dict(data=bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")), content_type="image/gif"))
]
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
try:
@ -414,6 +432,8 @@ class Server():
self._logger.debug("Adding maximum body size of {size}B for {method} requests to {route})".format(**locals()))
max_body_sizes.append((method, route, size))
self._stop_intermediary_server()
self._server = util.tornado.CustomHTTPServer(self._tornado_app, max_body_sizes=max_body_sizes, default_max_body_size=s.getInt(["server", "maxSize"]))
self._server.listen(self._port, address=self._host)
@ -467,6 +487,10 @@ class Server():
implementation.on_after_startup()
pluginLifecycleManager.add_callback("enabled", call_on_after_startup)
# when we are through with that we also run our preemptive cache
if settings().getBoolean(["devel", "cache", "preemptive"]):
self._execute_preemptive_flask_caching(preemptiveCache)
import threading
threading.Thread(target=work).start()
ioloop.add_callback(on_after_startup)
@ -513,7 +537,7 @@ class Server():
if "l10n" in request.values:
return Locale.negotiate([request.values["l10n"]], LANGUAGES)
if hasattr(g, "identity") and g.identity and userManager is not None:
if hasattr(g, "identity") and g.identity and userManager.enabled:
userid = g.identity.id
try:
user_language = userManager.getUserSetting(userid, ("interface", "language"))
@ -526,7 +550,7 @@ class Server():
if default_language is not None and not default_language == "_default" and default_language in LANGUAGES:
return Locale.negotiate([default_language], LANGUAGES)
return request.accept_languages.best_match(LANGUAGES)
return Locale.parse(request.accept_languages.best_match(LANGUAGES))
def _setup_logging(self, debug, logConf=None):
defaultConfig = {
@ -649,7 +673,9 @@ class Server():
# configure additional template folders for jinja2
import jinja2
filesystem_loader = jinja2.FileSystemLoader([])
import octoprint.util.jinja
filesystem_loader = octoprint.util.jinja.FilteredFileSystemLoader([],
path_filter=lambda x: not octoprint.util.is_hidden_path(x))
filesystem_loader.searchpath = self._template_searchpaths
jinja_loader = jinja2.ChoiceLoader([
@ -661,6 +687,46 @@ class Server():
self._register_template_plugins()
def _execute_preemptive_flask_caching(self, preemptive_cache):
from werkzeug.test import EnvironBuilder
import time
# we clean up entries from our preemptive cache settings that haven't been
# accessed longer than server.preemptiveCache.until days
preemptive_cache_timeout = settings().getInt(["server", "preemptiveCache", "until"])
cutoff_timestamp = time.time() - preemptive_cache_timeout * 24 * 60 * 60
def filter_old_entries(entry):
return "_timestamp" in entry and entry["_timestamp"] > cutoff_timestamp
cache_data = preemptive_cache.clean_all_data(lambda root, entries: filter(filter_old_entries, entries))
if not cache_data:
return
def execute_caching():
for route in sorted(cache_data.keys(), key=lambda x: (x.count("/"), x)):
entries = reversed(sorted(cache_data[route], key=lambda x: x.get("_count", 0)))
for kwargs in entries:
plugin = kwargs.get("plugin", None)
additional_request_data = kwargs.get("_additional_request_data", dict())
kwargs = dict((k, v) for k, v in kwargs.items() if not k.startswith("_") and not k == "plugin")
kwargs.update(additional_request_data)
try:
if plugin:
self._logger.info("Preemptively caching {} (plugin {}) for {!r}".format(route, plugin, kwargs))
else:
self._logger.info("Preemptively caching {} for {!r}".format(route, kwargs))
builder = EnvironBuilder(**kwargs)
with preemptive_cache.disable_access_logging():
app(builder.get_environ(), lambda *a, **kw: None)
except:
self._logger.exception("Error while trying to preemptively cache {} for {!r}".format(route, kwargs))
import threading
cache_thread = threading.Thread(target=execute_caching, name="Preemptive Cache Worker")
cache_thread.daemon = True
cache_thread.start()
def _register_template_plugins(self):
template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)
for plugin in template_plugins:
@ -936,6 +1002,90 @@ class Server():
assets.register("css_app", css_app_bundle)
assets.register("less_app", all_less_bundle)
def _start_intermediary_server(self, s):
import BaseHTTPServer
import SimpleHTTPServer
import threading
host = self._host
port = self._port
if host is None:
host = s.get(["server", "host"])
if port is None:
port = s.getInt(["server", "port"])
self._logger.debug("Starting intermediary server on {}:{}".format(host, port))
class IntermediaryServerHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def __init__(self, rules=None, *args, **kwargs):
if rules is None:
rules = []
self.rules = rules
SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, *args, **kwargs)
def do_GET(self):
request_path = self.path
if "?" in request_path:
request_path = request_path[0:request_path.find("?")]
for rule in self.rules:
path, data, content_type = rule
if request_path == path:
self.send_response(200)
if content_type:
self.send_header("Content-Type", content_type)
self.end_headers()
self.wfile.write(data)
break
else:
self.send_response(404)
self.wfile.write("Not found")
base_path = os.path.realpath(os.path.join(os.path.dirname(__file__), "..", "static"))
rules = [
("/", ["intermediary.html",], "text/html"),
("/favicon.ico", ["img", "tentacle-20x20.png"], "image/png"),
("/intermediary.gif", bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")), "image/gif")
]
def contents(args):
path = os.path.join(base_path, *args)
if not os.path.isfile(path):
return ""
with open(path, "rb") as f:
data = f.read()
return data
def process(rule):
if len(rule) == 2:
path, data = rule
content_type = None
else:
path, data, content_type = rule
if isinstance(data, (list, tuple)):
data = contents(data)
return path, data, content_type
rules = map(process, filter(lambda rule: len(rule) == 2 or len(rule) == 3, rules))
self._intermediary_server = BaseHTTPServer.HTTPServer((host, port), lambda *args, **kwargs: IntermediaryServerHandler(rules, *args, **kwargs))
thread = threading.Thread(target=self._intermediary_server.serve_forever)
thread.daemon = True
thread.start()
self._logger.debug("Intermediary server started")
def _stop_intermediary_server(self):
if self._intermediary_server is None:
return
self._logger.debug("Shutting down intermediary server...")
self._intermediary_server.shutdown()
self._intermediary_server.server_close()
self._logger.debug("Intermediary server shut down")
class LifecycleManager(object):
def __init__(self, plugin_manager):

View file

@ -111,6 +111,7 @@ def firstRunSetup():
"pass2" in request.values.keys() and request.values["pass1"] == request.values["pass2"]:
# configure access control
s().setBoolean(["accessControl", "enabled"], True)
octoprint.server.userManager.enable()
octoprint.server.userManager.addUser(request.values["user"], request.values["pass1"], True, ["user", "admin"])
s().setBoolean(["server", "firstRun"], False)
elif "ac" in request.values.keys() and not request.values["ac"] in valid_boolean_trues:
@ -120,6 +121,7 @@ def firstRunSetup():
octoprint.server.loginManager.anonymous_user = octoprint.users.DummyUser
octoprint.server.principals.identity_loaders.appendleft(octoprint.users.dummy_identity_loader)
octoprint.server.userManager.disable()
s().save()
return NO_CONTENT
@ -181,7 +183,7 @@ def performSystemAction():
@api.route("/login", methods=["POST"])
def login():
if octoprint.server.userManager is not None and "user" in request.values.keys() and "pass" in request.values.keys():
if octoprint.server.userManager.enabled and "user" in request.values.keys() and "pass" in request.values.keys():
username = request.values["user"]
password = request.values["pass"]
@ -196,7 +198,7 @@ def login():
user = octoprint.server.userManager.findUser(username)
if user is not None:
if octoprint.server.userManager.checkPassword(username, password):
if octoprint.server.userManager is not None:
if octoprint.server.userManager.enabled:
user = octoprint.server.userManager.login_user(user)
session["usersession.id"] = user.get_session()
g.user = user

View file

@ -10,10 +10,9 @@ from werkzeug.exceptions import BadRequest
import re
from octoprint.settings import settings, valid_boolean_trues
from octoprint.server import printer, NO_CONTENT
from octoprint.server import printer, printerProfileManager, NO_CONTENT
from octoprint.server.api import api
from octoprint.server.util.flask import restricted_access, get_json_command_from_request
import octoprint.util as util
from octoprint.printer import UnknownScript
@ -34,9 +33,13 @@ def printerState():
result = {}
processor = lambda x: x
if not printerProfileManager.get_current_or_default()["heatedBed"]:
processor = _delete_bed
# add temperature information
if not "temperature" in excludes:
result.update({"temperature": _getTemperatureData(lambda x: x)})
result.update({"temperature": _get_temperature_data(processor)})
# add sd information
if not "sd" in excludes and settings().getBoolean(["feature", "sdSupport"]):
@ -145,14 +148,7 @@ def printerToolState():
if not printer.is_operational():
return make_response("Printer is not operational", 409)
def deleteBed(x):
data = dict(x)
if "bed" in data.keys():
del data["bed"]
return data
return jsonify(_getTemperatureData(deleteBed))
return jsonify(_get_temperature_data(_delete_bed))
##~~ Heated bed
@ -164,6 +160,9 @@ def printerBedCommand():
if not printer.is_operational():
return make_response("Printer is not operational", 409)
if not printerProfileManager.get_current_or_default()["heatedBed"]:
return make_response("Printer does not have a heated bed", 409)
valid_commands = {
"target": ["target"],
"offset": ["offset"]
@ -204,15 +203,10 @@ def printerBedState():
if not printer.is_operational():
return make_response("Printer is not operational", 409)
def deleteTools(x):
data = dict(x)
if not printerProfileManager.get_current_or_default()["heatedBed"]:
return make_response("Printer does not have a heated bed", 409)
for k in data.keys():
if k.startswith("tool"):
del data[k]
return data
data = _getTemperatureData(deleteTools)
data = _get_temperature_data(_delete_tools)
if isinstance(data, Response):
return data
else:
@ -382,7 +376,7 @@ def getCustomControls():
return jsonify(controls=customControls)
def _getTemperatureData(filter):
def _get_temperature_data(preprocessor):
if not printer.is_operational():
return make_response("Printer is not operational", 409)
@ -399,8 +393,23 @@ def _getTemperatureData(filter):
limit = min(limit, len(history))
tempData.update({
"history": map(lambda x: filter(x), history[-limit:])
"history": map(lambda x: preprocessor(x), history[-limit:])
})
return filter(tempData)
return preprocessor(tempData)
def _delete_tools(x):
return _delete_from_data(x, lambda k: k.startswith("tool"))
def _delete_bed(x):
return _delete_from_data(x, lambda k: k == "bed")
def _delete_from_data(x, key_matcher):
data = dict(x)
for k in data.keys():
if key_matcher(k):
del data[k]
return data

View file

@ -23,7 +23,7 @@ from octoprint.server.util.flask import restricted_access
@restricted_access
@admin_permission.require(403)
def getUsers():
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
return jsonify({"users": userManager.getAllUsers()})
@ -33,7 +33,7 @@ def getUsers():
@restricted_access
@admin_permission.require(403)
def addUser():
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if not "application/json" in request.headers["Content-Type"]:
@ -62,7 +62,7 @@ def addUser():
@api.route("/users/<username>", methods=["GET"])
@restricted_access
def getUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
@ -79,7 +79,7 @@ def getUser(username):
@restricted_access
@admin_permission.require(403)
def updateUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
user = userManager.findUser(username)
@ -110,7 +110,7 @@ def updateUser(username):
@restricted_access
@admin_permission.require(http_exception=403)
def removeUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
try:
@ -123,7 +123,7 @@ def removeUser(username):
@api.route("/users/<username>/password", methods=["PUT"])
@restricted_access
def changePasswordForUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
@ -151,7 +151,7 @@ def changePasswordForUser(username):
@api.route("/users/<username>/settings", methods=["GET"])
@restricted_access
def getSettingsForUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()):
@ -165,7 +165,7 @@ def getSettingsForUser(username):
@api.route("/users/<username>/settings", methods=["PATCH"])
@restricted_access
def changeSettingsForUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if current_user is None or current_user.is_anonymous() or (current_user.get_name() != username and not current_user.is_admin()):
@ -185,7 +185,7 @@ def changeSettingsForUser(username):
@api.route("/users/<username>/apikey", methods=["DELETE"])
@restricted_access
def deleteApikeyForUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):
@ -201,7 +201,7 @@ def deleteApikeyForUser(username):
@api.route("/users/<username>/apikey", methods=["POST"])
@restricted_access
def generateApikeyForUser(username):
if userManager is None:
if not userManager.enabled:
return jsonify(SUCCESS)
if current_user is not None and not current_user.is_anonymous() and (current_user.get_name() == username or current_user.is_admin()):

View file

@ -120,7 +120,7 @@ def get_user_for_apikey(apikey):
if apikey == settings().get(["api", "key"]) or octoprint.server.appSessionManager.validate(apikey):
# master key or an app session key was used
return ApiUser()
elif octoprint.server.userManager is not None:
elif octoprint.server.userManager.enabled:
# user key might have been used
return octoprint.server.userManager.findUser(apikey=apikey)
return None
@ -143,6 +143,20 @@ def get_api_key(request):
return None
def get_plugin_hash():
from octoprint.plugin import plugin_manager
plugin_signature = lambda impl: "{}:{}".format(impl._identifier, impl._plugin_version)
template_plugins = map(plugin_signature, plugin_manager().get_implementations(octoprint.plugin.TemplatePlugin))
asset_plugins = map(plugin_signature, plugin_manager().get_implementations(octoprint.plugin.AssetPlugin))
ui_plugins = sorted(set(template_plugins + asset_plugins))
import hashlib
plugin_hash = hashlib.sha1()
plugin_hash.update(",".join(ui_plugins))
return plugin_hash.hexdigest()
#~~ reverse proxy compatible WSGI middleware

View file

@ -14,6 +14,7 @@ import flask.ext.assets
import webassets.updater
import webassets.utils
import functools
import contextlib
import time
import uuid
import threading
@ -221,7 +222,7 @@ def fix_webassets_filtertool():
#~~ passive login helper
def passive_login():
if octoprint.server.userManager is not None:
if octoprint.server.userManager.enabled:
user = octoprint.server.userManager.login_user(flask.ext.login.current_user)
else:
user = flask.ext.login.current_user
@ -313,7 +314,7 @@ class LessSimpleCache(BaseCache):
_cache = LessSimpleCache()
def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=None, refreshif=None, unless_response=None):
def cached(timeout=5 * 60, key=lambda: "view:%s" % flask.request.path, unless=None, refreshif=None, unless_response=None):
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
@ -330,16 +331,17 @@ def cached(timeout=5 * 60, key=lambda: "view/%s" % flask.request.path, unless=No
return f(*args, **kwargs)
cache_key = key()
rv = _cache.get(cache_key)
# only take the value from the cache if we are not required to refresh it from the wrapped function
if not callable(refreshif) or not refreshif():
rv = _cache.get(cache_key)
if rv is not None:
logger.debug("Serving entry for {path} from cache".format(path=flask.request.path))
return rv
if rv is not None and (not callable(refreshif) or not refreshif(rv)):
logger.debug("Serving entry for {path} from cache".format(path=flask.request.path))
if not "X-From-Cache" in rv.headers:
rv.headers["X-From-Cache"] = "true"
return rv
# get value from wrapped function
logger.debug("No cache entry or refreshing cache for {path}, calling wrapped function".format(path=flask.request.path))
logger.debug("No cache entry or refreshing cache for {path} (key: {key}), calling wrapped function".format(path=flask.request.path, key=cache_key))
rv = f(*args, **kwargs)
# do not store if the "unless_response" condition is true
@ -377,6 +379,222 @@ def cache_check_response_headers(response):
return False
class PreemptiveCache(object):
def __init__(self, cachefile):
self.cachefile = cachefile
self._lock = threading.RLock()
self._logger = logging.getLogger(__name__ + "." + self.__class__.__name__)
self._log_access = True
def record(self, data, unless=None):
if callable(unless) and unless():
return
entry_data = data
if callable(entry_data):
entry_data = entry_data()
if entry_data is not None:
from flask import request
self.add_data(request.path, entry_data)
@contextlib.contextmanager
def disable_access_logging(self):
with self._lock:
self._log_access = False
yield
self._log_access = True
def clean_all_data(self, cleanup_function):
assert callable(cleanup_function)
with self._lock:
all_data = self.get_all_data()
for root, entries in all_data.items():
old_count = len(entries)
entries = cleanup_function(root, entries)
if not entries:
del all_data[root]
self._logger.debug("Removed root {} from preemptive cache".format(root))
elif len(entries) < old_count:
all_data[root] = entries
self._logger.debug("Removed {} from preemptive cache for root {}".format(old_count - len(entries), root))
self.set_all_data(all_data)
return all_data
def get_all_data(self):
import yaml
cache_data = None
with self._lock:
try:
with open(self.cachefile, "r") as f:
cache_data = yaml.safe_load(f)
except IOError as e:
import errno
if e.errno != errno.ENOENT:
raise
except:
self._logger.exception("Error while reading {}".format(self.cachefile))
if cache_data is None:
cache_data = dict()
return cache_data
def get_data(self, root):
cache_data = self.get_all_data()
return cache_data.get(root, dict())
def set_all_data(self, data):
from octoprint.util import atomic_write
import yaml
with self._lock:
try:
with atomic_write(self.cachefile, "wb") as handle:
yaml.safe_dump(data, handle,default_flow_style=False, indent=" ", allow_unicode=True)
except:
self._logger.exception("Error while writing {}".format(self.cachefile))
def set_data(self, root, data):
with self._lock:
all_data = self.get_all_data()
all_data[root] = data
self.set_all_data(all_data)
def add_data(self, root, data):
from octoprint.util import dict_filter
def strip_ignored(d):
return dict_filter(d, lambda k, v: not k.startswith("_"))
def compare(a, b):
return set(strip_ignored(a).items()) == set(strip_ignored(b).items())
def split_matched_and_unmatched(entry, entries):
matched = []
unmatched = []
for e in entries:
if compare(e, entry):
matched.append(e)
else:
unmatched.append(e)
return matched, unmatched
with self._lock:
cache_data = self.get_all_data()
if not root in cache_data:
cache_data[root] = []
existing, other = split_matched_and_unmatched(data, cache_data[root])
def get_newest(entries):
result = None
for entry in entries:
if "_timestamp" in entry and (result is None or ("_timestamp" in entry and result["_timestamp"] < entry["_timestamp"])):
result = entry
return result
to_persist = get_newest(existing)
if not to_persist:
import copy
to_persist = copy.deepcopy(data)
to_persist["_timestamp"] = time.time()
to_persist["_count"] = 1
self._logger.info("Adding entry for {} and {!r}".format(root, to_persist))
elif self._log_access:
to_persist["_timestamp"] = time.time()
to_persist["_count"] = to_persist.get("_count", 0) + 1
self._logger.debug("Updating timestamp and counter for {} and {!r}".format(root, data))
else:
self._logger.debug("Not updating timestamp and counter for {} and {!r}, currently flagged as disabled".format(root, data))
self.set_data(root, [to_persist] + other)
def preemptively_cached(cache, data, unless=None):
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
cache.record(data, unless=unless)
return f(*args, **kwargs)
return decorated_function
return decorator
def etagged(etag):
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
rv = f(*args, **kwargs)
if isinstance(rv, flask.Response):
result = etag
if callable(result):
result = result(rv)
if result:
rv.set_etag(result)
return rv
return decorated_function
return decorator
def lastmodified(date):
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
rv = f(*args, **kwargs)
if not "Last-Modified" in rv.headers:
result = date
if callable(result):
result = result(rv)
if not isinstance(result, basestring):
from werkzeug.http import http_date
result = http_date(result)
if result:
rv.headers["Last-Modified"] = result
return rv
return decorated_function
return decorator
def conditional(condition, met):
def decorator(f):
@functools.wraps(f)
def decorated_function(*args, **kwargs):
if callable(condition) and condition():
# condition has been met, return met-response
rv = met
if callable(met):
rv = met()
return rv
# condition hasn't been met, call decorated function
return f(*args, **kwargs)
return decorated_function
return decorator
def check_etag(etag):
return flask.request.method in ("GET", "HEAD") and \
flask.request.if_none_match and \
etag in flask.request.if_none_match
def check_lastmodified(lastmodified):
return flask.request.method in ("GET", "HEAD") and \
flask.request.if_modified_since and \
lastmodified >= flask.request.if_modified_since
def add_non_caching_response_headers(response):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0"
response.headers["Pragma"] = "no-cache"
@ -482,7 +700,7 @@ def restricted_access(func):
@functools.wraps(func)
def decorated_view(*args, **kwargs):
# if OctoPrint hasn't been set up yet, abort
if settings().getBoolean(["server", "firstRun"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()):
if settings().getBoolean(["server", "firstRun"]) and settings().getBoolean(["accessControl", "enabled"]) and (octoprint.server.userManager is None or not octoprint.server.userManager.hasBeenCustomized()):
return flask.make_response("OctoPrint isn't setup yet", 403)
apikey = octoprint.server.util.get_api_key(flask.request)

View file

@ -927,6 +927,19 @@ class UrlProxyHandler(tornado.web.RequestHandler):
return "%s%s" % (self._basename, extension)
class StaticDataHandler(tornado.web.RequestHandler):
def initialize(self, data="", content_type="text/plain"):
self.data = data
self.content_type = content_type
def get(self, *args, **kwargs):
self.set_status(200)
self.set_header("Content-Type", self.content_type)
self.write(self.data)
self.flush()
self.finish()
#~~ Factory method for creating Flask access validation wrappers from the Tornado request context

View file

@ -6,6 +6,7 @@ __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agp
__copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License"
import logging
import os
import watchdog.events
import octoprint.filemanager
@ -14,6 +15,7 @@ import octoprint.util
class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
"""
Takes care of automatically "uploading" files that get added to the watched folder.
"""
@ -27,40 +29,42 @@ class GcodeWatchdogHandler(watchdog.events.PatternMatchingEventHandler):
self._printer = printer
def _upload(self, path):
import os
file_wrapper = octoprint.filemanager.util.DiskFileWrapper(os.path.basename(path), path)
# determine current job
currentFilename = None
currentOrigin = None
currentJob = self._printer.get_current_job()
if currentJob is not None and "file" in currentJob.keys():
currentJobFile = currentJob["file"]
if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys():
currentFilename = currentJobFile["name"]
currentOrigin = currentJobFile["origin"]
# determine future filename of file to be uploaded, abort if it can't be uploaded
try:
futureFilename = self._file_manager.sanitize_name(octoprint.filemanager.FileDestinations.LOCAL, file_wrapper.filename)
except:
futureFilename = None
if futureFilename is None or (len(self._file_manager.registered_slicers) == 0 and not octoprint.filemanager.valid_file_type(futureFilename)):
return
file_wrapper = octoprint.filemanager.util.DiskFileWrapper(os.path.basename(path), path)
# prohibit overwriting currently selected file while it's being printed
if futureFilename == currentFilename and currentOrigin == octoprint.filemanager.FileDestinations.LOCAL and self._printer.is_printing() or self._printer.is_paused():
return
# determine current job
currentFilename = None
currentOrigin = None
currentJob = self._printer.get_current_job()
if currentJob is not None and "file" in currentJob.keys():
currentJobFile = currentJob["file"]
if "name" in currentJobFile.keys() and "origin" in currentJobFile.keys():
currentFilename = currentJobFile["name"]
currentOrigin = currentJobFile["origin"]
self._file_manager.add_file(octoprint.filemanager.FileDestinations.LOCAL,
file_wrapper.filename,
file_wrapper,
allow_overwrite=True)
if os.path.exists(path):
# determine future filename of file to be uploaded, abort if it can't be uploaded
try:
os.remove(path)
futureFilename = self._file_manager.sanitize_name(octoprint.filemanager.FileDestinations.LOCAL, file_wrapper.filename)
except:
self._logger.exception("Error while trying to clear a file from the watched folder")
futureFilename = None
if futureFilename is None or (len(self._file_manager.registered_slicers) == 0 and not octoprint.filemanager.valid_file_type(futureFilename)):
return
# prohibit overwriting currently selected file while it's being printed
if futureFilename == currentFilename and currentOrigin == octoprint.filemanager.FileDestinations.LOCAL and self._printer.is_printing() or self._printer.is_paused():
return
self._file_manager.add_file(octoprint.filemanager.FileDestinations.LOCAL,
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")
except:
self._logger.exception("There was an error while processing the file {} in the watched folder".format(path))
def on_created(self, event):
self._upload(event.src_path)

View file

@ -13,7 +13,8 @@ from flask import request, g, url_for, make_response, render_template, send_from
import octoprint.plugin
from octoprint.server import app, userManager, pluginManager, gettext, \
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY, BRANCH
debug, LOCALES, VERSION, DISPLAY_VERSION, UI_API_KEY, BRANCH, preemptiveCache, \
NOT_MODIFIED
from octoprint.settings import settings
import re
@ -27,18 +28,23 @@ _valid_id_re = re.compile("[a-z_]+")
_valid_div_re = re.compile("[a-zA-Z_-]+")
@app.route("/")
@util.flask.preemptively_cached(cache=preemptiveCache,
data=lambda: dict(path=request.path, base_url=request.url_root, query_string="l10n={}".format(g.locale.language)) if g.locale else "en",
unless=lambda: request.url_root in settings().get(["server", "preemptiveCache", "exceptions"]))
@util.flask.conditional(lambda: _check_etag_and_lastmodified_for_index(), NOT_MODIFIED)
@util.flask.cached(timeout=-1,
refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values,
key=lambda: "view/%s/%s" % (request.path, g.locale),
unless_response=util.flask.cache_check_response_headers)
refreshif=lambda cached: _validate_cache_for_index(cached),
key=lambda: "view:{}:{}".format(request.base_url, g.locale.language if g.locale else "en"),
unless_response=lambda response: util.flask.cache_check_response_headers(response))
@util.flask.etagged(lambda _: _compute_etag_for_index())
@util.flask.lastmodified(lambda _: _compute_date_for_index())
def index():
#~~ a bunch of settings
enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"])
enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"]))
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None and len(settings().get(["system", "actions"])) > 0
enable_accesscontrol = userManager is not None
enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None
enable_accesscontrol = userManager.enabled
preferred_stylesheet = settings().get(["devel", "stylesheet"])
locales = dict((l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)
@ -275,11 +281,11 @@ def index():
#~~ prepare full set of template vars for rendering
first_run = settings().getBoolean(["server", "firstRun"]) and (userManager is None or not userManager.hasBeenCustomized())
first_run = settings().getBoolean(["server", "firstRun"]) and userManager.enabled and not userManager.hasBeenCustomized()
render_kwargs = dict(
webcamStream=settings().get(["webcam", "stream"]),
enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
enableAccessControl=userManager is not None,
enableAccessControl=userManager.enabled,
enableSdSupport=settings().get(["feature", "sdSupport"]),
firstRun=first_run,
debug=debug,
@ -297,13 +303,10 @@ def index():
#~~ render!
import datetime
response = make_response(render_template(
"index.jinja2",
**render_kwargs
))
response.headers["Last-Modified"] = datetime.datetime.now()
if first_run:
response = util.flask.add_non_caching_response_headers(response)
@ -354,6 +357,7 @@ def _process_template_configs(name, implementation, configs, rules):
return includes
def _process_template_config(name, implementation, rule, config=None, counter=1):
if "mandatory" in rule:
for mandatory in rule["mandatory"]:
@ -396,78 +400,23 @@ def _process_template_config(name, implementation, rule, config=None, counter=1)
return data
@app.route("/robots.txt")
@util.flask.cached(timeout=-1)
def robotsTxt():
return send_from_directory(app.static_folder, "robots.txt")
@app.route("/i18n/<string:locale>/<string:domain>.js")
@util.flask.cached(timeout=-1,
refreshif=lambda: util.flask.cache_check_headers() or "_refresh" in request.values,
key=lambda: "view/%s/%s" % (request.path, g.locale))
@util.flask.conditional(lambda: _check_etag_and_lastmodified_for_i18n(), NOT_MODIFIED)
@util.flask.etagged(lambda _: _compute_etag_for_i18n(request.view_args["locale"], request.view_args["domain"]))
@util.flask.lastmodified(lambda _: _compute_date_for_i18n(request.view_args["locale"], request.view_args["domain"]))
def localeJs(locale, domain):
messages = dict()
plural_expr = None
if locale != "en":
from flask import _request_ctx_stack
from babel.messages.pofile import read_po
def messages_from_po(base_path, locale, domain):
path = os.path.join(base_path, locale)
if not os.path.isdir(path):
return None, None
path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals()))
if not os.path.isfile(path):
return None, None
messages = dict()
with file(path) as f:
catalog = read_po(f, locale=locale, domain=domain)
for message in catalog:
message_id = message.id
if isinstance(message_id, (list, tuple)):
message_id = message_id[0]
messages[message_id] = message.string
return messages, catalog.plural_expr
user_base_path = os.path.join(settings().getBaseFolder("translations"))
user_plugin_path = os.path.join(user_base_path, "_plugins")
# plugin translations
plugins = octoprint.plugin.plugin_manager().enabled_plugins
for name, plugin in plugins.items():
dirs = [os.path.join(user_plugin_path, name), os.path.join(plugin.location, 'translations')]
for dirname in dirs:
if not os.path.isdir(dirname):
continue
plugin_messages, _ = messages_from_po(dirname, locale, domain)
if plugin_messages is not None:
messages = octoprint.util.dict_merge(messages, plugin_messages)
_logger.debug("Using translation folder {dirname} for locale {locale} of plugin {name}".format(**locals()))
break
else:
_logger.debug("No translations for locale {locale} for plugin {name}".format(**locals()))
# core translations
ctx = _request_ctx_stack.top
base_path = os.path.join(ctx.app.root_path, "translations")
dirs = [user_base_path, base_path]
for dirname in dirs:
core_messages, plural_expr = messages_from_po(dirname, locale, domain)
if core_messages is not None:
messages = octoprint.util.dict_merge(messages, core_messages)
_logger.debug("Using translation folder {dirname} for locale {locale} of core translations".format(**locals()))
break
else:
_logger.debug("No core translations for locale {locale}".format(**locals()))
messages, plural_expr = _get_translations(locale, domain)
catalog = dict(
messages=messages,
@ -485,3 +434,192 @@ def plugin_assets(name, filename):
return redirect(url_for("plugin." + name + ".static", filename=filename))
def _compute_etag_for_index(files=None, lastmodified=None):
if files is None:
files = _files_for_index()
if lastmodified is None:
lastmodified = _compute_date(files)
if lastmodified and not isinstance(lastmodified, basestring):
from werkzeug.http import http_date
lastmodified = http_date(lastmodified)
from octoprint import __version__
from octoprint.server import UI_API_KEY
import hashlib
hash = hashlib.sha1()
hash.update(__version__)
hash.update(UI_API_KEY)
hash.update(",".join(sorted(files)))
if lastmodified:
hash.update(lastmodified)
return hash.hexdigest()
def _compute_etag_for_i18n(locale, domain, files=None, lastmodified=None):
if files is None:
files = _get_all_translationfiles(locale, domain)
if lastmodified is None:
lastmodified = _compute_date(files)
if lastmodified and not isinstance(lastmodified, basestring):
from werkzeug.http import http_date
lastmodified = http_date(lastmodified)
import hashlib
hash = hashlib.sha1()
hash.update(",".join(sorted(files)))
if lastmodified:
hash.update(lastmodified)
return hash.hexdigest()
def _compute_date_for_i18n(locale, domain):
return _compute_date(_get_all_translationfiles(locale, domain))
def _compute_date_for_index():
return _compute_date(_files_for_index())
def _validate_cache_for_index(cached):
no_cache_headers = util.flask.cache_check_headers()
refresh_flag = "_refresh" in request.values
etag_different = _compute_etag_for_index() != cached.get_etag()[0]
return no_cache_headers or refresh_flag or etag_different
def _files_for_index():
"""
Collects all paths of files that the index page depends on.
The relevant files are:
* all jinja2 templates: they might be used within the index page, so
any changes here change the rendering outcome
* all defined assets: if one of them changes, the webassets bundle will
be regenerated and hence the URL included in the cached page won't be
valid anymore
* all translation files used for our current locale: if any of those change
we also need to re-render
"""
templates = _get_all_templates()
assets = _get_all_assets()
translations = _get_all_translationfiles(g.locale.language if g.locale else "en", "messages")
return sorted(set(templates + assets + translations))
def _compute_date(files):
from datetime import datetime
timestamps = map(lambda path: os.stat(path).st_mtime, files)
max_timestamp = max(*timestamps) if timestamps else None
if max_timestamp:
# we set the micros to 0 since microseconds are not speced for HTTP
max_timestamp = datetime.fromtimestamp(max_timestamp).replace(microsecond=0)
return max_timestamp
def _check_etag_and_lastmodified_for_index():
files = _files_for_index()
lastmodified = _compute_date(files)
lastmodified_ok = util.flask.check_lastmodified(lastmodified)
etag_ok = util.flask.check_etag(_compute_etag_for_index(files, lastmodified))
return etag_ok and lastmodified_ok
def _check_etag_and_lastmodified_for_i18n():
locale = request.view_args["locale"]
domain = request.view_args["domain"]
etag_ok = util.flask.check_etag(_compute_etag_for_i18n(request.view_args["locale"], request.view_args["domain"]))
lastmodified = _compute_date_for_i18n(locale, domain)
lastmodified_ok = lastmodified is None or util.flask.check_lastmodified(lastmodified)
return etag_ok and lastmodified_ok
def _get_all_templates():
from octoprint.util.jinja import get_all_template_paths
return get_all_template_paths(app.jinja_loader)
def _get_all_assets():
from octoprint.util.jinja import get_all_asset_paths
return get_all_asset_paths(app.jinja_env.assets_environment)
def _get_all_translationfiles(locale, domain):
from flask import _request_ctx_stack
def get_po_path(basedir, locale, domain):
path = os.path.join(basedir, locale)
if not os.path.isdir(path):
return None
path = os.path.join(path, "LC_MESSAGES", "{domain}.po".format(**locals()))
if not os.path.isfile(path):
return None
return path
po_files = []
user_base_path = os.path.join(settings().getBaseFolder("translations"))
user_plugin_path = os.path.join(user_base_path, "_plugins")
# plugin translations
plugins = octoprint.plugin.plugin_manager().enabled_plugins
for name, plugin in plugins.items():
dirs = [os.path.join(user_plugin_path, name), os.path.join(plugin.location, 'translations')]
for dirname in dirs:
if not os.path.isdir(dirname):
continue
po_file = get_po_path(dirname, locale, domain)
if po_file:
po_files.append(po_file)
break
# core translations
ctx = _request_ctx_stack.top
base_path = os.path.join(ctx.app.root_path, "translations")
dirs = [user_base_path, base_path]
for dirname in dirs:
po_file = get_po_path(dirname, locale, domain)
if po_file:
po_files.append(po_file)
break
return po_files
def _get_translations(locale, domain):
from babel.messages.pofile import read_po
from octoprint.util import dict_merge
messages = dict()
plural_expr = None
def messages_from_po(path, locale, domain):
messages = dict()
with file(path) as f:
catalog = read_po(f, locale=locale, domain=domain)
for message in catalog:
message_id = message.id
if isinstance(message_id, (list, tuple)):
message_id = message_id[0]
messages[message_id] = message.string
return messages, catalog.plural_expr
po_files = _get_all_translationfiles(locale, domain)
for po_file in po_files:
po_messages, plural_expr = messages_from_po(po_file, locale, domain)
if po_messages is not None:
messages = dict_merge(messages, po_messages)
return messages, plural_expr

View file

@ -30,7 +30,7 @@ import logging
import re
import uuid
from octoprint.util import atomic_write
from octoprint.util import atomic_write, is_hidden_path
_APPNAME = "OctoPrint"
@ -114,6 +114,10 @@ default_settings = {
"diskspace": {
"warning": 500 * 1024 * 1024, # 500 MB
"critical": 200 * 1024 * 1024, # 200 MB
},
"preemptiveCache": {
"exceptions": [],
"until": 7
}
},
"webcam": {
@ -253,16 +257,18 @@ default_settings = {
},
"scripts": {
"gcode": {
"afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\nM140 S0\n\n;disable fan\nM106 S0",
"afterPrintCancelled": "; disable motors\nM84\n\n;disable all heaters\n{% snippet 'disable_hotends' %}\n{% snippet 'disable_bed' %}\n;disable fan\nM106 S0",
"snippets": {
"disable_hotends": "{% for tool in range(printer_profile.extruder.count) %}M104 T{{ tool }} S0\n{% endfor %}"
"disable_hotends": "{% for tool in range(printer_profile.extruder.count) %}M104 T{{ tool }} S0\n{% endfor %}",
"disable_bed": "{% if printer_profile.heatedBed %}M140 S0\n{% endif %}"
}
}
},
"devel": {
"stylesheet": "css",
"cache": {
"enabled": True
"enabled": True,
"preemptive": True
},
"webassets": {
"minify": False,
@ -295,7 +301,8 @@ default_settings = {
"commandBuffer": 4,
"sendWait": True,
"waitInterval": 1.0,
"supportM112": True
"supportM112": True,
"echoOnM117": True
}
}
}
@ -405,10 +412,12 @@ class Settings(object):
return folder
def _init_script_templating(self):
from jinja2 import Environment, BaseLoader, FileSystemLoader, ChoiceLoader, TemplateNotFound
from jinja2.nodes import Include, Const
from jinja2 import Environment, BaseLoader, ChoiceLoader, TemplateNotFound
from jinja2.nodes import Include
from jinja2.ext import Extension
from octoprint.util.jinja import FilteredFileSystemLoader
class SnippetExtension(Extension):
tags = {"snippet"}
fields = Include.fields
@ -497,10 +506,14 @@ class Settings(object):
else:
return template
file_system_loader = FileSystemLoader(self.getBaseFolder("scripts"))
path_filter = lambda path: not is_hidden_path(path)
file_system_loader = FilteredFileSystemLoader(self.getBaseFolder("scripts"),
path_filter=path_filter)
settings_loader = SettingsScriptLoader(self)
choice_loader = ChoiceLoader([file_system_loader, settings_loader])
select_loader = SelectLoader(choice_loader, dict(bundled=settings_loader, file=file_system_loader))
select_loader = SelectLoader(choice_loader,
dict(bundled=settings_loader,
file=file_system_loader))
return RelEnvironment(loader=select_loader, extensions=[SnippetExtension])
def _get_script_template(self, script_type, name, source=False):
@ -1100,7 +1113,7 @@ class Settings(object):
def saveScript(self, script_type, name, script):
script_folder = self.getBaseFolder("scripts")
filename = os.path.realpath(os.path.join(script_folder, script_type, name))
if not filename.startswith(script_folder):
if not filename.startswith(os.path.realpath(script_folder)):
# oops, jail break, that shouldn't happen
raise ValueError("Invalid script path to save to: {filename} (from {script_type}:{name})".format(**locals()))

View file

@ -22,6 +22,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import os
import octoprint.plugin
import octoprint.events
import octoprint.util
from octoprint.settings import settings
import logging
@ -478,7 +479,7 @@ class SlicingManager(object):
profiles = dict()
slicer_profile_path = self.get_slicer_profile_path(slicer)
for entry in os.listdir(slicer_profile_path):
if not entry.endswith(".profile") or entry.startswith("."):
if not entry.endswith(".profile") or octoprint.util.is_hidden_path(entry):
# we are only interested in profiles and no hidden files
continue
@ -539,7 +540,7 @@ class SlicingManager(object):
name = self._sanitize(name)
path = os.path.join(self.get_slicer_profile_path(slicer), "{name}.profile".format(name=name))
if not os.path.realpath(path).startswith(self._profile_path):
if not os.path.realpath(path).startswith(os.path.realpath(self._profile_path)):
raise IOError("Path to profile {name} tried to break out of allows sub path".format(**locals()))
if must_exist and not (os.path.exists(path) and os.path.isfile(path)):
raise UnknownProfile(slicer, name)

File diff suppressed because one or more lines are too long

View file

@ -379,7 +379,7 @@ GCODE.renderer = (function(){
};
var drawLayer = function(layerNum, fromProgress, toProgress, isNotCurrentLayer){
console.log("Drawing layer " + layerNum + " from " + fromProgress + " to " + toProgress + " (current: " + !isNotCurrentLayer + ")");
log.trace("Drawing layer " + layerNum + " from " + fromProgress + " to " + toProgress + " (current: " + !isNotCurrentLayer + ")");
var i;

View file

@ -0,0 +1,222 @@
<html>
<head>
<title>OctoPrint is still starting</title>
<style>
body {
margin: 0;
font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333333;
background-color: #ffffff;
}
#startup-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 10000;
display: block;
}
.background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.wrapper {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.wrapper .outer {
display: table;
width: 100%;
height: 100%;
}
.wrapper .outer .inner {
display: table-cell;
vertical-align: middle;
}
.wrapper .outer .inner .content {
text-align: center;
}
.green {
color: #169300;
}
.red {
color: #990000;
}
.pulsate3 {
-webkit-animation: pulsate 3s ease-out;
-webkit-animation-iteration-count: infinite;
opacity: 0.5;
}
.pulsate1 {
-webkit-animation: pulsate 1s ease-out;
-webkit-animation-iteration-count: infinite;
opacity: 0.5;
}
@-webkit-keyframes pulsate {
0% {
opacity: 0.5;
}
50% {
opacity: 1.0;
}
100% {
opacity: 0.5;
}
}
</style>
<script>
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(searchString, position) {
var subjectString = this.toString();
if (typeof position !== 'number' || !isFinite(position) || Math.floor(position) !== position || position > subjectString.length) {
position = subjectString.length;
}
position -= searchString.length;
var lastIndex = subjectString.indexOf(searchString, position);
return lastIndex !== -1 && lastIndex === position;
};
}
function ping(url, timeout, callback) {
var img = new Image();
var calledBack = false;
var urlToUse = url;
var postfix = "_=" + Date.now();
if (url.indexOf("?") > -1) {
urlToUse += "&" + postfix;
} else {
urlToUse += "?" + postfix;
}
console.log("Pinging " + url);
img.onload = function() {
callback("load");
calledBack = true;
};
img.onerror = function() {
if (!calledBack) {
callback("error");
calledBack = true;
}
};
img.src = urlToUse;
setTimeout(function() {
if (!calledBack) {
callback("timeout");
calledBack = true;
}
}, timeout);
}
window.onload = function() {
var intervals = [1, 1, 2, 3, 5, 8];
var timeout = 1500;
var baseUrl = window.location.href;
if (baseUrl.indexOf("#") > -1) {
baseUrl = baseUrl.substring(0, baseUrl.indexOf("#"));
}
if (baseUrl.indexOf("/static") > -1) {
baseUrl = baseUrl.substring(0, baseUrl.indexOf("/static"));
}
if (baseUrl[baseUrl.length - 1] != "/") {
baseUrl += "/";
}
var serverOnlineUrl = baseUrl + "online.gif";
var backendOnlineUrl = baseUrl + "intermediary.gif";
var serverTimeout;
var message = window.document.getElementById("message");
var serverIsOnline = false;
var serverOnlineCallback = function(result) {
if (result == "load") {
// our online.gif loaded, so the server is up, let's reload
serverIsOnline = true;
message.className = "pulsate1 green";
message.innerText = "OctoPrint server online, reloading page...";
window.location = baseUrl;
} else {
// online.gif still not available, let's look at
var interval = 15;
if (intervals.length) {
interval = intervals.shift();
}
serverTimeout = setTimeout(function() {
ping(serverOnlineUrl, timeout, serverOnlineCallback);
}, interval * 1000)
}
};
var backendOfflineCounter = 0;
var backendOnlineCallback = function(result) {
if (serverIsOnline) {
return;
}
if (result == "load") {
setTimeout(function() {
ping(backendOnlineUrl, timeout, backendOnlineCallback);
}, 1000);
return;
}
backendOfflineCounter++;
if (backendOfflineCounter > 10) {
if (serverTimeout) {
window.clearTimeout(serverTimeout);
}
message.className = "red";
message.innerHTML = "Looks like something went wrong during startup, the server is gone again. You should check <code>octoprint.log</code>.";
} else {
setTimeout(function() {
ping(backendOnlineUrl, timeout, backendOnlineCallback);
}, 1000);
}
};
ping(backendOnlineUrl, timeout, backendOnlineCallback);
ping(serverOnlineUrl, timeout, serverOnlineCallback);
}
</script>
</head>
<body>
<div id="startup-overlay">
<div class="background"></div>
<div class="wrapper">
<div class="outer">
<div class="inner">
<div class="content">
<h1 id="message" class="pulsate3">OctoPrint is still starting up, please wait...</h1>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -11,9 +11,6 @@ function DataUpdater(allViewModels) {
self._pluginHash = undefined;
self.reloadOverlay = $("#reloadui_overlay");
$("#reloadui_overlay_reload").click(function() { location.reload(true); });
self.connect = function() {
var options = {};
if (SOCKJS_DEBUG) {
@ -142,7 +139,7 @@ function DataUpdater(allViewModels) {
}
if (oldVersion != VERSION || (oldPluginHash != undefined && oldPluginHash != self._pluginHash)) {
self.reloadOverlay.show();
showReloadOverlay();
}
break;
@ -250,7 +247,6 @@ function DataUpdater(allViewModels) {
text: _.sprintf(gettext("Streamed %(local)s to %(remote)s on SD, took %(time).2f seconds"), payload),
type: "success"
});
gcodeFilesViewModel.requestData(payload.remote, "sdcard");
}
var legacyEventHandlers = {

View file

@ -476,6 +476,10 @@ function showConfirmationDialog(message, onacknowledge) {
confirmationDialog.modal("show");
}
function showReloadOverlay() {
$("#reloadui_overlay").show();
}
function commentableLinesToArray(lines) {
return splitTextToArray(lines, "\n", true, function(item) {return !_.startsWith(item, "#")});
}

View file

@ -7,6 +7,72 @@ $(function() {
log.setLevel(CONFIG_DEBUG ? "debug" : "info");
//~~ setup browser and internal tab tracking (in 1.3.0 that will be
// much nicer with the global OctoPrint object...)
var tabTracking = (function() {
var exports = {
browserTabVisibility: undefined,
selectedTab: undefined
};
var browserVisibilityCallbacks = [];
var getHiddenProp = function() {
var prefixes = ["webkit", "moz", "ms", "o"];
// if "hidden" is natively supported just return it
if ("hidden" in document) {
return "hidden"
}
// otherwise loop over all the known prefixes until we find one
var vendorPrefix = _.find(prefixes, function(prefix) {
return (prefix + "Hidden" in document);
});
if (vendorPrefix !== undefined) {
return vendorPrefix + "Hidden";
}
// nothing found
return undefined;
};
var isHidden = function() {
var prop = getHiddenProp();
if (!prop) return false;
return document[prop];
};
var updateBrowserVisibility = function() {
var visible = !isHidden();
exports.browserTabVisible = visible;
_.each(browserVisibilityCallbacks, function(callback) {
callback(visible);
})
};
// register for browser visibility tracking
var prop = getHiddenProp();
if (!prop) return undefined;
var eventName = prop.replace(/[H|h]idden/, "") + "visibilitychange";
document.addEventListener(eventName, updateBrowserVisibility);
updateBrowserVisibility();
// exports
exports.isVisible = function() { return !isHidden() };
exports.onBrowserVisibilityChange = function(callback) {
browserVisibilityCallbacks.push(callback);
};
return exports;
})();
//~~ AJAX setup
// work around a stupid iOS6 bug where ajax requests get cached and only work once, as described at
@ -67,6 +133,18 @@ $(function() {
// the view model map is our basic look up table for dependencies that may be injected into other view models
var viewModelMap = {};
// We put our tabTracking into the viewModelMap as a workaround until
// our global OctoPrint object becomes available in 1.3.0. This way
// we'll still be able to access it in our view models.
//
// NOTE TO DEVELOPERS: Do NOT depend on this dependency in your custom
// view models. It is ONLY provided for the core application to be able
// to backport a fix from the 1.3.0 development branch and WILL BE
// REMOVED once 1.3.0 gets released without any fallback!
//
// TODO: Remove with release of 1.3.0
viewModelMap.tabTracking = tabTracking;
// Fix Function#name on browsers that do not support it (IE):
// see: http://stackoverflow.com/questions/6903762/function-name-not-supported-in-ie
if (!(function f() {}).name) {
@ -371,16 +449,22 @@ $(function() {
$('.nav-pills, .nav-tabs').tabdrop();
// Allow components to react to tab change
var tabs = $('#tabs a[data-toggle="tab"]');
tabs.on('show', function (e) {
var current = e.target.hash;
var previous = e.relatedTarget.hash;
var onTabChange = function(current, previous) {
log.debug("Selected OctoPrint tab changed: previous = " + previous + ", current = " + current);
tabTracking.selectedTab = current;
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onTabChange")) {
viewModel.onTabChange(current, previous);
}
});
};
var tabs = $('#tabs a[data-toggle="tab"]');
tabs.on('show', function (e) {
var current = e.target.hash;
var previous = e.relatedTarget.hash;
onTabChange(current, previous);
});
tabs.on('shown', function (e) {
@ -394,6 +478,8 @@ $(function() {
});
});
onTabChange(OCTOPRINT_INITIAL_TAB);
// Fix input element click problems on dropdowns
$(".dropdown input, .dropdown label").click(function(e) {
e.stopPropagation();
@ -404,6 +490,9 @@ $(function() {
e.preventDefault();
});
// reload overlay
$("#reloadui_overlay_reload").click(function() { location.reload(); });
//~~ Starting up the app
_.each(allViewModels, function(viewModel) {
@ -485,11 +574,22 @@ $(function() {
});
log.info("... binding done");
// startup complete
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onStartupComplete")) {
viewModel.onStartupComplete();
}
});
// make sure we can track the browser tab visibility
tabTracking.onBrowserVisibilityChange(function(status) {
log.debug("Browser tab is now " + (status ? "visible" : "hidden"));
_.each(allViewModels, function(viewModel) {
if (viewModel.hasOwnProperty("onBrowserTabVisibilityChange")) {
viewModel.onBrowserTabVisibilityChange(status);
}
});
});
};
if (!_.has(viewModelMap, "settingsViewModel")) {

View file

@ -5,6 +5,9 @@ $(function() {
self.loginState = parameters[0];
self.settings = parameters[1];
// TODO remove with release of 1.3.0 and switch to OctoPrint.coreui usage
self.tabTracking = parameters[2];
self._createToolEntry = function () {
return {
name: ko.observable(),
@ -415,31 +418,51 @@ $(function() {
self.onSettingsBeforeSave = self.updateRotatorWidth;
self._disableWebcam = function() {
// only disable webcam stream if tab is out of focus for more than 5s, otherwise we might cause
// more load by the constant connection creation than by the actual webcam stream
self.webcamDisableTimeout = setTimeout(function () {
$("#webcam_image").attr("src", "");
}, 5000);
};
self._enableWebcam = function() {
if (self.tabTracking.selectedTab != "#control" || !self.tabTracking.browserTabVisible) {
return;
}
if (self.webcamDisableTimeout != undefined) {
clearTimeout(self.webcamDisableTimeout);
}
var webcamImage = $("#webcam_image");
var currentSrc = webcamImage.attr("src");
if (currentSrc === undefined || currentSrc.trim() == "") {
var newSrc = CONFIG_WEBCAM_STREAM;
if (CONFIG_WEBCAM_STREAM.lastIndexOf("?") > -1) {
newSrc += "&";
} else {
newSrc += "?";
}
newSrc += new Date().getTime();
self.updateRotatorWidth();
webcamImage.attr("src", newSrc);
}
};
self.onTabChange = function (current, previous) {
if (current == "#control") {
if (self.webcamDisableTimeout != undefined) {
clearTimeout(self.webcamDisableTimeout);
}
var webcamImage = $("#webcam_image");
var currentSrc = webcamImage.attr("src");
if (currentSrc === undefined || currentSrc.trim() == "") {
var newSrc = CONFIG_WEBCAM_STREAM;
if (CONFIG_WEBCAM_STREAM.lastIndexOf("?") > -1) {
newSrc += "&";
} else {
newSrc += "?";
}
newSrc += new Date().getTime();
self.updateRotatorWidth();
webcamImage.attr("src", newSrc);
}
self._enableWebcam();
} else if (previous == "#control") {
// only disable webcam stream if tab is out of focus for more than 5s, otherwise we might cause
// more load by the constant connection creation than by the actual webcam stream
self.webcamDisableTimeout = setTimeout(function () {
$("#webcam_image").attr("src", "");
}, 5000);
self._disableWebcam();
}
};
self.onBrowserTabVisibilityChange = function(status) {
if (status) {
self._enableWebcam();
} else {
self._disableWebcam();
}
};
@ -565,7 +588,7 @@ $(function() {
OCTOPRINT_VIEWMODELS.push([
ControlViewModel,
["loginStateViewModel", "settingsViewModel"],
["loginStateViewModel", "settingsViewModel", "tabTracking"],
"#control"
]);
});

View file

@ -600,6 +600,10 @@ $(function() {
self.onEventMetadataStatisticsUpdated = function(payload) {
self.requestData();
};
self.onEventTransferDone = function(payload) {
self.requestData(payload.remote, "sdcard");
};
}
OCTOPRINT_VIEWMODELS.push([

View file

@ -46,7 +46,7 @@ $(function() {
};
self._sendData(data, function() {
// if the user indeed disables access control, we'll need to reload the page for this to take effect
//location.reload(true); // TODO: clear cache doesn't work properly, needs a better way, same issue with reloading plugins
showReloadOverlay();
});
});
$("#confirmation_dialog").modal("show");

View file

@ -5,6 +5,9 @@ $(function() {
self.loginState = parameters[0];
self.settings = parameters[1];
// TODO remove with release of 1.3.0 and switch to OctoPrint.coreui usage
self.tabTracking = parameters[2];
self.ui_progress_percentage = ko.observable();
self.ui_progress_type = ko.observable();
self.ui_progress_text = ko.computed(function() {
@ -339,6 +342,21 @@ $(function() {
self._processData(data);
};
self._renderPercentage = function(percentage) {
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(percentage);
if (!cmdIndex) return;
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer);
if (self.layerSlider != undefined) {
self.layerSlider.slider("setValue", cmdIndex.layer);
}
if (self.layerCommandSlider != undefined) {
self.layerCommandSlider.slider("setValue", [0, cmdIndex.cmd]);
}
};
self._processData = function(data) {
if (!data.job.file || !data.job.file.name && (self.loadedFilename || self.loadedFileDate)) {
self.waitForApproval(false);
@ -358,19 +376,8 @@ $(function() {
if(self.loadedFilename
&& self.loadedFilename == data.job.file.name
&& self.loadedFileDate == data.job.file.date) {
if (self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) {
var cmdIndex = GCODE.gCodeReader.getCmdIndexForPercentage(data.progress.completion);
if(cmdIndex){
GCODE.renderer.render(cmdIndex.layer, 0, cmdIndex.cmd);
GCODE.ui.updateLayerInfo(cmdIndex.layer);
if (self.layerSlider != undefined) {
self.layerSlider.slider("setValue", cmdIndex.layer);
}
if (self.layerCommandSlider != undefined) {
self.layerCommandSlider.slider("setValue", [0, cmdIndex.cmd]);
}
}
if (self.tabTracking.browserTabVisible && self.tabActive && self.currentlyPrinting && self.renderer_syncProgress() && !self.waitForApproval()) {
self._renderPercentage(data.progress.completion);
}
self.errorCount = 0
} else {
@ -394,6 +401,12 @@ $(function() {
}
};
self.onEventPrintDone = function() {
if (self.renderer_syncProgress() && !self.waitForApproval()) {
self._renderPercentage(100.0);
}
};
self.approveLargeFile = function() {
self.waitForApproval(false);
self.loadFile(self.selectedFile.name(), self.selectedFile.date());
@ -504,13 +517,16 @@ $(function() {
self.onBeforeBinding = function() {
self.initialize();
}
};
self.onTabChange = function(current, previous) {
self.tabActive = current == "#gcode";
};
}
OCTOPRINT_VIEWMODELS.push([
GcodeViewModel,
["loginStateViewModel", "settingsViewModel"],
["loginStateViewModel", "settingsViewModel", "tabTracking"],
"#gcode"
]);
});

View file

@ -2,8 +2,8 @@ $(function() {
function LoginStateViewModel() {
var self = this;
self.loginUser = ko.observable();
self.loginPass = ko.observable();
self.loginUser = ko.observable("");
self.loginPass = ko.observable("");
self.loginRemember = ko.observable(false);
self.loggedIn = ko.observable(false);
@ -79,10 +79,6 @@ $(function() {
var password = self.loginPass();
var remember = self.loginRemember();
self.loginUser("");
self.loginPass("");
self.loginRemember(false);
$.ajax({
url: API_BASEURL + "login",
type: "POST",
@ -90,6 +86,10 @@ $(function() {
success: function(response) {
new PNotify({title: gettext("Login successful"), text: _.sprintf(gettext('You are now logged in as "%(username)s"'), {username: response.name}), type: "success"});
self.fromResponse(response);
self.loginUser("");
self.loginPass("");
self.loginRemember(false);
},
error: function(jqXHR, textStatus, errorThrown) {
new PNotify({title: gettext("Login failed"), text: gettext("User unknown or wrong password"), type: "error"});
@ -138,4 +138,4 @@ $(function() {
[],
[]
]);
});
});

View file

@ -15,7 +15,7 @@ $(function() {
height: 200,
origin: "lowerleft"
},
heatedBed: false,
heatedBed: true,
axes: {
x: {speed: 6000, inverted: false},
y: {speed: 6000, inverted: false},

View file

@ -250,9 +250,20 @@ $(function() {
}
});
});
// reset scroll position on tab change
$('ul.nav-list a[data-toggle="tab"]', self.settingsDialog).on("show", function() {
self._resetScrollPosition();
});
};
self.show = function() {
self.show = function(tab) {
// select first or specified tab
self.selectTab(tab);
// reset scroll position
self._resetScrollPosition();
// show settings, ensure centered position
self.settingsDialog.modal({
minHeight: function() { return Math.max($.fn.modal.defaults.maxHeight() - 80, 250); }
@ -587,6 +598,21 @@ $(function() {
self.onEventSettingsUpdated = function() {
self.requestData();
};
self._resetScrollPosition = function() {
$('.scrollable', self.settingsDialog).scrollTop(0);
};
self.selectTab = function(tab) {
if (tab != undefined) {
if (!_.startsWith(tab, "#")) {
tab = "#" + tab;
}
$('ul.nav-list a[href="' + tab + '"]', self.settingsDialog).tab("show");
} else {
$('ul.nav-list a:first', self.settingsDialog).tab("show");
}
};
}
OCTOPRINT_VIEWMODELS.push([

View file

@ -36,7 +36,7 @@ $(function() {
self.heaterOptions = ko.observable({});
self._numExtrudersUpdated = function() {
self._printerProfileUpdated = function() {
var graphColors = ["red", "orange", "green", "brown", "purple"];
var heaterOptions = {};
var tools = self.tools();
@ -69,15 +69,21 @@ $(function() {
}
// print bed
heaterOptions["bed"] = {name: gettext("Bed"), color: "blue"};
if (self.settingsViewModel.printerProfiles.currentProfileData().heatedBed()) {
self.hasBed(true);
heaterOptions["bed"] = {name: gettext("Bed"), color: "blue"};
} else {
self.hasBed(false);
}
// write back
self.heaterOptions(heaterOptions);
self.tools(tools);
};
self.settingsViewModel.printerProfiles.currentProfileData.subscribe(function() {
self._numExtrudersUpdated();
self.settingsViewModel.printerProfiles.currentProfileData().extruder.count.subscribe(self._numExtrudersUpdated);
self._printerProfileUpdated();
self.settingsViewModel.printerProfiles.currentProfileData().extruder.count.subscribe(self._printerProfileUpdated);
self.settingsViewModel.printerProfiles.currentProfileData().heatedBed.subscribe(self._printerProfileUpdated());
});
self.temperatures = [];
@ -152,11 +158,8 @@ $(function() {
}
if (lastData.hasOwnProperty("bed")) {
self.hasBed(true);
self.bedTemp["actual"](lastData.bed.actual);
self.bedTemp["target"](lastData.bed.target);
} else {
self.hasBed(false);
}
if (!CONFIG_TEMPERATURE_GRAPH) return;
@ -208,8 +211,6 @@ $(function() {
if (!d[type]) return;
result[type].actual.push([time, d[type].actual]);
result[type].target.push([time, d[type].target]);
self.hasBed(self.hasBed() || (type == "bed"));
})
});
@ -375,4 +376,4 @@ $(function() {
["loginStateViewModel", "settingsViewModel"],
"#temp"
]);
});
});

View file

@ -7,6 +7,7 @@ $(function() {
self.log = ko.observableArray([]);
self.buffer = ko.observable(300);
self.upperLimit = ko.observable(3000);
self.command = ko.observable(undefined);
@ -34,7 +35,8 @@ $(function() {
var filtered = false;
var result = [];
_.each(self.log(), function(entry) {
var lines = self.log();
_.each(lines, function(entry) {
if (lineVisible(entry)) {
result.push(entry);
filtered = false;
@ -46,19 +48,30 @@ $(function() {
return result;
});
self.displayedLines.subscribe(function() {
self.updateOutput();
});
self.lineCount = ko.computed(function() {
var total = self.log().length;
var displayed = _.filter(self.displayedLines(), function(entry) { return entry.type == "line" }).length;
var regex = self.filterRegex();
var lineVisible = function(entry) {
return regex == undefined || !entry.line.match(regex);
};
var lines = self.log();
var total = lines.length;
var displayed = _.filter(lines, lineVisible).length;
var filtered = total - displayed;
if (total == displayed) {
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
if (filtered > 0) {
if (total > self.upperLimit()) {
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered, buffer full)"), {displayed: displayed, total: total, filtered: filtered});
} else {
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
}
} else {
return _.sprintf(gettext("showing %(displayed)d lines (%(filtered)d of %(total)d total lines filtered)"), {displayed: displayed, total: total, filtered: filtered});
if (total > self.upperLimit()) {
return _.sprintf(gettext("showing %(displayed)d lines (buffer full)"), {displayed: displayed});
} else {
return _.sprintf(gettext("showing %(displayed)d lines"), {displayed: displayed});
}
}
});
@ -84,14 +97,31 @@ $(function() {
};
self._processCurrentLogData = function(data) {
self.log(self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) })));
if (self.autoscrollEnabled()) {
self.log(self.log.slice(-self.buffer()));
var length = self.log().length;
if (length >= self.upperLimit()) {
var cutoff = "--- too many lines to buffer, cut off ---";
var last = self.log()[length-1];
if (!last || last.type != "cut" || last.line != cutoff) {
self.log(self.log().concat(self._toInternalFormat(cutoff, "cut")));
}
return;
}
var newLog = self.log().concat(_.map(data, function(line) { return self._toInternalFormat(line) }));
if (self.autoscrollEnabled()) {
// we only keep the last <buffer> entries
newLog = newLog.slice(-self.buffer());
} else if (newLog.length > self.upperLimit()) {
// we only keep the first <upperLimit> entries
newLog = newLog.slice(0, self.upperLimit());
}
self.log(newLog);
self.updateOutput();
};
self._processHistoryLogData = function(data) {
self.log(_.map(data, function(line) { return self._toInternalFormat(line) }));
self.updateOutput();
};
self._toInternalFormat = function(line, type) {
@ -141,7 +171,7 @@ $(function() {
self.scrollToEnd = function() {
var container = $("#terminal-output");
if (container.length) {
container.scrollTop(container[0].scrollHeight - container.height())
container.scrollTop(container[0].scrollHeight);
}
};
@ -232,4 +262,4 @@ $(function() {
["loginStateViewModel", "settingsViewModel"],
"#term"
]);
});
});

View file

@ -396,10 +396,17 @@ ul.dropdown-menu li a {
left: 0;
width: 100%;
height: 100%;
z-index: 10001;
display: none;
}
#offline_overlay {
z-index: 10002;
}
#reloadui_overlay {
z-index: 10001;
}
#offline_overlay_background,
#reloadui_overlay_background {
position: fixed;

View file

@ -33,4 +33,12 @@
var OCTOPRINT_VIEWMODELS = [];
var ADDITIONAL_VIEWMODELS = [];
var OCTOPRINT_ADDITIONAL_BINDINGS = [];
{% if templates.tab.order %}
{% set first_tab = templates.tab.order[0] %}
{% set entry, data = templates.tab.entries[first_tab] %}
var OCTOPRINT_INITIAL_TAB = "#{{ data._div }}";
{% else %}
var OCTOPRINT_INITIAL_TAB = undefined;
{% endif %}
</script>

View file

@ -1,3 +1,3 @@
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog" data-bind="click: show">
<a id="navbar_show_settings" class="pull-right" href="#settings_dialog" data-bind="click: function() { $root.show(); }">
<i class="icon-wrench"></i> {{ _('Settings') }}
</a>

View file

@ -1,4 +1,4 @@
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-bind="visible: systemActions().length > 0">
<i class="icon-off"></i> {{ _('System') }}
<b class="caret"></b>
</a>

View file

@ -1,5 +1,5 @@
<div class="settings-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<a class="dropdown-toggle" data-toggle="dropdown" href="#" title="{{ _('File list settings') }}">
<span class="icon-wrench"></span>
</a>
<ul class="dropdown-menu">
@ -19,9 +19,15 @@
</ul>
</div>
<div class="refresh-trigger accordion-heading-button btn-group">
<a href="#" data-bind="click: function() { $root.requestData(); }" title="{{ _('Refresh file list') }}">
<span class="icon-refresh"></span>
</a>
</div>
{% if enableSdSupport %}
<div class="sd-trigger accordion-heading-button btn-group">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<a class="dropdown-toggle" data-toggle="dropdown" href="#" title="{{ _('SD Card operations') }}">
<span class="icon-sd-black-14"></span>
</a>
<ul class="dropdown-menu">

View file

@ -1,5 +1,5 @@
<div class="terminal">
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered'}"></span><br></pre>
<pre id="terminal-output" class="pre-scrollable" data-bind="foreach: displayedLines"><span data-bind="text: line, css: {muted: type == 'filtered' || type == 'cut'}"></span><br></pre>
<small class="pull-left"><button class="btn btn-mini" data-bind="click: toggleAutoscroll, css: {active: autoscrollEnabled}">{{ _('Autoscroll') }}</button> <span data-bind="text: lineCount"></span></small>
<small class="pull-right"><a href="#" data-bind="click: scrollToEnd">{{ _("Scroll to end") }}</a>&nbsp;|&nbsp;<a href="#" data-bind="click: selectAll">{{ _("Select all") }}</a></small>
</div>

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,21 @@ class UserManager(object):
self._logger = logging.getLogger(__name__)
self._session_users_by_session = dict()
self._session_users_by_userid = dict()
self._enabled = True
@property
def enabled(self):
return self._enabled
@enabled.setter
def enabled(self, value):
self._enabled = value
def enable(self):
self._enabled = True
def disable(self):
self._enabled = False
def login_user(self, user):
self._cleanup_sessions()

View file

@ -542,6 +542,48 @@ def dict_contains_keys(keys, dictionary):
return True
def dict_filter(dictionary, filter_function):
"""
Filters a dictionary with the provided filter_function
Example::
>>> data = dict(key1="value1", key2="value2", other_key="other_value", foo="bar", bar="foo")
>>> dict_filter(data, lambda k, v: k.startswith("key")) == dict(key1="value1", key2="value2")
True
>>> dict_filter(data, lambda k, v: v.startswith("value")) == dict(key1="value1", key2="value2")
True
>>> dict_filter(data, lambda k, v: k == "foo" or v == "foo") == dict(foo="bar", bar="foo")
True
>>> dict_filter(data, lambda k, v: False) == dict()
True
>>> dict_filter(data, lambda k, v: True) == data
True
>>> dict_filter(None, lambda k, v: True)
Traceback (most recent call last):
...
AssertionError
>>> dict_filter(data, None)
Traceback (most recent call last):
...
AssertionError
Arguments:
dictionary (dict): The dictionary to filter
filter_function (callable): The filter function to apply, called with key and
value of an entry in the dictionary, must return ``True`` for values to
keep and ``False`` for values to strip
Returns:
dict: A shallow copy of the provided dictionary, stripped of the key-value-pairs
for which the ``filter_function`` returned ``False``
"""
assert isinstance(dictionary, dict)
assert callable(filter_function)
return dict((k, v) for k, v in dictionary.items() if filter_function(k, v))
class Object(object):
pass
@ -584,8 +626,10 @@ def address_for_client(host, port):
@contextlib.contextmanager
def atomic_write(filename, mode="w+b", prefix="tmp", suffix=""):
temp_config = tempfile.NamedTemporaryFile(mode=mode, prefix=prefix, suffix=suffix, delete=False)
yield temp_config
temp_config.close()
try:
yield temp_config
finally:
temp_config.close()
shutil.move(temp_config.name, filename)
@ -609,7 +653,32 @@ def bom_aware_open(filename, encoding="ascii", mode="r", **kwargs):
if header.startswith(bom):
encoding += "-sig"
return codecs.open(filename, encoding=encoding, **kwargs)
return codecs.open(filename, encoding=encoding, mode=mode, **kwargs)
def is_hidden_path(path):
if path is None:
# we define a None path as not hidden here
return False
filename = os.path.basename(path)
if filename.startswith("."):
# filenames starting with a . are hidden
return True
if sys.platform == "win32":
# if we are running on windows we also try to read the hidden file
# attribute via the windows api
try:
import ctypes
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(path))
assert attrs != -1 # INVALID_FILE_ATTRIBUTES == -1
return bool(attrs & 2) # FILE_ATTRIBUTE_HIDDEN == 2
except (AttributeError, AssertionError):
pass
# if we reach that point, the path is not hidden
return False
class RepeatedTimer(threading.Thread):

View file

@ -262,6 +262,7 @@ class MachineCom(object):
self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")
# SD status data
self._sdEnabled = settings().getBoolean(["feature", "sdSupport"])
self._sdAvailable = False
self._sdFileList = False
self._sdFiles = []
@ -719,21 +720,28 @@ class MachineCom(object):
return self._sdFiles
def startSdFileTransfer(self, filename):
if not self.isOperational() or self.isBusy():
if not self._sdEnabled:
return
if not self.isOperational() or self.isBusy():
return
self._changeState(self.STATE_TRANSFERING_FILE)
self.sendCommand("M28 %s" % filename.lower())
def endSdFileTransfer(self, filename):
if not self.isOperational() or self.isBusy():
if not self._sdEnabled:
return
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M29 %s" % filename.lower())
self._changeState(self.STATE_OPERATIONAL)
self.refreshSdFiles()
def deleteSdFile(self, filename):
if not self._sdEnabled:
return
if not self.isOperational() or (self.isBusy() and
isinstance(self._currentFile, PrintingSdFileInformation) and
self._currentFile.getFilename() == filename):
@ -744,13 +752,21 @@ class MachineCom(object):
self.refreshSdFiles()
def refreshSdFiles(self):
if not self._sdEnabled:
return
if not self.isOperational() or self.isBusy():
return
self.sendCommand("M20")
def initSdCard(self):
if not self._sdEnabled:
return
if not self.isOperational():
return
self.sendCommand("M21")
if settings().getBoolean(["feature", "sdAlwaysAvailable"]):
self._sdAvailable = True
@ -758,6 +774,9 @@ class MachineCom(object):
self._callback.on_comm_sd_state_change(self._sdAvailable)
def releaseSdCard(self):
if not self._sdEnabled:
return
if not self.isOperational() or (self.isBusy() and self.isSdFileSelected()):
# do not release the sd card if we are currently printing from it
return
@ -1098,13 +1117,7 @@ class MachineCom(object):
### Operational
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
if "ok" in line:
# if we still have commands to process, process them
if self._resendSwallowNextOk:
self._resendSwallowNextOk = False
elif self._resendDelta is not None:
self._resendNextCommand()
elif self._sendFromQueue():
pass
self._handle_ok()
# resend -> start resend procedure from requested line
elif line.lower().startswith("resend") or line.lower().startswith("rs"):
@ -1122,18 +1135,8 @@ class MachineCom(object):
if "ok" in line or (supportWait and "wait" in line):
# a wait while printing means our printer's buffer ran out, probably due to some ok getting
# swallowed, so we treat it the same as an ok here teo take up communication again
if self._resendSwallowNextOk:
self._resendSwallowNextOk = False
elif self._resendDelta is not None:
self._resendNextCommand()
else:
if self._sendFromQueue():
pass
elif not self.isSdPrinting():
self._sendNext()
# swallowed, so we treat it the same as an ok here to take up communication again
self._handle_ok()
elif line.lower().startswith("resend") or line.lower().startswith("rs"):
self._handleResendRequest(line)
@ -1147,6 +1150,24 @@ class MachineCom(object):
eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
self._log("Connection closed, closing down monitor")
def _handle_ok(self):
if not self._state in (self.STATE_PRINTING, self.STATE_OPERATIONAL, self.STATE_PAUSED):
return
if self._resendSwallowNextOk:
self._resendSwallowNextOk = False
elif self._resendDelta is not None:
self._resendNextCommand()
else:
self._continue_sending()
def _continue_sending(self):
if self._state == self.STATE_PRINTING:
if not self._sendFromQueue() and not self.isSdPrinting():
self._sendNext()
elif self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PAUSED:
self._sendFromQueue()
def _process_registered_message(self, line, feedback_matcher, feedback_controls, feedback_errors):
feedback_match = feedback_matcher.search(line)
if feedback_match is None:
@ -1221,20 +1242,29 @@ class MachineCom(object):
self.sendGcodeScript("afterPrinterConnected", replacements=dict(event=payload))
def _sendFromQueue(self):
if not self._commandQueue.empty() and not self.isStreaming():
# We loop here to make sure that if we do NOT send the first command
# from the queue, we'll send the second (if there is one). We do not
# want to get stuck here by throwing away commands.
while True:
if self._commandQueue.empty() or self.isStreaming():
# no command queue or irrelevant command queue => return
return False
entry = self._commandQueue.get()
if isinstance(entry, tuple):
if not len(entry) == 2:
return False
# something with that entry is broken, ignore it and fetch
# the next one
continue
cmd, cmd_type = entry
else:
cmd = entry
cmd_type = None
self._sendCommand(cmd, cmd_type=cmd_type)
return True
else:
return False
if self._sendCommand(cmd, cmd_type=cmd_type):
# we actually did add this cmd to the send queue, so let's
# return, we are done here
return True
def _detectPort(self, close):
programmer = stk500v2.Stk500v2()
@ -1314,6 +1344,11 @@ class MachineCom(object):
return False
def _handleErrors(self, line):
if line is None:
return
lower_line = line.lower()
# No matter the state, if we see an error, goto the error state and store the error for reference.
if line.startswith('Error:') or line.startswith('!!'):
#Oh YEAH, consistency.
@ -1323,14 +1358,18 @@ class MachineCom(object):
if regex_minMaxError.match(line):
line = line.rstrip() + self._readline()
if 'line number' in line.lower() or 'checksum' in line.lower() or 'format error' in line.lower() or 'expected line' in line.lower():
if 'line number' in lower_line or 'checksum' in lower_line or 'format error' in lower_line or 'expected line' in lower_line:
#Skip the communication errors, as those get corrected.
self._lastCommError = line[6:] if line.startswith("Error:") else line[2:]
pass
elif 'volume.init' in line.lower() or "openroot" in line.lower() or 'workdir' in line.lower()\
or "error writing to file" in line.lower():
elif 'volume.init' in lower_line or "openroot" in lower_line or 'workdir' in lower_line\
or "error writing to file" in lower_line or "cannot open" in lower_line\
or "cannot enter" in lower_line:
#Also skip errors with the SD card
pass
elif 'unknown command' in lower_line:
#Ignore unkown command errors, it could be a typo or some missing feature
pass
elif not self.isError():
self._errorValue = line[6:] if line.startswith("Error:") else line[2:]
self._changeState(self.STATE_ERROR)
@ -1471,7 +1510,7 @@ class MachineCom(object):
# Make sure we are only handling one sending job at a time
with self._sendingLock:
if self._serial is None:
return
return False
gcode = None
if not self.isStreaming():
@ -1480,7 +1519,7 @@ class MachineCom(object):
if cmd is None:
# command is no more, return
return
return False
if gcode and gcode in gcodeToEvent:
# if this is a gcode bound to an event, trigger that now
@ -1493,6 +1532,8 @@ class MachineCom(object):
# trigger the "queued" phase only if we are not streaming to sd right now
self._process_command_phase("queued", cmd, cmd_type, gcode=gcode)
return True
##~~ send loop handling
def _enqueue_for_sending(self, command, linenumber=None, command_type=None):
@ -1545,7 +1586,14 @@ class MachineCom(object):
command, _, gcode = self._process_command_phase("sending", command, command_type, gcode=gcode)
if command is None:
# so no, we are not going to send this, that was a last-minute bail, let's fetch the next item from the queue
# No, we are not going to send this, that was a last-minute bail.
# However, since we already are in the send queue, our _monitor
# loop won't be triggered with the reply from this unsent command
# now, so we try to tickle the processing of any active
# command queues manually
self._continue_sending()
# and now let's fetch the next item from the queue
continue
# now comes the part where we increase line numbers and send stuff - no turning back now
@ -1564,9 +1612,14 @@ class MachineCom(object):
if gcode is not None:
use_up_clear = True
# if we need to use up a clear, do that now
if use_up_clear:
# if we need to use up a clear, do that now
self._clear_to_send.clear()
else:
# Otherwise we need to tickle the read queue - there might not be a reply
# to this command, so our _monitor loop will stay waiting until timeout. We
# definitely do not want that, so we tickle the queue manually here
self._continue_sending()
# now we just wait for the next clear and then start again
self._clear_to_send.wait()
@ -1707,6 +1760,12 @@ class MachineCom(object):
if self.isPrinting() and not self.isSdPrinting():
self.setPause(True)
def _gcode_M140_queuing(self, cmd, cmd_type=None):
if not self._printerProfileManager.get_current_or_default()["heatedBed"]:
self._log("Warn: Not sending \"{}\", printer profile has no heated bed".format(cmd))
return None, # Don't send bed commands if we don't have a heated bed
_gcode_M190_queuing = _gcode_M140_queuing
def _gcode_M104_sent(self, cmd, cmd_type=None):
toolNum = self._currentTool
toolMatch = regexes_parameters["intT"].search(cmd)
@ -1780,7 +1839,8 @@ class MachineCom(object):
# are going to shutdown the connection in a second anyhow.
for tool in range(self._printerProfileManager.get_current_or_default()["extruder"]["count"]):
self._doIncrementAndSendWithChecksum("M104 T{tool} S0".format(tool=tool))
self._doIncrementAndSendWithChecksum("M140 S0")
if self._printerProfileManager.get_current_or_default()["heatedBed"]:
self._doIncrementAndSendWithChecksum("M140 S0")
# close to reset host state
self._errorValue = "Closing serial port due to emergency stop M112."

108
src/octoprint/util/jinja.py Normal file
View file

@ -0,0 +1,108 @@
# coding=utf-8
from __future__ import absolute_import
__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 os
from jinja2.loaders import FileSystemLoader, PrefixLoader, ChoiceLoader, \
TemplateNotFound, split_template_path
class FilteredFileSystemLoader(FileSystemLoader):
"""
Jinja2 ``FileSystemLoader`` subclass that allows filtering templates.
Only such templates will be accessible for whose paths the provided
``path_filter`` filter function returns True.
``path_filter`` will receive the actual path on disc and should behave just
like callables provided to Python's internal ``filter`` function, returning
``True`` if the path is cleared and ``False`` if it is supposed to be removed
from results and hence ``filter(path_filter, iterable)`` should be
equivalent to ``[item for item in iterable if path_filter(item)]``.
If ``path_filter`` is not set or not a ``callable``, the loader will
behave just like the regular Jinja2 ``FileSystemLoader``.
"""
def __init__(self, searchpath, path_filter=None, **kwargs):
FileSystemLoader.__init__(self, searchpath, **kwargs)
self.path_filter = path_filter
def get_source(self, environment, template):
if callable(self.path_filter):
pieces = split_template_path(template)
if not self._combined_filter(os.path.join(*pieces)):
raise TemplateNotFound(template)
return FileSystemLoader.get_source(self, environment, template)
def list_templates(self):
result = FileSystemLoader.list_templates(self)
if callable(self.path_filter):
result = sorted(filter(self._combined_filter, result))
return result
def _combined_filter(self, path):
filter_results = map(lambda x: not os.path.exists(os.path.join(x, path)) or self.path_filter(os.path.join(x, path)),
self.searchpath)
return all(filter_results)
def get_all_template_paths(loader):
def walk_folder(folder):
files = []
walk_dir = os.walk(folder, followlinks=True)
for dirpath, dirnames, filenames in walk_dir:
for filename in filenames:
path = os.path.join(dirpath, filename)
files.append(path)
return files
def collect_templates_for_loader(loader):
if isinstance(loader, FilteredFileSystemLoader):
result = []
for folder in loader.searchpath:
result += walk_folder(folder)
return filter(loader.path_filter, result)
elif isinstance(loader, FileSystemLoader):
result = []
for folder in loader.searchpath:
result += walk_folder(folder)
return result
elif isinstance(loader, PrefixLoader):
result = []
for subloader in loader.mapping.values():
result += collect_templates_for_loader(subloader)
return result
elif isinstance(loader, ChoiceLoader):
result = []
for subloader in loader.loaders:
result += collect_templates_for_loader(subloader)
return result
return []
return collect_templates_for_loader(loader)
def get_all_asset_paths(env):
result = []
for bundle in env:
for content in bundle.resolve_contents():
try:
if not content:
continue
path = content[1]
if not os.path.isfile(path):
continue
result.append(path)
except IndexError:
# intentionally ignored
pass
return result

View file

@ -309,8 +309,8 @@ class LocalStorageTest(unittest.TestCase):
@data(
("some_file.gco", "some_file.gco"),
("some_file with (parentheses) and ümläuts and digits 123.gco", "some_file_with_(parentheses)_and_mluts_and_digits_123.gco"),
("pengüino pequeño.stl", "pengino_pequeo.stl")
("some_file with (parentheses) and ümläuts and digits 123.gco", "some_file_with_(parentheses)_and_umlauts_and_digits_123.gco"),
("pengüino pequeño.stl", "penguino_pequeno.stl")
)
@unpack
def test_sanitize_name(self, input, expected):

View file

@ -0,0 +1 @@
hidden_everywhere

View file

@ -0,0 +1 @@
normal_text

View file

@ -0,0 +1 @@
not_a_text

View file

@ -0,0 +1,3 @@
# This is a text file encoded in UTF8 with a BOM
Here are some umlauts: äöüÄÖÜß

View file

@ -0,0 +1,3 @@
# This is a text file encoded in UTF8 without a BOM
Here are some umlauts: äöüÄÖÜß

View file

@ -0,0 +1,208 @@
# coding=utf-8
from __future__ import absolute_import
__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 unittest
import mock
import os
import ddt
import sys
import octoprint.util
class BomAwareOpenTest(unittest.TestCase):
"""
Tests for :func:`octoprint.util.bom_aware_open`.
"""
def setUp(self):
self.filename_utf8_with_bom = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "utf8_with_bom.txt")
self.filename_utf8_without_bom = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "utf8_without_bom.txt")
def test_bom_aware_open_with_bom(self):
"""Tests that the contents of a UTF8 file with BOM are loaded correctly (without the BOM)."""
# test
with octoprint.util.bom_aware_open(self.filename_utf8_with_bom, encoding="utf-8") as f:
contents = f.readlines()
# assert
self.assertEquals(len(contents), 3)
self.assertTrue(contents[0].startswith("#"))
def test_bom_aware_open_without_bom(self):
"""Tests that the contents of a UTF8 file without BOM are loaded correctly."""
# test
with octoprint.util.bom_aware_open(self.filename_utf8_without_bom, encoding="utf-8") as f:
contents = f.readlines()
# assert
self.assertEquals(len(contents), 3)
self.assertTrue(contents[0].startswith("#"))
def test_bom_aware_open_ascii(self):
"""Tests that the contents of a UTF8 file loaded as ASCII are replaced correctly if "replace" is specified on errors."""
# test
with octoprint.util.bom_aware_open(self.filename_utf8_with_bom, errors="replace") as f:
contents = f.readlines()
# assert
self.assertEquals(len(contents), 3)
self.assertTrue(contents[0].startswith(u"\ufffd" * 3 + "#"))
self.assertTrue(contents[2].endswith(u"\ufffd\ufffd" * 6))
def test_bom_aware_open_encoding_error(self):
"""Tests that an encoding error is thrown if not suppressed when opening a UTF8 file as ASCII."""
try:
with octoprint.util.bom_aware_open(self.filename_utf8_without_bom) as f:
f.readlines()
self.fail("Expected an exception")
except UnicodeDecodeError:
pass
def test_bom_aware_open_parameters(self):
"""Tests that the parameters are propagated properly."""
with mock.patch("codecs.open") as mock_open:
with octoprint.util.bom_aware_open(self.filename_utf8_without_bom, mode="rb", encoding="utf-8", errors="ignore") as f:
f.readlines()
mock_open.assert_called_once_with(self.filename_utf8_without_bom, encoding="utf-8", mode="rb", errors="ignore")
class TestAtomicWrite(unittest.TestCase):
"""
Tests for :func:`octoprint.util.atomic_write`.
"""
def setUp(self):
pass
@mock.patch("shutil.move")
@mock.patch("tempfile.NamedTemporaryFile")
def test_atomic_write(self, mock_tempfile, mock_move):
"""Tests the regular basic "good" case."""
# setup
mock_file = mock.MagicMock()
mock_file.name = "tempfile.tmp"
mock_tempfile.return_value = mock_file
# test
with octoprint.util.atomic_write("somefile.yaml") as f:
f.write("test")
# assert
mock_tempfile.assert_called_once_with(mode="w+b", prefix="tmp", suffix="", delete=False)
mock_file.write.assert_called_once_with("test")
mock_file.close.assert_called_once_with()
mock_move.assert_called_once_with("tempfile.tmp", "somefile.yaml")
@mock.patch("shutil.move")
@mock.patch("tempfile.NamedTemporaryFile")
def test_atomic_write_error_on_write(self, mock_tempfile, mock_move):
"""Tests the error case where something in the wrapped code fails."""
# setup
mock_file = mock.MagicMock()
mock_file.name = "tempfile.tmp"
mock_file.write.side_effect = RuntimeError()
mock_tempfile.return_value = mock_file
# test
try:
with octoprint.util.atomic_write("somefile.yaml") as f:
f.write("test")
self.fail("Expected an exception")
except RuntimeError:
pass
# assert
mock_tempfile.assert_called_once_with(mode="w+b", prefix="tmp", suffix="", delete=False)
mock_file.close.assert_called_once_with()
self.assertFalse(mock_move.called)
@mock.patch("shutil.move")
@mock.patch("tempfile.NamedTemporaryFile")
def test_atomic_write_error_on_move(self, mock_tempfile, mock_move):
"""Tests the error case where the final move fails."""
# setup
mock_file = mock.MagicMock()
mock_file.name = "tempfile.tmp"
mock_tempfile.return_value = mock_file
mock_move.side_effect = RuntimeError()
# test
try:
with octoprint.util.atomic_write("somefile.yaml") as f:
f.write("test")
self.fail("Expected an exception")
except RuntimeError:
pass
# assert
mock_tempfile.assert_called_once_with(mode="w+b", prefix="tmp", suffix="", delete=False)
mock_file.close.assert_called_once_with()
self.assertTrue(mock_move.called)
@mock.patch("shutil.move")
@mock.patch("tempfile.NamedTemporaryFile")
def test_atomic_write_parameters(self, mock_tempfile, mock_move):
"""Tests that the open parameters are propagated properly."""
# setup
mock_file = mock.MagicMock()
mock_file.name = "tempfile.tmp"
mock_tempfile.return_value = mock_file
# test
with octoprint.util.atomic_write("somefile.yaml", mode="w", prefix="foo", suffix="bar") as f:
f.write("test")
# assert
mock_tempfile.assert_called_once_with(mode="w", prefix="foo", suffix="bar", delete=False)
mock_file.close.assert_called_once_with()
mock_move.assert_called_once_with("tempfile.tmp", "somefile.yaml")
@ddt.ddt
class IsHiddenPathTest(unittest.TestCase):
def setUp(self):
import tempfile
self.basepath = tempfile.mkdtemp()
self.path_always_visible = os.path.join(self.basepath, "always_visible.txt")
self.path_hidden_on_windows = os.path.join(self.basepath, "hidden_on_windows.txt")
self.path_always_hidden = os.path.join(self.basepath, ".always_hidden.txt")
for attr in ("path_always_visible", "path_hidden_on_windows", "path_always_hidden"):
path = getattr(self, attr)
with open(path, "w+b") as f:
f.write(attr)
import sys
if sys.platform == "win32":
# we use ctypes and the windows API to set the hidden attribute on the file
# only hidden on windows
import ctypes
ctypes.windll.kernel32.SetFileAttributesW(unicode(self.path_hidden_on_windows), 2)
def tearDown(self):
import shutil
shutil.rmtree(self.basepath)
@ddt.data(
(None, False),
("path_always_visible", False),
("path_always_hidden", True),
("path_hidden_on_windows", sys.platform == "win32")
)
@ddt.unpack
def test_is_hidden_path(self, path_id, expected):
path = getattr(self, path_id) if path_id is not None else None
self.assertEqual(octoprint.util.is_hidden_path(path), expected)

76
tests/util/test_jinja.py Normal file
View file

@ -0,0 +1,76 @@
# coding=utf-8
from __future__ import absolute_import
__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 unittest
import os
import jinja2
from ddt import ddt, data, unpack
import octoprint.util.jinja
NONE_FILTER = None
HIDDEN_FILTER = lambda x: not os.path.basename(x).startswith(".")
NO_TXT_FILTER = lambda x: x.endswith(".txt")
COMBINED_FILTER = lambda x: HIDDEN_FILTER(x) and NO_TXT_FILTER(x)
@ddt
class FilteredFileSystemLoaderTest(unittest.TestCase):
def setUp(self):
self.basepath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "_files", "jinja_test_data")
self.environment = jinja2.Environment()
def loader_factory(self, path_filter):
return octoprint.util.jinja.FilteredFileSystemLoader(self.basepath,
path_filter=path_filter)
@data(
(NONE_FILTER, [".hidden_everywhere.txt", "normal_text.txt", "not_a_text.dat"]),
(HIDDEN_FILTER, ["normal_text.txt", "not_a_text.dat"]),
(NO_TXT_FILTER, [".hidden_everywhere.txt", "normal_text.txt"]),
(COMBINED_FILTER, ["normal_text.txt"])
)
@unpack
def test_list_templates(self, path_filter, expected):
loader = self.loader_factory(path_filter=path_filter)
templates = loader.list_templates()
self.assertListEqual(templates, expected)
@data(
(NONE_FILTER, ((".hidden_everywhere.txt", True),
("normal_text.txt", True),
("not_a_text.dat", True))),
(HIDDEN_FILTER, ((".hidden_everywhere.txt", False),
("normal_text.txt", True),
("not_a_text.dat", True))),
(NO_TXT_FILTER, ((".hidden_everywhere.txt", True),
("normal_text.txt", True),
("not_a_text.dat", False))),
(COMBINED_FILTER, ((".hidden_everywhere.txt", False),
("normal_text.txt", True),
("not_a_text.dat", False)))
)
@unpack
def test_get_source_none_filter(self, path_filter, param_sets):
loader = self.loader_factory(path_filter=path_filter)
for param_set in param_sets:
template, success = param_set
if success:
self._test_get_source_success(loader, template)
else:
self._test_get_source_notfound(loader, template)
def _test_get_source_success(self, loader, template):
loader.get_source(self.environment, template)
def _test_get_source_notfound(self, loader, template):
try:
loader.get_source(self.environment, template)
self.fail("Expected an exception")
except jinja2.TemplateNotFound:
pass

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff