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

Add various pty functions #556

Merged
merged 1 commit into from
May 17, 2017
Merged

Add various pty functions #556

merged 1 commit into from
May 17, 2017

Conversation

Susurrus
Copy link
Contributor

@Susurrus Susurrus commented Mar 15, 2017

  • grantpt
  • posix_openpt
  • ptsname/ptsname_r
  • unlockpt

I've refactored my code to use these functions and they work correctly at least in the non-error case. Only issue is what to do for failure in ptsname when converting strings. This should effectively never happen. I think maybe adding an Unknown to the Error type and returning that might be useful.

src/pty.rs Outdated
pub fn ptsname(fd: RawFd) -> Result<String> {
let name_ptr = unsafe { libc::ptsname(fd) };
if name_ptr.is_null() {
return Err(Errno::ENOTTY.into());
Copy link
Contributor

@Mic92 Mic92 Mar 17, 2017

Choose a reason for hiding this comment

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

Is Err(Error::last().into()) not safer here? I mean according to the manpage ENOTTY is the only error, but better safe then sorry.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that's probably the correct thing to do, though I do think they'll end up with the same functionality. I'll change it.

@Susurrus
Copy link
Contributor Author

@Mic92 Also do you have any comment on the two FIXMEs I have in there? I didn't see a way to return a generic unknown error.

src/pty.rs Outdated
}

let name_cstr = unsafe { CStr::from_ptr(name_ptr) };
let name = match name_cstr.to_str() {
Copy link
Contributor

Choose a reason for hiding this comment

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

It is probably safe to ignore utf8 validation errors as we get the string from the operating system:
see https://github.com/nix-rust/nix/blob/master/src/sys/utsname.rs#L65

Copy link
Contributor

Choose a reason for hiding this comment

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

If it is not safe, then it is better to return the value as it is.

@Susurrus
Copy link
Contributor Author

@Mic92 I think I've resolved both issues. Let me know what you think. I left them as separate commits so they're easier to review, but I'll squash them before they're pulled.

src/pty.rs Outdated
};
Ok(name)
Ok(name.to_string_lossy().into_owned())
Copy link
Contributor

Choose a reason for hiding this comment

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

....replacing any invalid UTF-8 sequences with U+FFFD REPLACEMENT CHARACTER...
Is the unmodified value of this function not meant to be as an input for functions like open(2)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should never receive invalid UTF-8 sequences from the OS, so that should never happen. I'd prefer to use from_utf8_unchecked (as you mentioned in your comment), which doesn't do any error detection, but I couldn't figure out how to use it. And note that using from_utf8_unchecked has the exact same problem, but instead of fixing invalid UTF-8 strings it will just crash later when it's attempted to be decoded as proper UTF-8.

Basically if we use the String type as the output from this function, it must be valid UTF-8 regardless of how we convert it. So the question really is, does ptsname*() always return valid UTF-8. I think the answer is yes, but I don't really know.

Copy link
Contributor

@Mic92 Mic92 Mar 20, 2017

Choose a reason for hiding this comment

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

Just wanted to note that gethostname returns CStr. So to be consistent I would either change both to String or both to CStr.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I would think that gethostname should return a Result<String> instead of returning it in an output argument as a CStr. @posborne Any comments here?

@Susurrus
Copy link
Contributor Author

@nix-rust/nix-maintainers Any comments on the APIs I've exposed here. Unlike with gethostname I'm exposing a String type, though a &str might be more appropriate. I do think we should stop exposing CStr types in our APIs as I see nix as being the interface between Rust and libc and that conversion as part of it.

Copy link
Member

@asomers asomers left a comment

Choose a reason for hiding this comment

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

I think this module could benefit from some type-safety, and it would be nice to eliminate the extra data copy in ptsname_r

src/pty.rs Outdated
/// `grantpt()` changezs the mode and owner of the slave pseudoterminal device corresponding to the
/// master pseudoterminal referred to by `fd`. This is a necessary step towards opening the slave.
#[inline]
pub fn grantpt(fd: RawFd) -> Result<()> {
Copy link
Member

Choose a reason for hiding this comment

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

How about creating a Newtype like struct PseudoTerminal(RawFd) to provide some static type checking on these functions? It could also close itself on Drop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, interesting. Would this be as simple as implementing that type and also impl From<RawFd> for it? We'd need to make sure that this can be easily used both with std and other nix functions. Additionally, is this sound in the context of #594?

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it should implement From<RawFd>, because that would make it too easy to accidentally convert an inappropriate value. Conversion should always be explicit. In fact, are there any other Rust functions, in nix or in std, that deal with pseudoterminals?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I agree, maybe just AsRawFd is a better trait instead? But no, nothing else really deals with this. I have some code in my serialport-rs library here that deals with this same thing, and that's like the only other thing working with safe ptty abstractions.

Copy link
Member

Choose a reason for hiding this comment

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

AsRawFd sounds good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So in continuing to think about this, I don't think it makes sense to provide an abstraction over RawFd. The use case of these ttys is generally what you can see in serialport-rs. Most commonly you call read and write, both of which take a RawFd. Additionally, the other functions I'm adding here all take RawFd. So every call you'd be doing fn(port.as_raw_fd()), so it's just a needless abstraction that doesn't actually add any safety. I think it's the role of a library like serialport-rs to provide abstractions on top of nix should it be desired.

Copy link
Member

Choose a reason for hiding this comment

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

What if the newtype implemented Into<RawFd>? Then fn could automatically coerce it, as long as fn's argument is a T: Into<RawFd>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But if you implement Into you also implement From, that's what my comment was about before. It's also bidirectional, so we're back to where we started. And you'd still need to use .into() everywhere since not all functions use T: Into<RawFd>. Since the topic of changing function signatures came up, there are 168 functions in libc that have fd's as inputs (as c_int, not RawFd) and 88 functions in nix that have RawFd inputs.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, From implies Into, but not the other way around. If this newtype implements Into<RawFd> , that does not automatically create an implementation for From<Newtype> on RawFd. Do you think that typing .into() is too much work to justify the free type checking on the pty functions? If the newtype implements Drop, that could save some typing too, because you wouldn't have to automatically close the ptys.

src/pty.rs Outdated
}

let name = unsafe {
CStr::from_ptr(name_buf.as_ptr())
Copy link
Member

Choose a reason for hiding this comment

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

The subsequent Cow::into_owned() will perform a data copy. How about something like this instead?

let mut name_buf = OsString::with_capacity(64);
libc::ptsname(fd, name.to_str().as_ptr(), 64);
return name_buf.into_string()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Two things: Is OsString the right type rather than CString? The docs don't clarify what the difference between the two really is, so I'm not certain.

Also, that code you provided doesn't compile. because name.to_str().as_ptr() doesn't return a mutable pointer and I don't see a way to get one. Additionally, I'm not certain how to map the error types. I'm thinking we should add to nix::Error a StringConversion type and map the into_string() failure to that, otherwise I think this should be an unwrap() or expect() since I don't think this will ever fail.

Copy link
Member

Choose a reason for hiding this comment

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

Take a look at getcwdin unistd.rs. It actually uses a Vec as a buffer, before transforming it into an OsString

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is that what you'd recommend doing instead of what you originally suggested?

Copy link
Member

Choose a reason for hiding this comment

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

My original suggestion was just a guess, a starting point. I didn't know exactly how to make it work. But getcwd presents exactly the same problem as this function, so its solution will probably work.

}

let name = unsafe {
CStr::from_ptr(name_ptr)
Copy link
Member

Choose a reason for hiding this comment

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

How about a test that calls ptsname on two separate pseudoterminals, and checks that their names are not the same? That would enforce that this function performs a data copy (as I think it probably should).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Wouldn't you instead want to call ptsname() on the same ptty and check that they don't point to the same underlying buffer? I'm unclear what the test you suggested actually tests.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that could do it. I'm really just trying to think up ways to improve the coverage.

test/test_pty.rs Outdated
/// Test equivalence of `ptsname` and `ptsname_r`
#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptsname() {
Copy link
Member

Choose a reason for hiding this comment

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

The test name is not very accurate.

// Open the slave device
let slave_fd = open(Path::new(&slave_name), O_RDWR, stat::Mode::empty()).unwrap();
assert!(slave_fd > 0);
}
Copy link
Member

Choose a reason for hiding this comment

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

This test leaks its file descriptors.

test/test_pty.rs Outdated
let slave_name = ptsname(master_fd).unwrap();
let slave_name_r = ptsname_r(master_fd).unwrap();
assert_eq!(slave_name, slave_name_r);
}
Copy link
Member

Choose a reason for hiding this comment

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

This test leaks its file descriptors, too.

@Susurrus
Copy link
Contributor Author

I'm still working on how to best return a String with the least number of data copies (gonna reach out on IRC tomorrow). But for now I've pushed an updated version that adds a datatype. I can use it in my serialport-rs lib pretty ergonomically.

One question I have is if errors when calling close on a PtyMaster fd should it panic or just silently ignore the error? The signalfd module does, but I don't know if that's how we should do things, though I'm doing the same thing here.

@Susurrus Susurrus force-pushed the pty branch 2 times, most recently from e8b7137 to ea2d03f Compare May 16, 2017 04:25
@Susurrus
Copy link
Contributor Author

Here's the commit to serialport-rs that adjusts it for these changes in nix. In general I think it's pretty clean looking.

#[test]
#[cfg(any(target_os = "android", target_os = "linux"))]
fn test_ptsname_equivalence() {
// Open a new PTTY master
Copy link
Member

Choose a reason for hiding this comment

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

s/PTTY/PTY here and below

test/test_pty.rs Outdated
let slave_name1 = ptsname(&master_fd).unwrap();
let slave_name2 = ptsname(&master_fd).unwrap();
assert!(slave_name1 == slave_name2);
assert!(slave_name1.as_ptr() != slave_name2.as_ptr());
Copy link
Member

Choose a reason for hiding this comment

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

This assertion probably deserves a comment. It's not obvious why a consumer should care about this behavior.

@asomers
Copy link
Member

asomers commented May 16, 2017

The new type looks nice, and it fixes all of the file descriptor leaks in the tests. The integration into serialport-rs looks good too.

I have conflicting feelings about errors during drop. On the one hand, the most likely error is an EBADF, which suggests a programming bug, so the user should be informed. OTOH, since PtyMaster implements AsRawFd, the library can't really assume that the user hasn't already closed it. So I suppose we should ignore errors. But that raises another question: Will PtyMaster.into_raw_fd() invoke drop? If so, then we're screwed. I don't think so, because std::net::TcpStream implements both traits. But it would be nice to verify that with a test.

@Susurrus Susurrus force-pushed the pty branch 2 times, most recently from aa9d724 to 51f7199 Compare May 16, 2017 18:31
@Susurrus
Copy link
Contributor Author

Yes, we need to explicitly forget the type. This is actually done in std::net::TcpStream via the into_raw() call. Additionally we ignore any errors when closing for the same reason they do in std and I added as similar comment.

Alright, I think this really should be everything that we need.

@asomers
Copy link
Member

asomers commented May 16, 2017

Did you ever figure out a way to reduce the data copies in ptsname_r?

@Susurrus
Copy link
Contributor Author

Whoops! I knew I was forgetting something. I'll hop on IRC again today and see if I can figure out the "right" way to do this.

@Susurrus
Copy link
Contributor Author

Popped onto IRC and found a reasonable implementation that should be pretty efficient. It's still not clear if String is the right type to return, but at least this will error hard if there's every a problem rather than us assuming it's safe. It did require me expanding the Error type, but I think it's reasonable.

Copy link
Member

@asomers asomers left a comment

Choose a reason for hiding this comment

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

Looks like some debugging code accidentally made it into the PR

src/lib.rs Outdated
Error::Sys(errno) => io::Error::from_raw_os_error(errno as i32),

Copy link
Member

Choose a reason for hiding this comment

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

I don't think you meant to leave a space here

src/pty.rs Outdated
pub fn ptsname_r(fd: &PtyMaster) -> Result<String> {
let mut name_buf = vec![0u8; 64];
let name_buf_ptr = name_buf.as_mut_ptr() as *mut libc::c_char;
println!("{}/{}", name_buf.len(), name_buf.capacity());
Copy link
Member

Choose a reason for hiding this comment

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

Please remove this debugging code

src/pty.rs Outdated
if unsafe { libc::ptsname_r(fd.as_raw_fd(), name_buf_ptr, name_buf.capacity()) } != 0 {
return Err(Error::last().into());
}
println!("{}/{}", name_buf.len(), name_buf.capacity());
Copy link
Member

Choose a reason for hiding this comment

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

This one too.

  * grantpt
  * ptsname/ptsname_r
  * posix_openpt
  * unlockpt
@Susurrus
Copy link
Contributor Author

Sorry, I thought I did one last look over of it, must have forgot! Okay, maybe now this is looking finalized.

@asomers
Copy link
Member

asomers commented May 17, 2017

bors r+

bors bot added a commit that referenced this pull request May 17, 2017
556: Add various pty functions r=asomers
  * grantpt
  * posix_openpt
  * ptsname/ptsname_r
  * unlockpt

I've refactored my code to use these functions and they work correctly at least in the non-error case. Only issue is what to do for failure in `ptsname` when converting strings. This should effectively never happen. I think maybe adding an `Unknown` to the `Error` type and returning that might be useful.
@bors
Copy link
Contributor

bors bot commented May 17, 2017

Build succeeded

@bors bors bot merged commit 4e97520 into nix-rust:master May 17, 2017
@Susurrus Susurrus mentioned this pull request May 17, 2017
@Susurrus Susurrus mentioned this pull request May 25, 2017
@Susurrus Susurrus deleted the pty branch July 13, 2017 19:13
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.

None yet

3 participants