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 support for Console apps #29

Merged
merged 4 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
"formal_name": "App Name",
"app_name": "{{ cookiecutter.formal_name|lower|replace(' ', '-') }}",
"module_name": "{{ cookiecutter.app_name|replace('-', '_') }}",
"bundle": "com.example",
"version_triple": "0.0.1",
"author": "Example Corporation",
"author_email": "contact@example.com",
"url": "http://example.com",
"description": "Short description of app",
"console_app": false,
"guid": "1409c8f5-c276-4cf3-a2fd-defcbdfef9a2",
"install_scope": "",
"use_full_install_path": true,
"python_version": "3.X.0",
"_extensions": [
"briefcase.integrations.cookiecutter.PythonVersionExtension"
"briefcase.integrations.cookiecutter.PythonVersionExtension",
"briefcase.integrations.cookiecutter.UUIDExtension"
]
}
20 changes: 18 additions & 2 deletions {{ cookiecutter.format }}/{{ cookiecutter.app_name }}.wxs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@
<Directory Id="{{ cookiecutter.module_name }}_ROOTDIR" Name="{{ cookiecutter.formal_name }}" />
{%- endif %}
</Directory>

{% if cookiecutter.console_app %}
<Component Id="SystemPathEnv" Guid="{{ ".".join(["system-path", cookiecutter.app_name] + cookiecutter.bundle.split(".")[::-1])|dns_uuid5 }}">
<Environment Id="AppendSystemPathEnvValue" Name="PATH" Action="set" Part="last" System="yes" Value="[{{ cookiecutter.module_name }}_ROOTDIR]" Permanent="no" />
</Component>
<Component Id="UserPathEnv" Guid="{{ ".".join(["user-path", cookiecutter.app_name] + cookiecutter.bundle.split(".")[::-1])|dns_uuid5 }}">
<Environment Id="AppendUserPathEnvValue" Name="PATH" Action="set" Part="last" System="no" Value="[{{ cookiecutter.module_name }}_ROOTDIR]" Permanent="no" />
</Component>
{% endif %}
<Directory Id="ProgramMenuFolder">
<Directory Id="ProgramMenuSubfolder" Name="{{ cookiecutter.formal_name }}">
<Component
Expand Down Expand Up @@ -99,7 +106,16 @@
<ComponentGroupRef Id="{{ cookiecutter.module_name }}_COMPONENTS" />
<ComponentRef Id="ApplicationShortcuts"/>
</Feature>

{% if cookiecutter.console_app %}
<Feature Id="SystemPathEnvFeature" Level="1">
<Condition Level="0">ALLUSERS=2 OR MSIINSTALLPERUSER=1</Condition>
<ComponentRef Id="SystemPathEnv"/>
</Feature>
<Feature Id="UserPathEnvFeature" Level="1">
<Condition Level="0">ALLUSERS=1</Condition>
<ComponentRef Id="UserPathEnv"/>
</Feature>
{% endif %}
<WixVariable Id="WixUISupportPerUser" Value="1" Overridable="yes" />
<WixVariable Id="WixUISupportPerMachine" Value="1" Overridable="yes" />

Expand Down
172 changes: 98 additions & 74 deletions {{ cookiecutter.format }}/{{ cookiecutter.formal_name }}/Main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

#include <Python.h>

{% if cookiecutter.console_app %}
{% else %}
#include "CrashDialog.h"

{% endif %}
#include <fcntl.h>
#include <windows.h>

Expand All @@ -13,18 +15,21 @@ using namespace System::IO;
using namespace System::Windows::Forms;


// A global indicator of the debug level
char *debug_mode;

#define info_log(...) printf(__VA_ARGS__)
#define debug_log(...) if (debug_mode) printf(__VA_ARGS__)

wchar_t* wstr(String^);
void setup_stdout(FileVersionInfo^);
void crash_dialog(String^);
String^ format_traceback(PyObject *type, PyObject *value, PyObject *traceback);

int Main(array<String^>^ args) {
int ret = 0;
size_t size;
FileVersionInfo^ version_info;
String^ log_folder;
String^ src_log;
String^ dst_log;
FILE* log;
PyStatus status;
PyConfig config;
String^ python_home;
Expand All @@ -42,62 +47,23 @@ int Main(array<String^>^ args) {
PyObject *exc_traceback;
PyObject *systemExit_code;

// Set the global debug state based on the runtime environment
_dupenv_s(&debug_mode, &size, "BRIEFCASE_DEBUG");

// Uninitialize the Windows threading model; allow apps to make
// their own threading model decisions.
CoUninitialize();

// Get details of the app from app metadata
version_info = FileVersionInfo::GetVersionInfo(Application::ExecutablePath);

// If we can attach to the console, then we're running in a terminal;
// we can use stdout and stderr as normal. However, if there's no
// console, we need to redirect stdout/err to a log file.
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
log_folder = Environment::GetFolderPath(Environment::SpecialFolder::LocalApplicationData) + "\\" +
version_info->CompanyName + "\\" +
version_info->ProductName + "\\Logs";
if (!Directory::Exists(log_folder)) {
// If log folder doesn't exist, create it
Directory::CreateDirectory(log_folder);
} else {
// If it does, rotate the logs in that folder.
// - Delete <app name>-9.log
src_log = log_folder + "\\" + version_info->InternalName + "-9.log";
if (File::Exists(src_log)) {
File::Delete(src_log);
}

// - Move <app name>-8.log -> <app name>-9.log
// - Move <app name>-7.log -> <app name>-8.log
// - ...
// - Move <app name>.log -> <app name>-2.log
for (int dst_index = 9; dst_index >= 2; dst_index--) {
if (dst_index == 2) {
src_log = log_folder + "\\" + version_info->InternalName + ".log";
} else {
src_log = log_folder + "\\" + version_info->InternalName + "-" + (dst_index - 1) + ".log";
}
dst_log = log_folder + "\\" + version_info->InternalName + "-" + dst_index + ".log";

if (File::Exists(src_log)) {
File::Move(src_log, dst_log);
}
}
}

// Redirect stdout to a log file <app name>.log, in the
// user's local Logs folder for the app.
// stderr doesn't exist when running without an attached console;
// sys.stderr will be None in the Python interpreter. This causes
// all error output to be written to stdout.
_wfreopen_s(&log, wstr(log_folder + "\\" + version_info->InternalName + ".log"), L"w", stdout);
}
// Set up stdout/err handling
setup_stdout(version_info);

printf("Log started: %S\n", wstr(DateTime::Now.ToString("yyyy-MM-dd HH:mm:ssZ")));
// Preconfigure the Python interpreter;
// This ensures the interpreter is in Isolated mode,
// and is using UTF-8 encoding.
printf("PreInitializing Python runtime...\n");
debug_log("PreInitializing Python runtime...\n");
PyPreConfig pre_config;
PyPreConfig_InitPythonConfig(&pre_config);
pre_config.utf8_mode = 1;
Expand All @@ -122,7 +88,7 @@ int Main(array<String^>^ args) {

// Set the home for the Python interpreter
python_home = Application::StartupPath;
printf("PythonHome: %S\n", wstr(python_home));
debug_log("PythonHome: %S\n", wstr(python_home));
status = PyConfig_SetString(&config, &config.home, wstr(python_home));
if (PyStatus_Exception(status)) {
crash_dialog("Unable to set PYTHONHOME: " + gcnew String(status.err_msg));
Expand Down Expand Up @@ -157,10 +123,10 @@ int Main(array<String^>^ args) {
}

// Set the full module path. This includes the stdlib, site-packages, and app code.
printf("PYTHONPATH:\n");
debug_log("PYTHONPATH:\n");
// The .zip form of the stdlib
path = python_home + "\\python{{ cookiecutter.python_version|py_libtag }}.zip";
printf("- %S\n", wstr(path));
debug_log("- %S\n", wstr(path));
status = PyWideStringList_Append(&config.module_search_paths, wstr(path));
if (PyStatus_Exception(status)) {
crash_dialog("Unable to set .zip form of stdlib path: " + gcnew String(status.err_msg));
Expand All @@ -170,7 +136,7 @@ int Main(array<String^>^ args) {

// The unpacked form of the stdlib
path = python_home;
printf("- %S\n", wstr(path));
debug_log("- %S\n", wstr(path));
status = PyWideStringList_Append(&config.module_search_paths, wstr(path));
if (PyStatus_Exception(status)) {
crash_dialog("Unable to set unpacked form of stdlib path: " + gcnew String(status.err_msg));
Expand All @@ -180,7 +146,7 @@ int Main(array<String^>^ args) {

// Add the app_packages path
path = System::Windows::Forms::Application::StartupPath + "\\app_packages";
printf("- %S\n", wstr(path));
debug_log("- %S\n", wstr(path));
status = PyWideStringList_Append(&config.module_search_paths, wstr(path));
if (PyStatus_Exception(status)) {
crash_dialog("Unable to set app packages path: " + gcnew String(status.err_msg));
Expand All @@ -190,15 +156,15 @@ int Main(array<String^>^ args) {

// Add the app path
path = System::Windows::Forms::Application::StartupPath + "\\app";
printf("- %S\n", wstr(path));
debug_log("- %S\n", wstr(path));
status = PyWideStringList_Append(&config.module_search_paths, wstr(path));
if (PyStatus_Exception(status)) {
crash_dialog("Unable to set app path: " + gcnew String(status.err_msg));
PyConfig_Clear(&config);
Py_ExitStatusException(status);
}

printf("Configure argc/argv...\n");
debug_log("Configure argc/argv...\n");
wchar_t** argv = new wchar_t* [args->Length + 1];
argv[0] = wstr(Application::ExecutablePath);
for (int i = 0; i < args->Length; i++) {
Expand All @@ -211,7 +177,7 @@ int Main(array<String^>^ args) {
Py_ExitStatusException(status);
}

printf("Initializing Python runtime...\n");
debug_log("Initializing Python runtime...\n");
status = Py_InitializeFromConfig(&config);
if (PyStatus_Exception(status)) {
crash_dialog("Unable to initialize Python interpreter: " + gcnew String(status.err_msg));
Expand All @@ -227,7 +193,7 @@ int Main(array<String^>^ args) {
// pymain_run_module() method); we need to re-implement it
// because we need to be able to inspect the error state of
// the interpreter, not just the return code of the module.
printf("Running app module: %S\n", app_module_str);
debug_log("Running app module: %S\n", app_module_str);

module = PyImport_ImportModule("runpy");
if (module == NULL) {
Expand Down Expand Up @@ -255,7 +221,7 @@ int Main(array<String^>^ args) {

// Print a separator to differentiate Python startup logs from app logs,
// then flush stdout/stderr to ensure all startup logs have been output.
printf("---------------------------------------------------------------------------\n");
debug_log("---------------------------------------------------------------------------\n");
fflush(stdout);
fflush(stderr);

Expand All @@ -275,31 +241,21 @@ int Main(array<String^>^ args) {
if (PyErr_GivenExceptionMatches(exc_value, PyExc_SystemExit)) {
systemExit_code = PyObject_GetAttrString(exc_value, "code");
if (systemExit_code == NULL) {
printf("Could not determine exit code\n");
debug_log("Could not determine exit code\n");
ret = -10;
}
else {
ret = (int) PyLong_AsLong(systemExit_code);
}
} else {
// Non-SystemExit; likely an uncaught exception
ret = -6;
}
info_log("---------------------------------------------------------------------------\n");
info_log("Application quit abnormally!\n");

if (ret != 0) {
// Display stack trace in the crash dialog.
traceback_str = format_traceback(exc_type, exc_value, exc_traceback);
printf("Application quit abnormally (Exit code %d)!\n", ret);

// Restore the error state of the interpreter.
PyErr_Restore(exc_type, exc_value, exc_traceback);

// Print exception to stderr.
// In case of SystemExit, this will call exit()
PyErr_Print();

// Display stack trace in the crash dialog.
crash_dialog(traceback_str);
exit(ret);
}
}
}
Expand All @@ -318,6 +274,73 @@ wchar_t *wstr(String^ str)
return (wchar_t*)pinned;
}

{% if cookiecutter.console_app %}
void setup_stdout(FileVersionInfo^ version_info) {
// No special stdout handling required
}

void crash_dialog(System::String^ details) {
// Write the error to the console
printf("%S\n", wstr(details));
}
{% else %}
/**
* Setup stdout and stderr to redirect to the console if it exists,
* but output to a file it doesn't.
*/
void setup_stdout(FileVersionInfo^ version_info) {
String^ log_folder;
String^ src_log;
String^ dst_log;
FILE *log;

// If we can attach to the console, then we're running in a terminal;
// we can use stdout and stderr as normal. However, if there's no
// console, we need to redirect stdout/err to a log file.
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
log_folder = Environment::GetFolderPath(Environment::SpecialFolder::LocalApplicationData) + "\\" +
version_info->CompanyName + "\\" +
version_info->ProductName + "\\Logs";
if (!Directory::Exists(log_folder)) {
// If log folder doesn't exist, create it
Directory::CreateDirectory(log_folder);
} else {
// If it does, rotate the logs in that folder.
// - Delete <app name>-9.log
src_log = log_folder + "\\" + version_info->InternalName + "-9.log";
if (File::Exists(src_log)) {
File::Delete(src_log);
}

// - Move <app name>-8.log -> <app name>-9.log
// - Move <app name>-7.log -> <app name>-8.log
// - ...
// - Move <app name>.log -> <app name>-2.log
for (int dst_index = 9; dst_index >= 2; dst_index--) {
if (dst_index == 2) {
src_log = log_folder + "\\" + version_info->InternalName + ".log";
} else {
src_log = log_folder + "\\" + version_info->InternalName + "-" + (dst_index - 1) + ".log";
}
dst_log = log_folder + "\\" + version_info->InternalName + "-" + dst_index + ".log";

if (File::Exists(src_log)) {
File::Move(src_log, dst_log);
}
}
}

// Redirect stdout to a log file <app name>.log, in the
// user's local Logs folder for the app.
// stderr doesn't exist when running without an attached console;
// sys.stderr will be None in the Python interpreter. This causes
// all error output to be written to stdout.
_wfreopen_s(&log, wstr(log_folder + "\\" + version_info->InternalName + ".log"), L"w", stdout);
}

debug_log("Log started: %S\n", wstr(DateTime::Now.ToString("yyyy-MM-dd HH:mm:ssZ")));
}

/**
* Construct and display a modal dialog to the user that contains
* details of an error during application execution (usually a traceback).
Expand All @@ -338,6 +361,7 @@ void crash_dialog(System::String^ details) {
form = gcnew Briefcase::CrashDialog(details);
form->ShowDialog();
}
{% endif %}

/**
* Convert a Python traceback object into a user-suitable string, stripping off
Expand Down
Loading