Skip to content

Commit

Permalink
Merge pull request #29 from beeware/console-app
Browse files Browse the repository at this point in the history
Add support for Console apps
  • Loading branch information
freakboy3742 authored Jun 4, 2024
2 parents 8b8987b + 2a912c7 commit 67a2b35
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 83 deletions.
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

0 comments on commit 67a2b35

Please sign in to comment.