Repository: locustio/locust

Issue:
SetConsoleMode throws an error when locust is run from Jenkins Powershell
<!-- 
If you have a general question about how to use Locust, please check Stack Overflow first https://stackoverflow.com/questions/tagged/locust

You can also ask new questions on SO, https://stackoverflow.com/questions/ask just remember to tag your question with "locust". Do not immediately post your issue here after posting to SO, wait for an answer there instead.

Use this form only for reporting actual bugs in locust. Be mindful that the developers of locust are unpaid volunteers, so make sure you have tried everything you can think of before filing a bug :) 
-->

### Describe the bug
<!-- A clear and concise description of what the bug is -->
Error message is printed out to console right after the test is started in Jenkins and job fails on exit code 2 even actual test passes. We have run locust performance tests over a year on Jenkins without similar problem. Tests started to fail recently after we updated Python and Locustio versions to latest. Locust test run without a problem if it is started on Windows desktop Powershell (or cmd).

### Expected behavior
<!-- Tell us what you think should happen -->
Locust tests should run on CI as they run on local machine.

### Actual behavior
<!-- Tell us what happens instead. Include screenshots if this an issue with the GUI. -->
Error "pywintypes.error: (6, 'SetConsoleMode', 'The handle is invalid.')" is printed out to console right after the test is started in Jenkins and job fails on exit code 2 even actual test passes.

### Steps to reproduce
<!-- Please provide a minimal reproducible code example (https://stackoverflow.com/help/minimal-reproducible-example) --> 
```
from locust import User, task, between


class MyUser(User):
    @task
    def my_task(self):
        print("executing my_task")

    wait_time = between(0.5, 10)
```
Start command
`locust -f .\locust_test.py --headless --run-time 3`

Console from Jenkins:
```
[Locust_test] $ powershell.exe -NonInteractive -ExecutionPolicy Bypass -File C:\Users\locust\AppData\Local\Temp\jenkins11138277147510956709.ps1
[2020-12-10 19:13:09,846] robot-vm/INFO/locust.main: Run time limit set to 3 seconds
[2020-12-10 19:13:09,847] robot-vm/INFO/locust.main: Starting Locust 1.4.1
[2020-12-10 19:13:09,847] robot-vm/INFO/locust.runners: Spawning 1 users at the rate 1 users/s (0 users already running)...
[2020-12-10 19:13:09,847] robot-vm/INFO/locust.runners: All users spawned: MyUser: 1 (1 total running)
Traceback (most recent call last):
  File "src/gevent/greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "c:\python39\lib\site-packages\locust\input_events.py", line 89, in input_listener_func
    poller = get_poller()
  File "c:\python39\lib\site-packages\locust\input_events.py", line 81, in get_poller
    return WindowsKeyPoller()
  File "c:\python39\lib\site-packages\locust\input_events.py", line 47, in __init__
    self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)
pywintypes.error: (6, 'SetConsoleMode', 'The handle is invalid.')
2020-12-10T17:13:09Z <Greenlet at 0x19066ffdd00: input_listener_func> failed with error

 Name                                                          # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregated                                                         0     0(0.00%)  |       0       0       0       0  |    0.00    0.00

[2020-12-10 19:13:09,855] robot-vm/CRITICAL/locust.main: Unhandled exception in greenlet: <Greenlet at 0x19066ffdd00: input_listener_func>
Traceback (most recent call last):
  File "src/gevent/greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "c:\python39\lib\site-packages\locust\input_events.py", line 89, in input_listener_func
    poller = get_poller()
  File "c:\python39\lib\site-packages\locust\input_events.py", line 81, in get_poller
    return WindowsKeyPoller()
  File "c:\python39\lib\site-packages\locust\input_events.py", line 47, in __init__
    self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)
pywintypes.error: (6, 'SetConsoleMode', 'The handle is invalid.')
 Name                                                          # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregated                                                         0     0(0.00%)  |       0       0       0       0  |    0.00    0.00

[2020-12-10 19:13:12,486] robot-vm/INFO/locust.main: Time limit reached. Stopping Locust.
[2020-12-10 19:13:12,486] robot-vm/INFO/locust.runners: Stopping 1 users
[2020-12-10 19:13:12,487] robot-vm/INFO/locust.runners: 1 Users have been stopped, 0 still running
[2020-12-10 19:13:12,487] robot-vm/INFO/locust.main: Running teardowns...
[2020-12-10 19:13:12,487] robot-vm/INFO/locust.main: Shutting down (exit code 2), bye.
[2020-12-10 19:13:12,487] robot-vm/INFO/locust.main: Cleaning up runner...
 Name                                                          # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregated                                                         0     0(0.00%)  |       0       0       0       0  |    0.00    0.00

Response time percentiles (approximated)
 Type     Name                                                              50%    66%    75%    80%    90%    95%    98%    99%  99.9% 99.99%   100% # reqs
--------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
--------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|

executing my_task
Build step 'PowerShell' marked build as failure
Archiving artifacts
Finished: FAILURE

REST API
Jenkins 2.249.3
```
Console from local PC's Powershell
```
PS C:\workspace\tmp> locust -f .\locust_test.py --headless --run-time 3
[2020-12-10 18:55:07,816] WL313558/INFO/locust.main: Run time limit set to 3 seconds
[2020-12-10 18:55:07,816] WL313558/INFO/locust.main: Starting Locust 1.4.1
[2020-12-10 18:55:07,816] WL313558/INFO/locust.runners: Spawning 1 users at the rate 1 users/s (0 users already running)...
[2020-12-10 18:55:07,816] WL313558/INFO/locust.runners: All users spawned: MyUser: 1 (1 total running)
 Name                                                          # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregated                                                         0     0(0.00%)  |       0       0       0       0  |    0.00    0.00

executing my_task
 Name                                                          # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregated                                                         0     0(0.00%)  |       0       0       0       0  |    0.00    0.00

executing my_task
[2020-12-10 18:55:10,512] WL313558/INFO/locust.main: Time limit reached. Stopping Locust.
[2020-12-10 18:55:10,513] WL313558/INFO/locust.runners: Stopping 1 users
[2020-12-10 18:55:10,513] WL313558/INFO/locust.runners: 1 Users have been stopped, 0 still running
[2020-12-10 18:55:10,513] WL313558/INFO/locust.main: Running teardowns...
[2020-12-10 18:55:10,513] WL313558/INFO/locust.main: Shutting down (exit code 0), bye.
[2020-12-10 18:55:10,513] WL313558/INFO/locust.main: Cleaning up runner...
 Name                                                          # reqs      # fails  |     Avg     Min     Max  Median  |   req/s failures/s
--------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------
 Aggregated                                                         0     0(0.00%)  |       0       0       0       0  |    0.00    0.00

Response time percentiles (approximated)
 Type     Name                                                              50%    66%    75%    80%    90%    95%    98%    99%  99.9% 99.99%   100% # reqs
--------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
--------|------------------------------------------------------------|---------|------|------|------|------|------|------|------|------|------|------|------|
```


### Environment

- OS: Windows 10
- Python version: 3.9.1
- Locust version: 1.4.1


Discussion:
I think this is related to keyboard input handling. Can you have a look @DennisKrone ? Probably we should just catch and ignore this error. Might want to output a log message about keyboard input being disabled when it happens as well.

For now, only workaround is going back to a pre 1.4 version.
The same to you,uers can not work when it happened.
python:3.7 locust:1.4.1
[2020-12-11 15:53:00,895] lep/INFO/locust.main: Starting web interface at http://0.0.0.0:8089 (accepting connections from all network interfaces)
[2020-12-11 15:53:00,901] lep/INFO/locust.main: Starting Locust 1.4.1
Traceback (most recent call last):
  File "src\\gevent\\greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "C:\Users\lep\AppData\Roaming\Python\Python37\site-packages\locust\input_events.py", line 89, in input_listener_func
    poller = get_poller()
  File "C:\Users\lep\AppData\Roaming\Python\Python37\site-packages\locust\input_events.py", line 81, in get_poller
    return WindowsKeyPoller()
  File "C:\Users\lep\AppData\Roaming\Python\Python37\site-packages\locust\input_events.py", line 47, in __init__
    self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)
pywintypes.error: (6, 'SetConsoleMode', '句柄无效。')
2020-12-11T07:53:00Z <Greenlet at 0x24797943908: input_listener_func> failed with error

[2020-12-11 15:53:00,912] lep/CRITICAL/locust.main: Unhandled exception in greenlet: <Greenlet at 0x24797943908: input_listener_func>
Traceback (most recent call last):
  File "src\\gevent\\greenlet.py", line 854, in gevent._gevent_cgreenlet.Greenlet.run
  File "C:\Users\lep\AppData\Roaming\Python\Python37\site-packages\locust\input_events.py", line 89, in input_listener_func
    poller = get_poller()
  File "C:\Users\lep\AppData\Roaming\Python\Python37\site-packages\locust\input_events.py", line 81, in get_poller
    return WindowsKeyPoller()
  File "C:\Users\lep\AppData\Roaming\Python\Python37\site-packages\locust\input_events.py", line 47, in __init__
    self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)
pywintypes.error: (6, 'SetConsoleMode', '句柄无效。')
@cyberw & @DennisKrone:
Maybe it would make sense to add a command line flag that turns on the keyboard controls? It would avoid fatal errors when running in unknown environments. Also, it would avoid causing problems for people accidentally pressing the keybinding unaware of their existence.
> @cyberw & @DennisKrone:
> Maybe it would make sense to add a command line flag that turns on the keyboard controls? It would avoid fatal errors when running in unknown environments. Also, it would avoid causing problems for people accidentally pressing the keybinding unaware of their existence.

We considered adding that when implementing it, but I think it is better to just fail and (maybe) log when it has failed to initialize. There should never be any fatal errors, so there should be no need to work around that (once we fix this issue).

We could of course make it opt-in, but the list of parameters is already getting very long, and I think keeping it simple is just as important :)
> We could of course make it opt-in, but the list of parameters is already getting very long, and I think keeping it simple is just as important :)

Ok :+1:

One thing I've thought about is to implement a fully fledged terminal GUI using [Urwid](https://urwid.org) (https://github.com/urwid/urwid), similar to somthing like [htop](https://htop.dev/). However, that would require quite a large development effort from someone.

Resolving patch:
diff --git a/locust/input_events.py b/locust/input_events.py
--- a/locust/input_events.py
+++ b/locust/input_events.py
@@ -1,6 +1,7 @@
 import gevent
 import logging
 import os
+import sys
 
 if os.name == "nt":
     from win32api import STD_INPUT_HANDLE
@@ -12,7 +13,6 @@
         ENABLE_PROCESSED_INPUT,
     )
 else:
-    import sys
     import select
     import termios
     import tty
@@ -43,11 +43,14 @@ def poll(_self):
 
 class WindowsKeyPoller:
     def __init__(self):
-        self.read_handle = GetStdHandle(STD_INPUT_HANDLE)
-        self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)
-        self.cur_event_length = 0
-        self.cur_keys_length = 0
-        self.captured_chars = []
+        if sys.stdin.isatty():
+            self.read_handle = GetStdHandle(STD_INPUT_HANDLE)
+            self.read_handle.SetConsoleMode(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT)
+            self.cur_event_length = 0
+            self.cur_keys_length = 0
+            self.captured_chars = []
+        else:
+            raise InitError("Terminal was not a tty. Keyboard input disabled")
 
     def cleanup(self):
         pass


Test patch: