Merge pull request #3 from flet-dev/v2

v2 - logging improvements
This commit is contained in:
Feodor Fitsner 2024-01-15 14:51:19 -08:00 committed by GitHub
commit 669f6301f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 254 additions and 37 deletions

View File

@ -9,7 +9,6 @@
"copyright": "Copyright (c) 2023 Your Company", "copyright": "Copyright (c) 2023 Your Company",
"sep": "/", "sep": "/",
"kotlin_dir": "{{ cookiecutter.org_name.replace('.', cookiecutter.sep) }}{{ cookiecutter.sep }}{{ cookiecutter.project_name }}{{ cookiecutter.sep }}", "kotlin_dir": "{{ cookiecutter.org_name.replace('.', cookiecutter.sep) }}{{ cookiecutter.sep }}{{ cookiecutter.project_name }}{{ cookiecutter.sep }}",
"windows_tcp_port": 63777,
"hide_loading_animation": true, "hide_loading_animation": true,
"team_id": "", "team_id": "",
"base_url": "/", "base_url": "/",

View File

@ -6,7 +6,7 @@ buildscript {
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.3.0' classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
} }
} }

View File

@ -1,14 +1,78 @@
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:flet/flet.dart'; import 'package:flet/flet.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:serious_python/serious_python.dart'; import 'package:serious_python/serious_python.dart';
import 'package:url_strategy/url_strategy.dart'; import 'package:url_strategy/url_strategy.dart';
const bool isProduction = bool.fromEnvironment('dart.vm.product'); const bool isProduction = bool.fromEnvironment('dart.vm.product');
const assetPath = "app/app.zip";
const pythonModuleName = "{{ cookiecutter.python_module_name }}";
final hideLoadingPage =
bool.tryParse("{{ cookiecutter.hide_loading_animation }}".toLowerCase()) ??
true;
const outLogFilename = "out.log";
const errorExitCode = 100;
const pythonScript = """
import certifi, os, runpy, socket, sys, traceback
os.environ["REQUESTS_CA_BUNDLE"] = certifi.where()
os.environ["SSL_CERT_FILE"] = certifi.where()
if os.getenv("FLET_PLATFORM") == "android":
import ssl
def create_default_context(
purpose=ssl.Purpose.SERVER_AUTH, *, cafile=None, capath=None, cadata=None
):
return ssl.create_default_context(
purpose=purpose, cafile=certifi.where(), capath=capath, cadata=cadata
)
ssl._create_default_https_context = create_default_context
out_file = open("$outLogFilename", "w+", buffering=1)
callback_socket_addr = os.environ.get("FLET_PYTHON_CALLBACK_SOCKET_ADDR")
if ":" in callback_socket_addr:
addr, port = callback_socket_addr.split(":")
callback_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
callback_socket.connect((addr, int(port)))
else:
callback_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
callback_socket.connect(callback_socket_addr)
sys.stdout = sys.stderr = out_file
def flet_exit(code=0):
callback_socket.sendall(str(code).encode())
out_file.close()
callback_socket.close()
sys.exit = flet_exit
ex = None
try:
runpy.run_module("{module_name}", run_name="__main__")
except Exception as e:
ex = e
traceback.print_exception(e)
sys.exit(0 if ex is None else $errorExitCode)
""";
// global vars
String pageUrl = "";
String assetsDir = "";
String appDir = "";
Map<String, String> environmentVariables = {};
void main() async { void main() async {
if (isProduction) { if (isProduction) {
// ignore: avoid_returning_null_for_void // ignore: avoid_returning_null_for_void
@ -17,29 +81,49 @@ void main() async {
runApp(FutureBuilder( runApp(FutureBuilder(
future: prepareApp(), future: prepareApp(),
builder: (BuildContext context, AsyncSnapshot<List<String>> snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
return FletApp( // OK - start Python program
pageUrl: snapshot.data![0], return kIsWeb
assetsDir: snapshot.data![1], ? FletApp(
hideLoadingPage: bool.tryParse( pageUrl: pageUrl,
"{{ cookiecutter.hide_loading_animation }}".toLowerCase()), assetsDir: assetsDir,
hideLoadingPage: hideLoadingPage,
)
: FutureBuilder(
future: runPythonApp(),
builder:
(BuildContext context, AsyncSnapshot<String?> snapshot) {
if (snapshot.hasData || snapshot.hasError) {
// error or premature finish
return MaterialApp(
home: ErrorScreen(
title: "Error running app",
text: snapshot.data ?? snapshot.error.toString()),
); );
} else {
// no result of error
return FletApp(
pageUrl: pageUrl,
assetsDir: assetsDir,
hideLoadingPage: hideLoadingPage,
);
}
});
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return Text('Error loading Flet app: ${snapshot.error}'); // error
return MaterialApp(
home: ErrorScreen(
title: "Error starting app",
text: snapshot.error.toString()));
} else { } else {
// loading // loading
return const SizedBox.shrink(); return const MaterialApp(home: BlankScreen());
} }
})); }));
} }
Future<List<String>> prepareApp() async { Future prepareApp() async {
await setupDesktop();
String pageUrl = "";
String assetsDir = "";
if (kIsWeb) { if (kIsWeb) {
// web mode - connect via HTTP // web mode - connect via HTTP
pageUrl = Uri.base.toString(); pageUrl = Uri.base.toString();
@ -48,33 +132,163 @@ Future<List<String>> prepareApp() async {
setPathUrlStrategy(); setPathUrlStrategy();
} }
} else { } else {
await setupDesktop();
// extract app from asset // extract app from asset
var appDir = await extractAssetZip("app/app.zip"); appDir = await extractAssetZip(assetPath, checkHash: true);
// set current directory to app path // set current directory to app path
Directory.current = appDir; Directory.current = appDir;
assetsDir = path.join(appDir, "assets"); assetsDir = path.join(appDir, "assets");
var environmentVariables = { environmentVariables["FLET_PLATFORM"] =
"FLET_PLATFORM": defaultTargetPlatform.name.toLowerCase() defaultTargetPlatform.name.toLowerCase();
};
if (defaultTargetPlatform == TargetPlatform.windows) { if (defaultTargetPlatform == TargetPlatform.windows) {
// use TCP on Windows // use TCP on Windows
var port = int.parse("{{ cookiecutter.windows_tcp_port }}"); var tcpPort = await getUnusedPort();
pageUrl = "tcp://localhost:$port"; pageUrl = "tcp://localhost:$tcpPort";
environmentVariables["FLET_SERVER_PORT"] = port.toString(); environmentVariables["FLET_SERVER_PORT"] = tcpPort.toString();
} else { } else {
// use UDS on other platforms // use UDS on other platforms
pageUrl = "flet.sock"; pageUrl = "flet.sock";
environmentVariables["FLET_SERVER_UDS_PATH"] = pageUrl; environmentVariables["FLET_SERVER_UDS_PATH"] = pageUrl;
} }
SeriousPython.runProgram(
path.join(appDir, "{{ cookiecutter.python_module_name }}.pyc"),
environmentVariables: environmentVariables);
} }
return [pageUrl, assetsDir]; return "";
}
Future<String?> runPythonApp() async {
var script = pythonScript.replaceAll('{module_name}', pythonModuleName);
var completer = Completer<String>();
ServerSocket outSocketServer;
String socketAddr = "";
StringBuffer pythonOut = StringBuffer();
if (defaultTargetPlatform == TargetPlatform.windows) {
var tcpAddr = "127.0.0.1";
outSocketServer = await ServerSocket.bind(tcpAddr, 0);
debugPrint(
'Python output TCP Server is listening on port ${outSocketServer.port}');
socketAddr = "$tcpAddr:${outSocketServer.port}";
} else {
socketAddr = "stdout.sock";
outSocketServer = await ServerSocket.bind(
InternetAddress(socketAddr, type: InternetAddressType.unix), 0);
debugPrint('Python output Socket Server is listening on $socketAddr');
}
environmentVariables["FLET_PYTHON_CALLBACK_SOCKET_ADDR"] = socketAddr;
void closeOutServer() async {
outSocketServer.close();
int exitCode = int.tryParse(pythonOut.toString().trim()) ?? 0;
if (exitCode == errorExitCode) {
var out = "";
if (await File(outLogFilename).exists()) {
out = await File(outLogFilename).readAsString();
}
completer.complete(out);
} else {
exit(exitCode);
}
}
outSocketServer.listen((client) {
debugPrint(
'Connection from: ${client.remoteAddress.address}:${client.remotePort}');
client.listen((data) {
var s = String.fromCharCodes(data);
pythonOut.write(s);
}, onError: (error) {
client.close();
closeOutServer();
}, onDone: () {
client.close();
closeOutServer();
});
});
// run python async
SeriousPython.runProgram(path.join(appDir, "$pythonModuleName.pyc"),
script: script, environmentVariables: environmentVariables);
// wait for client connection to close
return completer.future;
}
class ErrorScreen extends StatelessWidget {
final String title;
final String text;
const ErrorScreen({super.key, required this.title, required this.text});
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Container(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
TextButton.icon(
onPressed: () {
Clipboard.setData(ClipboardData(text: text));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Copied to clipboard')),
);
},
icon: const Icon(
Icons.copy,
size: 16,
),
label: const Text("Copy"),
)
],
),
Expanded(
child: SingleChildScrollView(
child: SelectableText(text,
style: Theme.of(context).textTheme.bodySmall),
))
],
),
)),
);
}
}
class BlankScreen extends StatelessWidget {
const BlankScreen({
super.key,
});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: SizedBox.shrink(),
);
}
}
Future<int> getUnusedPort() {
return ServerSocket.bind("127.0.0.1", 0).then((socket) {
var port = socket.port;
socket.close();
return port;
});
} }

View File

@ -1,4 +1,4 @@
platform :osx, '10.15' platform :osx, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency. # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true' ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@ -457,7 +457,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;
@ -536,7 +536,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@ -583,7 +583,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.15; MACOSX_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule; SWIFT_COMPILATION_MODE = wholemodule;

View File

@ -11,7 +11,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
serious_python: ^0.6.1 serious_python: ^0.7.0
# serious_python: # serious_python:
# git: # git:
# url: https://github.com/flet-dev/serious-python # url: https://github.com/flet-dev/serious-python
@ -43,6 +43,7 @@ flutter:
assets: assets:
- app/app.zip - app/app.zip
- app/app.zip.hash
# dart run flutter_launcher_icons # dart run flutter_launcher_icons
flutter_launcher_icons: flutter_launcher_icons:

View File

@ -9,13 +9,16 @@ self.initPyodide = async function () {
self.pyodide.registerJsModule("flet_js", flet_js); self.pyodide.registerJsModule("flet_js", flet_js);
flet_js.documentUrl = documentUrl; flet_js.documentUrl = documentUrl;
await self.pyodide.runPythonAsync(` await self.pyodide.runPythonAsync(`
import sys import sys, runpy, traceback
from pyodide.http import pyfetch from pyodide.http import pyfetch
response = await pyfetch("assets/app/app.zip") response = await pyfetch("assets/app/app.zip")
await response.unpack_archive() await response.unpack_archive()
sys.path.append("__pypackages__") sys.path.append("__pypackages__")
try:
runpy.run_module("${self.pythonModuleName}", run_name="__main__")
except Exception as e:
traceback.print_exception(e)
`); `);
pyodide.pyimport(self.pythonModuleName);
await self.flet_js.start_connection(self.receiveCallback); await self.flet_js.start_connection(self.receiveCallback);
self.postMessage("initialized"); self.postMessage("initialized");
}; };