-
Notifications
You must be signed in to change notification settings - Fork 63
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
Provide a try_clone()
method for ports/devices
#21
Comments
@apoloval That's a valid use case, and actually it's one that I'm going to run into on an upcoming project. I'm not convinced that |
I agree cloning is not available in all implementations and that should be considered. Standard library authors agree as well with
In case the underlying implementation is not able to duplicate the serial port, this operation would fail with the appropriate error. New implementations can be added that does not support cloning. My proposal is to provide the same signature to the Do you still think this is not enough to dispel your concerns about future implementations? |
I'm looking at a similar use case where I want to block + read from one thread why sending from another. Rather than adding a new API why not let the respective serial implementations mark their struct as Sync+Send? AFAIK both win32 ReadFile/WriteFile are threadsafe, as are read/write. This would let you do the read/write from separate threads and allow platforms future platforms that don't allow this to mark their implementation appropriately. Happy to work on a PR+testing if you think this is a viable path. |
@vvanders @apoloval I've been working on a crate that I thought would motivate this use case, but I ended up needing to wrap I/O in a mutex anyway. Implementing Another idea could be to provide |
Hmm, no clue on EscapeCommFunction() and related. I would assume so given that most HANDLE functions are but the docs are incredibly sparse WRT thread safety. I was thinking about this a bit more and if we really wanted to be correct it seems like you only want one reader + one writer. Otherwise the semantics around reading/writing are a little weird. However that ends up complicating/changing the API significantly and I'm not sure if you want to go through the deprecation/work. I kinda like the try_clone() as a trait that you mentioned. Seems flexible and matches what the underlying APIs somewhat expect. I really like that callers can use it to constrain their signatures. It's also less invasive than a whole new read/write API. |
@vvanders Yeah, I couldn't find much in the docs regarding thread safety, and I'm not very familiar with Windows APIs in general. It sounds like it may be possible to mark I thought about defining separate readers and writers, too, maybe with a method like I'm leaning toward adding |
For my specific use case, having a |
So I got something working for Unix TTY ports: https://github.com/dcuddeback/serial-rs/compare/feature/try-clone. It's not as simple as I expected because of the It's also not clear from reading the man pages whether this scenario is supported: {
let port2 = {
let port1 = serial::open("/dev/ttyUSB0").unwrap(); // ioctl(port1, TIOCEXCL);
port1.try_clone().unwrap() // port2 = dup(port1);
}; // close(port1);
} // ioctl(port2, TIOCNXCL); close(port2); The part that concerns me is that @apoloval With the version in your fork, I was able to open the same port from two programs: let port1 = serial::open("/dev/ttyUSB0").unwrap();
{
let port2 = port1.try_clone().unwrap();
}
// port2 is closed here, which executes the TIOCNXCL ioctl,
// releasing the port to other programs while port1 is still open |
So is this still on it's way ? It would be really neat to have the ability to have one thread reading from a serial connection, and the other one writing to it ! |
@gbip For your use case, you can probably work around it by wrapping the port in That said, there are some use cases that require cloning (e.g., using |
That is what I have done, but then to write stuff I have to wait until my reading function returns, which can take quite some time. Is there a way I could help with this feature ? I am not very familiar with file descriptors and things like that, but I would be glad to at least try helping you ! |
I think since Arc<Mutex> lock is scope lock, you could try this in your thread: let mut port = serial::open("COM6")
.expect("Error opening serial port");
// do your configuration and settings for the port here
// then "move" the port to a mutex:
let port_handle = Arc::new(Mutex::new(port));
let my_read_thread = thread::spawn(move || {
let mut buffer = [0u8; 32];
loop { // loop forever
let mut read_result: Result<usize, Error>;
{ // this scope will reduce time on read
read_result = port_handle.lock().unwrap().read(&mut buffer);
}
// the scope is gone and mutex is unlocked
match read_result {
// now we deal with the result
}
}
}); Then similarly for the write thread. But you will need to clone() the mutex for the write thread. This way the read and write is not truly independent, but at least you get to minimise the time spent trying to do it. I still prefer using a clone if possible. Or maybe we need to update the documentation? I am happy to share the example code I made. https://gist.github.com/lowlifer/3166de711680006da42661256f926e23 I could help with Windows Duplicate Handle stuff and testing. Do you mind if I play around a bit forking the feature branch you got? |
This feature was implemented recently on serialport-rs if you need some inspiration ! |
@Lowlifer That would be great! I could use help with Windows. It may even be easier than Unix, since Windows automatically locks COM ports when they're open and doesn't require a userland call to
I believe the feature branch you're referring to only implements |
@gbip I'd appreciate it if you don't plug other projects here. I'm aware of serialport-rs. It's actually a fork of this project. Besides violating this project's MIT license by deleting the license text (https://github.com/dcuddeback/serial-rs/blob/master/LICENSE#L11-L12), they've also rewritten their git history for some reason, and then they relentlessly spam my projects to promote theirs. That's not the kind of project I would normally take inspiration from. If there's something novel in that project that addresses the issues discussed in this ticket, I'd appreciate it if you could enlighten us on the solution. However, it looks to me like they're doing the same |
I'm just trying to come up with a reliable way of testing this. I was thinking maybe I could clone the handle twice and then try writing to two handles from two difference threads, and then ensure the reads show all the characters I have written. So writer thread 1 writes number 1-9 and writer thread 2 writes letters a to i. The reader should get all the unique characters, 18 in total. What are you thoughts on my scenario? Please suggest any improvements =)
This should be relatively easy to test. I can have a separate process which will try to connect to the same serial port after using serial-rs to opened the port. It should continue failing to connect until I have closed all the clones from serial-rs. I will get started on test 2 first. And then do test 1 as I have described. |
@dcuddeback okay, dumb question, and off topic, but how do I get the diff from the link you gave me to apply to the git fork I just made? dumb question 2: how do i build this on windows? |
You can add
That should be good enough for testing. I wouldn't put the work into porting a solution to
Should be just
Yeah, this is the easier one to start with. I would test closing the COM ports out of order as well as in order.
That seems like a decent test. My only suggestion would be that it would likely take more than 18 characters to notice any thread-safety issues. Maybe pipe a couple of large files through each thread. There's also the functions For both questions, it'd be great to have an answer from the documentation. Since these questions could be dependent on the hardware drivers, it'd be great to know if Microsoft has said if it should be safe to call these from different threads. That's where it would be helpful to me if someone more familiar with the Windows API could chime in. |
All good and built as per your instructions. Thank you so much. Your instructions were really clear and it was really easy to follow from there.
I performed this test and yes it passes. Opened one handle and try_clone() another and regardless of order of closing the port is locked until both handles have drop()ped. I am currently using a different programming language to try to open the port while it was locked. I will re-write the test to use serial-rs only. And need to add some communications to see the port is still working while locked. My assumptions is that ideally you would like to add this test to the code when I'm done?
Let me read the MSDN documentation and get back to you. |
@dcuddeback https://msdn.microsoft.com/en-us/library/windows/desktop/ms724251(v=vs.85).aspx The example was on duplicating a mutex handle. So the mutex object was created and duplicated to be passed into a child thread. The main thread and child thread now shares the same handle to the same mutex. I'm guessing that this example wouldn't be given if it weren't thread safe. |
My plan for testing thread safety for writing to the handle
|
@dcuddeback Okay so the above test passed. Now I have these two tests, what's the best way to keep it for future use so someone else doesn't have to repeat what I did? Also, I needed to impl Sync for COMPort in order for the cloned handle to be passed into threads. |
@Lowlifer Thanks for taking the time to research this. This is helpful.
Good to know. This will probably make the Windows implementation a little simpler than the Unix one. In case this is dependent on hardware drivers, would you mind sharing what serial port hardware you tested with? For example, is it a serial port built into your motherboard, an addon card, or (more likely these days) some kind of USB to serial converter? FTDI? Prolific? Another vendor? (Hopefully it's something different than the hardware I have available to test with.)
It's actually better that you tested with a different programming language, because the point of locking the port is to be inter-operable with the rest of the platform.
Hmm... the whole point of a mutex is to implement thread-safety, so that doesn't really say anything about COM ports.
That's a pretty good indicator as well.
Are you referring to the test code you wrote? You could paste it as a Gist and link to it from this thread. Or if it's short, you could just paste it inline in a comment in this thread, too. |
Hi @dcuddeback Sorry for the late reply. Got busy lately. https://gist.github.com/lowlifer/7c7628106308356f7766ce2fc58a44e2 Please see the gist with my test code. |
@dcuddeback A simple solution to something like #40 is to let the user choose whether they want exclusive access to the serial port. Then they can open the same port from multiple threads, relying on the OS's internal locking. On *nix this is done with an |
I needed this (the split write/read ends) as well. I think the model where it is possible to split the read end from the write end is independently useful from the ability to clone the whole port entirely and caters to different sets of use-cases and has different downsides. Note, that serial handles are inherently not thread safe, because the underlying hardware has state (the port configuration), unlike the usual sockets and file handles. The following "wants" are inherently incompatible and one will have to pick 2 out of 1) full-duplex communication; 2) configurable ports; 3) ability to share handle between threads.1 To expand on those… to allow 1 and 2, the handle must be unique, making reading, writing and configuration state changes well ordered; to allow 2 and 3, some sort of locking must employed to ensure (re-)configuration does not invalidate in-flight I/O; to allow 1 and 3, port state must be eliminated or become constant. I feel like an ideal API would encompass all these variants, but I don’t think it is likely that any single API will handle all these cases well.
The majority of the APIs in Windows are thread-safe by default, apparently. To the point that they point out when API is not thread-safe only. Footnotes
|
I've done some more testing on this. Turns out Windows will not let you open a serial port twice, so @nagisa Windows and Linux both do OS-level locking on read/write operations to file handles, so I don't think changing the settings from another thread is a problem. In any case |
I’m about 100% sure that on Windows changing settings even within the same thread is already a problem. See, on Windows writing to a serial port does not block – the data instead goes into an internal buffer of sorts, and doing something like this:
will likely end up switching the baudrate in the middle of the preceding write. |
As I understand it, Now, my point is undermined by the fact that this flag is required if we want to concurrently read and write to the same serial port. I would argue, however, that protecting the user from such effects isn't the library's job. More concretely, we can say that changing the baud rate concurrently during a write will result in the bytes being transferred at an unspecified/variable baud rate—this would be consistent with the unix implementation as well. If on the other hand doing so would crash the program or otherwise cause undefined behavior, then we wouldn't want to expose this functionality (or at least do so in |
So I've decided to look at this with new eyes. From my testing (see gist), it seems that POSIX will drop exclusive access to the serial port once all file descriptors are closed. This should mean that we can freely implement Windows behaves similarly with I can make a PR implementing what I've written above if @dcuddeback agrees with this reasoning. It should be noted, however, that Windows will serialize reads and writes if this approach is used to implement #40. We can get around that by using Also: On POSIX having exclusive access to the serial port is not the default (the crate asks for it explicitly), whereas on Windows it is and you cannot override it. It might be a good idea to add that as a config option, though unfortunately it would need to throw errors at people attempting to use it from Windows. P.S. It's not necro-posting if the issue is still open, right? :) |
Some IO types of standard library as
std::net::TcpStream
support cloning. This is necessary to implement some non-trivial communication patterns in which one thread performs the reads and a different one performs the writes.serial-rs
lacks this functionality. It would be great to have atry_clone(&self)
function inSerialPort
andSerialDevice
that makes possible to clone the port so copies can be used by different threads.The text was updated successfully, but these errors were encountered: