Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor export service to accomodate polling printer and disk #1615

Conversation

gonzalo-bulnes
Copy link
Contributor

@gonzalo-bulnes gonzalo-bulnes commented Dec 29, 2022

Description

Towards #1435, #590, #910, #988, #990, #1605, #1021

Potentially towards #1349, #1088

@cfm The preliminary refactoring in #1584 was already voluminous, and keeping the legacy interfaces was making the API large enough that I think it is worth pushing it until the legacy interfaces can be removed, as a refactoring, and from there add the "Print conversation" action, dialogs etc. A very large fraction of the export.Printer code in this PR comes directly from #1584.

Scope

This PR doesn't intend to address the issues it works towards, only enable future PRs to fix them (e.g. #1584).

Test Plan

  • Confirm that the test files that have changed make sense
  • Regression testing for exporting files
  • Regression testing for printing files

@gonzalo-bulnes gonzalo-bulnes force-pushed the refactor-export-service-to-accomodate-polling-printer-and-disk branch 7 times, most recently from 9e3abe2 to fbc4327 Compare December 30, 2022 02:39
@gonzalo-bulnes gonzalo-bulnes force-pushed the refactor-export-service-to-accomodate-polling-printer-and-disk branch 2 times, most recently from 6080b55 to 984e137 Compare January 2, 2023 02:55
@gonzalo-bulnes gonzalo-bulnes force-pushed the refactor-export-service-to-accomodate-polling-printer-and-disk branch 3 times, most recently from 5438466 to 9ff352c Compare January 16, 2023 23:59
The new directory structure will make extension easier.
Besides being the dedicated tool to assert on Qt signal emissions,
QSignalSpy can track when signals are emitted from other signals,
which the mocking strategy misses.

Example:

    some_signal.connect(some_other_signal)
    # verify that some_other_signal is emitted

See https://doc.qt.io/qt-5.15/qsignalspy.html
I'll use an expand and contract pattern to refactor
the export.Service tests, and keeping this file distinct
while it is used as a reference will be more convenient
and minimize the change sets.

Important: this file is still fully operational and will be
kept as such until all deprecated methods have been removed
from export.Export (soon to be renamed export.Service).
Refactor tests to:

- distinguish testing of GUI API, and CLI API internals
- improve readability
Decorate static methods (from what didn't need to be class methods)

Tests were minially modified: patching static methods must be done
unsing unittest.mock.patch.object to preserve the independence of
the unit tests. Assigning to the class attributes directly does
modify the class for the entire test session. (Not desirable.)
I am leaving the deprecated tests as untouched as possible to make
easier to review the changes.
This will ensure that one one instance of the export service
queries the sd-devices CLI.

That was already the case, but nothing was done to suggest it.

Additionally, this will allow to simplify the export service
injection by making possible to retrieve it like a logger.

Note: I haven't stopped injecting the export_service into
the main.Window because I couldn't figure out how to prevent
what seems to be a fixture scope error that affects only
half of the integration tests. That seems like something that
can be resolved, so I don't consider it to be a sign of
export.Service being a bad idea.
…ce (1)

These are now replaced by "printer check" and "disk check" which
have the advantage of referring to concrete physical devices.
@gonzalo-bulnes gonzalo-bulnes force-pushed the refactor-export-service-to-accomodate-polling-printer-and-disk branch from 9ff352c to c634cee Compare January 17, 2023 00:06
@gonzalo-bulnes
Copy link
Contributor Author

gonzalo-bulnes commented Jan 17, 2023

(rebased onto latest main, added all current work in progress)

@rocodes I need to go through this PR myself for a self-review, but everything should be pretty much in place.


Edit: I'll mark the PR as ready for review even with the caveat above, and let's start wrapping it up.

@gonzalo-bulnes gonzalo-bulnes marked this pull request as ready for review January 17, 2023 00:12
@gonzalo-bulnes gonzalo-bulnes requested a review from a team as a code owner January 17, 2023 00:12
@gonzalo-bulnes gonzalo-bulnes requested review from cfm and rocodes January 17, 2023 00:12
@rocodes rocodes self-assigned this Jan 17, 2023

class _Poller(QStateMachine):
"""Allow a function to ge called repeatedly, with a delay between calls.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]: typo, "ge" for "be"

securedrop_client/app.py Show resolved Hide resolved
Comment on lines +1 to +2
from .cli import Error as ExportError # noqa: F401
from .cli import Status as ExportStatus # noqa: F401
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two wouldn't be exported if it was up to me, because they leak an implementation detail IMHO. But I decided that fixing that was out of scope. Please let me know if you disagree, I'd happily fix it!!

securedrop_client/export/disk.py Outdated Show resolved Hide resolved
securedrop_client/export/printer.py Outdated Show resolved Hide resolved
securedrop_client/gui/conversation/export/dialog.py Outdated Show resolved Hide resolved
securedrop_client/gui/conversation/export/print_dialog.py Outdated Show resolved Hide resolved
Comment on lines -2445 to +2435
dialog = conversation.PrintFileDialog(self._export_device, self.uuid, self.file.filename)
file_location = self.file.location(self.controller.data_dir)
dialog = conversation.PrintFileDialog(self._printer, file_location, self.file.filename)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: mypy didn't catch when conversation.PrintFileDialog was receiving an export.Device but requiring an export.Printer. : /

Copy link
Contributor

@rocodes rocodes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @gonzalo-bulnes, thank you for preparing this. This is an initial visual review- I plan to step through regression testing tomorrow and may have more feedback then, but will finish my review before end of week. I also committed to looking at the testing a bit and that's still on my plate. :)

A few of the smaller inline comments are about documentation--since this code introduces some new patterns, it would be great to have more explanation in docstrings etc for those of us new to this style/changes.

One slightly larger question I have is surrounding service.py, disk.py, and printer.py. Since Disk and Printer are very similar, and are created/used in similar places, and rely on similar state machines, is there maybe an opportunity to DRY things up a bit? Or, further: do you think it's necessary to have all 3 classes, as opposed to moving the poll/state machine logic to service.py and dispensing with the other classes? (Essentially, I see a generic "Is the thing connected?" service, with configurable poll interval and a last known status, and I wonder if the printer and disk classes are adding extra complexity).

Another question I have is around the polling. Do you feel there's a benefit to introducing that code now, though we won't make use of it until farther down the line? (This is something I might tap another maintainer on for their opinion as well. I know there is precedent, since we introduced the service.py code in sd-export that is not yet in use, but my impression of polling was that it might be farther off as a milestone, since it would probably mean more client changes first. My inline comment goes into a bit more depth on that--would like to know your thoughts.)

Thank you again for all your hard work on this. More TK.


paused --> started: registered_starting_signal_emitted
started --> paused: registered_pausing_signal_emitted
"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the first time we are introducing this kind of architecture (QStateMachine) into the client, maybe we can include a bit more explanation here before the Mermaid diagram to outline the rationale for our future selves.

As well, in terms of polling, does it make sense to include this code now if we're not yet using polling? AIUI polling here will be a benefit/add-on that will only make it in once the 'wizard' workflow is working, or potentially concurrently with the wizard. Since that's a bigger change (i.e. we may not want to make an export tarball every time we send a qrexec command if we move to more frequent commands) I wonder if we should hold off on including this polling logic until we are closer to making use of it.

Copy link
Contributor

@rocodes rocodes Jan 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This comment is in kind of a weird place, it's meant to reference the Poller class)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From offline conversation: I'll add a comment to explain that given there are more than two states to keep track of, with multiple possible transitions, a dedicated state machine makes the logic easier to audit.



class Disk(QObject):
"""Allows to export files to an ecrypted disk and track its availability."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to have a little more detail in this docstring, especially on specific expectations around use (eg expectation of singleton/same Disk object being returned?), especially given this comment around getDisk:

# Store disks to prevent concurrent access to the export service. See getDisk.

AIUI disk and export service are both singletons retrieved in main.py and injected into the export dialog? Maybe we can explain a little further how they're used, since with a name like Disk I would have expected something that represents a specific physically inserted USB, not a singleton object

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(And I think this question of documenting the usage of Disk (and Printer) is superseded by my later comment, about whether Disk, Printer, and Export Service are all needed, but in any case the extra comment/documentation will be helpful)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From offline conversation: I'll add a note stating that the expected use of the Disk is getting an instance from getDisk.

securedrop_client/app.py Show resolved Hide resolved
self.status = status


class CLI:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Nit] Is this CLI, or more of a wrapper for Qrexec? (Same question for the filename)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From offline conversation: CLI may imply parsing of options. Let's rename this to Qrexec.

Status = NewType("Status", str)
StatusUnknown = Status("unknown-isw32")
StatusLUKSEncrypted = Status("luks-encrypted-8563d")
StatusUnreachable = Status("unreachable-ofbu4")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something I'm missing about these status strings and their suffixes (ofbu4, 8563d)?

Also, similar to a comment I made elsewhere, do we want StatusLUKSEncrypted, or do we want to abstract away the encryption type (is this basically a "valid" device? A valid writable device?)?

Copy link
Contributor Author

@gonzalo-bulnes gonzalo-bulnes Jan 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there something I'm missing about these status strings and their suffixes (ofbu4, 8563d)?

The suffixes are there to ensure we only compare them as enum constants, not by their string value which is meant to be an opaque string. In other words: if you ever find yourself wondering what the prefix was, you're doing something wrong and should use the constant instead (e.g. status == Disk.StatusUnreachable, not status == "unreachable").

I do that as standard practice whenever values are cast automatically instead of raising errors if comparisons are made that involve mismatched types. (e.g. comparing status: Disk.Status with "unreachable": str)

@gonzalo-bulnes gonzalo-bulnes force-pushed the refactor-export-service-to-accomodate-polling-printer-and-disk branch from 1cbf493 to 6d08613 Compare January 26, 2023 07:29
@gonzalo-bulnes
Copy link
Contributor Author

Given the decisions documented in #1621 that obsolete the improvements on the Printer/Disk tracking from the GUI, I'll leave this branch as it is now to focus on #1621 and next.

Thanks for your review @rocodes anyway, the comments and chat we had are still good context for whatever comes next.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants