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

Console stdout stderr #148

Merged
merged 28 commits into from
Nov 24, 2022
Merged

Console stdout stderr #148

merged 28 commits into from
Nov 24, 2022

Conversation

tigercosmos
Copy link
Collaborator

@tigercosmos tigercosmos commented Nov 9, 2022

TODO checklist:

  • runtime configuration
  • print(module)
  • mouse double click and select
  • scroll to the end
  • prompt with >>>
  • save logs of execution (not in this PR)

@yungyuc
Copy link
Member

yungyuc commented Nov 9, 2022

Is it correct that the PR is still in draft mode and not ready for review?

@yungyuc
Copy link
Member

yungyuc commented Nov 9, 2022

I tested the code a little bit. Some issues:

  • print(module) does not show anything.
  • Mouse double-clicking in the history box selects either a single word (good; that's what we want) or the whole box (not helpful). I would like double-clicking selects either a single words or a whole line but excluding the prompt. Selecting only an output section may be useful too.
  • The history text box does not scroll to the bottom when new command is added beyond the text area.
  • Conventionally the Python interactive command prompt is >>> , not $ , which looks strange to a Python user.

image

@yungyuc yungyuc added the enhancement New feature or request label Nov 10, 2022
@tigercosmos tigercosmos force-pushed the console-stdout branch 5 times, most recently from ee49c58 to 1e65c98 Compare November 10, 2022 17:01
@tigercosmos tigercosmos changed the title WIP: Console stdout stderr Console stdout stderr Nov 12, 2022
# Each run of the application appends a new environment.
environ[name] = self
environ[config['appEnvName']] = self
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

IMO, this line should live in new_appenv

Copy link
Member

Choose a reason for hiding this comment

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

Do not mingle the non-GUI construct environ with GUi widget. Avoid taking the name from config. The config object should be removed too.

modmesh/apputil.py Outdated Show resolved Hide resolved
modmesh/system.py Outdated Show resolved Hide resolved
'redirectStdErrFile': 'stderr_viewer.txt',
}

# setup new app environment
Copy link
Collaborator Author

@tigercosmos tigercosmos Nov 12, 2022

Choose a reason for hiding this comment

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

Python Console shares the same config with AppEnvironment

currently, only one AppEnvironment is created at the beginning.

in the future, if there is new AppEnvironment created, Python Console should also update its config

modmesh/apputil.py Outdated Show resolved Hide resolved
Copy link
Member

@yungyuc yungyuc 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 most of the change in apputil.py and system.py should be reverted. You added too much Python code and it does not look right. Most of the I/O redirection should be done in C++, not Python. The change in Python should simply changing the underneath file descriptors.

cpp/modmesh/view/RPythonConsoleDockWidget.cpp Outdated Show resolved Hide resolved
cpp/modmesh/view/RPythonConsoleDockWidget.cpp Outdated Show resolved Hide resolved
cpp/modmesh/view/RPythonConsoleDockWidget.hpp Outdated Show resolved Hide resolved
cpp/modmesh/view/wrap_view.cpp Outdated Show resolved Hide resolved
return self.command().toStdString();
},
"command", [](wrapped_type const & self)
{ return self.command().toStdString(); },
Copy link
Member

Choose a reason for hiding this comment

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

Do not change the style.

# Each run of the application appends a new environment.
environ[name] = self
environ[config['appEnvName']] = self
Copy link
Member

Choose a reason for hiding this comment

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

Do not mingle the non-GUI construct environ with GUi widget. Avoid taking the name from config. The config object should be removed too.

modmesh/apputil.py Outdated Show resolved Hide resolved
modmesh/apputil.py Outdated Show resolved Hide resolved
@@ -83,6 +82,19 @@ def _run_viewer(argv):
wm.windowTitle = "Modmesh Viewer"
wm.resize(w=1000, h=600)
wm.show()

config = {
Copy link
Member

Choose a reason for hiding this comment

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

Do not use a Python dictionary or C++ hash or tree to keep configurations. They are slow for HPC.

There are cases that we may tolerate the slowness of a hash/tree, but I do not want to make it enter modmesh so soon. It delivers a wrong impression to devs and users.

modmesh/system.py Outdated Show resolved Hide resolved
modmesh/system.py Outdated Show resolved Hide resolved
with open(self._config['redirectStdOutFile'], 'w') as f1:
with contextlib.redirect_stdout(f1):
with open(self._config['redirectStdErrFile'], 'w') as f2:
with contextlib.redirect_stderr(f2):

Choose a reason for hiding this comment

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

Here you can use (parenthesized context managers)[https://docs.python.org/3.10/whatsnew/3.10.html#parenthesized-context-managers) or ExitStack to avoid the multiple levels of indentation.

modmesh/apputil.py Outdated Show resolved Hide resolved
Comment on lines 96 to 97
sys.stdout.flush()
sys.stderr.flush()
Copy link

@ezio-melotti ezio-melotti Nov 16, 2022

Choose a reason for hiding this comment

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

You should be able to do print(..., flush=True) directly if you only have one print. If you have multiple, doing it once at the end might be better.


def new_appenv(config=None):
name = config.get('appEnvName', None)
if None is name or name == '':

Choose a reason for hiding this comment

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

Suggested change
if None is name or name == '':
if name is None or name == '':

Copy link
Member

@yungyuc yungyuc Nov 16, 2022

Choose a reason for hiding this comment

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

For making the two conditions in the line use consistent style, changing the order is good for reading.

If not in this scenario, (like a single condition), I prefer putting constant in the first place in the conditional checking. It is the same as a convention commonplace in C code, which is to reduce the chance of typo to treat equality == as assignment =, because

if (nullptr = pointer)

is an error compiler must complain about.

With the aid of modern compilers or clang-tidy, the trick is not absolutely necessary, but good to have.

My preference may change if a convincing argument is provided.

Choose a reason for hiding this comment

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

Three reasons:

  1. With a single = in the condition Python already complains, so there is no risk of accidental assignment:
>>> name = 'foo'
>>> if name = None: pass
  File "<stdin>", line 1
    if name = None: pass
       ^^^^^^^^^^^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?
  1. In this case we are using is instead of ==, so there is no risk of typos and accidentally missing an =.
  2. variable == constant reads better and is the convention followed in Python (and many other languages), and in this specific situation it is also symmetric with the second part of the condition

modmesh/apputil.py Outdated Show resolved Hide resolved
modmesh/apputil.py Outdated Show resolved Hide resolved
i = 0
name = 'anonymous%d' % i
while name in environ:
i += 1
name = 'anonymous%d' % i
app = environ.get(name, None)

if None is app:

Choose a reason for hiding this comment

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

Suggested change
if None is app:
if app is None:

Copy link
Member

Choose a reason for hiding this comment

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

This is where constant should be put in the first position of the condition.

raise KeyError("No AppEnviron is available")
aenv = environ[k]
aenv.run_code(code)
if None is app:

Choose a reason for hiding this comment

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

Suggested change
if None is app:
if app is None:

if self._stderrFd <= 0:
raise Exception("wrong stderr fd:", self._stderrFd)

sys.stdout = msvcrt.get_osfhandle(self._stdoutFd)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

failed here, although self._stdoutFd could be 3 from C++ fopen, and msvcrt.get_osfhandle translates it to Python's handle, but it seems python cannot take this FD, probably cannot share the same FD for different processes.

OSError: [Errno 9] Bad file descriptortraceback:  File "C:\Users\tiger\modmesh\modmesh\system.py", line 105, in enter_main
    ret = _run_viewer(argv)
  File "C:\Users\tiger\modmesh\modmesh\system.py", line 80, in _run_viewer
    return view.launch()
  File "C:\Users\tiger\modmesh\modmesh\view.py", line 95, in launch
    return app.exec()
  File "C:\Users\tiger\modmesh\modmesh\system.py", line 117, in exec_code
    apputil.run_code(code, redirectStdOutFd, redirectStdErrFd)
  File "C:\Users\tiger\modmesh\modmesh\apputil.py", line 129, in run_code
    aenv.run_code(code)
  File "C:\Users\tiger\modmesh\modmesh\apputil.py", line 99, in run_code
    traceback.print_stack()

Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason that you have been trying to deal with the fd in Python, but not directly in C/C++? Wouldn't it be easier to simply handle it in CPython API?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I haven't tried if I can get a python file object in C++, I could try.

modmesh/apputil.py Outdated Show resolved Hide resolved

sys.stdout = oldStdoutFd
sys.stderr = oldStderrFd
except Exception as e:

Choose a reason for hiding this comment

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

Here you could either use ValueError or leave Exception if you want to catch other types of exception too.

except Exception as e:
sys.stdout = oldStdoutFd
sys.stderr = oldStderrFd
sys.stderr.write(("{}: {}".format(type(e).__name__, str(e))))

Choose a reason for hiding this comment

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

Suggested change
sys.stderr.write(("{}: {}".format(type(e).__name__, str(e))))
sys.stderr.write(f"{type(e).__name__}: {e}")

There might be something in the traceback module that does this for you already.

cpp/modmesh/view/RPythonConsoleDockWidget.cpp Outdated Show resolved Hide resolved
exec(code, self.globals, self.locals)
import msvcrt

oldStdoutFd = sys.stdout
Copy link
Member

Choose a reason for hiding this comment

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

The names baffled me. I think you intended to mean old_stdout_fobj?

modmesh/apputil.py Outdated Show resolved Hide resolved
if self._stderrFd <= 0:
raise Exception("wrong stderr fd:", self._stderrFd)

sys.stdout = msvcrt.get_osfhandle(self._stdoutFd)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason that you have been trying to deal with the fd in Python, but not directly in C/C++? Wouldn't it be easier to simply handle it in CPython API?

@@ -88,14 +114,17 @@ def get_appenv(name=None):
get_appenv(name='master')


def run_code(code):
def run_code(code, redirectStdOutFd=-1, redirectStdErrFd=-1):
Copy link
Member

Choose a reason for hiding this comment

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

In my imagination, the code handling fd would be more straight-forward if they are in C/C++. In Python, the fd should be wrapped by the Python file object, which is used almost everywhere in Python.

try:
sys.stdout = msvcrt.get_osfhandle(redirectStdOutFd)
Copy link
Collaborator Author

@tigercosmos tigercosmos Nov 22, 2022

Choose a reason for hiding this comment

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

still OSError: [Errno 9] Bad file descriptor

@yungyuc I feel that we cannot open fds in C++, because Python cannot access those fds.
I also tried opening fds in Python and share to C++, but also not work.
C++ and Python should be the same process, so fd should be able to share?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Is there a reason that you have been trying to deal with the fd in Python, but not directly in C/C++? Wouldn't it be easier to simply handle it in CPython API?

maybe I should try this

@tigercosmos
Copy link
Collaborator Author

I also tried to add the following in system.py

r_stdout, w_stdout = os.pipe()
r_stderr, w_stderr = os.pipe()

sys.stdout = os.fdopen(r_stdout, "w+")
sys.stderr = os.fdopen(r_stderr, "w+")

def get_stdout_redirect_fd():
    return w_stdout

def get_stderr_redirect_fd():
    return w_stderr

and in the C++ code, call interp.get_stdout_redirect_fd() to get the fd, and then call _read(fd) ; however, this way doesn't work as well.

@tychuang1211
Copy link
Collaborator

tychuang1211 commented Nov 22, 2022

I was testing the code and it seemed that not only did print(module) not show anything, but everything enclosed in angle brackets also didn't show. And I also checked the stream before and after redirection, both were correct.

After this I found that the inserted Html in RPythonConsoleDockWidget::printCommandStdout and RPythonConsoleDockWidget::printCommandStderr were not escaped. We could simply change QString::fromStdString(message) to QString::fromStdString(message).toHtmlEscaped() in both function to fix the problem.

image

QTextCursor cursor = m_history_edit->textCursor();
cursor.movePosition(QTextCursor::End);
m_history_edit->setTextCursor(cursor);
m_history_edit->insertHtml(m_stdoutHtml + QString::fromStdString(stdout_message) + m_endHtml);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
m_history_edit->insertHtml(m_stdoutHtml + QString::fromStdString(stdout_message) + m_endHtml);
m_history_edit->insertHtml(m_stdoutHtml + QString::fromStdString(stdout_message).toHtmlEscaped() + m_endHtml);

Copy link
Member

Choose a reason for hiding this comment

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

I think this is good! But perhaps for simple coloring, we may also consider to directly use Qt API for changing color? I am curious which way works better for the terminal.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

thanks @tychuang1211, I didn't think about toHtmlEscaped. btw, which code did you test, I think it's not the latest commit?
@yungyuc, it's inconvenient to set different colors for text if we use QT way. let's just use HTML way.

Copy link
Member

Choose a reason for hiding this comment

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

@tigercosmos I am OK to use HTML there for now, but convenience should not be the major factor for deciding which way to go. The more indirect the longer runtime it takes. To some point we will need to evaluate and pick a "better" way, of which the metrics are not defined at the moment.

tigercosmos and others added 10 commits November 23, 2022 13:39
Co-authored-by: tychuang1211 <33109379+tychuang1211@users.noreply.github.com>
Co-authored-by: tychuang1211 <33109379+tychuang1211@users.noreply.github.com>
This reverts commit a9d5f7d.
This reverts commit a637592.
Copy link
Member

@yungyuc yungyuc left a comment

Choose a reason for hiding this comment

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

With some cosmetic improvements we are good to check in!

PyStdErrOutStreamRedirect()
{
auto sysm = pybind11::module::import("sys");
_stdout = sysm.attr("stdout");
Copy link
Member

Choose a reason for hiding this comment

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

Please avoid using underscore _ in the beginning of a variable, because the standard reserves them: https://softwareengineering.stackexchange.com/a/191833

@@ -33,6 +33,43 @@
namespace modmesh
{

class PyStdErrOutStreamRedirect
{
pybind11::object _stdout;
Copy link
Member

Choose a reason for hiding this comment

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

Member data names should start with m_.

sysm.attr("stdout") = _stdout_buffer;
sysm.attr("stderr") = _stderr_buffer;
}
std::string stdoutString()
Copy link
Member

Choose a reason for hiding this comment

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

It's not a Qt function, and the name should follow the snake_case convention.

m_command_edit->setPlainText("");
m_command_string = "";
m_current_command_index = static_cast<int>(m_past_command_strings.size());
auto & interp = modmesh::python::Interpreter::instance();
PyStdErrOutStreamRedirect pyOutputRedirect{};
Copy link
Member

Choose a reason for hiding this comment

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

snake_case for local variables.

Copy link
Member

Choose a reason for hiding this comment

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

This is elegant. Not even need a line of comment.

@tigercosmos
Copy link
Collaborator Author

@yungyuc ready for merge

@tigercosmos
Copy link
Collaborator Author

/home/runner/work/modmesh/modmesh/cpp/modmesh/python/common.cpp:208:28: error: an exception may be thrown in function '~PyStdErrOutStreamRedirect' which should not throw exceptions [bugprone-exception-escape,-warnings-as-errors]

any idea?

@yungyuc
Copy link
Member

yungyuc commented Nov 24, 2022

/home/runner/work/modmesh/modmesh/cpp/modmesh/python/common.cpp:208:28: error: an exception may be thrown in function '~PyStdErrOutStreamRedirect' which should not throw exceptions [bugprone-exception-escape,-warnings-as-errors]

any idea?

https://releases.llvm.org/13.0.0/tools/clang/tools/extra/docs/clang-tidy/checks/bugprone-exception-escape.html

@tychuang1211
Copy link
Collaborator

/home/runner/work/modmesh/modmesh/cpp/modmesh/python/common.cpp:208:28: error: an exception may be thrown in function '~PyStdErrOutStreamRedirect' which should not throw exceptions [bugprone-exception-escape,-warnings-as-errors]

any idea?

the pybind11::module::import("sys") in ~PyStdErrOutStreamRedirect may throw an exception (pybind11::error_already_set)

return pybind11::str(m_stderr_buffer.attr("read")());
}

PyStdErrOutStreamRedirect::~PyStdErrOutStreamRedirect() noexcept(true)
Copy link
Member

@yungyuc yungyuc Nov 24, 2022

Choose a reason for hiding this comment

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

Because you still may throw an exception, it should be noexcept(false). Since no specifying noexcept means noexcept(false), it's not really needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

because I only catch pybind11::error_already_set here?

also, if I use try catch(...) for the whole deconstruction function, will it throw?

Copy link
Member

Choose a reason for hiding this comment

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

I do not fully understand your questions. Could you be more specific?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Because you still may throw an exception, it should be noexcept(false)

is it because I only catch pybind11::error_already_set in the code?

if I write:

    try
    {
     // ..
    }
    catch (...) // capture all exception
    {
        // ...
    }

will it still throw exception?

Copy link
Member

Choose a reason for hiding this comment

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

I do not know. noexcept means the function does not throw exception. I am not sure it means no exception handling is allowed in the function, or no except is thrown outside the function. I guess it's the latter, but if it's the former, a try catch block in noexcept would be problematic.

I would be interested in the behavior too.

Copy link
Collaborator Author

@tigercosmos tigercosmos Nov 24, 2022

Choose a reason for hiding this comment

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

it seems no difference for the behavior when noexcept is true or false if we catch all errors

struct Foo {
    Foo(){}
    
    ~Foo() noexcept(true){
        try {
            throw "123";
        }
        catch(...) {
            //
        }
    }
};

int main()
{
  Foo foo;
}

the program can run without panic with both conditions.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

however, this panic

struct Foo {
    Foo(){}

    ~Foo() noexcept(true){
        try {
            throw "123";
        }
        catch(int a) {
                std::cerr << "XX" ;
        }
    }
};

int main()
{
  Foo foo;
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

so I think the answer is, the current code might need to use noexcept(false) because it only capture error_already_set.
if it is catch(...) then we can set it noexcept(true)

@yungyuc yungyuc merged commit b485565 into solvcon:master Nov 24, 2022
@tigercosmos tigercosmos deleted the console-stdout branch November 24, 2022 13:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants