-
Notifications
You must be signed in to change notification settings - Fork 35
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
Conversation
Is it correct that the PR is still in draft mode and not ready for review? |
I tested the code a little bit. Some issues:
|
ee49c58
to
1e65c98
Compare
modmesh/apputil.py
Outdated
# Each run of the application appends a new environment. | ||
environ[name] = self | ||
environ[config['appEnvName']] = self |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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/system.py
Outdated
'redirectStdErrFile': 'stderr_viewer.txt', | ||
} | ||
|
||
# setup new app environment |
There was a problem hiding this comment.
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
There was a problem hiding this 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/wrap_view.cpp
Outdated
return self.command().toStdString(); | ||
}, | ||
"command", [](wrapped_type const & self) | ||
{ return self.command().toStdString(); }, |
There was a problem hiding this comment.
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.
modmesh/apputil.py
Outdated
# Each run of the application appends a new environment. | ||
environ[name] = self | ||
environ[config['appEnvName']] = self |
There was a problem hiding this comment.
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/system.py
Outdated
@@ -83,6 +82,19 @@ def _run_viewer(argv): | |||
wm.windowTitle = "Modmesh Viewer" | |||
wm.resize(w=1000, h=600) | |||
wm.show() | |||
|
|||
config = { |
There was a problem hiding this comment.
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/apputil.py
Outdated
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): |
There was a problem hiding this comment.
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
sys.stdout.flush() | ||
sys.stderr.flush() |
There was a problem hiding this comment.
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.
modmesh/apputil.py
Outdated
|
||
def new_appenv(config=None): | ||
name = config.get('appEnvName', None) | ||
if None is name or name == '': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if None is name or name == '': | |
if name is None or name == '': |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Three reasons:
- 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 '='?
- In this case we are using
is
instead of==
, so there is no risk of typos and accidentally missing an=
. 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
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if None is app: | |
if app is None: |
There was a problem hiding this comment.
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.
modmesh/apputil.py
Outdated
raise KeyError("No AppEnviron is available") | ||
aenv = environ[k] | ||
aenv.run_code(code) | ||
if None is app: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if None is app: | |
if app is None: |
af83c05
to
a935773
Compare
modmesh/apputil.py
Outdated
if self._stderrFd <= 0: | ||
raise Exception("wrong stderr fd:", self._stderrFd) | ||
|
||
sys.stdout = msvcrt.get_osfhandle(self._stdoutFd) |
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
|
||
sys.stdout = oldStdoutFd | ||
sys.stderr = oldStderrFd | ||
except Exception as e: |
There was a problem hiding this comment.
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.
modmesh/apputil.py
Outdated
except Exception as e: | ||
sys.stdout = oldStdoutFd | ||
sys.stderr = oldStderrFd | ||
sys.stderr.write(("{}: {}".format(type(e).__name__, str(e)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
modmesh/apputil.py
Outdated
exec(code, self.globals, self.locals) | ||
import msvcrt | ||
|
||
oldStdoutFd = sys.stdout |
There was a problem hiding this comment.
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
if self._stderrFd <= 0: | ||
raise Exception("wrong stderr fd:", self._stderrFd) | ||
|
||
sys.stdout = msvcrt.get_osfhandle(self._stdoutFd) |
There was a problem hiding this comment.
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?
modmesh/apputil.py
Outdated
@@ -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): |
There was a problem hiding this comment.
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.
modmesh/system.py
Outdated
try: | ||
sys.stdout = msvcrt.get_osfhandle(redirectStdOutFd) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
I also tried to add the following in 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 |
I was testing the code and it seemed that not only did After this I found that the inserted Html in |
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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"); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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{}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@yungyuc ready for merge |
any idea? |
|
the |
cpp/modmesh/python/common.cpp
Outdated
return pybind11::str(m_stderr_buffer.attr("read")()); | ||
} | ||
|
||
PyStdErrOutStreamRedirect::~PyStdErrOutStreamRedirect() noexcept(true) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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)
TODO checklist:
>>>
save logs of execution(not in this PR)