OpenPrinting / ipp-usb

ipp-usb -- HTTP reverse proxy, backed by IPP-over-USB connection to device
BSD 2-Clause "Simplified" License
129 stars 11 forks source link

ipp-usb and USB backend: do not claim IPP-over-USB endpoints until needed #28

Closed michaelrsweet closed 3 years ago

michaelrsweet commented 3 years ago

@bitwombat reported issues printing to a Brother printer via USB: Apple Issue #5908

Basically, this is an AirPrint printer that supports IPP-USB. When connected, ippusbxd claims the endpoints used for IPP-USB, which happen to also be the endpoints needed for legacy USB printing. Therefore, the USB backend can see it but not access (claim) it.

We should hide such printers so that the IPP-USB listing gets used. And in particular that means the IPP Everywhere driver can be used instead of a vendor driver.

Possible solutions:

  1. When we see that a USB printer supports protocol 4 (IPP-USB), see if that endpoint/device is already claimed and if so skip it. (not sure libusb offers this functionality)
  2. When we see that a USB printer supports protocol 4 (IPP-USB), see if ippusbxd is present and if so skip it (might not work so well for snapped CUPS)
  3. Add a configure option that enables code in the USB backend to skip IPP-USB printers ("--with-ipp-usb")

Option 1 is probably the best solution, then 3.

michaelrsweet commented 3 years ago

Actually, it would be better if ippusbxd did not claim the IPP-USB interfaces until a connection comes in.

Transferring this to the cups-filters project.

tillkamppeter commented 3 years ago

@michaelrsweet The IPP-over-USB daemon which is currently used by default is ipp-usb and this is not part of CUPS filters but it is its own project on OpenPrinting.

Moving the issue on to ipp-usb ...

@alexpevzner, could you have a look and help @bitwombat here? Thanks.

@michaelrsweet by the way, ippusbxd is discontinued due to the superior ipp-usb.

alexpevzner commented 3 years ago

Simultaneous access to device using IPP over USB on one interface and legacy printer driver (Class 7 Proto 2) on another interface on some devices work unreliable; it causes IPP over USB session to fail (see #26, for example).

Also, CUPS performs class-specific SOFT_RESET on interfaces it uses. I experimented with SOFT_RESET in the ipp-usb, with a purpose to restore synchronization with device after previously failed ippusbxd session; on some devices it causes problems (see #17)

So I'd prefer to avoid dynamic allocation of the IPP over USB interfaces, I believe it will not work reliable in all cases.

There are also devices, that claims IPP over USB support (Class 7 Proto 4), but it actually doesn't work (see https://github.com/alexpevzner/sane-airscan/issues/68). So it's not enough for CUPS to skip all devices that support Class 7, Proto 4; CUPS should check that ipp-usb actually handles the device.

So the solution 1 from the Michael's list looks most reliable and promising. libusb returns LIBUSB_ERROR_BUSY on attempt to open an interface already claimed by another process; CUPS should attempt to claim appropriate USB interfaces before offering the device.

Alternatively, we can add some IPC channel (in a form of AF_UNIX socket), where ipp-usb could report which devices it actually serves.

michaelrsweet commented 3 years ago

@alexpevzner If ipp-usb has claimed a USB device then the USB backend can never do so. We explicitly don't try to claim a device when enumerating available printers because a) the printer might be in use, and b) doing so can have some unfortunate side-effects (slows down enumeration, wakes up printer, potential kernel driver issues, etc.)

The reason this bug ended up here is that there are multiple Printer Applications (i.e. not just CUPS) that will soon be competing to talk to USB-connected printers, and if ipp-usb always claims the IPP-USB interfaces then they will never be able to print over legacy USB (I'm primarily thinking of Gutenprint users). Their solution will be to remove ipp-usb entirely vs. just using the protocol they want to talk to the printer, which probably isn't what we want... :)

I'm not asking for dynamic allocation. I'm saying that until you get a socket connection there is no need to claim any of the IPP-USB interfaces. Once ipp-usb is in use, claim them all and hold them until you haven't had any traffic for some amount of time (2 minutes), then shut everything down.

alexpevzner commented 3 years ago

@michaelrsweet,

I need to claim USB interfaces at least to query for device characteristics, which I announce via DNS-SD.

If one half of device (say, printing) is served by legacy driver, while another half (say, scanning) is served driverlessly, it may lead to ugly interference, when user switches from scanning to printing or visa versa.

I understand the need of switching between IPP over USB and legacy access for some users (mostly in cases when device offers more functionality via proprietary USB driver rather that in driverless manner), but can't figure out the reliable approach to implement it.

michaelrsweet commented 3 years ago

@alexpevzner Well, can you claim the interface long enough to do the query, then release it until somebody else wants to talk over IPP-USB?

alexpevzner commented 3 years ago

If somebody will steal a device when it is already advertised, we will get a lot of complains about "device found but doesn't work" both at printing and scanning side.

Can we make selection between legacy/driverless access modes more explicit for users rather that "first attempt wins"?

michaelrsweet commented 3 years ago

@alexpevzner I don't see how. Right now a legacy USB printer is only claimed when there is an active print job. However, an IPP-USB printer is claimed as soon as the printer is connected and never released. Either we need to have ipp-usb only claim the IPP-USB interfaces when they are needed, or we need to have the USB backend hide all printers that support IPP-USB (and make sure things like PAPPL do the same) so that we don't get a lot of frustrated users asking why they can't print using driver/Printer Application XYZ.

alexpevzner commented 3 years ago

@michaelrsweet,

26 demonstrates the possible scenario on a real hardware.

Canon MF745C/746C has two IPP over USB interfaces, both of them is shared with legacy printing interfaces (Class 7, Proto 2).

After using one of these interfaces for printing with proprietary USB driver, sane-airscan connected via ipp-usb to another interface stops working (it was working before printing).

This is definitely a firmware bug, but who knows how many devices are affected?

tillkamppeter commented 3 years ago

Why not do the following:

On startup ipp-usb claims the printer to poll its properties needed for advertising the emulated IPP network printer, then it releases the printer to allow access for everyone. Then if a job comes in later the access software (ipp-usb or CUPS backend) through which the job is sent claims the printer, passes on the job, and releases the printer afterwards. As a printer can only print one job at the same time anyway this should be no problem.

Imporetant is also that if a job is printing and therefore the printer claimed/busy and another job is coming up through the other access method, this second job has to wait somehow until the first finishes and the printer gets released.

alexpevzner commented 3 years ago

@tillkamppeter,

From the ipp-usb perspective, it is not clear when job is started and when it is finished.

Is it possible to implement an explicit arbitration? Say, ipp-usb provides a mechanism to claim/release a device (via some side channel, like AF_UNIX socket), all interested drivers use this mechanism, when they need to communicate with device.

All open source drivers are in our hands, so it should not be impossible to implement this approach.

michaelrsweet commented 3 years ago

@alexpevzner There is no perfect way to know when an IPP print job is done (short of eavedropping on the IPP messages), but an inactivity timeout should be sufficient for this scenario.

Doing an explicit arbitration mechanism introduces a dependency on yet another piece of software and is not something I'd want to do just to support what is essentially an edge case - and IPP-USB printer should immediately get auto-added using the IPP Everywhere "driver" since IPP-USB support implies AirPrint (which requires it and eSCL support if there is a scanner) so there is no hard requirement for us to support a specialized printer driver/application.

So that brings us back to what to do about IPP-USB printers and the USB backend:

  1. If ipp-usb is going to claim the interfaces and never release them, then the CUPS backend and PAPPL should just hide IPP-USB printers (and I'll add a --with-ipp-usb configure option for distros to force this, in addition to a presence check).
  2. If ipp-usb can release the interfaces when it has no active TCP/IP connections (with some sort of timeout or option to disable it for printers that are broken) then no changes need to be made to CUPS/PAPPL.

(I should note that macOS claims IPP-USB interfaces and doesn't give them up, so there is precedent to just running IPP-USB printers in IPP-USB mode all the time...)

alexpevzner commented 3 years ago

@michaelrsweet,

OK, let's do as Apple does :-) It is simple and clear, and will work in most cases.

Anyway, when skipping IPP-USB devices, it's better to check that ipp-usb actually handles them, to avoid duplication of blacklisting logic (although currently it skips only one device, who knows where we will come in a future), and to work correctly, if ipp-usb is not installed or disabled.

michaelrsweet commented 3 years ago

@alexpevzner That's the trick - there is no reliable way to determine whether ipp-usb is installed/active on the system.

alexpevzner commented 3 years ago

We can add it. Say, in a form of AF_UNIX socket, speaking a simple protocol.

michaelrsweet commented 3 years ago

@alexpevzner But what would be the server? And what would the interface look like?

michaelrsweet commented 3 years ago

@alexpevzner Also, don't forget that this needs to work with a snapped CUPS... :/

alexpevzner commented 3 years ago

ipp-usb will be the server. If it cannot be connected, it means, ipp-usb is not running. A single ipp-usb instance serves all compatible devices, so it can respond for all from a single socket.

Interface would be as we will agree. Probably, something simple, text-based, potentially extendible. Some text-based protocol, like FTP or SMTP.

alexpevzner commented 3 years ago

We can also speak through Avahi, if we will add USB "coordinates" to the TXT part of DNS-SD announces. But I'm not sure it will be convenient at the CUPS side.

tillkamppeter commented 3 years ago

The snapped CUPS can read the DNS-SD records of the printers which ipp-usb advertises, as it uses a Snap interface which allows it to use Avahi, already for simply discovering printers (and also for sharing printers). If you add a TXT record entry telling that this device is IPP-over-USB and which USB coordinates it has and which channels are occupied (in a simple text string), then the USB backend in the snapped CUPS could use this info to know that a printer device is there but occupied by ipp-usb.

michaelrsweet commented 3 years ago

@tillkamppeter If we were to use Avahi, it would probably be better to advertise a separate service instance using the bus and port numbers, e.g. BBPP._ipp-usb._tcp (where "BB" and "PP" are the bus and port numbers in hex) with a port number of 0.

tillkamppeter commented 3 years ago

@michaelrsweet as in case of a successful connection of ipp-usb to printer we have to advertise the printer as IPP printer anyway, why would we need the separate service entry for the IPP-over-USB connection? Why can't we add a TXT record entry like IPP-USB=003.061:C0.I0.A0:070104;C0.I2.A1:070102;...?

michaelrsweet commented 3 years ago

@tillkamppeter Because the USB backend has to look up the device! If we look for all _ipp._tcp services, we will have a lot more than just the local IPP-USB printers. And then we need to look at the TXT records to find the key(s) with the bus and port numbers.

But if we have a secondary service entry specifically for IPP-USB that doesn't depend on parsing the TXT record, then the USB backend can just query for the device it is interested in (or browse up front so it has a list of USB devices to blacklist).

The Avahi API makes adding another service entry super-easy, so it is one line of code to add it to ipp-usb (same as a TXT key in complexity) but vastly more efficient code in the USB backend to do the lookup.

alexpevzner commented 3 years ago

@tillkamppeter, I strongly discourage against simultaneous access to the same device via the ipp-usb and via the legacy driver, even if USB interfaces are different. Although in theory it should work, in real life it is not reliable. #26 demonstrates these problems on a real hardware.

I agree with @michaelrsweet, if there is no other considerations, let's go this way.

Should I add any additional information to the TXT records of these BBPP._ipp-usb._tcp entries, or bare name is enough?

tillkamppeter commented 3 years ago

@michaelrsweet, great, this eliminates the time-consuming resolving with Avahi, one of the things which also makes tools like driverless and cups-browsed slow and I have students working on reducing the number of resolve calls to optimize ... This way we only do a browse and no resolves, so it gets really fast.

tillkamppeter commented 3 years ago

@alexpevzner do not use the TXT record, stuff everything into the service name, to avoid time-consuming resolve calls on Avahi.

alexpevzner commented 3 years ago

OK. One small thing. When new device is plugged into the system, the legacy driver may receive PnP notification from libusb and attempt to make skip/handle decision before ipp-usb has advertised device and Avahi has published this advertising.

I believe, a small delay (say, 1 second) would be enough to resolve this race.

michaelrsweet commented 3 years ago

@alexpevzner Just adding the service with the necessary addressing information (bus+address? or do we also need the ports list?) and no TXT record is sufficient. Use a port number of 0 (which basically says "I'm defending this name").

@tillkamppeter You do not need to resolve a service to get its TXT record. You can query the TXT record directly, and this is what we do in the dnssd backend, in the libcups cupsEnumDests function, and in PAPPL's DNS-SD printer discovery function - all so that you don't wake up printers. You only need to resolve to get the address(s) and port of a service.

alexpevzner commented 3 years ago

Done.

@michaelrsweet, RFC requires that service name consist of exactly 2 DNS labels, and Avahi enforces it, so I use _BBPP-ipp-usb._tcp format for the service names

michaelrsweet commented 3 years ago

@alexpevzner No, the service type is _ipp-usb._tcp and the service instance name is the bus and address, e.g.:

AvahiEntryGroup *group;
libusb_device *device;
char bus_address[32];

snprintf(bus_address, sizeof(bus_address), "%02X%02X", libusb_get_bus_number(device), libusb_get_device_address(device));

avahi_entry_group_add_service_strlst(group, if_nametoindex("lo"), AVAHI_PROTO_UNSPEC, 0, bus_address, "_ipp-usb._tcp", NULL, NULL, 0, NULL);
alexpevzner commented 3 years ago

OK, updated.

I've just though that it may make sense to advertise the real TCP port number instead of 0, to help matching of USB devices with corresponding TCP port. Currently nobody needs it, but it may be useful in the future. What do you think?

michaelrsweet commented 3 years ago

@alexpevzner Sure, since we have a port number, let's use it.

alexpevzner commented 3 years ago

Done too

alexpevzner commented 3 years ago

Now in release 0.9.18