{"action":"created","issue":{"url":"https://api.github.com/repos/pyinvoke/invoke/issues/289","labels_url":"https://api.github.com/repos/pyinvoke/invoke/issues/289/labels{/name}","comments_url":"https://api.github.com/repos/pyinvoke/invoke/issues/289/comments","events_url":"https://api.github.com/repos/pyinvoke/invoke/issues/289/events","html_url":"https://github.com/pyinvoke/invoke/issues/289","id":122867918,"number":289,"title":"Prompt/etc autoresponder","user":{"login":"bitprophet","id":6088,"avatar_url":"https://avatars.githubusercontent.com/u/6088?v=3","gravatar_id":"","url":"https://api.github.com/users/bitprophet","html_url":"https://github.com/bitprophet","followers_url":"https://api.github.com/users/bitprophet/followers","following_url":"https://api.github.com/users/bitprophet/following{/other_user}","gists_url":"https://api.github.com/users/bitprophet/gists{/gist_id}","starred_url":"https://api.github.com/users/bitprophet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bitprophet/subscriptions","organizations_url":"https://api.github.com/users/bitprophet/orgs","repos_url":"https://api.github.com/users/bitprophet/repos","events_url":"https://api.github.com/users/bitprophet/events{/privacy}","received_events_url":"https://api.github.com/users/bitprophet/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/pyinvoke/invoke/labels/Feature","name":"Feature","color":"40a056"},{"url":"https://api.github.com/repos/pyinvoke/invoke/labels/run()","name":"run()","color":"444444"}],"state":"open","locked":false,"assignee":null,"milestone":{"url":"https://api.github.com/repos/pyinvoke/invoke/milestones/4","html_url":"https://github.com/pyinvoke/invoke/milestones/1.0","labels_url":"https://api.github.com/repos/pyinvoke/invoke/milestones/4/labels","id":1294336,"number":4,"title":"1.0","description":null,"creator":{"login":"bitprophet","id":6088,"avatar_url":"https://avatars.githubusercontent.com/u/6088?v=3","gravatar_id":"","url":"https://api.github.com/users/bitprophet","html_url":"https://github.com/bitprophet","followers_url":"https://api.github.com/users/bitprophet/followers","following_url":"https://api.github.com/users/bitprophet/following{/other_user}","gists_url":"https://api.github.com/users/bitprophet/gists{/gist_id}","starred_url":"https://api.github.com/users/bitprophet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bitprophet/subscriptions","organizations_url":"https://api.github.com/users/bitprophet/orgs","repos_url":"https://api.github.com/users/bitprophet/repos","events_url":"https://api.github.com/users/bitprophet/events{/privacy}","received_events_url":"https://api.github.com/users/bitprophet/received_events","type":"User","site_admin":false},"open_issues":9,"closed_issues":1,"state":"open","created_at":"2015-09-07T20:54:17Z","updated_at":"2015-12-18T02:46:09Z","due_on":null,"closed_at":null},"comments":19,"created_at":"2015-12-18T02:46:06Z","updated_at":"2016-01-23T14:32:12Z","closed_at":null,"body":"This represents porting Fabric 1.x's limited ability to autorespond to sudo prompts, to Invoke, and making it Better™.\r\n\r\nFor now, I've gone with a basic \"when you see X, always respond with Y\" functionality that can be easily configured via a `{pattern: response}` dict.\r\n\r\nThis isn't the most powerful implementation; e.g. one might want \"only respond to patterns once\", \"respond to X pattern the first 3 times only\", \"respond to pattern X but only if you've already responded to pattern Y\", etc etc.\r\n\r\nHowever it's arguably more flexible than using e.g. `pexpect` (which requires you to anticipate exactly what prompts will happen, in what order; Invoke/Fabric use cases tend to have prompts appear arbitrarily) and I expect it will be enhanced in the future, perhaps by ripping out the responder aspects into their own class or something.\r\n\r\nThe functionality is already pretty much complete after a ~week of hacking, fucking up, getting confused, becoming less confused, and more hacking. I'm making this ticket just so I have something to link in the changelog & a checklist for the random minor tasks left to perform.\r\n\r\n----\r\n\r\nLeft to do:\r\n\r\n* [x] Deal with recently-discovered funkiness in interactive stdin use case\r\n* [x] Make sure the basic functionality works for Fabric 2 as well as with local/subprocess\r\n* [x] Reinstate the Windows support for `handle_input`, which mirrors real stdin to the subprocess' stdin.\r\n* [ ] Doublecheck whether we actually need Windows `getch` as per https://github.com/pyinvoke/invoke/pull/304#issuecomment-174005880\r\n* [x] Figure out whether to retain the `sleep` call in `handle_input`; git-blame the Fab 1 line and doublecheck the rationale. Probably do want it back tho, probably chews more CPU than it really needs to as-is.\r\n* [ ] Reinstate the \"print read stdin data to stdout when pty=False\" behavior from fab 1 - it is necessary\r\n* [ ] Ditto the \"halt stdin mirroring while the local codebase is explicitly trying to prompt for shit it needs\" functionality from Fab 1.\r\n    * ...does this mean Fab 1 literally can't deal with users manually executing things like `raw_input` and `getpass`? Cuz if it *can* then that implies said halting/locking might not have been necessary? Or is a corner case?\r\n* [ ] Try to replicate fabric/fabric#1339 (both in Fabric 1, and here, and in Fabric 2)\r\n* [ ] Figure out if it's worth implementing a(n easily disabled and well documented!) convenience \"add `\\n` to response values if not present\" feature.\r\n    * Ye olde \"helpfulness can be evil\"...\r\n    * but in MY OWN TESTING I've fucked up and assumed an implicit `\\n` multiple times already, so there's no way tons of users wouldn't make the same assumption.\r\n* [x] Determine what's up with swallowed exceptions in the out/err/in threads when the main thread gets an eg KeyboardException. Feels like a thing we need to at least partially port from fab v1.\r\n* [x] Python 3 test pass so Travis doesn't get mad at me\r\n* [x] Ascertain why the test suite got slower again and how much of it's due to this branch\r\n* [ ] Double-check the doc situation; may want to make a \"feature list!\" section on index page?\r\n* [x] Changelog entry"},"comment":{"url":"https://api.github.com/repos/pyinvoke/invoke/issues/comments/174190214","html_url":"https://github.com/pyinvoke/invoke/issues/289#issuecomment-174190214","issue_url":"https://api.github.com/repos/pyinvoke/invoke/issues/289","id":174190214,"user":{"login":"pfmoore","id":1110419,"avatar_url":"https://avatars.githubusercontent.com/u/1110419?v=3","gravatar_id":"","url":"https://api.github.com/users/pfmoore","html_url":"https://github.com/pfmoore","followers_url":"https://api.github.com/users/pfmoore/followers","following_url":"https://api.github.com/users/pfmoore/following{/other_user}","gists_url":"https://api.github.com/users/pfmoore/gists{/gist_id}","starred_url":"https://api.github.com/users/pfmoore/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/pfmoore/subscriptions","organizations_url":"https://api.github.com/users/pfmoore/orgs","repos_url":"https://api.github.com/users/pfmoore/repos","events_url":"https://api.github.com/users/pfmoore/events{/privacy}","received_events_url":"https://api.github.com/users/pfmoore/received_events","type":"User","site_admin":false},"created_at":"2016-01-23T14:32:12Z","updated_at":"2016-01-23T14:32:12Z","body":"Suggestion for refactoring Reader.handle_stdin:\r\n\r\n```python\r\ndef handle_stdin(self, input_):\r\n    for chars in read_chars_unbuffered(input_):\r\n        self.write_stdin_text(chars) # wrapper round encode + self.write_stdin\r\n        if self.program_finished.is_set() and not chars:\r\n            break\r\n```\r\n\r\nThen ```read_chars_unbuffered``` can be defined as a platform specific function that yields (unicode) characters one at a time as soon as they become available. It wants to return Unicode, not bytes, because we can't guarantee that our stdin and the subprocess' stdin are using the same encoding, so we need to transcode. On Unix, this would use the existing select approach:\r\n\r\n```python\r\ndef read_chars_unbuffered(input_):\r\n    use_select = isatty(input_)\r\n    with character_buffered(input_):\r\n        while True:\r\n            chars = None\r\n            # \"real\" terminal stdin needs select() to tell us when it's\r\n            # ready for a nonblocking read().\r\n            if use_select:\r\n                reads, _, _ = select.select([input_], [], [], 0.0)\r\n                ready = bool(reads and reads[0] is input_)\r\n            # Otherwise, assume a \"safer\" file-like object that can be read\r\n            # from in a nonblocking fashion (e.g. a StringIO or regular\r\n            # file).\r\n            else:\r\n                ready = True\r\n            if ready:\r\n                # Read 1 character at a time for interactivity's sake.\r\n                chars = input_.read(1)\r\n                # Short-circuit if not using select() and appeared to hit\r\n                # end-of-stream.\r\n                if not use_select and not chars:\r\n                    break\r\n                # We yield the (unicode) characters here. Note that input_ will be\r\n                # opened in text mode, so that in Python 3, we get a string (not bytes)\r\n                # Python 2 text mode returns an 8-bit string (ugh, Python 2 :-() so we\r\n                # encode it so we don't have to deal with the ugliness elsewhere\r\n                # This is so ugly it should probably go into a Python 2/3 compatibility function\r\n                if isinstance(chars, six.binary_type):\r\n                    # Seriously, Python 2 - how are we supposed to know the encoding\r\n                    # of these bytes???\r\n                    if hasattr(input_, 'encoding'):\r\n                        if hasattr(input_, 'errors'):\r\n                            chars = chars.decode(input_.encoding, input_.errors)\r\n                        else:\r\n                            chars = chars.decode(input_.encoding)\r\n                    else:\r\n                        chars = chars.decode() # Sigh. Assume the default encoding\r\n                yield chars\r\n            # Take a nap so we're not chewing CPU.\r\n            time.sleep(0.01)\r\n````\r\n\r\nThis is still ugly, but (IMO) it puts the ugliness where it belongs, in the low-level and platform-specific read_chars_unbuffered, while leaving the higher level handle_stdin loop that feeds our stdin onto the subprocess, looking clean so that it's easy to see the logic.\r\n\r\nThoughts?"}}