diff --git a/.gitignore b/.gitignore index 790fc72..77d7b33 100755 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,7 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/app/build +/android/.gradle local diff --git a/README.md b/README.md index dd049d9..312f37f 100755 --- a/README.md +++ b/README.md @@ -1,144 +1,18 @@ -# nodebase - - - -Running Node.js application over Wifi and share with your friends. - -For previous mature version, please explore source code on - kotlin branch and - v0flutter branch. - -## Rewrite Progress - -- [x] migrate kotlin service code -- [x] implement windows adapter for MethodChannel and EventChannel -- [x] rewrite new design UI in dart (basic flow) -- [ ] polish new design UI in dart -- [x] implement linux adapter -- [ ] implement macosx adapter -- [ ] implement web+ios adapter - -## How to use - -- make sure your device can connect to Internet - - choose application for downloading (e.g. file\_transfer-1.0.0) - - choose platform for downloading (e.g. node-v10.10.0) -- application - - click `play` button to start app via a wizard - -### Market Structure - -``` -/nodebase.json -{ - "version": "...", - "platform--": "...", ... - "platform-windows-x64": "e.g." -} - -/plm--.json -{ - "items": [ - "-", ... - "node-v10.10.0" - ] -} - -/app--.json -{ - "items": [ - "-", ... - "app-1.0.0:node" - ] -} - -/plm///-))>.json -{ - "name": "e.g. node", - "version": "e.g. v20.10.0", - "source": ", e.g. https://nodejs.org/dist/v20.11.0/node-v20.11.0-win-x64.zip", - "executable": ["", "e.g. node-v20.11.0-win-x64\\node.exe"] -} - -/app///-))>.json -{ - "name": "e.g. file_transfer", - "version": "e.g. 1.0", - "source": ", e.g. https://raw.githubusercontent.com/wiki/dna2github/NodeBase/quick/app/node/file-transfer.zip", - "type": "web.server", - "argRequire": [{ "help": "folder path", "default": "." }], - "envRequire": [], - "entryPoint": ["index.js"] -} - -``` - -App examples: [https://github.com/nodebase0](https://github.com/nodebase0), includes file-viewer-uploader, nodepad, ... - - -## Development - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev/), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - - -##### NodeJS/Python binary for ARM - -ref: https://github.com/dna2github/dna2oslab/releases/tag/0.2.0-android-gt6-arm - -##### Java binary - -write a shell script `java` and `adb push java /sdcard` -``` -#!/system/bin/sh - -exec dalvikvm $@ -``` - -create a new platform in NodeBase and download java wrapper from `file:///sdcard/java` - -then write a command line tool to have a try. ref: https://github.com/dna2github/dna2sevord/tree/master/past/others/walkserver/javacmd - -##### Golang binary - -``` -# download go source package and extract -cd src -GOOS=android GOARCH=arm64 ./bootstrap.bash - -tar zcf go-android-arm64-bootstrap.tar.gz go-android-arm64-bootstrap -adb push go-android-arm64-bootstrap.tar.gz /sdcard/ -# we suggest write a javascript script to set up golang environment on your Android -# to extract tar package to NodeBase app zone /data/user/0/net.seven.nodebase/ -# e.g. /data/user/0/net.seven.nodebase/go-android-arm64-bootstrap -``` - -write a shell script `go` and `adb push go /sdcard` - -``` -#!/system/bin/sh - -SELF=$(cd `dirname $0`; pwd) -BASE=/data/user/0/net.seven.nodebase/go-android-arm64-bootstrap -CACHEBASE=${BASE}/cache -mkdir -p ${CACHEBASE}/{cache,tmp,local} -export GOROOT=${BASE} -export GOPATH=${CACHEBASE}/golang/local -export GOCACHE=${CACHEBASE}/golang/cache -export GOTMPDIR=${CACHEBASE}/golang/tmp -export CGO_ENABLED=0 -exec ${BASE}/bin/go run $@ -``` - -create a new platform in NodeBase and download go wrapper from `file:///sdcard/go`; - -then write a tiny server to have a try. ref: https://github.com/stallpool/halfbase/blob/master/golang/tinyserver/main.go - +# NodeBase + + +Platform to Build Sharable Application for Android + +Running Websocket application over Wifi and share with your friends. + +> WSTun: it is a base server on http/websocket with netty; and project name is "WSTun" + +The design is changed, we never use a pre-built binary of NodeJS to run server anymore. + +We run a native http/websocket with netty. + +Any JS client can register itself as a service so that it can be a control center of data. + +Others can connect as clients of the service and do more flexible things like file sharing, board gaming! + +> It is also a project enjoy vibe coding! (save lots of time) diff --git a/analysis_options.yaml b/analysis_options.yaml deleted file mode 100755 index d4e0f0c..0000000 --- a/analysis_options.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. - -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml - -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. - rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100755 index 5d99765..0000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/android/README.md b/android/README.md new file mode 100644 index 0000000..8f633da --- /dev/null +++ b/android/README.md @@ -0,0 +1,166 @@ +# WSTun - WebSocket Tunnel Server + +An Android app that runs an HTTP/WebSocket server for service sharing in local networks. + +## Features + +### Server (Android App) +- HTTP server with WebSocket support using Netty +- Optional HTTPS with self-signed certificate +- Service registration via WebSocket +- HTTP request relay to connected services +- Foreground service for background operation +- Service management UI (view, kick) + +### Services +- Connect via WebSocket and register endpoints +- Define HTTP routes that relay to the service +- Provide static resources (HTML/JS/CSS) +- No server-side storage - everything relayed + +## Architecture + +``` +┌─────────────┐ WebSocket ┌─────────────────┐ +│ Service │◄───────────────────►│ WSTun App │ +│ Client │ │ (HTTP Server) │ +└─────────────┘ └────────┬────────┘ + │ HTTP + ▼ + ┌─────────────────┐ + │ Web Browser │ + │ (User) │ + └─────────────────┘ +``` + +1. Service connects via WebSocket and registers +2. User accesses `http://server/[service]/main` +3. Server relays request to service via WebSocket +4. Service sends response via WebSocket +5. Server sends HTTP response to user + +## Building + +### Android App + +```bash +cd wstun +./gradlew assembleDebug +``` + +### Service Clients + +```bash +cd services/fileshare +npm install + +cd services/chat +npm install +``` + +## Usage + +### Start the Server + +1. Install the APK on an Android device +2. Configure port and HTTPS option +3. Tap "Start Server" +4. Note the displayed IP address + +### Connect Services + +```bash +# File sharing +cd services/fileshare +node client.js ws://192.168.1.100:8080/ws + +# Chat +cd services/chat +node client.js ws://192.168.1.100:8080/ws +``` + +### Access Services + +- Server info: `http://192.168.1.100:8080/` +- FileShare: `http://192.168.1.100:8080/fileshare/main` +- Chat: `http://192.168.1.100:8080/chat/main` + +## Protocol + +### Service Registration + +```json +{ + "type": "register", + "id": "unique-id", + "payload": { + "name": "servicename", + "type": "service-type", + "description": "Service description", + "endpoints": [ + { "path": "/main", "method": "GET", "relay": true } + ], + "static_resources": { + "/main": "..." + } + } +} +``` + +### HTTP Request Relay + +Server sends to service: +```json +{ + "type": "http_request", + "payload": { + "request_id": "req-123", + "method": "GET", + "path": "/servicename/main", + "headers": { "Content-Type": "text/html" }, + "body": "..." + } +} +``` + +Service responds: +```json +{ + "type": "http_response", + "payload": { + "request_id": "req-123", + "status": 200, + "headers": { "Content-Type": "text/html" }, + "body": "..." + } +} +``` + +## Included Services + +### FileShare + +Share files without server storage: +- Virtual folder tree +- File upload/download via browser +- Relay mode for temporary sharing +- All data flows through the client + +### Chat + +Real-time chat with rich messages: +- Text, Card, and Poll message types +- Broadcast to all or private messages +- Abstract JSON format for custom rendering +- Self-contained HTML/JS/CSS + +## Security Notes + +- HTTPS uses self-signed certificate (browser warning expected) +- No authentication built-in (add as needed) +- For local network use only +- Services should validate inputs + +## License + +MIT diff --git a/android/app/build.gradle b/android/app/build.gradle old mode 100755 new mode 100644 index 068c3ae..334f5b2 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,57 +1,48 @@ -plugins { - id "com.android.application" - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "net.seven.nodebase.nodebase" - compileSdk 33 - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "net.seven.nodebase.nodebase" - // You can update the following values to match your application needs. - // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 26 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} +plugins { + id 'com.android.application' +} + +android { + namespace 'seven.lab.wstun' + compileSdk 34 + + defaultConfig { + applicationId "seven.lab.wstun" + minSdk 24 + targetSdk 34 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + packagingOptions { + exclude 'META-INF/INDEX.LIST' + exclude 'META-INF/io.netty.versions.properties' + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.3.2' + implementation 'com.google.code.gson:gson:2.10.1' + + // Netty + implementation 'io.netty:netty-all:4.1.100.Final' + + // Bouncy Castle for SSL + implementation 'org.bouncycastle:bcprov-jdk15on:1.70' + implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..d52bf27 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,20 @@ +# Netty +-keepclassmembers class io.netty.** { *; } +-keep class io.netty.** { *; } +-dontwarn io.netty.** + +# Bouncy Castle +-keep class org.bouncycastle.** { *; } +-dontwarn org.bouncycastle.** + +# Gson +-keepattributes Signature +-keepattributes *Annotation* +-keep class com.google.gson.** { *; } +-keep class * implements java.io.Serializable { *; } + +# Protocol classes +-keep class seven.lab.wstun.protocol.** { *; } + +# Server classes +-keep class seven.lab.wstun.server.** { *; } diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100755 index 8ffe024..0000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml old mode 100755 new mode 100644 index a6b5ed6..9d0275f --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,51 +1,43 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/assets/libwstun.js b/android/app/src/main/assets/libwstun.js new file mode 100644 index 0000000..a014927 --- /dev/null +++ b/android/app/src/main/assets/libwstun.js @@ -0,0 +1,274 @@ +/** + * WSTun Client Library v2.0 + * Supports: Server auth, Service instances, User management + */ +(function(global) { +'use strict'; + +const WSTun = { + version: '1.0.0', + + /** Create instance host (creates and manages an instance) */ + createInstanceHost: function(options) { return new InstanceHost(options); }, + + /** Create instance client (joins an existing instance) */ + createInstanceClient: function(options) { return new InstanceClient(options); }, + + /** Generate unique ID */ + generateId: function() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 6); }, + + /** Build WebSocket URL */ + buildWsUrl: function(serverToken) { + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + let url = protocol + '//' + location.host + '/ws'; + if (serverToken) url += '?token=' + encodeURIComponent(serverToken); + return url; + } +}; + +/** Base WebSocket client */ +class BaseClient { + constructor(options) { + this.service = options.service; + this.serverToken = options.serverToken || null; + this.onOpen = options.onOpen || (() => {}); + this.onMessage = options.onMessage || (() => {}); + this.onClose = options.onClose || (() => {}); + this.onError = options.onError || (() => {}); + this.ws = null; + this.connected = false; + } + + connect() { + try { this.ws = new WebSocket(WSTun.buildWsUrl(this.serverToken)); } + catch (err) { this.onError(err); return; } + this.ws.onopen = () => { this.connected = true; this.onOpen(); this._onConnected(); }; + this.ws.onmessage = (e) => { try { this._handleMessage(JSON.parse(e.data)); } catch(err) { console.error(err); } }; + this.ws.onclose = () => { this.connected = false; this.onClose(); }; + this.ws.onerror = (err) => { this.onError(err); }; + } + + disconnect() { if (this.ws) { this.ws.close(); this.ws = null; } } + + send(type, payload) { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.ws.send(JSON.stringify({ type, service: this.service, payload })); + } + } + + _onConnected() {} + _handleMessage(msg) { this.onMessage(msg); } +} + +/** + * Instance Host - Creates and manages a service instance (room) + * @param {Object} options + * @param {string} options.service - Service type (e.g., 'fileshare', 'chat') + * @param {string} options.name - Instance name (room name) + * @param {string} [options.serverToken] - Server auth token + * @param {string} [options.instanceToken] - Token users need to join + * @param {string} [options.reclaimUuid] - UUID of instance to reclaim (for reconnection) + * @param {function} [options.onInstanceCreated] - Called when instance is created + * @param {function} [options.onInstanceReclaimed] - Called when instance is reclaimed + */ +class InstanceHost extends BaseClient { + constructor(options) { + super(options); + this.instanceName = options.name || 'Room'; + this.instanceToken = options.instanceToken || null; + this.reclaimUuid = options.reclaimUuid || null; + this.onInstanceCreated = options.onInstanceCreated || (() => {}); + this.onInstanceReclaimed = options.onInstanceReclaimed || options.onInstanceCreated || (() => {}); + this.instanceUuid = null; + } + + _onConnected() { + if (this.reclaimUuid) { + // Try to reclaim an existing instance + this.send('reclaim_instance', { + uuid: this.reclaimUuid, + token: this.instanceToken, + server_token: this.serverToken + }); + } else { + // Create new instance + this.send('create_instance', { + name: this.instanceName, + token: this.instanceToken, + server_token: this.serverToken + }); + } + } + + _handleMessage(msg) { + if (msg.type === 'instance_created' && msg.payload?.success) { + this.instanceUuid = msg.payload.uuid; + this.onInstanceCreated(msg.payload); + } else if (msg.type === 'instance_reclaimed' && msg.payload?.success) { + this.instanceUuid = msg.payload.uuid; + this.onInstanceReclaimed(msg.payload); + } else if (msg.type === 'instance_reclaimed' && !msg.payload?.success) { + // Reclaim failed, create new instance instead + this.reclaimUuid = null; + this.send('create_instance', { + name: this.instanceName, + token: this.instanceToken, + server_token: this.serverToken + }); + } else if (msg.type === 'error') { + this.onError(new Error(msg.payload?.message || 'Unknown error')); + } else { + this.onMessage(msg); + } + } + + /** Broadcast message to all users in instance */ + broadcast(type, payload) { + if (this.instanceUuid) { + payload = payload || {}; + payload.instanceUuid = this.instanceUuid; + this.send(type, payload); + } + } + + /** Kick a user from instance */ + kickUser(userId) { + this.send('kick_client', { userId, instanceUuid: this.instanceUuid }); + } +} + +/** + * Instance Client - Joins an existing instance + * @param {Object} options + * @param {string} options.service - Service type + * @param {string} options.instanceUuid - Instance UUID to join + * @param {string} [options.serverToken] - Server auth token + * @param {string} [options.instanceToken] - Instance access token + * @param {string} [options.userId] - User ID (auto-generated if not provided) + * @param {function} [options.onJoined] - Called when joined instance + * @param {function} [options.onKicked] - Called when kicked from instance + */ +class InstanceClient extends BaseClient { + constructor(options) { + super(options); + this.instanceUuid = options.instanceUuid; + this.instanceToken = options.instanceToken || null; + this.userId = options.userId || WSTun.generateId(); + this.onJoined = options.onJoined || (() => {}); + this.onKicked = options.onKicked || (() => {}); + this.instanceName = null; + } + + _onConnected() { + this.send('join_instance', { + uuid: this.instanceUuid, + userId: this.userId, + token: this.instanceToken + }); + } + + _handleMessage(msg) { + if (msg.type === 'ack' && msg.payload?.success && msg.payload?.instanceUuid) { + this.instanceName = msg.payload.instanceName; + this.onJoined(msg.payload); + } else if (msg.type === 'kick') { + this.onKicked(msg.payload); + this.disconnect(); + } else if (msg.type === 'error') { + this.onError(new Error(msg.payload?.message || 'Unknown error')); + } else { + this.onMessage(msg); + } + } +} + +/** + * List available instances for a service + * @param {string} service - Service type + * @param {string} [serverToken] - Server auth token + * @returns {Promise} List of instances + */ +WSTun.listInstances = function(service, serverToken) { + return new Promise((resolve, reject) => { + let resolved = false; + let ws; + + try { + ws = new WebSocket(WSTun.buildWsUrl(serverToken)); + } catch(err) { + reject(err); + return; + } + + const cleanup = () => { + resolved = true; + if (ws && ws.readyState !== WebSocket.CLOSED) { + try { ws.close(); } catch(e) {} + } + }; + + ws.onopen = () => { + if (resolved) return; + ws.send(JSON.stringify({ type: 'list_instances', service, payload: {} })); + }; + ws.onmessage = (e) => { + if (resolved) return; + try { + const msg = JSON.parse(e.data); + if (msg.type === 'instance_list') { + cleanup(); + resolve(msg.payload?.instances || []); + } else if (msg.type === 'error') { + cleanup(); + reject(new Error(msg.payload?.message || msg.payload?.error || 'Failed to list instances')); + } + } catch(err) { + cleanup(); + reject(err); + } + }; + ws.onerror = (err) => { + if (resolved) return; + cleanup(); + reject(new Error('WebSocket connection failed')); + }; + ws.onclose = () => { + if (resolved) return; + cleanup(); + resolve([]); // Return empty list if connection closes without response + }; + + // Shorter timeout (3 seconds) to avoid long waits + setTimeout(() => { + if (resolved) return; + cleanup(); + resolve([]); // Return empty list on timeout instead of rejecting + }, 3000); + }); +}; + +// Legacy support - createService and createClient still work for simple cases +WSTun.createService = function(options) { return new InstanceHost(options); }; +WSTun.createClient = function(options) { + // If instanceUuid provided, use InstanceClient, otherwise fallback + if (options.instanceUuid) return new InstanceClient(options); + // Simple client without instance support (legacy) + const client = new BaseClient(options); + client.userId = options.userId || WSTun.generateId(); + client.onRegistered = options.onRegistered || (() => {}); + client.onKicked = options.onKicked || (() => {}); + client._onConnected = function() { + this.send('client_register', { clientType: this.service, userId: this.userId, auth_token: options.serviceToken }); + }; + client._handleMessage = function(msg) { + if (msg.type === 'ack' && msg.payload?.success) { this.onRegistered(msg.payload); } + else if (msg.type === 'kick') { this.onKicked(msg.payload); this.disconnect(); } + else if (msg.type === 'error') { this.onError(new Error(msg.payload?.message || 'Error')); } + else { this.onMessage(msg); } + }; + return client; +}; +WSTun.generateUserId = WSTun.generateId; + +global.WSTun = WSTun; +})(typeof window !== 'undefined' ? window : this); diff --git a/android/app/src/main/assets/services/chat/index.html b/android/app/src/main/assets/services/chat/index.html new file mode 100644 index 0000000..86aa902 --- /dev/null +++ b/android/app/src/main/assets/services/chat/index.html @@ -0,0 +1,274 @@ + + + + + + Chat Instance Manager - WSTun + + + +
+
+

Chat

+

Create a chat room

+ +
+ + +
+
+ + +
+
+ + +
+ + + + +
+ +
+

Existing Rooms

+

Click to open the chat page

+
+

Loading...

+
+ +
+
+ + + + + diff --git a/android/app/src/main/assets/services/chat/main.html b/android/app/src/main/assets/services/chat/main.html new file mode 100644 index 0000000..7d2fac6 --- /dev/null +++ b/android/app/src/main/assets/services/chat/main.html @@ -0,0 +1,375 @@ + + + + + + Chat - WSTun + + + +
+

Join Chat Room

+
Loading rooms...
+ +
+ +
+
Or enter room details manually
+ + + + +
+
+ +
+
+
+

Chat Room

+ 0 users +
+ +
+
+
+ + +
+
+ + + + + diff --git a/android/app/src/main/assets/services/fileshare/index.html b/android/app/src/main/assets/services/fileshare/index.html new file mode 100644 index 0000000..a350c59 --- /dev/null +++ b/android/app/src/main/assets/services/fileshare/index.html @@ -0,0 +1,276 @@ + + + + + + FileShare Instance Manager - WSTun + + + +
+
+

FileShare

+

Create a file sharing room

+ +
+ + +
+
+ + +
+
+ + +
+ + + + +
+ +
+

Existing Rooms

+

Click to open the file sharing page

+
+

Loading...

+
+ +
+
+ + + + + diff --git a/android/app/src/main/assets/services/fileshare/main.html b/android/app/src/main/assets/services/fileshare/main.html new file mode 100644 index 0000000..2ddb8d8 --- /dev/null +++ b/android/app/src/main/assets/services/fileshare/main.html @@ -0,0 +1,463 @@ + + + + + + FileShare - WSTun + + + +
+
+

FileShare

+

Share files with others

+
Loading...
+
+ + +
+

Join a Room

+
+

Loading available rooms...

+
+
+

Or Enter Room Details

+
+ + +
+
+ + +
+ +
+
+ + + +
+ + + + + diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/ChannelResult.java b/android/app/src/main/java/net/seven/nodebase/nodebase/ChannelResult.java deleted file mode 100755 index f661a24..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/ChannelResult.java +++ /dev/null @@ -1,6 +0,0 @@ -package net.seven.nodebase.nodebase; - -public enum ChannelResult { - OK, - FAILED -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/Command.java b/android/app/src/main/java/net/seven/nodebase/nodebase/Command.java deleted file mode 100755 index 24ee694..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/Command.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.util.ArrayList; - -public class Command { - public static String checkOutput(String[] cmd) { - return checkOutput(cmd, false); - } - public static String checkOutput(String[] cmd, boolean joinStderr) { - try { - ProcessBuilder build = new ProcessBuilder(cmd); - if (joinStderr) build.redirectErrorStream(true); - Process p = build.start(); - BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line); - sb.append('\n'); - } - p.waitFor(); - reader.close(); - return sb.toString().substring(0, sb.length() - 1); - } catch(Exception e) { - Logger.d( - "NodeBase", - "command", - String.format( - "%s :: %s", - String.join(" ", cmd), - e.toString() - ) - ); - return null; - } - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/External.java b/android/app/src/main/java/net/seven/nodebase/nodebase/External.java deleted file mode 100755 index fabcc37..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/External.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Build; - -import java.io.File; -import java.util.ArrayList; - -public class External { - public static void openBrowser(Context context, String url) { - Intent it = new Intent(Intent.ACTION_SEND); - it.setAction("android.intent.action.VIEW"); - it.setData(Uri.parse(url)); - context.startActivity(it); - } - - private static String transformAbiToArch(String name) { - switch(name) { - case "armeabi-v8a": case "arm64-v8a": return "arm64"; - case "x86_64": return "x64"; - case "x86": return "x86"; - case "armeabi-v7a": case "armeabi": return "arm"; - // mips, mips64 - } - return "unknown:" + name; - } - public static String getArch() { - ArrayList arch = new ArrayList<>(); - StringBuilder sb = new StringBuilder(); - sb.append("android-"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - for (String one : Build.SUPPORTED_ABIS) { - arch.add(transformAbiToArch(one)); - } - sb.append(String.join("|", arch)); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) { - // deprecated api - sb.append(transformAbiToArch(Build.CPU_ABI)); - sb.append('|'); - sb.append(transformAbiToArch(Build.CPU_ABI2)); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.DONUT) { - // deprecated api - sb.append(Build.CPU_ABI); - } - return sb.toString(); - } - - public static void shareInformation( - Context context, - String title, - String subtitle, - String text - ) { - shareInformation(context, title, subtitle, text, null); - } - public static void shareInformation( - Context context, - String title, - String subtitle, - String text, - String imgFilePath - ) { - Intent it = new Intent(Intent.ACTION_SEND); - if (imgFilePath == null || imgFilePath.length() == 0) { - it.setType("text/plain"); - } else { - File f = new File(imgFilePath); - if (f.exists() && f.isFile()) { - it.setType("image/jpg"); - it.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(f)); - } else { - it.setType("text/plain"); - } - } - if (subtitle != null) it.putExtra(Intent.EXTRA_SUBJECT, subtitle); - if (text != null) it.putExtra(Intent.EXTRA_TEXT, text); - it.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - context.startActivity(Intent.createChooser(it, title)); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/ForegroundNodeService.java b/android/app/src/main/java/net/seven/nodebase/nodebase/ForegroundNodeService.java deleted file mode 100755 index 5b48b6d..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/ForegroundNodeService.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.IBinder; - -import androidx.core.app.NotificationCompat; - -public class ForegroundNodeService extends Service { - private static final String CHANNEL_ID = "net.seven.nodebase.foregroundservice"; - public static final String ARGV = "NodeService"; - - private Thread watchDog; - - public ForegroundNodeService() { } - - private void registerNotificationItem() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel srvChannel = new NotificationChannel( - CHANNEL_ID, - "NodeBase Service", - NotificationManager.IMPORTANCE_DEFAULT - ); - NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(srvChannel); - } - - Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("NodeBase") - .setContentText("application nodebase service running ...") - .build(); - startForeground(1, notification); - - watchDog = new Thread(new Runnable() { - @Override - public void run() { - Intent it = new Intent( - ForegroundNodeService.this.getApplicationContext(), - ForegroundNodeService.class - ); - while(true) { - int count = NodeAppService.getRunningNodeAppCount(); - if (count <= 0) break; - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - break; - } - }; - ForegroundNodeService.this.stopService(it); - } - }); - watchDog.start(); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - registerNotificationItem(); - return Service.START_STICKY; - } - - @Override - public void onCreate() { - super.onCreate(); - } - - @Override - public void onDestroy() { - NodeAppService.stopNodeApps(); - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } -} \ No newline at end of file diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/Logger.java b/android/app/src/main/java/net/seven/nodebase/nodebase/Logger.java deleted file mode 100755 index 762f0a8..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/Logger.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.util.Log; - -public class Logger { - public static void i(String tag, String sub, String message) { - Log.i(tag, String.format("[I] %s :: %s", sub, message)); - } - - public static void w(String tag, String sub, String message) { - Log.w(tag, String.format("[W] %s :: %s", sub, message)); - } - - public static void e(String tag, String sub, String message) { - Log.e(tag, String.format("[E] %s :: %s", sub, message)); - } - - public static void d(String tag, String sub, String message) { - Log.d(tag, String.format("[D] %s :: %s", sub, message)); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/MainActivity.java b/android/app/src/main/java/net/seven/nodebase/nodebase/MainActivity.java deleted file mode 100755 index 8b418fc..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/MainActivity.java +++ /dev/null @@ -1,164 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.content.Intent; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - listenNodeAppChannel(flutterEngine); - } - - private void listenNodeAppChannel(@NonNull FlutterEngine flutterEngine) { - new MethodChannel( - flutterEngine.getDartExecutor().getBinaryMessenger(), - "net.seven.nodebase/app" - ).setMethodCallHandler(new MethodChannel.MethodCallHandler() { - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - switch(call.method) { - case "app.start" -> { - String name = call.argument("name"); - ArrayList cmd = call.argument("cmd"); - HashMap env = call.argument("env"); - if (name == null || cmd == null || cmd.size() == 0) { - result.error("app.start", "empty name or cmd", null); - return; - } - if (startNodeApp(name, cmd, env) == ChannelResult.OK) { - result.success(true); - } else { - result.error("app.start", "internal error", null); - } - } - case "app.stop" -> { - String name = call.argument("name"); - if (name == null) { - result.error("app.stop", "empty name", null); - return; - } - if (stopNodeApp(name) == ChannelResult.OK) { - result.success(true); - } else { - result.error("app.stop", "internal error", null); - } - } - case "app.stat" -> { - String name = call.argument("name"); - if (name == null) { - result.error("app.stat", "empty name", null); - return; - } - var stat = getNodeAppStatus(name); - if (stat == null) { - result.error("app.stat", "no stat", null); - return; - } - result.success(stat); - } - case "util.ip" -> { - var ipJsonString = getIPs(); - result.success(ipJsonString); - } - case "util.browser.open" -> { - String url = call.argument("url"); - if (url == null) { - result.error("util.browser", "no url", null); - return; - } - External.openBrowser(MainActivity.this.getContext(), url); - result.success(true); - } - case "util.file.executable" -> { - String fname = call.argument("filename"); - if (fname == null) { - result.error("util.file.executable", "no filename", null); - return; - } - fileExecutablize(fname); - result.success(true); - } - case "util.arch" -> { - result.success(External.getArch()); - } - case "util.workspace" -> { - result.success(workspaceBaseDir()); - } - } - } - }); - - new EventChannel( - flutterEngine.getDartExecutor().getBinaryMessenger(), - "net.seven.nodebase/event" - ).setStreamHandler(NodeAppService.getEventHandler()); - } - - public ChannelResult startNodeApp(String name, ArrayList cmd, HashMap env) { - Logger.i( - "NodeBase", - "startNodeApp", - String.format("start node app (%s) -> %s", name, cmd)); - Intent it = new Intent(getContext(), ForegroundNodeService.class); - ContextCompat.startForegroundService(getContext(), it); - String[] cmd_ = new String[cmd.size()]; - cmd.toArray(cmd_); - NodeAppService.startNodeApp(name, cmd_, env); - return ChannelResult.OK; - } - - public ChannelResult stopNodeApp(String name) { - Logger.i( - "NodeBase", - "stopNodeApp", - String.format("stop node app (%s)", name)); - Intent it = new Intent(getContext(), ForegroundNodeService.class); - if (NodeAppService.getRunningNodeAppCount() <= 1) { - stopService(it); - } else { - NodeAppService.stopNodeApp(name); - } - return ChannelResult.OK; - } - - public HashMap getNodeAppStatus(String name) { - // TODO get NodeMonitor and assemble data as json - NodeAppMonitor app = NodeAppService.getNodeApp(name); - HashMap json = new HashMap<>(); - String state = "none"; - if (app != null) { - if (app.nodebaseIsReady()) state = "new"; - else if (app.nodebaseIsRunning()) state = "running"; - else if (app.nodebaseIsDead()) state = "dead"; - json.put("state", state); - } - return json; - } - - public HashMap> getIPs() { - var json = Network.getIPs(); - return json; - } - - public boolean fileExecutablize(String fname) { - File f = new File(fname); - return f.setExecutable(true, true); - } - - public String workspaceBaseDir() { - return getContext().getApplicationInfo().dataDir; - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/Network.java b/android/app/src/main/java/net/seven/nodebase/nodebase/Network.java deleted file mode 100755 index 0f9b361..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/Network.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.net.InterfaceAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -public class Network { - public static HashMap> getIPs() { - return getIPsFromAndroidApi(); - } - - public static HashMap> getIPsFromAndroidApi() { - HashMap> r = new HashMap<>(); - try { - for (NetworkInterface nic : Collections.list(NetworkInterface.getNetworkInterfaces())) { - String nic_name = nic.getName(); - List nic_addr = nic.getInterfaceAddresses(); - ArrayList ips = new ArrayList<>(); - for (InterfaceAddress ia : nic_addr) { - String addr = ia.getAddress().getHostAddress(); - // skip the address % - if (addr == null || addr.indexOf('%') >= 0) continue; - ips.add(addr); - } - if (ips.size() > 0) r.put(nic_name, ips); - } - Logger.d( - "NodeBase", - "getIPsFromAndroidApi", - r.toString() - ); - } catch (SocketException e) { - Logger.e( - "NodeBase", - "getIPsFromAndroidApi", - e.toString() - ); - } - return r; - } - -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppMonitor.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppMonitor.java deleted file mode 100755 index 52c9b22..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppMonitor.java +++ /dev/null @@ -1,234 +0,0 @@ -package net.seven.nodebase.nodebase; - -import android.os.Build; -import android.os.Handler; -import android.os.Looper; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Locale; - -public class NodeAppMonitor extends Thread { - private enum STAT { - UNKNOWN, - BORN, - READY, - RUNNING, - DEAD - } - - private STAT nodebaseStat; - private Process nodebaseProcess; - private final String nodebaseName; - private final String[] nodebaseCmd; - private final HashMap nodebaseEnv; - - public NodeAppMonitor(String name, String[] cmd, HashMap env) { - super(); - this.nodebaseName = name; - this.nodebaseCmd = cmd; - this.nodebaseEnv = new HashMap<>(); - if (env != null) this.nodebaseEnv.putAll(env); - this.nodebaseStat = STAT.BORN; - this.nodebaseProcess = null; - } - - public boolean nodebaseIsRunning() { return STAT.RUNNING == this.nodebaseStat; } - public boolean nodebaseIsReady() { return STAT.READY == this.nodebaseStat; } - public boolean nodebaseIsDead() { return STAT.DEAD == this.nodebaseStat; } - - private int nodebasePid() { - Process p = this.nodebaseProcess; - if (p == null) return -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (!p.isAlive()) return -1; - } - Class k = p.getClass(); - if ("java.lang.UNIXProcess".equals(k.getName())) { - try { - int pid = -1; - Field f = k.getDeclaredField("pid"); - f.setAccessible(true); - // this try to make sure if getInt throw an error, - // `setAccessible(false)` can be executed - // so that `pid` is protected after this access - try { pid = f.getInt(p); } catch (IllegalAccessException e) { } - f.setAccessible(false); - return pid; - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); - } - } - return -1; - } - - private ArrayList nodebaseChildrenPids(int pid) { - // XXX maybe we can get sub processes pids from a stable way like /proc - // for a workaround, currently we just use `ps` - ArrayList children = new ArrayList<>(); - String output = Command.checkOutput(new String[] { - "/system/bin/ps", - "-o", "pid=", "--ppid", String.valueOf(pid) - }); - if (output == null || output.length() == 0) return children; - String[] lines = output.trim().split("\n"); - for(String line : lines) { - line = line.trim(); - if (line.length() == 0) continue; - // TODO: try...catch... - int cpid = Integer.parseInt(line); - if (pid != cpid) children.add(cpid); - } - return children; - } - - private int[] nodebaseCollectPidsForKilling(ArrayList collected, int pid, boolean nested) { - if (pid < 0) return new int[0]; - if (collected == null) collected = new ArrayList<>(); - ArrayList pids = nodebaseChildrenPids(pid); - for (int childPid : pids) { - if (childPid < 0) continue; - collected.add(childPid); - Logger.i( - "NodeBase", - "monitor", - String.format( - Locale.getDefault(), - "killing (%d / %s) -> %d | parent=%d", - getId(), this.nodebaseName, - childPid, pid) - ); - if (nested) nodebaseCollectPidsForKilling(collected, childPid, true); - } - int n = collected.size(); - int[] r = new int[n]; - for (int i = 0; i < n; i++) { - r[i] = collected.get(i); - } - return r; - } - - public void nodebaseStart() { - this.start(); - } - - public boolean nodebaseStop() { - // XXX only RUNNING app can be stopped to avoid race condition - // stop non-RUNNING app will return false - if (this.nodebaseProcess == null) return false; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (!this.nodebaseProcess.isAlive()) return false; - } - int[] pids = this.nodebaseCollectPidsForKilling(null, this.nodebasePid(), true); - for (int pid : pids) { - android.os.Process.killProcess(pid); - } - this.nodebaseProcess.destroy(); - return true; - } - - public NodeAppMonitor nodebaseRestart() { - this.nodebaseStop(); - NodeAppMonitor m = new NodeAppMonitor(this.nodebaseName, this.nodebaseCmd, this.nodebaseEnv); - m.start(); - return m; - } - - @Override - public void run() { - Handler uiRunner; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - uiRunner = Handler.createAsync(Looper.getMainLooper()); - } else { - uiRunner = new Handler(Looper.getMainLooper()); - } - try { - this.nodebaseStat = STAT.READY; - // TODO event/onReady - Logger.i( - "NodeBase", - "monitor", - String.format( - Locale.getDefault(), - "starting (%d / %s) - %s", - getId(), this.nodebaseName, - String.join(" ", this.nodebaseCmd)) - ); - - // set RUNNING before set up the process - // when stop the service, it guarantees that no race condition for - // setting nodebaseStat - this.nodebaseStat = STAT.RUNNING; - ProcessBuilder build = new ProcessBuilder(this.nodebaseCmd); - // XXX: do we need to check some important variable should not be changed - // e.g. HOME, USER, PWD, ... - build.environment().putAll(this.nodebaseEnv); - this.nodebaseProcess = build.start(); - // TODO event/onStart - Logger.i( - "NodeBase", - "monitor", - String.format( - Locale.getDefault(), - "started (%d / %s)", - getId(), this.nodebaseName) - ); - - uiRunner.post(new Runnable() { - @Override - public void run() { - ArrayList r = new ArrayList<>(); - r.add("start"); - r.add(nodebaseName); - NodeAppService.getEventHandler().postMessage("app", r); - } - }); - - this.nodebaseProcess.waitFor(); - } catch (IOException e) { - Logger.e( - "NodeService", - "monitor", - String.format( - Locale.getDefault(), - "io error (%d / %s) - %s", - getId(), this.nodebaseName, e) - ); - this.nodebaseProcess = null; - // TODO event/onError - } catch (InterruptedException e) { - Logger.e( - "NodeService", - "monitor", - String.format( - Locale.getDefault(), - "interrupted error (%d / %s) - %s", - getId(), this.nodebaseName, e) - ); - // TODO event/onError - } - - this.nodebaseStat = STAT.DEAD; - // TODO: event/onPost - Logger.e( - "NodeService", - "monitor", - String.format( - Locale.getDefault(), - "stopped (%d / %s)", - getId(), this.nodebaseName) - ); - - uiRunner.post(new Runnable() { - @Override - public void run() { - ArrayList r = new ArrayList<>(); - r.add("stop"); - r.add(nodebaseName); - NodeAppService.getEventHandler().postMessage("app", r); - } - }); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppService.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppService.java deleted file mode 100755 index 0deffca..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeAppService.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.util.HashMap; - -public class NodeAppService { - private static final NodeBaseEventHandler eventHandler = new NodeBaseEventHandler(); - private static final HashMap services = new HashMap<>(); - - public static NodeBaseEventHandler getEventHandler() { - return eventHandler; - } - - public static int getRunningNodeAppCount() { - int count = 0; - for (NodeAppMonitor app : services.values()) { - if (app != null && !app.nodebaseIsDead()) count ++; - } - return count; - } - - public static void startNodeApp(String name, String[] cmd, HashMap env) { - synchronized (services) { - NodeAppMonitor app; - if (services.containsKey(name)) { - app = services.get(name); - if (app != null && !app.nodebaseIsDead()) return; - // only process if the service is null/dead - } - app = new NodeAppMonitor(name, cmd, env); - app.nodebaseStart(); - services.put(name, app); - } - } - - public static void restartNodeApp(String name) { - synchronized (services) { - if (!services.containsKey(name)) return; - NodeAppMonitor app = services.get(name); - if (app == null) { - services.remove(name); - return; - } - services.put(name, app.nodebaseRestart()); - } - } - - public static void stopNodeApp(String name) { - synchronized (services) { - if (!services.containsKey(name)) return; - NodeAppMonitor app = services.get(name); - if (app == null) return; - if (app.nodebaseIsDead()) return; - app.nodebaseStop(); - } - } - - public static void stopNodeApps() { - for (String name : services.keySet()) { - stopNodeApp(name); - } - } - - public static NodeAppMonitor getNodeApp(String name) { - if (!services.containsKey(name)) return null; - return services.get(name); - } -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBase.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBase.java deleted file mode 100755 index 86f311d..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBase.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.seven.nodebase.nodebase; - -public class NodeBase { -} diff --git a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBaseEventHandler.java b/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBaseEventHandler.java deleted file mode 100755 index be91ae4..0000000 --- a/android/app/src/main/java/net/seven/nodebase/nodebase/NodeBaseEventHandler.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.seven.nodebase.nodebase; - -import java.util.ArrayList; -import java.util.HashMap; - -import io.flutter.plugin.common.EventChannel; - -public class NodeBaseEventHandler implements EventChannel.StreamHandler { - private HashMap sink = new HashMap<>(); - - @Override - public synchronized void onListen(Object name, EventChannel.EventSink events) { - if (name instanceof String) { - sink.put((String) name, events); - } - } - - @Override - public synchronized void onCancel(Object name) { - if (name instanceof String) { - sink.remove((String) name); - } - } - - public synchronized void postMessage(String name, Object message) { - EventChannel.EventSink ch = sink.get(name); - if (ch == null) return; - ch.success(message); - } -} diff --git a/android/app/src/main/java/seven/lab/wstun/config/ServerConfig.java b/android/app/src/main/java/seven/lab/wstun/config/ServerConfig.java new file mode 100644 index 0000000..349a3c2 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/config/ServerConfig.java @@ -0,0 +1,157 @@ +package seven.lab.wstun.config; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * Server configuration stored in SharedPreferences. + */ +public class ServerConfig { + + private static final String PREFS_NAME = "wstun_config"; + private static final String KEY_PORT = "port"; + private static final String KEY_HTTPS_ENABLED = "https_enabled"; + private static final String KEY_CERT_GENERATED = "cert_generated"; + private static final String KEY_CORS_ORIGINS = "cors_origins"; + private static final String KEY_FILESHARE_ENABLED = "fileshare_enabled"; + private static final String KEY_CHAT_ENABLED = "chat_enabled"; + private static final String KEY_SERVER_AUTH_TOKEN = "server_auth_token"; + private static final String KEY_AUTH_ENABLED = "auth_enabled"; + private static final String KEY_DEBUG_LOGS_ENABLED = "debug_logs_enabled"; + + private final SharedPreferences prefs; + + public ServerConfig(Context context) { + this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public int getPort() { + return prefs.getInt(KEY_PORT, 8080); + } + + public void setPort(int port) { + prefs.edit().putInt(KEY_PORT, port).apply(); + } + + public boolean isHttpsEnabled() { + return prefs.getBoolean(KEY_HTTPS_ENABLED, false); + } + + public void setHttpsEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_HTTPS_ENABLED, enabled).apply(); + } + + public boolean isCertGenerated() { + return prefs.getBoolean(KEY_CERT_GENERATED, false); + } + + public void setCertGenerated(boolean generated) { + prefs.edit().putBoolean(KEY_CERT_GENERATED, generated).apply(); + } + + /** + * Get CORS allowed origins. Default is "*" (all origins). + * Can be comma-separated list of origins or "*". + */ + public String getCorsOrigins() { + return prefs.getString(KEY_CORS_ORIGINS, "*"); + } + + /** + * Set CORS allowed origins. + * Use "*" to allow all origins, or comma-separated list like: + * "http://localhost:3000,http://192.168.1.100:8080" + */ + public void setCorsOrigins(String origins) { + prefs.edit().putString(KEY_CORS_ORIGINS, origins).apply(); + } + + /** + * Check if FileShare service is enabled. + */ + public boolean isFileshareEnabled() { + return prefs.getBoolean(KEY_FILESHARE_ENABLED, false); + } + + /** + * Enable or disable FileShare service. + */ + public void setFileshareEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_FILESHARE_ENABLED, enabled).apply(); + } + + /** + * Check if Chat service is enabled. + */ + public boolean isChatEnabled() { + return prefs.getBoolean(KEY_CHAT_ENABLED, false); + } + + /** + * Enable or disable Chat service. + */ + public void setChatEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_CHAT_ENABLED, enabled).apply(); + } + + /** + * Check if server authentication is enabled. + */ + public boolean isAuthEnabled() { + return prefs.getBoolean(KEY_AUTH_ENABLED, false); + } + + /** + * Enable or disable server authentication. + */ + public void setAuthEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_AUTH_ENABLED, enabled).apply(); + } + + /** + * Get the server auth token. Returns null if not set. + */ + public String getServerAuthToken() { + return prefs.getString(KEY_SERVER_AUTH_TOKEN, null); + } + + /** + * Set the server auth token. Set to null or empty to disable. + */ + public void setServerAuthToken(String token) { + if (token == null || token.isEmpty()) { + prefs.edit().remove(KEY_SERVER_AUTH_TOKEN).apply(); + } else { + prefs.edit().putString(KEY_SERVER_AUTH_TOKEN, token).apply(); + } + } + + /** + * Validate server auth token. + * Returns true if auth is disabled or token matches. + */ + public boolean validateServerAuth(String token) { + if (!isAuthEnabled()) { + return true; + } + String serverToken = getServerAuthToken(); + if (serverToken == null || serverToken.isEmpty()) { + return true; + } + return serverToken.equals(token); + } + + /** + * Check if debug logs endpoint is enabled. + */ + public boolean isDebugLogsEnabled() { + return prefs.getBoolean(KEY_DEBUG_LOGS_ENABLED, false); + } + + /** + * Enable or disable debug logs endpoint. + */ + public void setDebugLogsEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_DEBUG_LOGS_ENABLED, enabled).apply(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/marketplace/InstalledService.java b/android/app/src/main/java/seven/lab/wstun/marketplace/InstalledService.java new file mode 100644 index 0000000..e711b5e --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/marketplace/InstalledService.java @@ -0,0 +1,122 @@ +package seven.lab.wstun.marketplace; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents an installed service with its manifest and files. + */ +public class InstalledService { + + private static final Gson gson = new Gson(); + + @SerializedName("manifest") + private ServiceManifest manifest; + + @SerializedName("enabled") + private boolean enabled; + + @SerializedName("installedAt") + private long installedAt; + + @SerializedName("source") + private String source; // marketplace URL or "builtin" + + // Runtime state (not persisted) + private transient Map files = new ConcurrentHashMap<>(); // path -> content + private transient boolean running; + + public InstalledService() { + this.enabled = false; + this.running = false; + this.installedAt = System.currentTimeMillis(); + } + + public InstalledService(ServiceManifest manifest, String source) { + this(); + this.manifest = manifest; + this.source = source; + } + + // Getters and setters + public ServiceManifest getManifest() { return manifest; } + public void setManifest(ServiceManifest manifest) { this.manifest = manifest; } + + public boolean isEnabled() { return enabled; } + public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public long getInstalledAt() { return installedAt; } + public void setInstalledAt(long installedAt) { this.installedAt = installedAt; } + + public String getSource() { return source; } + public void setSource(String source) { this.source = source; } + + public Map getFiles() { return files; } + public void setFiles(Map files) { this.files = files; } + + public boolean isRunning() { return running; } + public void setRunning(boolean running) { this.running = running; } + + /** + * Get file content for an endpoint path. + */ + public String getFileContent(String path) { + if (manifest == null || manifest.getEndpoints() == null) { + return null; + } + + for (ServiceManifest.Endpoint ep : manifest.getEndpoints()) { + if (path.equals(ep.getPath())) { + return files.get(ep.getFile()); + } + } + return null; + } + + /** + * Get service name. + */ + public String getName() { + return manifest != null ? manifest.getName() : null; + } + + /** + * Get display name. + */ + public String getDisplayName() { + return manifest != null ? manifest.getDisplayName() : null; + } + + /** + * Convert to JSON for API. + */ + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + if (manifest != null) { + obj.add("manifest", manifest.toJson()); + } + obj.addProperty("enabled", enabled); + obj.addProperty("running", running); + obj.addProperty("installedAt", installedAt); + obj.addProperty("source", source); + return obj; + } + + /** + * Convert to JSON string for persistence. + */ + public String toJsonString() { + return gson.toJson(this); + } + + /** + * Parse from JSON string. + */ + public static InstalledService fromJson(String json) { + return gson.fromJson(json, InstalledService.class); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/marketplace/MarketplaceService.java b/android/app/src/main/java/seven/lab/wstun/marketplace/MarketplaceService.java new file mode 100644 index 0000000..cd17294 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/marketplace/MarketplaceService.java @@ -0,0 +1,503 @@ +package seven.lab.wstun.marketplace; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Manages marketplace operations: browsing, downloading, installing services. + * + * Marketplace API: + * - GET {baseUrl}/list - Returns JSON array of available services with name, description + * - GET {baseUrl}/service/{name}.json - Returns service manifest + * - GET {baseUrl}/service/{name}/{file} - Downloads service file + */ +public class MarketplaceService { + + private static final String TAG = "MarketplaceService"; + private static final String PREFS_NAME = "wstun_marketplace"; + private static final String KEY_INSTALLED_SERVICES = "installed_services"; + private static final String KEY_MARKETPLACE_URL = "marketplace_url"; + private static final Gson gson = new Gson(); + + private final Context context; + private final File servicesDir; + private final SharedPreferences prefs; + private final ExecutorService executor; + + // Installed services cache + private final Map installedServices = new ConcurrentHashMap<>(); + + // Callback interfaces + public interface MarketplaceCallback { + void onSuccess(T result); + void onError(String error); + } + + public MarketplaceService(Context context) { + this.context = context; + this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + this.executor = Executors.newCachedThreadPool(); + + // Create services directory + this.servicesDir = new File(context.getFilesDir(), "services"); + if (!servicesDir.exists()) { + servicesDir.mkdirs(); + } + + // Load installed services + loadInstalledServices(); + + // Load built-in services + loadBuiltinServices(); + } + + /** + * Load installed services from storage. + */ + private void loadInstalledServices() { + String json = prefs.getString(KEY_INSTALLED_SERVICES, "{}"); + try { + Type type = new TypeToken>(){}.getType(); + Map loaded = gson.fromJson(json, type); + if (loaded != null) { + installedServices.putAll(loaded); + } + + // Load file contents from disk + for (InstalledService service : installedServices.values()) { + loadServiceFiles(service); + } + + Log.i(TAG, "Loaded " + installedServices.size() + " installed services"); + } catch (Exception e) { + Log.e(TAG, "Failed to load installed services", e); + } + } + + /** + * Load built-in services (fileshare, chat) as installed services. + */ + private void loadBuiltinServices() { + // Load fileshare + if (!installedServices.containsKey("fileshare")) { + InstalledService fileshare = createBuiltinService("fileshare", "FileShare", + "Share files with others in real-time"); + installedServices.put("fileshare", fileshare); + } + + // Load chat + if (!installedServices.containsKey("chat")) { + InstalledService chat = createBuiltinService("chat", "Chat", + "Real-time chat rooms"); + installedServices.put("chat", chat); + } + + // Load built-in HTML from assets + loadBuiltinAssets(); + } + + /** + * Create a built-in service entry. + */ + private InstalledService createBuiltinService(String name, String displayName, String description) { + ServiceManifest manifest = new ServiceManifest(); + manifest.setName(name); + manifest.setDisplayName(displayName); + manifest.setDescription(description); + manifest.setVersion("1.0.0"); + manifest.setAuthor("Built-in"); + + List endpoints = new ArrayList<>(); + + ServiceManifest.Endpoint serviceEp = new ServiceManifest.Endpoint(); + serviceEp.setPath("/service"); + serviceEp.setFile("index.html"); + serviceEp.setType("service"); + endpoints.add(serviceEp); + + ServiceManifest.Endpoint mainEp = new ServiceManifest.Endpoint(); + mainEp.setPath("/main"); + mainEp.setFile("main.html"); + mainEp.setType("client"); + endpoints.add(mainEp); + + manifest.setEndpoints(endpoints); + + InstalledService service = new InstalledService(manifest, "builtin"); + service.setInstalledAt(0); // Built-in + service.setEnabled(false); // Disabled by default - user enables on the fly + return service; + } + + /** + * Load built-in HTML assets. + */ + private void loadBuiltinAssets() { + try { + InstalledService fileshare = installedServices.get("fileshare"); + if (fileshare != null && fileshare.getFiles().isEmpty()) { + fileshare.getFiles().put("index.html", loadAsset("services/fileshare/index.html")); + fileshare.getFiles().put("main.html", loadAsset("services/fileshare/main.html")); + } + + InstalledService chat = installedServices.get("chat"); + if (chat != null && chat.getFiles().isEmpty()) { + chat.getFiles().put("index.html", loadAsset("services/chat/index.html")); + chat.getFiles().put("main.html", loadAsset("services/chat/main.html")); + } + } catch (IOException e) { + Log.e(TAG, "Failed to load built-in assets", e); + } + } + + private String loadAsset(String path) throws IOException { + InputStream is = context.getAssets().open(path); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } + + /** + * Save installed services to storage. + */ + private void saveInstalledServices() { + try { + // Create a copy without file contents for storage + Map toSave = new ConcurrentHashMap<>(); + for (Map.Entry entry : installedServices.entrySet()) { + if (!"builtin".equals(entry.getValue().getSource())) { + toSave.put(entry.getKey(), entry.getValue()); + } + } + String json = gson.toJson(toSave); + prefs.edit().putString(KEY_INSTALLED_SERVICES, json).apply(); + } catch (Exception e) { + Log.e(TAG, "Failed to save installed services", e); + } + } + + /** + * Load service files from disk. + */ + private void loadServiceFiles(InstalledService service) { + if (service == null || service.getManifest() == null) return; + if ("builtin".equals(service.getSource())) return; // Built-ins loaded from assets + + File serviceDir = new File(servicesDir, service.getName()); + if (!serviceDir.exists()) return; + + List endpoints = service.getManifest().getEndpoints(); + if (endpoints == null) return; + + for (ServiceManifest.Endpoint ep : endpoints) { + File file = new File(serviceDir, ep.getFile()); + if (file.exists()) { + try { + String content = readFile(file); + service.getFiles().put(ep.getFile(), content); + } catch (IOException e) { + Log.e(TAG, "Failed to load file: " + file.getPath(), e); + } + } + } + } + + private String readFile(File file) throws IOException { + FileInputStream fis = new FileInputStream(file); + BufferedReader reader = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } + + /** + * Get or set marketplace URL. + */ + public String getMarketplaceUrl() { + return prefs.getString(KEY_MARKETPLACE_URL, ""); + } + + public void setMarketplaceUrl(String url) { + prefs.edit().putString(KEY_MARKETPLACE_URL, url).apply(); + } + + /** + * List services from marketplace. + */ + public void listMarketplace(String baseUrl, MarketplaceCallback> callback) { + executor.execute(() -> { + try { + String listUrl = baseUrl.endsWith("/") ? baseUrl + "list" : baseUrl + "/list"; + String json = httpGet(listUrl); + JsonArray arr = gson.fromJson(json, JsonArray.class); + + List services = new ArrayList<>(); + for (JsonElement el : arr) { + services.add(el.getAsJsonObject()); + } + + callback.onSuccess(services); + } catch (Exception e) { + Log.e(TAG, "Failed to list marketplace", e); + callback.onError("Failed to fetch marketplace: " + e.getMessage()); + } + }); + } + + /** + * Get service manifest from marketplace. + */ + public void getServiceManifest(String baseUrl, String serviceName, + MarketplaceCallback callback) { + executor.execute(() -> { + try { + String manifestUrl = baseUrl.endsWith("/") + ? baseUrl + "service/" + serviceName + ".json" + : baseUrl + "/service/" + serviceName + ".json"; + String json = httpGet(manifestUrl); + ServiceManifest manifest = ServiceManifest.fromJson(json); + callback.onSuccess(manifest); + } catch (Exception e) { + Log.e(TAG, "Failed to get manifest for: " + serviceName, e); + callback.onError("Failed to fetch manifest: " + e.getMessage()); + } + }); + } + + /** + * Install a service from marketplace. + */ + public void installService(String baseUrl, String serviceName, + MarketplaceCallback callback) { + executor.execute(() -> { + try { + // Get manifest first + String manifestUrl = baseUrl.endsWith("/") + ? baseUrl + "service/" + serviceName + ".json" + : baseUrl + "/service/" + serviceName + ".json"; + String manifestJson = httpGet(manifestUrl); + ServiceManifest manifest = ServiceManifest.fromJson(manifestJson); + + if (manifest == null || manifest.getName() == null) { + callback.onError("Invalid manifest"); + return; + } + + // Create service directory + File serviceDir = new File(servicesDir, manifest.getName()); + if (!serviceDir.exists()) { + serviceDir.mkdirs(); + } + + // Download all endpoint files + InstalledService service = new InstalledService(manifest, baseUrl); + List endpoints = manifest.getEndpoints(); + + if (endpoints != null) { + for (ServiceManifest.Endpoint ep : endpoints) { + String fileUrl = baseUrl.endsWith("/") + ? baseUrl + "service/" + serviceName + "/" + ep.getFile() + : baseUrl + "/service/" + serviceName + "/" + ep.getFile(); + String content = httpGet(fileUrl); + + // Save to disk + File file = new File(serviceDir, ep.getFile()); + writeFile(file, content); + + // Cache in memory + service.getFiles().put(ep.getFile(), content); + } + } + + // Save manifest to disk + File manifestFile = new File(serviceDir, "manifest.json"); + writeFile(manifestFile, manifestJson); + + // Add to installed services + installedServices.put(manifest.getName(), service); + saveInstalledServices(); + + Log.i(TAG, "Installed service: " + manifest.getName()); + callback.onSuccess(service); + + } catch (Exception e) { + Log.e(TAG, "Failed to install service: " + serviceName, e); + callback.onError("Failed to install: " + e.getMessage()); + } + }); + } + + private void writeFile(File file, String content) throws IOException { + FileOutputStream fos = new FileOutputStream(file); + OutputStreamWriter writer = new OutputStreamWriter(fos, StandardCharsets.UTF_8); + writer.write(content); + writer.close(); + } + + /** + * Uninstall a service. + */ + public boolean uninstallService(String serviceName) { + InstalledService service = installedServices.get(serviceName); + if (service == null) { + return false; + } + + // Don't allow uninstalling built-in services + if ("builtin".equals(service.getSource())) { + Log.w(TAG, "Cannot uninstall built-in service: " + serviceName); + return false; + } + + // Delete service directory + File serviceDir = new File(servicesDir, serviceName); + if (serviceDir.exists()) { + deleteDirectory(serviceDir); + } + + // Remove from cache + installedServices.remove(serviceName); + saveInstalledServices(); + + Log.i(TAG, "Uninstalled service: " + serviceName); + return true; + } + + private void deleteDirectory(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + dir.delete(); + } + + /** + * Enable a service (attach endpoints). + */ + public boolean enableService(String serviceName) { + InstalledService service = installedServices.get(serviceName); + if (service == null) { + return false; + } + + service.setEnabled(true); + saveInstalledServices(); + Log.i(TAG, "Enabled service: " + serviceName); + return true; + } + + /** + * Disable a service (detach endpoints, close instances). + */ + public boolean disableService(String serviceName) { + InstalledService service = installedServices.get(serviceName); + if (service == null) { + return false; + } + + service.setEnabled(false); + service.setRunning(false); + saveInstalledServices(); + Log.i(TAG, "Disabled service: " + serviceName); + return true; + } + + /** + * Get all installed services. + */ + public Map getInstalledServices() { + return installedServices; + } + + /** + * Get installed service by name. + */ + public InstalledService getInstalledService(String name) { + return installedServices.get(name); + } + + /** + * Check if a service is installed. + */ + public boolean isInstalled(String name) { + return installedServices.containsKey(name); + } + + /** + * HTTP GET request. + */ + private String httpGet(String urlStr) throws IOException { + URL url = new URL(urlStr); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + + try { + int responseCode = conn.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK) { + throw new IOException("HTTP " + responseCode); + } + + BufferedReader reader = new BufferedReader( + new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } finally { + conn.disconnect(); + } + } + + /** + * Shutdown executor. + */ + public void shutdown() { + executor.shutdown(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/marketplace/ServiceManifest.java b/android/app/src/main/java/seven/lab/wstun/marketplace/ServiceManifest.java new file mode 100644 index 0000000..d0b365b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/marketplace/ServiceManifest.java @@ -0,0 +1,150 @@ +package seven.lab.wstun.marketplace; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Represents a service manifest downloaded from marketplace. + * The manifest defines service metadata and files to be downloaded. + * + * Example manifest JSON: + * { + * "name": "test", + * "displayName": "Test Service", + * "description": "A test service for demonstration", + * "version": "1.0.0", + * "author": "Example Author", + * "endpoints": [ + * {"path": "/service", "file": "index.html", "type": "service"}, + * {"path": "/main", "file": "main.html", "type": "client"} + * ] + * } + */ +public class ServiceManifest { + + private static final Gson gson = new Gson(); + + @SerializedName("name") + private String name; + + @SerializedName("displayName") + private String displayName; + + @SerializedName("description") + private String description; + + @SerializedName("version") + private String version; + + @SerializedName("author") + private String author; + + @SerializedName("icon") + private String icon; + + @SerializedName("endpoints") + private List endpoints; + + /** + * Represents an endpoint defined by the service. + */ + public static class Endpoint { + @SerializedName("path") + private String path; // e.g., "/service", "/main" + + @SerializedName("file") + private String file; // e.g., "index.html", "main.html" + + @SerializedName("type") + private String type; // "service" or "client" + + @SerializedName("contentType") + private String contentType; // optional, defaults based on file extension + + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + + public String getFile() { return file; } + public void setFile(String file) { this.file = file; } + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + + public String getContentType() { return contentType; } + public void setContentType(String contentType) { this.contentType = contentType; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("path", path); + obj.addProperty("file", file); + obj.addProperty("type", type); + if (contentType != null) { + obj.addProperty("contentType", contentType); + } + return obj; + } + } + + // Getters and setters + public String getName() { return name; } + public void setName(String name) { this.name = name; } + + public String getDisplayName() { return displayName != null ? displayName : name; } + public void setDisplayName(String displayName) { this.displayName = displayName; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public String getVersion() { return version; } + public void setVersion(String version) { this.version = version; } + + public String getAuthor() { return author; } + public void setAuthor(String author) { this.author = author; } + + public String getIcon() { return icon; } + public void setIcon(String icon) { this.icon = icon; } + + public List getEndpoints() { return endpoints; } + public void setEndpoints(List endpoints) { this.endpoints = endpoints; } + + /** + * Parse manifest from JSON string. + */ + public static ServiceManifest fromJson(String json) { + return gson.fromJson(json, ServiceManifest.class); + } + + /** + * Convert to JSON string. + */ + public String toJsonString() { + return gson.toJson(this); + } + + /** + * Convert to JsonObject for API responses. + */ + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("name", name); + obj.addProperty("displayName", getDisplayName()); + obj.addProperty("description", description); + obj.addProperty("version", version); + obj.addProperty("author", author); + if (icon != null) { + obj.addProperty("icon", icon); + } + if (endpoints != null) { + com.google.gson.JsonArray arr = new com.google.gson.JsonArray(); + for (Endpoint ep : endpoints) { + arr.add(ep.toJson()); + } + obj.add("endpoints", arr); + } + return obj; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayRequest.java b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayRequest.java new file mode 100644 index 0000000..7197f2d --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayRequest.java @@ -0,0 +1,88 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * HTTP request relayed to a service client. + */ +public class HttpRelayRequest { + + @SerializedName("request_id") + private String requestId; + + @SerializedName("method") + private String method; + + @SerializedName("path") + private String path; + + @SerializedName("query") + private String query; + + @SerializedName("headers") + private Map headers; + + @SerializedName("body") + private String body; + + @SerializedName("body_base64") + private String bodyBase64; // For binary data + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getBodyBase64() { + return bodyBase64; + } + + public void setBodyBase64(String bodyBase64) { + this.bodyBase64 = bodyBase64; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayResponse.java b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayResponse.java new file mode 100644 index 0000000..8b18582 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/HttpRelayResponse.java @@ -0,0 +1,99 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; + +/** + * HTTP response from a service client. + */ +public class HttpRelayResponse { + + @SerializedName("request_id") + private String requestId; + + @SerializedName("status") + private int status; + + @SerializedName("headers") + private Map headers; + + @SerializedName("body") + private String body; + + @SerializedName("body_base64") + private String bodyBase64; // For binary data + + @SerializedName("streaming") + private boolean streaming; + + @SerializedName("chunk_index") + private int chunkIndex; + + @SerializedName("is_final") + private boolean isFinal; + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public String getBodyBase64() { + return bodyBase64; + } + + public void setBodyBase64(String bodyBase64) { + this.bodyBase64 = bodyBase64; + } + + public boolean isStreaming() { + return streaming; + } + + public void setStreaming(boolean streaming) { + this.streaming = streaming; + } + + public int getChunkIndex() { + return chunkIndex; + } + + public void setChunkIndex(int chunkIndex) { + this.chunkIndex = chunkIndex; + } + + public boolean isFinal() { + return isFinal; + } + + public void setFinal(boolean aFinal) { + isFinal = aFinal; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/Message.java b/android/app/src/main/java/seven/lab/wstun/protocol/Message.java new file mode 100644 index 0000000..dcb5726 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/Message.java @@ -0,0 +1,155 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; + +/** + * Base message class for WebSocket communication. + * Supports both text (JSON) and binary modes. + */ +public class Message { + + private static final Gson gson = new Gson(); + + // Message types + public static final String TYPE_REGISTER = "register"; + public static final String TYPE_UNREGISTER = "unregister"; + public static final String TYPE_DATA = "data"; + public static final String TYPE_FILE_REQUEST = "file_request"; + public static final String TYPE_FILE_DATA = "file_data"; + public static final String TYPE_FILE_END = "file_end"; + public static final String TYPE_CHAT_MESSAGE = "chat_message"; + public static final String TYPE_CHAT_JOIN = "chat_join"; + public static final String TYPE_CHAT_LEAVE = "chat_leave"; + public static final String TYPE_ERROR = "error"; + public static final String TYPE_ACK = "ack"; + public static final String TYPE_RELAY_REGISTER = "relay_register"; + public static final String TYPE_RELAY_REQUEST = "relay_request"; + public static final String TYPE_HTTP_REQUEST = "http_request"; + public static final String TYPE_HTTP_RESPONSE = "http_response"; + + // Streaming response types for large file transfers + public static final String TYPE_HTTP_RESPONSE_START = "http_response_start"; + public static final String TYPE_HTTP_RESPONSE_CHUNK = "http_response_chunk"; + + // File registry types for relay file sharing + public static final String TYPE_FILE_REGISTER = "file_register"; + public static final String TYPE_FILE_UNREGISTER = "file_unregister"; + public static final String TYPE_FILE_LIST = "file_list"; + + // Client registration (for user clients, not services) + public static final String TYPE_CLIENT_REGISTER = "client_register"; + + // Chat broadcast types + public static final String TYPE_CHAT_USER_LIST = "chat_user_list"; + + // Service management + public static final String TYPE_KICK_CLIENT = "kick_client"; + + // Instance management + public static final String TYPE_CREATE_INSTANCE = "create_instance"; + public static final String TYPE_JOIN_INSTANCE = "join_instance"; + public static final String TYPE_LEAVE_INSTANCE = "leave_instance"; + public static final String TYPE_LIST_INSTANCES = "list_instances"; + public static final String TYPE_INSTANCE_LIST = "instance_list"; + public static final String TYPE_INSTANCE_CREATED = "instance_created"; + public static final String TYPE_RECLAIM_INSTANCE = "reclaim_instance"; + public static final String TYPE_INSTANCE_RECLAIMED = "instance_reclaimed"; + + // Instance-scoped messages (for fileshare/chat within a room) + public static final String TYPE_USER_JOIN = "user_join"; + public static final String TYPE_USER_LEAVE = "user_leave"; + public static final String TYPE_FILE_ADD = "file_add"; + public static final String TYPE_FILE_REMOVE = "file_remove"; + public static final String TYPE_REQUEST_STATE = "request_state"; + public static final String TYPE_STATE_RESPONSE = "state_response"; + + @SerializedName("type") + private String type; + + @SerializedName("id") + private String id; + + @SerializedName("service") + private String service; + + @SerializedName("payload") + private JsonObject payload; + + @SerializedName("binary") + private boolean binary; + + public Message() { + } + + public Message(String type) { + this.type = type; + this.id = generateId(); + } + + public Message(String type, String service) { + this(type); + this.service = service; + } + + private String generateId() { + return String.valueOf(System.currentTimeMillis()) + "-" + (int)(Math.random() * 10000); + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getService() { + return service; + } + + public void setService(String service) { + this.service = service; + } + + public JsonObject getPayload() { + return payload; + } + + public void setPayload(JsonObject payload) { + this.payload = payload; + } + + public boolean isBinary() { + return binary; + } + + public void setBinary(boolean binary) { + this.binary = binary; + } + + public String toJson() { + return gson.toJson(this); + } + + public static Message fromJson(String json) { + return gson.fromJson(json, Message.class); + } + + public static T payloadAs(JsonObject payload, Class clazz) { + return gson.fromJson(payload, clazz); + } + + public static JsonObject toPayload(Object obj) { + return gson.toJsonTree(obj).getAsJsonObject(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/protocol/ServiceRegistration.java b/android/app/src/main/java/seven/lab/wstun/protocol/ServiceRegistration.java new file mode 100644 index 0000000..8d78202 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/protocol/ServiceRegistration.java @@ -0,0 +1,124 @@ +package seven.lab.wstun.protocol; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; +import java.util.Map; + +/** + * Service registration data sent by clients. + */ +public class ServiceRegistration { + + @SerializedName("name") + private String name; + + @SerializedName("type") + private String type; + + @SerializedName("description") + private String description; + + @SerializedName("endpoints") + private List endpoints; + + @SerializedName("static_resources") + private Map staticResources; // path -> content + + @SerializedName("auth_token") + private String authToken; // Optional auth token for clients to access this service + + public static class Endpoint { + @SerializedName("path") + private String path; + + @SerializedName("method") + private String method; // GET, POST, etc. + + @SerializedName("description") + private String description; + + @SerializedName("relay") + private boolean relay; // Whether to relay requests to the client + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean isRelay() { + return relay; + } + + public void setRelay(boolean relay) { + this.relay = relay; + } + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public List getEndpoints() { + return endpoints; + } + + public void setEndpoints(List endpoints) { + this.endpoints = endpoints; + } + + public Map getStaticResources() { + return staticResources; + } + + public void setStaticResources(Map staticResources) { + this.staticResources = staticResources; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/HttpHandler.java b/android/app/src/main/java/seven/lab/wstun/server/HttpHandler.java new file mode 100644 index 0000000..4d08699 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/HttpHandler.java @@ -0,0 +1,1448 @@ +package seven.lab.wstun.server; + +import android.util.Base64; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.netty.handler.codec.http.DefaultHttpContent; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpUtil; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory; +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.protocol.HttpRelayRequest; +import seven.lab.wstun.protocol.HttpRelayResponse; +import seven.lab.wstun.protocol.Message; + +/** + * HTTP request handler that routes requests to registered services. + */ +public class HttpHandler extends SimpleChannelInboundHandler { + + private static final String TAG = "HttpHandler"; + private static final Gson gson = new Gson(); + + private final ServiceManager serviceManager; + private final RequestManager requestManager; + private final LocalServiceManager localServiceManager; + private final boolean ssl; + private final String corsOrigins; + private final int port; + private final ServerConfig serverConfig; + + private WebSocketServerHandshaker handshaker; + + // Static reference for relay responses + private static String staticCorsOrigins = "*"; + + // Static reference for server config (for WebSocket handler) + private static ServerConfig staticServerConfig; + + // Track channels with active streaming responses + private static final Map streamingResponses = new ConcurrentHashMap<>(); + + public HttpHandler(ServiceManager serviceManager, RequestManager requestManager, + LocalServiceManager localServiceManager, boolean ssl, String corsOrigins, int port, + ServerConfig serverConfig) { + this.serviceManager = serviceManager; + this.requestManager = requestManager; + this.localServiceManager = localServiceManager; + this.ssl = ssl; + this.corsOrigins = corsOrigins != null ? corsOrigins : "*"; + this.port = port; + this.serverConfig = serverConfig; + staticCorsOrigins = this.corsOrigins; + staticServerConfig = serverConfig; + } + + public HttpHandler(ServiceManager serviceManager, RequestManager requestManager, boolean ssl) { + this(serviceManager, requestManager, null, ssl, "*", 8080, null); + } + + public static ServerConfig getServerConfig() { + return staticServerConfig; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { + Log.d(TAG, "HTTP Request: " + request.method() + " " + request.uri()); + try { + // Check for WebSocket upgrade + if (isWebSocketUpgrade(request)) { + handleWebSocketUpgrade(ctx, request); + return; + } + + // Handle HTTP request + handleHttpRequest(ctx, request); + } catch (Exception e) { + Log.e(TAG, "Error handling HTTP request", e); + try { + sendInternalServerError(ctx, request, e.getMessage()); + } catch (Exception e2) { + Log.e(TAG, "Error sending error response", e2); + ctx.close(); + } + } + } + + private boolean isWebSocketUpgrade(FullHttpRequest request) { + String upgrade = request.headers().get(HttpHeaderNames.UPGRADE); + return "websocket".equalsIgnoreCase(upgrade); + } + + private void handleWebSocketUpgrade(ChannelHandlerContext ctx, FullHttpRequest request) { + String wsUrl = (ssl ? "wss" : "ws") + "://" + request.headers().get(HttpHeaderNames.HOST) + "/ws"; + + WebSocketServerHandshakerFactory wsFactory = new WebSocketServerHandshakerFactory( + wsUrl, null, true, 65536 + ); + + handshaker = wsFactory.newHandshaker(request); + if (handshaker == null) { + WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); + } else { + handshaker.handshake(ctx.channel(), request).addListener(future -> { + if (future.isSuccess()) { + // Replace this handler with WebSocket handler + ctx.pipeline().replace(this, "websocket", + new WebSocketHandler(serviceManager, requestManager)); + Log.i(TAG, "WebSocket connection established"); + } + }); + } + } + + private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) { + String uri = request.uri(); + QueryStringDecoder decoder = new QueryStringDecoder(uri); + String path = decoder.path(); + + // Only log at INFO level to reduce overhead + // Log.d(TAG, "HTTP " + request.method() + " " + path); + + // Handle CORS preflight + if (request.method() == HttpMethod.OPTIONS) { + sendCorsPreflightResponse(ctx, request); + return; + } + + // Check server auth (skip for libwstun.js which is public) + if (!"/libwstun.js".equals(path) && !validateServerAuth(request)) { + sendUnauthorized(ctx, request, "Invalid or missing server auth token"); + return; + } + + // Root path - show server info + if ("/".equals(path)) { + sendServerInfo(ctx, request); + return; + } + + // Admin/management page + if ("/admin".equals(path) || "/admin/".equals(path)) { + sendAdminPage(ctx, request); + return; + } + + // Debug logs endpoint - streams logcat (only if enabled in config) + if ("/debug/logs".equals(path)) { + if (serverConfig != null && serverConfig.isDebugLogsEnabled()) { + handleDebugLogs(ctx, request); + } else { + sendNotFound(ctx, request); + } + return; + } + + // Serve libwstun.js library (public, no auth) + if ("/libwstun.js".equals(path)) { + sendLibWstun(ctx, request); + return; + } + + // Parse service name from path + String[] pathParts = path.split("/"); + if (pathParts.length < 2) { + sendNotFound(ctx, request); + return; + } + + String serviceName = pathParts[1]; + + // Handle marketplace and service management API + if ("_api".equals(serviceName)) { + handleApiRequest(ctx, request, pathParts); + return; + } + + // Check for local/installed service endpoints + if (localServiceManager != null) { + seven.lab.wstun.marketplace.InstalledService installedSvc = localServiceManager.getInstalledService(serviceName); + if (installedSvc != null) { + if (handleLocalService(ctx, request, serviceName, pathParts)) { + return; + } + } + } + + // Check for file download requests (/fileshare/download/{fileId}) + if ("fileshare".equals(serviceName) && pathParts.length >= 4 && "download".equals(pathParts[2])) { + handleFileDownload(ctx, request, pathParts); + return; + } + + ServiceManager.ServiceEntry service = serviceManager.getService(serviceName); + + if (service == null) { + sendNotFound(ctx, request); + return; + } + + // Check for static resources + if (service.getRegistration().getStaticResources() != null) { + String resourcePath = path.substring(serviceName.length() + 1); + String content = service.getRegistration().getStaticResources().get(resourcePath); + if (content != null) { + sendStaticResource(ctx, request, content, resourcePath); + return; + } + } + + // Relay request to service client + relayRequest(ctx, request, service, path); + } + + private void relayRequest(ChannelHandlerContext ctx, FullHttpRequest request, + ServiceManager.ServiceEntry service, String path) { + // Create relay request + String requestId = String.valueOf(System.currentTimeMillis()) + "-" + (int)(Math.random() * 10000); + + HttpRelayRequest relayRequest = new HttpRelayRequest(); + relayRequest.setRequestId(requestId); + relayRequest.setMethod(request.method().name()); + relayRequest.setPath(path); + + QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); + relayRequest.setQuery(request.uri().contains("?") ? + request.uri().substring(request.uri().indexOf("?") + 1) : ""); + + // Copy headers + Map headers = new HashMap<>(); + for (Map.Entry entry : request.headers()) { + headers.put(entry.getKey(), entry.getValue()); + } + relayRequest.setHeaders(headers); + + // Copy body + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.readBytes(bodyBytes); + + // Check if binary + String contentType = request.headers().get(HttpHeaderNames.CONTENT_TYPE); + if (contentType != null && isBinaryContentType(contentType)) { + relayRequest.setBodyBase64(Base64.encodeToString(bodyBytes, Base64.NO_WRAP)); + } else { + relayRequest.setBody(new String(bodyBytes, StandardCharsets.UTF_8)); + } + } + + // Store pending request + PendingRequest pending = new PendingRequest(requestId, ctx, request, service.getName()); + requestManager.addPendingRequest(pending); + + // Send to service via WebSocket + Message message = new Message(Message.TYPE_HTTP_REQUEST, service.getName()); + message.setPayload(Message.toPayload(relayRequest)); + + if (service.getChannel() != null && service.getChannel().isActive()) { + service.getChannel().writeAndFlush(new TextWebSocketFrame(message.toJson())); + } else { + requestManager.removePendingRequest(requestId); + sendServiceUnavailable(ctx, request); + } + } + + private boolean isBinaryContentType(String contentType) { + return contentType.startsWith("application/octet-stream") || + contentType.startsWith("image/") || + contentType.startsWith("audio/") || + contentType.startsWith("video/"); + } + + /** + * Send relay response to HTTP client. + */ + public static void sendRelayResponse(ChannelHandlerContext ctx, HttpRelayResponse response) { + HttpResponseStatus status = HttpResponseStatus.valueOf(response.getStatus()); + + byte[] body; + if (response.getBodyBase64() != null) { + body = Base64.decode(response.getBodyBase64(), Base64.NO_WRAP); + } else if (response.getBody() != null) { + body = response.getBody().getBytes(StandardCharsets.UTF_8); + } else { + body = new byte[0]; + } + + FullHttpResponse httpResponse = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + status, + Unpooled.wrappedBuffer(body) + ); + + // Add CORS headers using static method + addStaticCorsHeaders(httpResponse); + + // Copy headers + if (response.getHeaders() != null) { + for (Map.Entry entry : response.getHeaders().entrySet()) { + httpResponse.headers().set(entry.getKey(), entry.getValue()); + } + } + + httpResponse.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, body.length); + httpResponse.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + + ctx.writeAndFlush(httpResponse).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Handle local service endpoints (/fileshare/service, /chat/service, etc.) + * Returns true if the request was handled. + */ + private boolean handleLocalService(ChannelHandlerContext ctx, FullHttpRequest request, + String serviceName, String[] pathParts) { + String subPath = pathParts.length > 2 ? pathParts[2] : ""; + + // Build server URL for generating links + String host = request.headers().get(HttpHeaderNames.HOST); + if (host == null) { + host = "localhost:" + port; + } + String protocol = ssl ? "https" : "http"; + String serverUrl = protocol + "://" + host; + + // /service - Service management page + if ("service".equals(subPath)) { + // Check for API endpoints under /service/api/* + if (pathParts.length > 3 && "api".equals(pathParts[3])) { + return handleLocalServiceApi(ctx, request, serviceName, pathParts); + } + + // Service management page + String html = localServiceManager.getServicePageHtml(serviceName, serverUrl); + if (html == null) { + sendNotFound(ctx, request); + } else { + sendHtmlResponse(ctx, request, html); + } + return true; + } + + // /main - Main service UI (serve directly when service is registered) + if ("main".equals(subPath)) { + // Serve the user client HTML directly from assets (regardless of service running state) + String html = localServiceManager.getServiceMainHtml(serviceName); + if (html != null) { + sendHtmlResponse(ctx, request, html); + return true; + } + + // Fallback: send not found + sendNotFound(ctx, request); + return true; + } + + return false; + } + + /** + * Handle local service API endpoints. + */ + private boolean handleLocalServiceApi(ChannelHandlerContext ctx, FullHttpRequest request, + String serviceName, String[] pathParts) { + if (pathParts.length < 5) { + return false; + } + + String apiAction = pathParts[4]; + + // GET /service/api/status - Get service status + if ("status".equals(apiAction) && request.method() == HttpMethod.GET) { + LocalServiceManager.ServiceStatus status = localServiceManager.getServiceStatus(serviceName); + if (status != null) { + sendJsonResponse(ctx, request, status.toJson().toString()); + } else { + sendJsonResponse(ctx, request, "{\"error\": \"Service not found\"}"); + } + return true; + } + + // POST /service/api/start - Start service + if ("start".equals(apiAction) && request.method() == HttpMethod.POST) { + boolean success = localServiceManager.startService(serviceName); + LocalServiceManager.ServiceStatus status = localServiceManager.getServiceStatus(serviceName); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (success && status != null) { + response.addProperty("uuid", status.getUuid()); + } else if (!success) { + response.addProperty("error", "Failed to start service"); + } + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + // POST /service/api/stop - Stop service + if ("stop".equals(apiAction) && request.method() == HttpMethod.POST) { + // Parse request body for UUID + String uuid = null; + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.readBytes(bodyBytes); + try { + JsonObject body = gson.fromJson(new String(bodyBytes, StandardCharsets.UTF_8), JsonObject.class); + if (body.has("uuid")) { + uuid = body.get("uuid").getAsString(); + } + } catch (Exception e) { + // Ignore parse errors + } + } + + boolean success; + if (uuid != null) { + success = localServiceManager.stopServiceByUuid(serviceName, uuid); + } else { + success = localServiceManager.stopService(serviceName); + } + + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to stop service (UUID mismatch or service not running)"); + } + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + // GET /service/api/clients - List connected clients + if ("clients".equals(apiAction) && request.method() == HttpMethod.GET) { + java.util.List clients = serviceManager.getClientsByType(serviceName); + com.google.gson.JsonArray clientsArray = new com.google.gson.JsonArray(); + for (ServiceManager.ClientInfo client : clients) { + JsonObject clientObj = new JsonObject(); + clientObj.addProperty("userId", client.getUserId()); + clientObj.addProperty("clientType", client.getClientType()); + clientObj.addProperty("connectedAt", client.getConnectedAt()); + clientObj.addProperty("connected", client.getChannel() != null && client.getChannel().isActive()); + clientsArray.add(clientObj); + } + JsonObject response = new JsonObject(); + response.add("clients", clientsArray); + response.addProperty("count", clients.size()); + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + // POST /service/api/kick - Kick a client + if ("kick".equals(apiAction) && request.method() == HttpMethod.POST) { + String userId = null; + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.readBytes(bodyBytes); + try { + JsonObject body = gson.fromJson(new String(bodyBytes, StandardCharsets.UTF_8), JsonObject.class); + if (body.has("userId")) { + userId = body.get("userId").getAsString(); + } + } catch (Exception e) { + // Ignore parse errors + } + } + + JsonObject response = new JsonObject(); + if (userId == null || userId.isEmpty()) { + response.addProperty("success", false); + response.addProperty("error", "userId is required"); + } else { + boolean success = serviceManager.kickClient(userId); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Client not found: " + userId); + } + } + sendJsonResponse(ctx, request, response.toString()); + return true; + } + + return false; + } + + /** + * Send response when service is not running. + */ + private void sendServiceNotRunningResponse(ChannelHandlerContext ctx, FullHttpRequest request, + String serviceName, String serverUrl) { + String displayName = "fileshare".equals(serviceName) ? "FileShare" : "Chat"; + String html = "\n" + + "\n" + + "" + displayName + " - Not Running\n" + + "\n" + + "

Service Not Running

\n" + + "

The " + displayName + " service is not currently running.

\n" + + "Start Service
"; + sendHtmlResponse(ctx, request, html); + } + + /** + * Handle API requests (/_api/...). + */ + private void handleApiRequest(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + if (pathParts.length < 3) { + sendJsonResponse(ctx, request, "{\"error\": \"Invalid API path\"}"); + return; + } + + String apiCategory = pathParts[2]; + + // /_api/services - Service management + if ("services".equals(apiCategory)) { + handleServicesApi(ctx, request, pathParts); + return; + } + + // /_api/instances - Instance management + if ("instances".equals(apiCategory)) { + handleInstancesApi(ctx, request, pathParts); + return; + } + + // /_api/marketplace - Marketplace operations + if ("marketplace".equals(apiCategory)) { + handleMarketplaceApi(ctx, request, pathParts); + return; + } + + sendJsonResponse(ctx, request, "{\"error\": \"Unknown API category\"}"); + } + + /** + * Handle /_api/services endpoints. + */ + private void handleServicesApi(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + // GET /_api/services - List all installed services + if (pathParts.length == 3 && request.method() == HttpMethod.GET) { + if (localServiceManager != null) { + JsonObject response = new JsonObject(); + response.add("services", localServiceManager.getInstalledServicesJson()); + sendJsonResponse(ctx, request, response.toString()); + } else { + sendJsonResponse(ctx, request, "{\"services\": []}"); + } + return; + } + + // /_api/services/{name}/... + if (pathParts.length >= 4) { + String serviceName = pathParts[3]; + + // GET /_api/services/{name} - Get service details + if (pathParts.length == 4 && request.method() == HttpMethod.GET) { + seven.lab.wstun.marketplace.InstalledService service = + localServiceManager != null ? localServiceManager.getInstalledService(serviceName) : null; + if (service != null) { + JsonObject obj = service.toJson(); + obj.addProperty("name", serviceName); + obj.addProperty("instanceCount", serviceManager.getInstanceCountForService(serviceName)); + sendJsonResponse(ctx, request, obj.toString()); + } else { + sendJsonResponse(ctx, request, "{\"error\": \"Service not found\"}"); + } + return; + } + + // POST /_api/services/{name}/enable + if (pathParts.length == 5 && "enable".equals(pathParts[4]) && request.method() == HttpMethod.POST) { + boolean success = localServiceManager != null && localServiceManager.enableService(serviceName); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to enable service"); + } + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // POST /_api/services/{name}/disable + if (pathParts.length == 5 && "disable".equals(pathParts[4]) && request.method() == HttpMethod.POST) { + boolean success = localServiceManager != null && + localServiceManager.disableService(serviceName, serviceManager); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to disable service"); + } + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // DELETE /_api/services/{name} - Uninstall service + if (pathParts.length == 4 && request.method() == HttpMethod.DELETE) { + boolean success = localServiceManager != null && + localServiceManager.uninstallService(serviceName, serviceManager); + JsonObject response = new JsonObject(); + response.addProperty("success", success); + if (!success) { + response.addProperty("error", "Failed to uninstall service (may be built-in)"); + } + sendJsonResponse(ctx, request, response.toString()); + return; + } + } + + sendJsonResponse(ctx, request, "{\"error\": \"Invalid services API path\"}"); + } + + /** + * Handle /_api/instances endpoints. + */ + private void handleInstancesApi(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + // GET /_api/instances - List all running instances + if (pathParts.length == 3 && request.method() == HttpMethod.GET) { + com.google.gson.JsonArray instancesArr = new com.google.gson.JsonArray(); + for (ServiceManager.ServiceInstance instance : getAllInstances()) { + instancesArr.add(instance.toJson()); + } + JsonObject response = new JsonObject(); + response.add("instances", instancesArr); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // GET /_api/instances/{service} - List instances for a service + if (pathParts.length == 4 && request.method() == HttpMethod.GET) { + String serviceName = pathParts[3]; + com.google.gson.JsonArray instancesArr = new com.google.gson.JsonArray(); + for (ServiceManager.ServiceInstance instance : serviceManager.getInstancesForService(serviceName)) { + instancesArr.add(instance.toJson()); + } + JsonObject response = new JsonObject(); + response.add("instances", instancesArr); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + sendJsonResponse(ctx, request, "{\"error\": \"Invalid instances API path\"}"); + } + + /** + * Get all instances across all services. + */ + private java.util.List getAllInstances() { + java.util.List all = new java.util.ArrayList<>(); + if (localServiceManager != null) { + for (String serviceName : localServiceManager.getInstalledServices().keySet()) { + all.addAll(serviceManager.getInstancesForService(serviceName)); + } + } + return all; + } + + /** + * Handle /_api/marketplace endpoints. + */ + private void handleMarketplaceApi(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + if (localServiceManager == null) { + sendJsonResponse(ctx, request, "{\"error\": \"Marketplace not available\"}"); + return; + } + + seven.lab.wstun.marketplace.MarketplaceService marketplace = + localServiceManager.getMarketplaceService(); + + // GET /_api/marketplace/url - Get current marketplace URL + if (pathParts.length == 4 && "url".equals(pathParts[3]) && request.method() == HttpMethod.GET) { + JsonObject response = new JsonObject(); + response.addProperty("url", marketplace.getMarketplaceUrl()); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // POST /_api/marketplace/url - Set marketplace URL + if (pathParts.length == 4 && "url".equals(pathParts[3]) && request.method() == HttpMethod.POST) { + String url = getRequestBodyString(request, "url"); + if (url != null) { + marketplace.setMarketplaceUrl(url); + } + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.addProperty("url", marketplace.getMarketplaceUrl()); + sendJsonResponse(ctx, request, response.toString()); + return; + } + + // POST /_api/marketplace/list - List services from marketplace + if (pathParts.length == 4 && "list".equals(pathParts[3]) && request.method() == HttpMethod.POST) { + String url = getRequestBodyString(request, "url"); + if (url == null || url.isEmpty()) { + url = marketplace.getMarketplaceUrl(); + } + if (url == null || url.isEmpty()) { + sendJsonResponse(ctx, request, "{\"error\": \"No marketplace URL specified\"}"); + return; + } + + final String marketplaceUrl = url; + marketplace.listMarketplace(marketplaceUrl, + new seven.lab.wstun.marketplace.MarketplaceService.MarketplaceCallback>() { + @Override + public void onSuccess(java.util.List result) { + ctx.executor().execute(() -> { + com.google.gson.JsonArray arr = new com.google.gson.JsonArray(); + for (JsonObject svc : result) { + // Add installed status + String name = svc.has("name") ? svc.get("name").getAsString() : null; + if (name != null) { + svc.addProperty("installed", marketplace.isInstalled(name)); + } + arr.add(svc); + } + JsonObject response = new JsonObject(); + response.add("services", arr); + response.addProperty("url", marketplaceUrl); + sendJsonResponse(ctx, request, response.toString()); + }); + } + + @Override + public void onError(String error) { + ctx.executor().execute(() -> { + JsonObject response = new JsonObject(); + response.addProperty("error", error); + sendJsonResponse(ctx, request, response.toString()); + }); + } + }); + return; + } + + // POST /_api/marketplace/install - Install a service + if (pathParts.length == 4 && "install".equals(pathParts[3]) && request.method() == HttpMethod.POST) { + String url = getRequestBodyString(request, "url"); + String name = getRequestBodyString(request, "name"); + + if (url == null || url.isEmpty()) { + url = marketplace.getMarketplaceUrl(); + } + if (name == null || name.isEmpty()) { + sendJsonResponse(ctx, request, "{\"error\": \"Service name required\"}"); + return; + } + + final String marketplaceUrl = url; + final String serviceName = name; + + marketplace.installService(marketplaceUrl, serviceName, + new seven.lab.wstun.marketplace.MarketplaceService.MarketplaceCallback() { + @Override + public void onSuccess(seven.lab.wstun.marketplace.InstalledService result) { + ctx.executor().execute(() -> { + JsonObject response = new JsonObject(); + response.addProperty("success", true); + response.add("service", result.toJson()); + sendJsonResponse(ctx, request, response.toString()); + }); + } + + @Override + public void onError(String error) { + ctx.executor().execute(() -> { + JsonObject response = new JsonObject(); + response.addProperty("success", false); + response.addProperty("error", error); + sendJsonResponse(ctx, request, response.toString()); + }); + } + }); + return; + } + + sendJsonResponse(ctx, request, "{\"error\": \"Invalid marketplace API path\"}"); + } + + /** + * Helper to get a string value from request JSON body. + */ + private String getRequestBodyString(FullHttpRequest request, String key) { + ByteBuf content = request.content(); + if (content.readableBytes() > 0) { + byte[] bodyBytes = new byte[content.readableBytes()]; + content.getBytes(content.readerIndex(), bodyBytes); + try { + JsonObject body = gson.fromJson(new String(bodyBytes, StandardCharsets.UTF_8), JsonObject.class); + if (body.has(key) && !body.get(key).isJsonNull()) { + return body.get(key).getAsString(); + } + } catch (Exception e) { + // Ignore parse errors + } + } + return null; + } + + /** + * Send JSON response. + */ + private void sendJsonResponse(ChannelHandlerContext ctx, FullHttpRequest request, String json) { + byte[] bytes = json.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + + sendResponse(ctx, request, response); + } + + private void sendServerInfo(ChannelHandlerContext ctx, FullHttpRequest request) { + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(""); + html.append("WSTun Server"); + html.append(""); + html.append("
"); + html.append("

WSTun Server

"); + html.append("

Service Manager

"); + + // Installed services section + if (localServiceManager != null) { + html.append("

Installed Services

"); + html.append("
    "); + + for (java.util.Map.Entry entry : + localServiceManager.getInstalledServices().entrySet()) { + String name = entry.getKey(); + seven.lab.wstun.marketplace.InstalledService svc = entry.getValue(); + String displayName = svc.getDisplayName(); + boolean enabled = svc.isEnabled(); + int instanceCount = serviceManager.getInstanceCountForService(name); + + html.append("
  • ").append(displayName).append(""); + + if (enabled) { + html.append("Enabled"); + if (instanceCount > 0) { + html.append("").append(instanceCount).append(" instances"); + } + html.append(" - Manage"); + } else { + html.append("Disabled"); + } + + html.append("
  • "); + } + + if (localServiceManager.getInstalledServices().isEmpty()) { + html.append("
  • No services installed
  • "); + } + + html.append("
"); + } + + html.append("

Registered Services

"); + + if (serviceManager.getServiceCount() == 0) { + html.append("

No services registered

"); + } else { + html.append("
    "); + for (ServiceManager.ServiceEntry service : serviceManager.getAllServices()) { + html.append("
  • ").append(service.getName()).append(""); + html.append(" (").append(service.getType()).append(")"); + html.append(" - ").append("/").append(service.getName()).append("/main"); + html.append("
  • "); + } + html.append("
"); + } + + html.append("
"); + + sendHtmlResponse(ctx, request, html.toString()); + } + + private void sendStaticResource(ChannelHandlerContext ctx, FullHttpRequest request, + String content, String path) { + String contentType = "text/plain"; + if (path.endsWith(".html")) { + contentType = "text/html; charset=UTF-8"; + } else if (path.endsWith(".js")) { + contentType = "application/javascript"; + } else if (path.endsWith(".css")) { + contentType = "text/css"; + } else if (path.endsWith(".json")) { + contentType = "application/json"; + } + + byte[] bytes = content.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + + sendResponse(ctx, request, response); + } + + private void sendHtmlResponse(ChannelHandlerContext ctx, FullHttpRequest request, String html) { + if (html == null) { + sendNotFound(ctx, request); + return; + } + byte[] bytes = html.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + + sendResponse(ctx, request, response); + } + + private void sendNotFound(ChannelHandlerContext ctx, FullHttpRequest request) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NOT_FOUND, + Unpooled.wrappedBuffer("Not Found".getBytes()) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 9); + + sendResponse(ctx, request, response); + } + + private void sendUnauthorized(ChannelHandlerContext ctx, FullHttpRequest request, String message) { + byte[] bytes = message.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.UNAUTHORIZED, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Bearer realm=\"wstun\""); + + sendResponse(ctx, request, response); + } + + /** + * Validate server-level authentication. + * Checks Authorization header or ?token query parameter. + */ + private boolean validateServerAuth(FullHttpRequest request) { + if (serverConfig == null || !serverConfig.isAuthEnabled()) { + return true; + } + + String token = null; + + // Check Authorization header (Bearer token) + String authHeader = request.headers().get(HttpHeaderNames.AUTHORIZATION); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + + // Check query parameter as fallback + if (token == null) { + QueryStringDecoder decoder = new QueryStringDecoder(request.uri()); + if (decoder.parameters().containsKey("token")) { + token = decoder.parameters().get("token").get(0); + } + } + + return serverConfig.validateServerAuth(token); + } + + /** + * Serve the admin page. + */ + private void sendAdminPage(ChannelHandlerContext ctx, FullHttpRequest request) { + String html = localServiceManager != null ? localServiceManager.getAdminHtml() : null; + if (html == null) { + sendNotFound(ctx, request); + return; + } + sendHtmlResponse(ctx, request, html); + } + + /** + * Serve the libwstun.js library. + */ + private void sendLibWstun(ChannelHandlerContext ctx, FullHttpRequest request) { + String js = localServiceManager != null ? localServiceManager.getLibWstunJs() : null; + if (js == null) { + sendNotFound(ctx, request); + return; + } + + byte[] bytes = js.getBytes(StandardCharsets.UTF_8); + + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.wrappedBuffer(bytes) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/javascript; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, bytes.length); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "public, max-age=3600"); + + sendResponse(ctx, request, response); + } + + /** + * Handle debug logs endpoint - streams logcat to the browser. + */ + private void handleDebugLogs(ChannelHandlerContext ctx, FullHttpRequest request) { + // Send initial response with chunked transfer encoding + HttpResponse response = new DefaultHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "no-cache"); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, corsOrigins); + + ctx.writeAndFlush(response); + + // Start a thread to stream logcat + Thread logThread = new Thread(() -> { + Process process = null; + BufferedReader reader = null; + try { + // Start logcat process - filter to show Info and above, with timestamp + process = Runtime.getRuntime().exec("logcat -v time *:I"); + reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + String line; + while (ctx.channel().isActive() && (line = reader.readLine()) != null) { + // Send each log line as a chunk + String chunk = line + "\n"; + ctx.writeAndFlush(new DefaultHttpContent( + Unpooled.copiedBuffer(chunk, StandardCharsets.UTF_8))); + } + } catch (Exception e) { + Log.e(TAG, "Error streaming logcat", e); + } finally { + // Clean up + if (reader != null) { + try { reader.close(); } catch (Exception ignored) {} + } + if (process != null) { + process.destroy(); + } + + // Send end marker and close + if (ctx.channel().isActive()) { + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + .addListener(ChannelFutureListener.CLOSE); + } + } + }); + logThread.setName("LogcatStreamer"); + logThread.setDaemon(true); + logThread.start(); + } + + private void sendServiceUnavailable(ChannelHandlerContext ctx, FullHttpRequest request) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.wrappedBuffer("Service Unavailable".getBytes()) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 19); + + sendResponse(ctx, request, response); + } + + private void sendInternalServerError(ChannelHandlerContext ctx, FullHttpRequest request, String message) { + String body = "Internal Server Error: " + (message != null ? message : "Unknown error"); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.INTERNAL_SERVER_ERROR, + Unpooled.wrappedBuffer(body.getBytes(StandardCharsets.UTF_8)) + ); + + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, body.getBytes(StandardCharsets.UTF_8).length); + + sendResponse(ctx, request, response); + } + + private void sendResponse(ChannelHandlerContext ctx, FullHttpRequest request, FullHttpResponse response) { + // Add CORS headers to all responses + addCorsHeaders(response); + + // Ensure Content-Length is set - this is critical for keep-alive to work properly + if (!response.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) { + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + } + + boolean keepAlive = HttpUtil.isKeepAlive(request); + + if (keepAlive) { + // For keep-alive, set the header and don't close + if (!response.headers().contains(HttpHeaderNames.CONNECTION)) { + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); + } + } else { + // For non-keep-alive, set Connection: close + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + } + + // Write and flush the response + ctx.writeAndFlush(response).addListener(f -> { + if (!keepAlive) { + ctx.close(); + } + }); + } + + /** + * Add CORS headers to response. + */ + private void addCorsHeaders(FullHttpResponse response) { + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, corsOrigins); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, X-Requested-With"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_MAX_AGE, "86400"); + } + + /** + * Add CORS headers with static origin (for relay responses). + */ + private static void addStaticCorsHeaders(FullHttpResponse response) { + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, staticCorsOrigins); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS, "Content-Type, Authorization, X-Requested-With"); + } + + /** + * Handle CORS preflight (OPTIONS) requests. + */ + private void sendCorsPreflightResponse(ChannelHandlerContext ctx, FullHttpRequest request) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.OK, + Unpooled.EMPTY_BUFFER + ); + + addCorsHeaders(response); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 0); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + Log.d(TAG, "Channel active: " + ctx.channel().remoteAddress()); + super.channelActive(ctx); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Log.d(TAG, "Channel inactive: " + ctx.channel().remoteAddress()); + super.channelInactive(ctx); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Log.e(TAG, "HTTP handler error", cause); + ctx.close(); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + if (evt instanceof io.netty.handler.timeout.IdleStateEvent) { + Log.d(TAG, "Idle timeout, closing connection"); + ctx.close(); + } else { + super.userEventTriggered(ctx, evt); + } + } + + // ==================== Streaming Response Methods ==================== + + /** + * Start a streaming HTTP response (send headers, prepare for chunks). + */ + public static void startStreamingResponse(ChannelHandlerContext ctx, String requestId, + int status, JsonObject headers) { + HttpResponse response = new DefaultHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.valueOf(status) + ); + + // Add CORS headers + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_ORIGIN, staticCorsOrigins); + response.headers().set(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS"); + + // Use chunked transfer encoding for streaming + response.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED); + + // Copy headers from payload + if (headers != null) { + for (String key : headers.keySet()) { + // Skip Content-Length as we're using chunked encoding + if (!key.equalsIgnoreCase("Content-Length")) { + response.headers().set(key, headers.get(key).getAsString()); + } + } + } + + // Flush the headers immediately so the client knows the response has started + ctx.writeAndFlush(response); + streamingResponses.put(requestId, ctx); + Log.d(TAG, "Started streaming response for: " + requestId); + } + + /** + * Send a chunk of streaming data. + */ + public static void sendStreamingChunk(ChannelHandlerContext ctx, String chunkBase64) { + if (ctx == null || !ctx.channel().isActive()) { + return; + } + + byte[] data = Base64.decode(chunkBase64, Base64.NO_WRAP); + ByteBuf buf = Unpooled.wrappedBuffer(data); + ctx.writeAndFlush(new io.netty.handler.codec.http.DefaultHttpContent(buf)); + } + + /** + * End a streaming response. + */ + public static void endStreamingResponse(ChannelHandlerContext ctx) { + if (ctx == null || !ctx.channel().isActive()) { + return; + } + + ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT) + .addListener(ChannelFutureListener.CLOSE); + Log.d(TAG, "Ended streaming response"); + } + + /** + * Send error and close streaming response. + */ + public static void sendStreamingError(ChannelHandlerContext ctx, String error) { + if (ctx == null || !ctx.channel().isActive()) { + return; + } + + // If headers not sent yet, send error response + byte[] errorBytes = error.getBytes(StandardCharsets.UTF_8); + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.INTERNAL_SERVER_ERROR, + Unpooled.wrappedBuffer(errorBytes) + ); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, errorBytes.length); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + addStaticCorsHeaders(response); + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Request file stream from owner for relay download. + */ + public static void requestFileStream(ServiceManager serviceManager, String requestId, + String fileId, ChannelHandlerContext httpCtx) { + ServiceManager.FileInfo file = serviceManager.getFile(fileId); + if (file == null) { + sendNotFoundResponse(httpCtx); + return; + } + + Channel ownerChannel = file.getOwnerChannel(); + if (ownerChannel == null || !ownerChannel.isActive()) { + sendServiceUnavailableResponse(httpCtx); + return; + } + + // Send file request to owner + Message message = new Message(Message.TYPE_FILE_REQUEST); + JsonObject payload = new JsonObject(); + payload.addProperty("requestId", requestId); + payload.addProperty("fileId", fileId); + message.setPayload(payload); + + ownerChannel.writeAndFlush(new TextWebSocketFrame(message.toJson())); + Log.d(TAG, "Requested file stream from owner: " + fileId); + } + + private static void sendNotFoundResponse(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.NOT_FOUND, + Unpooled.wrappedBuffer("File not found".getBytes()) + ); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 14); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + addStaticCorsHeaders(response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + private static void sendServiceUnavailableResponse(ChannelHandlerContext ctx) { + FullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.wrappedBuffer("File owner not connected".getBytes()) + ); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain"); + response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, 24); + response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE); + addStaticCorsHeaders(response); + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + + /** + * Handle file download request - streams file from owner client. + * URL format: /fileshare/download/{globalFileId} + * where globalFileId = userId/localFileId + * This allows stateless routing - the server doesn't need to store file info. + */ + private void handleFileDownload(ChannelHandlerContext ctx, FullHttpRequest request, String[] pathParts) { + // Extract global file ID from path: /fileshare/download/{globalFileId} + String globalFileId; + try { + globalFileId = java.net.URLDecoder.decode(pathParts[3], "UTF-8"); + } catch (Exception e) { + globalFileId = pathParts[3]; + } + + Log.d(TAG, "File download request: " + globalFileId); + + // Parse globalFileId to extract userId + // Format: userId/localFileId + String ownerId = null; + String localFileId = globalFileId; + + if (globalFileId.contains("/")) { + int slashIdx = globalFileId.indexOf('/'); + ownerId = globalFileId.substring(0, slashIdx); + localFileId = globalFileId.substring(slashIdx + 1); + } + + if (ownerId == null || ownerId.isEmpty()) { + // Fallback: try the old registry-based lookup for backward compatibility + ServiceManager.FileInfo file = serviceManager.getFile(globalFileId); + if (file != null) { + ownerId = file.getOwnerId(); + Channel ownerChannel = file.getOwnerChannel(); + if (ownerChannel != null && ownerChannel.isActive()) { + sendFileRequest(ctx, request, ownerChannel, globalFileId); + return; + } + } + sendNotFoundResponse(ctx); + return; + } + + // Find the owner's channel by userId + ServiceManager.ClientInfo ownerClient = serviceManager.getClient(ownerId); + if (ownerClient == null || ownerClient.getChannel() == null || !ownerClient.getChannel().isActive()) { + Log.w(TAG, "File owner not connected: " + ownerId); + sendServiceUnavailableResponse(ctx); + return; + } + + sendFileRequest(ctx, request, ownerClient.getChannel(), globalFileId); + } + + /** + * Send a file request to the owner and set up pending request for streaming response. + */ + private void sendFileRequest(ChannelHandlerContext ctx, FullHttpRequest request, + Channel ownerChannel, String fileId) { + // Create pending request for streaming response + String requestId = String.valueOf(System.currentTimeMillis()) + "-" + (int)(Math.random() * 10000); + PendingRequest pending = new PendingRequest(requestId, ctx, request, "fileshare"); + requestManager.addPendingRequest(pending); + + // Send file_request to owner + Message message = new Message(Message.TYPE_FILE_REQUEST); + JsonObject payload = new JsonObject(); + payload.addProperty("requestId", requestId); + payload.addProperty("fileId", fileId); + message.setPayload(payload); + + ownerChannel.writeAndFlush(new TextWebSocketFrame(message.toJson())); + Log.d(TAG, "Sent file request to owner: " + fileId); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/LocalServiceManager.java b/android/app/src/main/java/seven/lab/wstun/server/LocalServiceManager.java new file mode 100644 index 0000000..248a212 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/LocalServiceManager.java @@ -0,0 +1,433 @@ +package seven.lab.wstun.server; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.marketplace.InstalledService; +import seven.lab.wstun.marketplace.MarketplaceService; +import seven.lab.wstun.marketplace.ServiceManifest; + +/** + * Manages local services including built-in and marketplace-installed services. + * Services can be enabled/disabled and have endpoints attached/detached dynamically. + */ +public class LocalServiceManager { + + private static final String TAG = "LocalServiceManager"; + private static final Gson gson = new Gson(); + + private final Context context; + private final ServerConfig config; + private final MarketplaceService marketplaceService; + + // Service status tracking (for running instances) + private final Map serviceStatuses = new ConcurrentHashMap<>(); + + // HTML/JS content cache + private String libwstunJs; + + /** + * Represents the status of a local service. + */ + public static class ServiceStatus { + private final String serviceName; + private String uuid; + private boolean running; + private long startedAt; + private long stoppedAt; + + public ServiceStatus(String serviceName) { + this.serviceName = serviceName; + this.running = false; + this.uuid = null; + } + + public void start() { + this.uuid = UUID.randomUUID().toString(); + this.running = true; + this.startedAt = System.currentTimeMillis(); + this.stoppedAt = 0; + } + + public void stop() { + this.running = false; + this.stoppedAt = System.currentTimeMillis(); + // Keep UUID so we can identify this was our service + } + + public String getServiceName() { return serviceName; } + public String getUuid() { return uuid; } + public boolean isRunning() { return running; } + public long getStartedAt() { return startedAt; } + public long getStoppedAt() { return stoppedAt; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("serviceName", serviceName); + obj.addProperty("uuid", uuid); + obj.addProperty("running", running); + obj.addProperty("startedAt", startedAt); + obj.addProperty("stoppedAt", stoppedAt); + return obj; + } + } + + public LocalServiceManager(Context context, ServerConfig config) { + this.context = context; + this.config = config; + this.marketplaceService = new MarketplaceService(context); + + // Initialize service statuses for all installed services + for (String name : marketplaceService.getInstalledServices().keySet()) { + serviceStatuses.put(name, new ServiceStatus(name)); + } + + // Load libwstun.js + loadLibwstunJs(); + } + + private void loadLibwstunJs() { + try { + libwstunJs = loadAsset("libwstun.js"); + Log.i(TAG, "Loaded libwstun.js from assets"); + } catch (IOException e) { + Log.e(TAG, "Failed to load assets", e); + libwstunJs = "// libwstun.js failed to load"; + } + } + + /** + * Generate the admin HTML page dynamically. + */ + public String getAdminHtml() { + StringBuilder html = new StringBuilder(); + html.append(""); + html.append(""); + html.append("WSTun Admin"); + html.append("

WSTun Service Manager

"); + html.append("

Use the WSTun Android app to manage services, or access services directly below.

"); + + html.append("

Installed Services

"); + for (Map.Entry entry : getInstalledServices().entrySet()) { + String name = entry.getKey(); + InstalledService svc = entry.getValue(); + String displayName = svc.getDisplayName() != null ? svc.getDisplayName() : name; + html.append("

").append(displayName).append(""); + html.append(""); + html.append(svc.isEnabled() ? "Enabled" : "Disabled").append(""); + if (svc.isEnabled()) { + html.append(" - Manage"); + } + html.append("

"); + } + html.append("
"); + + html.append(""); + return html.toString(); + } + + private String loadAsset(String path) throws IOException { + InputStream is = context.getAssets().open(path); + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } + + /** + * Check if a service is enabled. + */ + public boolean isServiceEnabled(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + return service != null && service.isEnabled(); + } + + /** + * Get the marketplace service. + */ + public MarketplaceService getMarketplaceService() { + return marketplaceService; + } + + /** + * Get all installed services. + */ + public Map getInstalledServices() { + return marketplaceService.getInstalledServices(); + } + + /** + * Get installed service by name. + */ + public InstalledService getInstalledService(String serviceName) { + return marketplaceService.getInstalledService(serviceName); + } + + /** + * Enable a service. + */ + public boolean enableService(String serviceName) { + boolean result = marketplaceService.enableService(serviceName); + if (result && !serviceStatuses.containsKey(serviceName)) { + serviceStatuses.put(serviceName, new ServiceStatus(serviceName)); + } + return result; + } + + /** + * Disable a service. + */ + public boolean disableService(String serviceName, ServiceManager serviceManager) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null) { + return false; + } + + // Close all instances for this service + if (serviceManager != null) { + serviceManager.closeInstancesForService(serviceName); + } + + // Stop the service status + ServiceStatus status = serviceStatuses.get(serviceName); + if (status != null && status.isRunning()) { + status.stop(); + } + + return marketplaceService.disableService(serviceName); + } + + /** + * Uninstall a service. + */ + public boolean uninstallService(String serviceName, ServiceManager serviceManager) { + // Disable first + disableService(serviceName, serviceManager); + + // Remove status + serviceStatuses.remove(serviceName); + + return marketplaceService.uninstallService(serviceName); + } + + /** + * Get service status. + */ + public ServiceStatus getServiceStatus(String serviceName) { + return serviceStatuses.get(serviceName); + } + + /** + * Start a local service. + */ + public boolean startService(String serviceName) { + if (!isServiceEnabled(serviceName)) { + Log.w(TAG, "Service not enabled: " + serviceName); + return false; + } + + ServiceStatus status = serviceStatuses.get(serviceName); + if (status == null) { + Log.w(TAG, "Unknown service: " + serviceName); + return false; + } + + if (status.isRunning()) { + Log.w(TAG, "Service already running: " + serviceName); + return false; + } + + status.start(); + Log.i(TAG, "Started local service: " + serviceName + " (UUID: " + status.getUuid() + ")"); + return true; + } + + /** + * Stop a local service. + */ + public boolean stopService(String serviceName) { + ServiceStatus status = serviceStatuses.get(serviceName); + if (status == null) { + Log.w(TAG, "Unknown service: " + serviceName); + return false; + } + + if (!status.isRunning()) { + Log.w(TAG, "Service not running: " + serviceName); + return false; + } + + status.stop(); + Log.i(TAG, "Stopped local service: " + serviceName); + return true; + } + + /** + * Stop a service by UUID (ensures it's exactly our service). + */ + public boolean stopServiceByUuid(String serviceName, String uuid) { + ServiceStatus status = serviceStatuses.get(serviceName); + if (status == null) { + return false; + } + + if (!uuid.equals(status.getUuid())) { + Log.w(TAG, "UUID mismatch for service: " + serviceName); + return false; + } + + return stopService(serviceName); + } + + /** + * Get the service management HTML page for a service. + * This returns the service controller page (index.html) which: + * - Registers as a SERVICE with the server + * - Shows in Android UI + * - Manages user clients + */ + public String getServicePageHtml(String serviceName, String serverUrl) { + if (!isServiceEnabled(serviceName)) { + return getServiceDisabledHtml(serviceName); + } + + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null) { + return getServiceDisabledHtml(serviceName); + } + + // Get the /service endpoint file + String content = service.getFileContent("/service"); + if (content == null || content.isEmpty()) { + return getServiceDisabledHtml(serviceName); + } + return content; + } + + /** + * Get the main service HTML (the user client UI). + * This is served directly from server when service is registered. + */ + public String getServiceMainHtml(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null) { + return null; + } + + return service.getFileContent("/main"); + } + + /** + * Get file content for any endpoint of a service. + */ + public String getServiceFileContent(String serviceName, String path) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null || !service.isEnabled()) { + return null; + } + + return service.getFileContent(path); + } + + /** + * Get list of all endpoints for a service. + */ + public List getServiceEndpoints(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + if (service == null || service.getManifest() == null) { + return new ArrayList<>(); + } + return service.getManifest().getEndpoints(); + } + + /** + * Get the libwstun.js library content. + */ + public String getLibWstunJs() { + return libwstunJs; + } + + private String getServiceDisabledHtml(String serviceName) { + InstalledService service = marketplaceService.getInstalledService(serviceName); + String displayName = service != null ? service.getDisplayName() : serviceName; + + return "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " " + displayName + " Service - Disabled\n" + + " \n" + + "\n" + + "\n" + + "
\n" + + "

Service Disabled

\n" + + "

The " + displayName + " service is not enabled on this server.

\n" + + "

Enable it in the WSTun app service management.

\n" + + "
\n" + + "\n" + + ""; + } + + /** + * Get all installed services as JSON array. + */ + public JsonArray getInstalledServicesJson() { + JsonArray arr = new JsonArray(); + for (Map.Entry entry : marketplaceService.getInstalledServices().entrySet()) { + JsonObject obj = entry.getValue().toJson(); + obj.addProperty("name", entry.getKey()); + + // Add running status + ServiceStatus status = serviceStatuses.get(entry.getKey()); + if (status != null) { + obj.addProperty("running", status.isRunning()); + if (status.getUuid() != null) { + obj.addProperty("uuid", status.getUuid()); + } + } + arr.add(obj); + } + return arr; + } + + /** + * Shutdown resources. + */ + public void shutdown() { + if (marketplaceService != null) { + marketplaceService.shutdown(); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/NettyServer.java b/android/app/src/main/java/seven/lab/wstun/server/NettyServer.java new file mode 100644 index 0000000..0a5b8c4 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/NettyServer.java @@ -0,0 +1,223 @@ +package seven.lab.wstun.server; + +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.stream.ChunkedWriteHandler; +import io.netty.handler.timeout.IdleStateHandler; +import io.netty.util.concurrent.DefaultThreadFactory; +import seven.lab.wstun.config.ServerConfig; + +/** + * Netty-based HTTP/WebSocket server with optional HTTPS support. + * Uses dedicated threads with high priority to ensure responsiveness + * even when the Android app is in the background. + */ +public class NettyServer { + + private static final String TAG = "NettyServer"; + + private final Context context; + private final ServerConfig config; + private final ServiceManager serviceManager; + private final RequestManager requestManager; + private final LocalServiceManager localServiceManager; + + private EventLoopGroup bossGroup; + private EventLoopGroup workerGroup; + private Channel serverChannel; + private boolean running = false; + private PowerManager.WakeLock wakeLock; + + public NettyServer(Context context, ServerConfig config, ServiceManager serviceManager) { + this.context = context; + this.config = config; + this.serviceManager = serviceManager; + this.requestManager = new RequestManager(); + this.localServiceManager = new LocalServiceManager(context, config); + + // Link ServiceManager to RequestManager for cleanup on disconnect + this.serviceManager.setRequestManager(this.requestManager); + } + + /** + * Start the server. + */ + public synchronized void start() throws Exception { + if (running) { + Log.w(TAG, "Server already running"); + return; + } + + // Acquire a partial wake lock to keep the CPU running for network I/O + // This is essential for the server to respond when the app is in the background + try { + PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + if (powerManager != null) { + wakeLock = powerManager.newWakeLock( + PowerManager.PARTIAL_WAKE_LOCK, + "WSTun:NettyServer" + ); + wakeLock.acquire(); + Log.i(TAG, "Acquired wake lock for background operation"); + } + } catch (Exception e) { + Log.w(TAG, "Failed to acquire wake lock: " + e.getMessage()); + } + + int port = config.getPort(); + boolean ssl = config.isHttpsEnabled(); + + // Get SSL context if needed + SslContext sslContext = null; + if (ssl) { + sslContext = SslContextFactory.getSslContext(context); + } + final SslContext finalSslContext = sslContext; + final String corsOrigins = config.getCorsOrigins(); + final int serverPort = port; + final LocalServiceManager localSvcMgr = localServiceManager; + final ServerConfig serverConfig = config; + + // Use custom thread factory to create high-priority daemon threads + // This ensures Netty threads keep running even when app is backgrounded + DefaultThreadFactory bossThreadFactory = new DefaultThreadFactory("wstun-boss", true, Thread.MAX_PRIORITY); + DefaultThreadFactory workerThreadFactory = new DefaultThreadFactory("wstun-worker", true, Thread.MAX_PRIORITY); + + bossGroup = new NioEventLoopGroup(1, bossThreadFactory); + // Use more worker threads to handle concurrent HTTP and WebSocket connections + workerGroup = new NioEventLoopGroup(Math.max(4, Runtime.getRuntime().availableProcessors() * 2), workerThreadFactory); + + ServerBootstrap bootstrap = new ServerBootstrap(); + bootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .option(ChannelOption.SO_BACKLOG, 1024) + .option(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.SO_KEEPALIVE, true) + .childOption(ChannelOption.TCP_NODELAY, true) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) throws Exception { + Log.d(TAG, "New connection from: " + ch.remoteAddress()); + ChannelPipeline pipeline = ch.pipeline(); + + // SSL handler + if (finalSslContext != null) { + pipeline.addLast("ssl", finalSslContext.newHandler(ch.alloc())); + } + + // HTTP codec + pipeline.addLast("http-codec", new HttpServerCodec()); + + // Aggregate HTTP message parts into FullHttpRequest + pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536)); + + // Idle state handler - longer timeouts for better stability + // Read idle: 120s, Write idle: 60s, All idle: 0 (disabled) + pipeline.addLast("idle", new IdleStateHandler(120, 60, 0)); + + // HTTP/WebSocket handler with CORS configuration and local service support + pipeline.addLast("http-handler", + new HttpHandler(serviceManager, requestManager, localSvcMgr, ssl, corsOrigins, serverPort, serverConfig)); + } + }); + + serverChannel = bootstrap.bind(port).sync().channel(); + running = true; + + Log.i(TAG, "Server started on port " + port + (ssl ? " (HTTPS)" : " (HTTP)")); + } + + /** + * Stop the server. + */ + public synchronized void stop() { + if (!running) { + return; + } + + running = false; + + // Close all service connections + serviceManager.clear(); + + // Shutdown request manager + requestManager.shutdown(); + + // Close server channel + if (serverChannel != null) { + try { + serverChannel.close().sync(); + } catch (InterruptedException e) { + Log.e(TAG, "Error closing server channel", e); + } + } + + // Shutdown event loops with timeout + if (workerGroup != null) { + workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); + } + if (bossGroup != null) { + bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); + } + + // Release wake lock + if (wakeLock != null && wakeLock.isHeld()) { + try { + wakeLock.release(); + Log.i(TAG, "Released wake lock"); + } catch (Exception e) { + Log.w(TAG, "Failed to release wake lock: " + e.getMessage()); + } + wakeLock = null; + } + + Log.i(TAG, "Server stopped"); + } + + /** + * Check if server is running. + */ + public boolean isRunning() { + return running; + } + + /** + * Get the service manager. + */ + public ServiceManager getServiceManager() { + return serviceManager; + } + + /** + * Get the request manager. + */ + public RequestManager getRequestManager() { + return requestManager; + } + + /** + * Get the local service manager. + */ + public LocalServiceManager getLocalServiceManager() { + return localServiceManager; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/PendingRequest.java b/android/app/src/main/java/seven/lab/wstun/server/PendingRequest.java new file mode 100644 index 0000000..d0a7e96 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/PendingRequest.java @@ -0,0 +1,48 @@ +package seven.lab.wstun.server; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; + +/** + * Represents a pending HTTP request waiting for response from a service client. + */ +public class PendingRequest { + + private final String requestId; + private final ChannelHandlerContext ctx; + private final HttpRequest request; + private final String serviceName; + private final long timestamp; + + public PendingRequest(String requestId, ChannelHandlerContext ctx, HttpRequest request, String serviceName) { + this.requestId = requestId; + this.ctx = ctx; + this.request = request; + this.serviceName = serviceName; + this.timestamp = System.currentTimeMillis(); + } + + public String getRequestId() { + return requestId; + } + + public ChannelHandlerContext getCtx() { + return ctx; + } + + public HttpRequest getRequest() { + return request; + } + + public String getServiceName() { + return serviceName; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isTimedOut(long timeoutMs) { + return System.currentTimeMillis() - timestamp > timeoutMs; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/RequestManager.java b/android/app/src/main/java/seven/lab/wstun/server/RequestManager.java new file mode 100644 index 0000000..ab98f71 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/RequestManager.java @@ -0,0 +1,152 @@ +package seven.lab.wstun.server; + +import android.util.Log; + +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; + +/** + * Manages pending HTTP requests that are being relayed to service clients. + */ +public class RequestManager { + + private static final String TAG = "RequestManager"; + private static final long REQUEST_TIMEOUT_MS = 10000; // 10 seconds - shorter timeout for better responsiveness + + private final Map pendingRequests = new ConcurrentHashMap<>(); + private final ScheduledExecutorService scheduler; + + public RequestManager() { + scheduler = Executors.newSingleThreadScheduledExecutor(); + scheduler.scheduleAtFixedRate(this::cleanupTimedOutRequests, 5, 5, TimeUnit.SECONDS); + } + + /** + * Register a pending request. + */ + public void addPendingRequest(PendingRequest request) { + pendingRequests.put(request.getRequestId(), request); + Log.d(TAG, "Added pending request: " + request.getRequestId()); + } + + /** + * Get and remove a pending request. + */ + public PendingRequest removePendingRequest(String requestId) { + PendingRequest request = pendingRequests.remove(requestId); + if (request != null) { + Log.d(TAG, "Removed pending request: " + requestId); + } + return request; + } + + /** + * Get a pending request without removing it. + */ + public PendingRequest getPendingRequest(String requestId) { + return pendingRequests.get(requestId); + } + + /** + * Fail all pending requests for a specific service (when service disconnects). + */ + public void failRequestsForService(String serviceName) { + Iterator> iterator = pendingRequests.entrySet().iterator(); + int count = 0; + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + PendingRequest request = entry.getValue(); + + if (serviceName.equals(request.getServiceName())) { + iterator.remove(); + count++; + + // Send error response on the correct event loop + if (request.getCtx().channel().isActive()) { + request.getCtx().executor().execute(() -> { + if (request.getCtx().channel().isActive()) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.copiedBuffer("Service disconnected".getBytes()) + ); + response.headers().set("Content-Type", "text/plain"); + response.headers().setInt("Content-Length", 20); + request.getCtx().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + } + + if (count > 0) { + Log.w(TAG, "Failed " + count + " pending requests for disconnected service: " + serviceName); + } + } + + /** + * Clean up timed out requests. + */ + private void cleanupTimedOutRequests() { + Iterator> iterator = pendingRequests.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + PendingRequest request = entry.getValue(); + + if (request.isTimedOut(REQUEST_TIMEOUT_MS)) { + iterator.remove(); + Log.w(TAG, "Request timed out: " + request.getRequestId()); + + // Send timeout response on the correct event loop + if (request.getCtx().channel().isActive()) { + request.getCtx().executor().execute(() -> { + if (request.getCtx().channel().isActive()) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.GATEWAY_TIMEOUT, + Unpooled.copiedBuffer("Request timed out".getBytes()) + ); + request.getCtx().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + } + } + + /** + * Shutdown the request manager. + */ + public void shutdown() { + scheduler.shutdown(); + + // Send error response to all pending requests on the correct event loop + for (PendingRequest request : pendingRequests.values()) { + if (request.getCtx().channel().isActive()) { + request.getCtx().executor().execute(() -> { + if (request.getCtx().channel().isActive()) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, + HttpResponseStatus.SERVICE_UNAVAILABLE, + Unpooled.copiedBuffer("Server shutting down".getBytes()) + ); + request.getCtx().writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + } + pendingRequests.clear(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/ServiceManager.java b/android/app/src/main/java/seven/lab/wstun/server/ServiceManager.java new file mode 100644 index 0000000..bc0acdd --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/ServiceManager.java @@ -0,0 +1,1106 @@ +package seven.lab.wstun.server; + +import android.util.Log; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import seven.lab.wstun.protocol.Message; +import seven.lab.wstun.protocol.ServiceRegistration; + +/** + * Manages registered services, instances, and their WebSocket connections. + * + * Architecture: + * - Service: A type of service (e.g., fileshare, chat) + * - ServiceInstance: A specific room/session with UUID, name, and optional token + * - User: Connected to a specific instance + */ +public class ServiceManager { + + private static final String TAG = "ServiceManager"; + + // Service name -> ServiceEntry (the service provider) + private final Map services = new ConcurrentHashMap<>(); + + // Instance UUID -> ServiceInstance + private final Map instances = new ConcurrentHashMap<>(); + + // Service name -> List of instance UUIDs + private final Map> serviceInstances = new ConcurrentHashMap<>(); + + // Channel -> Service name (for cleanup on disconnect) + private final Map channelToService = new ConcurrentHashMap<>(); + + // Channel -> Instance UUID (for instance owner cleanup) + private final Map channelToInstance = new ConcurrentHashMap<>(); + + // File registry: fileId -> FileInfo (for relay file sharing) + private final Map fileRegistry = new ConcurrentHashMap<>(); + + // Channel -> List of file IDs owned by that channel + private final Map> channelToFiles = new ConcurrentHashMap<>(); + + // Client registry: userId -> ClientInfo (for user clients) + private final Map clients = new ConcurrentHashMap<>(); + + // Channel -> userId (for client cleanup on disconnect) + private final Map channelToClient = new ConcurrentHashMap<>(); + + // Chat users: userId -> ChatUser + private final Map chatUsers = new ConcurrentHashMap<>(); + + private ServiceChangeListener listener; + private RequestManager requestManager; + + /** + * Represents a service instance (room/session). + */ + public static class ServiceInstance { + private final String uuid; + private final String serviceName; + private final String name; + private final String token; + private Channel ownerChannel; + private final long createdAt; + private final List userIds = new ArrayList<>(); + // Grace period for reconnection (10 seconds) + private static final long OWNER_GRACE_PERIOD_MS = 10000; + private long ownerDisconnectedAt = 0; + + public ServiceInstance(String uuid, String serviceName, String name, String token, Channel ownerChannel) { + this.uuid = uuid; + this.serviceName = serviceName; + this.name = name; + this.token = token; + this.ownerChannel = ownerChannel; + this.createdAt = System.currentTimeMillis(); + } + + public String getUuid() { return uuid; } + public String getServiceName() { return serviceName; } + public String getName() { return name; } + public String getToken() { return token; } + public Channel getOwnerChannel() { return ownerChannel; } + public long getCreatedAt() { return createdAt; } + public List getUserIds() { return userIds; } + + public boolean hasToken() { + return token != null && !token.isEmpty(); + } + + public boolean validateToken(String inputToken) { + if (!hasToken()) return true; + return token.equals(inputToken); + } + + public void addUser(String userId) { + if (!userIds.contains(userId)) { + userIds.add(userId); + } + } + + public void removeUser(String userId) { + userIds.remove(userId); + } + + public boolean isOwnerConnected() { + return ownerChannel != null && ownerChannel.isActive(); + } + + /** + * Check if instance is still valid (owner connected or within grace period). + */ + public boolean isValid() { + if (isOwnerConnected()) { + return true; + } + // Allow grace period for owner to reconnect + if (ownerDisconnectedAt > 0) { + return (System.currentTimeMillis() - ownerDisconnectedAt) < OWNER_GRACE_PERIOD_MS; + } + return false; + } + + /** + * Mark owner as disconnected but keep instance alive for grace period. + */ + public void markOwnerDisconnected() { + this.ownerChannel = null; + this.ownerDisconnectedAt = System.currentTimeMillis(); + } + + /** + * Reconnect owner to this instance. + */ + public void reconnectOwner(Channel newOwnerChannel) { + this.ownerChannel = newOwnerChannel; + this.ownerDisconnectedAt = 0; + } + + /** + * Check if owner can be reclaimed (within grace period). + */ + public boolean canReclaim() { + return !isOwnerConnected() && ownerDisconnectedAt > 0 && + (System.currentTimeMillis() - ownerDisconnectedAt) < OWNER_GRACE_PERIOD_MS; + } + + public int getUserCount() { + return userIds.size(); + } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("uuid", uuid); + obj.addProperty("service", serviceName); + obj.addProperty("name", name); + obj.addProperty("hasToken", hasToken()); + obj.addProperty("createdAt", createdAt); + obj.addProperty("userCount", userIds.size()); + obj.addProperty("ownerConnected", isOwnerConnected()); + return obj; + } + } + + /** + * Represents a connected client (user, not service). + */ + public static class ClientInfo { + private final String userId; + private final String clientType; + private final String instanceUuid; + private final Channel channel; + private final long connectedAt; + + public ClientInfo(String userId, String clientType, String instanceUuid, Channel channel) { + this.userId = userId; + this.clientType = clientType; + this.instanceUuid = instanceUuid; + this.channel = channel; + this.connectedAt = System.currentTimeMillis(); + } + + public String getUserId() { return userId; } + public String getClientType() { return clientType; } + public String getInstanceUuid() { return instanceUuid; } + public Channel getChannel() { return channel; } + public long getConnectedAt() { return connectedAt; } + } + + /** + * Represents a chat user. + */ + public static class ChatUser { + private final String userId; + private String name; + private final Channel channel; + private final long joinedAt; + + public ChatUser(String userId, String name, Channel channel) { + this.userId = userId; + this.name = name; + this.channel = channel; + this.joinedAt = System.currentTimeMillis(); + } + + public String getUserId() { return userId; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Channel getChannel() { return channel; } + public long getJoinedAt() { return joinedAt; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("id", userId); + obj.addProperty("name", name); + obj.addProperty("joinedAt", joinedAt); + return obj; + } + } + + /** + * Represents a file registered for relay sharing. + */ + public static class FileInfo { + private final String fileId; + private final String filename; + private final long size; + private final String mimeType; + private final String ownerId; + private final Channel ownerChannel; + + public FileInfo(String fileId, String filename, long size, String mimeType, + String ownerId, Channel ownerChannel) { + this.fileId = fileId; + this.filename = filename; + this.size = size; + this.mimeType = mimeType; + this.ownerId = ownerId; + this.ownerChannel = ownerChannel; + } + + public String getFileId() { return fileId; } + public String getFilename() { return filename; } + public long getSize() { return size; } + public String getMimeType() { return mimeType; } + public String getOwnerId() { return ownerId; } + public Channel getOwnerChannel() { return ownerChannel; } + + public JsonObject toJson() { + JsonObject obj = new JsonObject(); + obj.addProperty("id", fileId); + obj.addProperty("filename", filename); + obj.addProperty("size", size); + obj.addProperty("mimeType", mimeType); + obj.addProperty("ownerId", ownerId); + return obj; + } + } + + public interface ServiceChangeListener { + void onServiceAdded(ServiceEntry service); + void onServiceRemoved(ServiceEntry service); + } + + /** + * Represents a registered service. + */ + public static class ServiceEntry { + private final String name; + private final String type; + private final String description; + private final Channel channel; + private final ServiceRegistration registration; + private final long registeredAt; + private final String authToken; + + public ServiceEntry(ServiceRegistration registration, Channel channel) { + this.name = registration.getName(); + this.type = registration.getType(); + this.description = registration.getDescription(); + this.channel = channel; + this.registration = registration; + this.registeredAt = System.currentTimeMillis(); + this.authToken = registration.getAuthToken(); + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public Channel getChannel() { + return channel; + } + + public ServiceRegistration getRegistration() { + return registration; + } + + public long getRegisteredAt() { + return registeredAt; + } + + public boolean isConnected() { + return channel != null && channel.isActive(); + } + + public String getAuthToken() { + return authToken; + } + + public boolean hasAuth() { + return authToken != null && !authToken.isEmpty(); + } + + /** + * Validate client auth token for this service. + * Returns true if no auth required or token matches. + */ + public boolean validateClientAuth(String token) { + if (!hasAuth()) { + return true; + } + return authToken.equals(token); + } + } + + public void setListener(ServiceChangeListener listener) { + this.listener = listener; + } + + public void setRequestManager(RequestManager requestManager) { + this.requestManager = requestManager; + } + + // Reserved service names that cannot be used + private static final java.util.Set RESERVED_NAMES = new java.util.HashSet<>( + java.util.Arrays.asList( + "debug", "admin", "_api", "ws", "websocket", + "api", "system", "config", "static", "assets", + "libwstun.js" + ) + ); + + /** + * Check if a service name is reserved. + */ + public static boolean isReservedName(String name) { + if (name == null) return true; + return RESERVED_NAMES.contains(name.toLowerCase()); + } + + /** + * Register a new service. + */ + public boolean registerService(ServiceRegistration registration, Channel channel) { + String name = registration.getName(); + + if (name == null || name.isEmpty()) { + Log.w(TAG, "Service registration failed: no name provided"); + return false; + } + + // Check for reserved names + if (isReservedName(name)) { + Log.w(TAG, "Service registration failed: reserved name: " + name); + return false; + } + + // Check if service with same name exists + if (services.containsKey(name)) { + Log.w(TAG, "Service already exists: " + name); + return false; + } + + ServiceEntry entry = new ServiceEntry(registration, channel); + services.put(name, entry); + channelToService.put(channel, name); + + Log.i(TAG, "Service registered: " + name + " (type: " + registration.getType() + ")"); + + if (listener != null) { + listener.onServiceAdded(entry); + } + + return true; + } + + /** + * Unregister a service by name. + */ + public void unregisterService(String name) { + ServiceEntry entry = services.remove(name); + if (entry != null) { + channelToService.remove(entry.getChannel()); + Log.i(TAG, "Service unregistered: " + name); + + if (listener != null) { + listener.onServiceRemoved(entry); + } + } + } + + /** + * Create a new service instance (room/session). + */ + public ServiceInstance createInstance(String serviceName, String name, String token, Channel ownerChannel) { + String uuid = java.util.UUID.randomUUID().toString().substring(0, 8); + + ServiceInstance instance = new ServiceInstance(uuid, serviceName, name, token, ownerChannel); + instances.put(uuid, instance); + channelToInstance.put(ownerChannel, uuid); + + // Track instances per service + serviceInstances.computeIfAbsent(serviceName, k -> new ArrayList<>()).add(uuid); + + Log.i(TAG, "Instance created: " + uuid + " (" + name + ") for service " + serviceName); + return instance; + } + + /** + * Get an instance by UUID. + */ + public ServiceInstance getInstance(String uuid) { + return instances.get(uuid); + } + + /** + * Get all instances for a service. + * Returns instances where owner is connected OR within grace period. + */ + public List getInstancesForService(String serviceName) { + List uuids = serviceInstances.get(serviceName); + if (uuids == null) return new ArrayList<>(); + + List result = new ArrayList<>(); + for (String uuid : uuids) { + ServiceInstance inst = instances.get(uuid); + if (inst != null && inst.isValid()) { + result.add(inst); + } + } + return result; + } + + /** + * Remove an instance. + */ + public void removeInstance(String uuid) { + ServiceInstance instance = instances.remove(uuid); + if (instance != null) { + channelToInstance.remove(instance.getOwnerChannel()); + List uuids = serviceInstances.get(instance.getServiceName()); + if (uuids != null) { + uuids.remove(uuid); + } + Log.i(TAG, "Instance removed: " + uuid); + } + } + + /** + * Destroy an instance (kick all users and close owner connection). + */ + public void destroyInstance(String uuid) { + ServiceInstance instance = instances.get(uuid); + if (instance == null) { + return; + } + + // Kick all users in the instance + for (String userId : new ArrayList<>(instance.getUserIds())) { + kickClient(userId); + } + + // Close owner connection + Channel ownerChannel = instance.getOwnerChannel(); + if (ownerChannel != null && ownerChannel.isActive()) { + Message msg = new Message("instance_destroyed"); + JsonObject payload = new JsonObject(); + payload.addProperty("uuid", uuid); + payload.addProperty("reason", "Instance destroyed by administrator"); + msg.setPayload(payload); + ownerChannel.writeAndFlush(new TextWebSocketFrame(msg.toJson())); + ownerChannel.close(); + } + + // Remove instance + removeInstance(uuid); + Log.i(TAG, "Instance destroyed: " + uuid); + } + + /** + * Close all instances for a service (when disabling/uninstalling). + */ + public void closeInstancesForService(String serviceName) { + List uuids = serviceInstances.get(serviceName); + if (uuids == null || uuids.isEmpty()) { + return; + } + + // Make a copy to avoid concurrent modification + List toRemove = new ArrayList<>(uuids); + + for (String uuid : toRemove) { + ServiceInstance instance = instances.get(uuid); + if (instance != null) { + // Kick all users in the instance + for (String userId : new ArrayList<>(instance.getUserIds())) { + kickClient(userId); + } + + // Close owner connection + Channel ownerChannel = instance.getOwnerChannel(); + if (ownerChannel != null && ownerChannel.isActive()) { + Message msg = new Message("service_disabled"); + JsonObject payload = new JsonObject(); + payload.addProperty("serviceName", serviceName); + payload.addProperty("reason", "Service disabled"); + msg.setPayload(payload); + ownerChannel.writeAndFlush(new TextWebSocketFrame(msg.toJson())); + ownerChannel.close(); + } + + // Remove instance + removeInstance(uuid); + } + } + + Log.i(TAG, "Closed all instances for service: " + serviceName); + } + + /** + * Get count of running instances for a service. + */ + public int getInstanceCountForService(String serviceName) { + return getInstancesForService(serviceName).size(); + } + + /** + * Clean up instance for a disconnected channel. + * Uses grace period to allow owner to reconnect. + */ + private void cleanupInstanceForChannel(Channel channel) { + String uuid = channelToInstance.remove(channel); + if (uuid != null) { + ServiceInstance instance = instances.get(uuid); + if (instance != null) { + // Mark owner as disconnected but keep instance for grace period + instance.markOwnerDisconnected(); + Log.i(TAG, "Instance owner disconnected, grace period started: " + uuid); + + // Schedule cleanup after grace period + new Thread(() -> { + try { + Thread.sleep(ServiceInstance.OWNER_GRACE_PERIOD_MS + 1000); + } catch (InterruptedException e) { + return; + } + // Check if instance still needs cleanup (owner didn't reconnect) + ServiceInstance inst = instances.get(uuid); + if (inst != null && !inst.isOwnerConnected()) { + instances.remove(uuid); + List uuids = serviceInstances.get(inst.getServiceName()); + if (uuids != null) { + uuids.remove(uuid); + } + Log.i(TAG, "Instance cleaned up after grace period: " + uuid); + } + }).start(); + } + } + } + + /** + * Reclaim an instance that lost its owner (within grace period). + * @return true if reclaim successful, false otherwise + */ + public boolean reclaimInstance(String uuid, Channel newOwnerChannel) { + ServiceInstance instance = instances.get(uuid); + if (instance == null) { + return false; + } + + if (!instance.canReclaim()) { + return false; + } + + instance.reconnectOwner(newOwnerChannel); + channelToInstance.put(newOwnerChannel, uuid); + Log.i(TAG, "Instance reclaimed by new owner: " + uuid); + return true; + } + + /** + * Handle channel disconnect - unregister associated service and clean up files. + */ + public void onChannelDisconnect(Channel channel) { + // Clean up files owned by this channel + cleanupFilesForChannel(channel); + + // Clean up client if any + cleanupClientForChannel(channel); + + // Clean up instance if any + cleanupInstanceForChannel(channel); + + String serviceName = channelToService.remove(channel); + if (serviceName != null) { + ServiceEntry entry = services.remove(serviceName); + if (entry != null) { + Log.i(TAG, "Service disconnected: " + serviceName); + + // Fail any pending HTTP requests for this service + if (requestManager != null) { + requestManager.failRequestsForService(serviceName); + } + + if (listener != null) { + listener.onServiceRemoved(entry); + } + } + } + } + + /** + * Get a service by name. + */ + public ServiceEntry getService(String name) { + return services.get(name); + } + + /** + * Get all registered services. + */ + public List getAllServices() { + return new ArrayList<>(services.values()); + } + + /** + * Check if a service exists. + */ + public boolean hasService(String name) { + return services.containsKey(name); + } + + /** + * Get the number of registered services. + */ + public int getServiceCount() { + return services.size(); + } + + /** + * Kick (disconnect) a service. + */ + public void kickService(String name) { + ServiceEntry entry = services.get(name); + if (entry != null && entry.getChannel() != null) { + entry.getChannel().close(); + // The channel close will trigger onChannelDisconnect + } + } + + /** + * Clear all services. + */ + public void clear() { + for (ServiceEntry entry : services.values()) { + if (entry.getChannel() != null) { + entry.getChannel().close(); + } + } + services.clear(); + channelToService.clear(); + fileRegistry.clear(); + channelToFiles.clear(); + } + + // ==================== File Registry Methods ==================== + + /** + * Register a file for relay sharing. + */ + public void registerFile(String fileId, String filename, long size, String mimeType, + String ownerId, Channel ownerChannel) { + FileInfo info = new FileInfo(fileId, filename, size, mimeType, ownerId, ownerChannel); + fileRegistry.put(fileId, info); + + // Track files by channel for cleanup + channelToFiles.computeIfAbsent(ownerChannel, k -> new ArrayList<>()).add(fileId); + + Log.i(TAG, "File registered: " + filename + " (" + fileId + ") by " + ownerId); + + // Broadcast updated file list to all fileshare service clients + broadcastFileList(); + } + + /** + * Unregister a file from relay sharing. + */ + public void unregisterFile(String fileId) { + FileInfo info = fileRegistry.remove(fileId); + if (info != null) { + List files = channelToFiles.get(info.getOwnerChannel()); + if (files != null) { + files.remove(fileId); + } + Log.i(TAG, "File unregistered: " + info.getFilename() + " (" + fileId + ")"); + broadcastFileList(); + } + } + + /** + * Get file info by ID. + */ + public FileInfo getFile(String fileId) { + return fileRegistry.get(fileId); + } + + /** + * Get all registered files. + */ + public List getAllFiles() { + return new ArrayList<>(fileRegistry.values()); + } + + /** + * Handle channel disconnect - clean up files owned by this channel. + */ + private void cleanupFilesForChannel(Channel channel) { + List files = channelToFiles.remove(channel); + if (files != null && !files.isEmpty()) { + for (String fileId : files) { + fileRegistry.remove(fileId); + } + Log.i(TAG, "Cleaned up " + files.size() + " files for disconnected channel"); + broadcastFileList(); + } + } + + /** + * Broadcast updated file list to all connected fileshare clients. + */ + public void broadcastFileList() { + // Build file list JSON + JsonArray filesArray = new JsonArray(); + for (FileInfo file : fileRegistry.values()) { + filesArray.add(file.toJson()); + } + + Message message = new Message(Message.TYPE_FILE_LIST); + JsonObject payload = new JsonObject(); + payload.add("files", filesArray); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all fileshare clients + for (ClientInfo client : clients.values()) { + if ("fileshare".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + + // Also send to any other channels that have registered files + for (Channel channel : channelToFiles.keySet()) { + if (channel.isActive()) { + channel.writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + // ==================== Client Registry Methods ==================== + + /** + * Register a user client (not a service). + */ + public void registerClient(String userId, String clientType, Channel channel) { + registerClient(userId, clientType, null, channel); + } + + /** + * Register a user client to a specific instance. + */ + public void registerClient(String userId, String clientType, String instanceUuid, Channel channel) { + ClientInfo info = new ClientInfo(userId, clientType, instanceUuid, channel); + clients.put(userId, info); + channelToClient.put(channel, userId); + + // Add user to instance if specified + if (instanceUuid != null) { + ServiceInstance instance = instances.get(instanceUuid); + if (instance != null) { + instance.addUser(userId); + } + } + + Log.i(TAG, "Client registered: " + userId + " (type: " + clientType + + (instanceUuid != null ? ", instance: " + instanceUuid : "") + ")"); + + // Notify the instance owner that a client connected + notifyInstanceOwner(instanceUuid, clientType, "client_connected", userId); + } + + /** + * Unregister a client. + */ + public void unregisterClient(String userId) { + ClientInfo info = clients.remove(userId); + if (info != null) { + channelToClient.remove(info.getChannel()); + Log.i(TAG, "Client unregistered: " + userId); + } + } + + /** + * Get a client by userId. + */ + public ClientInfo getClient(String userId) { + return clients.get(userId); + } + + /** + * Get all clients of a specific type. + */ + public List getClientsByType(String clientType) { + List result = new ArrayList<>(); + for (ClientInfo client : clients.values()) { + if (clientType.equals(client.getClientType())) { + result.add(client); + } + } + return result; + } + + /** + * Get a client by their channel. + */ + public ClientInfo getClientByChannel(Channel channel) { + String userId = channelToClient.get(channel); + if (userId != null) { + return clients.get(userId); + } + return null; + } + + /** + * Get all clients in a specific instance. + */ + public List getClientsInInstance(String instanceUuid) { + List result = new ArrayList<>(); + ServiceInstance instance = instances.get(instanceUuid); + if (instance == null) { + return result; + } + + for (String userId : instance.getUserIds()) { + ClientInfo client = clients.get(userId); + if (client != null) { + result.add(client); + } + } + return result; + } + + /** + * Kick a client by userId. + * @return true if client was found and kicked + */ + public boolean kickClient(String userId) { + ClientInfo client = clients.get(userId); + if (client == null) { + return false; + } + + // Send kick message to client + Message kickMsg = new Message("kick"); + JsonObject payload = new JsonObject(); + payload.addProperty("reason", "Kicked by administrator"); + kickMsg.setPayload(payload); + + if (client.getChannel() != null && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(kickMsg.toJson())); + client.getChannel().close(); + } + + // Clean up + clients.remove(userId); + channelToClient.remove(client.getChannel()); + + // Also remove from chat users if applicable + ChatUser chatUser = chatUsers.remove(userId); + if (chatUser != null) { + broadcastChatLeave(userId, chatUser.getName()); + broadcastChatUserList(); + } + + Log.i(TAG, "Kicked client: " + userId); + return true; + } + + /** + * Clean up clients for a disconnected channel. + */ + private void cleanupClientForChannel(Channel channel) { + String userId = channelToClient.remove(channel); + if (userId != null) { + ClientInfo client = clients.remove(userId); + Log.i(TAG, "Cleaned up client for disconnected channel: " + userId); + + // Remove from instance + if (client != null && client.getInstanceUuid() != null) { + ServiceInstance instance = instances.get(client.getInstanceUuid()); + if (instance != null) { + instance.removeUser(userId); + } + notifyInstanceOwner(client.getInstanceUuid(), client.getClientType(), "client_disconnected", userId); + } + + // Also clean up chat user if any + ChatUser chatUser = chatUsers.remove(userId); + if (chatUser != null) { + broadcastChatLeave(userId, chatUser.getName()); + broadcastChatUserList(); + } + } + } + + /** + * Notify an instance owner when a client connects or disconnects. + */ + private void notifyInstanceOwner(String instanceUuid, String serviceName, String eventType, String userId) { + ServiceInstance instance = instanceUuid != null ? instances.get(instanceUuid) : null; + Channel ownerChannel = instance != null ? instance.getOwnerChannel() : null; + + // If no instance, fall back to service channel + if (ownerChannel == null) { + ServiceEntry service = services.get(serviceName); + if (service != null && service.isConnected()) { + ownerChannel = service.getChannel(); + } + } + + if (ownerChannel != null && ownerChannel.isActive()) { + Message message = new Message(eventType); + message.setService(serviceName); + JsonObject payload = new JsonObject(); + payload.addProperty("userId", userId); + if (instanceUuid != null) { + payload.addProperty("instanceUuid", instanceUuid); + } + message.setPayload(payload); + ownerChannel.writeAndFlush(new TextWebSocketFrame(message.toJson())); + } + } + + // ==================== Chat User Methods ==================== + + /** + * Register a chat user. + */ + public void registerChatUser(String userId, String name, Channel channel) { + ChatUser user = new ChatUser(userId, name, channel); + chatUsers.put(userId, user); + Log.i(TAG, "Chat user registered: " + name + " (" + userId + ")"); + } + + /** + * Unregister a chat user. + */ + public void unregisterChatUser(String userId) { + chatUsers.remove(userId); + Log.i(TAG, "Chat user unregistered: " + userId); + } + + /** + * Get chat user name. + */ + public String getChatUserName(String userId) { + ChatUser user = chatUsers.get(userId); + return user != null ? user.getName() : null; + } + + /** + * Broadcast chat user list to all chat clients. + */ + public void broadcastChatUserList() { + JsonArray usersArray = new JsonArray(); + for (ChatUser user : chatUsers.values()) { + usersArray.add(user.toJson()); + } + + Message message = new Message(Message.TYPE_CHAT_USER_LIST); + JsonObject payload = new JsonObject(); + payload.add("users", usersArray); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all chat clients + for (ClientInfo client : clients.values()) { + if ("chat".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + /** + * Broadcast chat join notification. + */ + public void broadcastChatJoin(String userId, String name) { + Message message = new Message(Message.TYPE_CHAT_JOIN); + JsonObject payload = new JsonObject(); + payload.addProperty("userId", userId); + payload.addProperty("name", name); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all chat clients + for (ClientInfo client : clients.values()) { + if ("chat".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + /** + * Broadcast chat leave notification. + */ + public void broadcastChatLeave(String userId, String name) { + Message message = new Message(Message.TYPE_CHAT_LEAVE); + JsonObject payload = new JsonObject(); + payload.addProperty("userId", userId); + payload.addProperty("name", name); + message.setPayload(payload); + + String json = message.toJson(); + + // Send to all chat clients + for (ClientInfo client : clients.values()) { + if ("chat".equals(client.getClientType()) && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } + + /** + * Broadcast chat message to all chat clients. + */ + public void broadcastChatMessage(JsonObject msgPayload, Channel senderChannel) { + Message message = new Message(Message.TYPE_CHAT_MESSAGE); + message.setPayload(msgPayload); + + String json = message.toJson(); + + // Check if there are specific recipients + JsonArray recipients = null; + if (msgPayload.has("recipients") && !msgPayload.get("recipients").isJsonNull()) { + recipients = msgPayload.getAsJsonArray("recipients"); + } + + // Send to all chat clients (or specific recipients) + for (ClientInfo client : clients.values()) { + if (!"chat".equals(client.getClientType()) || !client.getChannel().isActive()) { + continue; + } + + // Don't send back to sender + if (client.getChannel() == senderChannel) { + continue; + } + + // If recipients specified, only send to them + if (recipients != null) { + boolean isRecipient = false; + for (int i = 0; i < recipients.size(); i++) { + if (client.getUserId().equals(recipients.get(i).getAsString())) { + isRecipient = true; + break; + } + } + if (!isRecipient) { + continue; + } + } + + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/SslContextFactory.java b/android/app/src/main/java/seven/lab/wstun/server/SslContextFactory.java new file mode 100644 index 0000000..5b71e5b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/SslContextFactory.java @@ -0,0 +1,146 @@ +package seven.lab.wstun.server; + +import android.content.Context; +import android.util.Log; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.Date; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; + +/** + * Factory for creating SSL context with self-signed certificates. + */ +public class SslContextFactory { + + private static final String TAG = "SslContextFactory"; + private static final String KEYSTORE_FILE = "wstun.keystore"; + private static final String KEYSTORE_PASSWORD = "wstunpass"; + private static final String KEY_ALIAS = "wstun"; + + static { + Security.addProvider(new BouncyCastleProvider()); + } + + /** + * Get or create SSL context with self-signed certificate. + */ + public static SslContext getSslContext(Context context) throws Exception { + File keystoreFile = new File(context.getFilesDir(), KEYSTORE_FILE); + + KeyStore keyStore; + if (!keystoreFile.exists()) { + Log.i(TAG, "Generating new self-signed certificate"); + keyStore = generateSelfSignedCertificate(); + saveKeyStore(keyStore, keystoreFile); + } else { + Log.i(TAG, "Loading existing certificate"); + keyStore = loadKeyStore(keystoreFile); + } + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, KEYSTORE_PASSWORD.toCharArray()); + + return SslContextBuilder.forServer(kmf).build(); + } + + /** + * Generate a new self-signed certificate. + */ + private static KeyStore generateSelfSignedCertificate() throws Exception { + // Generate key pair + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); + keyPairGen.initialize(2048); + KeyPair keyPair = keyPairGen.generateKeyPair(); + + // Certificate validity period + long now = System.currentTimeMillis(); + Date startDate = new Date(now); + Date endDate = new Date(now + 365L * 24 * 60 * 60 * 1000); // 1 year + + // Build certificate + X500Name issuer = new X500Name("CN=WSTun, O=WSTun, L=Local, C=US"); + BigInteger serial = BigInteger.valueOf(now); + + X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder( + issuer, + serial, + startDate, + endDate, + issuer, + keyPair.getPublic() + ); + + ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSA") + .setProvider("BC") + .build(keyPair.getPrivate()); + + X509CertificateHolder certHolder = certBuilder.build(signer); + X509Certificate cert = new JcaX509CertificateConverter() + .setProvider("BC") + .getCertificate(certHolder); + + // Create keystore + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null, null); + keyStore.setKeyEntry( + KEY_ALIAS, + keyPair.getPrivate(), + KEYSTORE_PASSWORD.toCharArray(), + new X509Certificate[]{cert} + ); + + return keyStore; + } + + /** + * Save keystore to file. + */ + private static void saveKeyStore(KeyStore keyStore, File file) throws Exception { + try (FileOutputStream fos = new FileOutputStream(file)) { + keyStore.store(fos, KEYSTORE_PASSWORD.toCharArray()); + } + } + + /** + * Load keystore from file. + */ + private static KeyStore loadKeyStore(File file) throws Exception { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream fis = new FileInputStream(file)) { + keyStore.load(fis, KEYSTORE_PASSWORD.toCharArray()); + } + return keyStore; + } + + /** + * Delete existing certificate to regenerate. + */ + public static void deleteCertificate(Context context) { + File keystoreFile = new File(context.getFilesDir(), KEYSTORE_FILE); + if (keystoreFile.exists()) { + keystoreFile.delete(); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/server/WebSocketHandler.java b/android/app/src/main/java/seven/lab/wstun/server/WebSocketHandler.java new file mode 100644 index 0000000..e83c023 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/server/WebSocketHandler.java @@ -0,0 +1,805 @@ +package seven.lab.wstun.server; + +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; + +import java.util.List; + +import seven.lab.wstun.config.ServerConfig; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import seven.lab.wstun.protocol.HttpRelayResponse; +import seven.lab.wstun.protocol.Message; +import seven.lab.wstun.protocol.ServiceRegistration; + +/** + * Handler for WebSocket connections from service clients. + */ +public class WebSocketHandler extends SimpleChannelInboundHandler { + + private static final String TAG = "WebSocketHandler"; + private static final Gson gson = new Gson(); + + private final ServiceManager serviceManager; + private final RequestManager requestManager; + + public WebSocketHandler(ServiceManager serviceManager, RequestManager requestManager) { + this.serviceManager = serviceManager; + this.requestManager = requestManager; + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception { + if (frame instanceof TextWebSocketFrame) { + handleTextFrame(ctx, (TextWebSocketFrame) frame); + } else if (frame instanceof BinaryWebSocketFrame) { + handleBinaryFrame(ctx, (BinaryWebSocketFrame) frame); + } else if (frame instanceof PingWebSocketFrame) { + ctx.writeAndFlush(new PongWebSocketFrame(frame.content().retain())); + } else if (frame instanceof CloseWebSocketFrame) { + ctx.close(); + } + } + + private void handleTextFrame(ChannelHandlerContext ctx, TextWebSocketFrame frame) { + String text = frame.text(); + + try { + Message message = Message.fromJson(text); + handleMessage(ctx, message); + } catch (Exception e) { + Log.e(TAG, "Failed to parse message", e); + sendError(ctx, "Invalid message format"); + } + } + + private void handleBinaryFrame(ChannelHandlerContext ctx, BinaryWebSocketFrame frame) { + ByteBuf content = frame.content(); + // Binary frames are used for file data streaming + // First byte is message type, rest is data + if (content.readableBytes() < 1) { + return; + } + + byte type = content.readByte(); + byte[] data = new byte[content.readableBytes()]; + content.readBytes(data); + + // Binary data is handled silently for performance + } + + private void handleMessage(ChannelHandlerContext ctx, Message message) { + String type = message.getType(); + + switch (type) { + case Message.TYPE_REGISTER: + handleRegister(ctx, message); + break; + case Message.TYPE_UNREGISTER: + handleUnregister(ctx, message); + break; + case Message.TYPE_CLIENT_REGISTER: + handleClientRegister(ctx, message); + break; + case Message.TYPE_HTTP_RESPONSE: + handleHttpResponse(ctx, message); + break; + case Message.TYPE_HTTP_RESPONSE_START: + handleHttpResponseStart(ctx, message); + break; + case Message.TYPE_HTTP_RESPONSE_CHUNK: + handleHttpResponseChunk(ctx, message); + break; + case Message.TYPE_FILE_REGISTER: + handleFileRegister(ctx, message); + break; + case Message.TYPE_FILE_UNREGISTER: + handleFileUnregister(ctx, message); + break; + case Message.TYPE_CHAT_JOIN: + handleChatJoin(ctx, message); + break; + case Message.TYPE_CHAT_LEAVE: + handleChatLeave(ctx, message); + break; + case Message.TYPE_DATA: + handleData(ctx, message); + break; + case Message.TYPE_KICK_CLIENT: + handleKickClient(ctx, message); + break; + case Message.TYPE_FILE_LIST: + case Message.TYPE_CHAT_USER_LIST: + // These are broadcast messages from service to clients + handleBroadcast(ctx, message); + break; + case Message.TYPE_FILE_REQUEST: + handleFileRequest(ctx, message); + break; + case Message.TYPE_CREATE_INSTANCE: + handleCreateInstance(ctx, message); + break; + case Message.TYPE_JOIN_INSTANCE: + handleJoinInstance(ctx, message); + break; + case Message.TYPE_LIST_INSTANCES: + handleListInstances(ctx, message); + break; + case Message.TYPE_RECLAIM_INSTANCE: + handleReclaimInstance(ctx, message); + break; + // Instance-aware messages for fileshare/chat + case Message.TYPE_USER_JOIN: + case Message.TYPE_USER_LEAVE: + case Message.TYPE_FILE_ADD: + case Message.TYPE_FILE_REMOVE: + case Message.TYPE_REQUEST_STATE: + case Message.TYPE_STATE_RESPONSE: + case Message.TYPE_CHAT_MESSAGE: + handleInstanceMessage(ctx, message); + break; + default: + Log.w(TAG, "Unknown message type: " + type); + } + } + + private void handleRegister(ChannelHandlerContext ctx, Message message) { + try { + ServiceRegistration registration = Message.payloadAs( + message.getPayload(), + ServiceRegistration.class + ); + + boolean success = serviceManager.registerService(registration, ctx.channel()); + + // Send acknowledgment + Message ack = new Message(Message.TYPE_ACK); + ack.setId(message.getId()); + JsonObject payload = new JsonObject(); + payload.addProperty("success", success); + payload.addProperty("message", success ? "Service registered" : "Registration failed"); + ack.setPayload(payload); + + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + } catch (Exception e) { + Log.e(TAG, "Failed to register service", e); + sendError(ctx, "Registration failed: " + e.getMessage()); + } + } + + private void handleUnregister(ChannelHandlerContext ctx, Message message) { + String serviceName = message.getService(); + if (serviceName != null) { + serviceManager.unregisterService(serviceName); + } + + Message ack = new Message(Message.TYPE_ACK); + ack.setId(message.getId()); + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + } + + private void handleHttpResponse(ChannelHandlerContext ctx, Message message) { + try { + HttpRelayResponse response = Message.payloadAs( + message.getPayload(), + HttpRelayResponse.class + ); + + PendingRequest pending = requestManager.removePendingRequest(response.getRequestId()); + if (pending != null) { + HttpHandler.sendRelayResponse(pending.getCtx(), response); + } else { + Log.w(TAG, "No pending request for: " + response.getRequestId()); + } + } catch (Exception e) { + Log.e(TAG, "Failed to handle HTTP response", e); + } + } + + private void handleData(ChannelHandlerContext ctx, Message message) { + // Handle service-specific data messages + String service = message.getService(); + if (service != null) { + ServiceManager.ServiceEntry entry = serviceManager.getService(service); + if (entry != null) { + // Broadcast or route data as needed + Log.d(TAG, "Data message for service: " + service); + } + } + } + + /** + * Handle streaming HTTP response start (headers). + */ + private void handleHttpResponseStart(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String requestId = payload.get("request_id").getAsString(); + int status = payload.get("status").getAsInt(); + JsonObject headers = payload.getAsJsonObject("headers"); + + PendingRequest pending = requestManager.getPendingRequest(requestId); + if (pending != null) { + HttpHandler.startStreamingResponse(pending.getCtx(), requestId, status, headers); + } else { + Log.w(TAG, "No pending request for streaming start: " + requestId); + } + } catch (Exception e) { + Log.e(TAG, "Failed to handle HTTP response start", e); + } + } + + /** + * Handle streaming HTTP response chunk. + */ + private void handleHttpResponseChunk(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String requestId = payload.get("request_id").getAsString(); + boolean done = payload.has("done") && payload.get("done").getAsBoolean(); + + String chunkBase64 = payload.has("chunk_base64") ? + payload.get("chunk_base64").getAsString() : null; + String error = payload.has("error") ? + payload.get("error").getAsString() : null; + + PendingRequest pending = done ? + requestManager.removePendingRequest(requestId) : + requestManager.getPendingRequest(requestId); + + if (pending != null) { + if (error != null) { + HttpHandler.sendStreamingError(pending.getCtx(), error); + } else if (chunkBase64 != null) { + HttpHandler.sendStreamingChunk(pending.getCtx(), chunkBase64); + } + + if (done) { + HttpHandler.endStreamingResponse(pending.getCtx()); + } + } else { + Log.w(TAG, "No pending request for streaming chunk: " + requestId); + } + } catch (Exception e) { + Log.e(TAG, "Failed to handle HTTP response chunk", e); + } + } + + /** + * Handle file registration for relay sharing. + */ + private void handleFileRegister(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String fileId = payload.get("fileId").getAsString(); + String filename = payload.get("filename").getAsString(); + long size = payload.get("size").getAsLong(); + String mimeType = payload.has("mimeType") ? + payload.get("mimeType").getAsString() : "application/octet-stream"; + String ownerId = payload.has("ownerId") ? + payload.get("ownerId").getAsString() : "unknown"; + + serviceManager.registerFile(fileId, filename, size, mimeType, ownerId, ctx.channel()); + } catch (Exception e) { + Log.e(TAG, "Failed to register file", e); + } + } + + /** + * Handle file unregistration. + */ + private void handleFileUnregister(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String fileId = payload.get("fileId").getAsString(); + serviceManager.unregisterFile(fileId); + } catch (Exception e) { + Log.e(TAG, "Failed to unregister file", e); + } + } + + /** + * Handle client registration (for user clients, not services). + * Clients can connect to share files or chat without taking over the service. + */ + private void handleClientRegister(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + // Use service field first, then payload.clientType as fallback + String clientType = message.getService(); + if (clientType == null || clientType.isEmpty()) { + clientType = payload.has("clientType") ? payload.get("clientType").getAsString() : "unknown"; + } + String userId = payload.has("userId") ? payload.get("userId").getAsString() : "u" + System.currentTimeMillis(); + + // Validate service auth token if service requires it + ServiceManager.ServiceEntry service = serviceManager.getService(clientType); + if (service != null && service.hasAuth()) { + String clientToken = payload.has("auth_token") ? payload.get("auth_token").getAsString() : null; + if (!service.validateClientAuth(clientToken)) { + sendError(ctx, "Invalid service auth token"); + return; + } + } + + // Register the client with the service manager + serviceManager.registerClient(userId, clientType, ctx.channel()); + + // Send acknowledgment + Message ack = new Message(Message.TYPE_ACK); + ack.setId(message.getId()); + JsonObject ackPayload = new JsonObject(); + ackPayload.addProperty("success", true); + ackPayload.addProperty("message", "Client registered"); + ackPayload.addProperty("userId", userId); + ack.setPayload(ackPayload); + + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + + // Send current file list if fileshare client + if ("fileshare".equals(clientType)) { + serviceManager.broadcastFileList(); + } + + // Send current user list if chat client + if ("chat".equals(clientType)) { + serviceManager.broadcastChatUserList(); + } + + Log.i(TAG, "Client registered: " + userId + " (type: " + clientType + ")"); + } catch (Exception e) { + Log.e(TAG, "Failed to register client", e); + sendError(ctx, "Client registration failed: " + e.getMessage()); + } + } + + /** + * Handle chat join message. + */ + private void handleChatJoin(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String userId = payload.get("userId").getAsString(); + String name = payload.has("name") ? payload.get("name").getAsString() : "Anonymous"; + + // Register chat user + serviceManager.registerChatUser(userId, name, ctx.channel()); + + // Broadcast to all chat clients + serviceManager.broadcastChatJoin(userId, name); + serviceManager.broadcastChatUserList(); + + Log.i(TAG, "Chat user joined: " + name + " (" + userId + ")"); + } catch (Exception e) { + Log.e(TAG, "Failed to handle chat join", e); + } + } + + /** + * Handle chat leave message. + */ + private void handleChatLeave(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String userId = payload.get("userId").getAsString(); + + String name = serviceManager.getChatUserName(userId); + serviceManager.unregisterChatUser(userId); + + // Broadcast to all chat clients + if (name != null) { + serviceManager.broadcastChatLeave(userId, name); + serviceManager.broadcastChatUserList(); + } + + Log.i(TAG, "Chat user left: " + userId); + } catch (Exception e) { + Log.e(TAG, "Failed to handle chat leave", e); + } + } + + /** + * Handle chat message. + */ + private void handleChatMessage(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + + // Broadcast to all chat clients (or specific recipients if specified) + serviceManager.broadcastChatMessage(payload, ctx.channel()); + + } catch (Exception e) { + Log.e(TAG, "Failed to handle chat message", e); + } + } + + /** + * Handle kick client request from service. + */ + private void handleKickClient(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String userId = payload.get("userId").getAsString(); + serviceManager.kickClient(userId); + } catch (Exception e) { + Log.e(TAG, "Failed to kick client", e); + } + } + + /** + * Handle create instance request from service client. + */ + private void handleCreateInstance(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + if (payload == null) { + payload = new JsonObject(); + } + + String serviceName = message.getService(); + if (serviceName == null || serviceName.isEmpty()) { + sendError(ctx, "Service name is required"); + return; + } + + String name = getStringFromPayload(payload, "name", "Room"); + String token = getStringFromPayload(payload, "token", null); + + // Validate server auth + ServerConfig config = HttpHandler.getServerConfig(); + if (config != null && config.isAuthEnabled()) { + String authToken = getStringFromPayload(payload, "server_token", null); + if (!config.validateServerAuth(authToken)) { + sendError(ctx, "Invalid server auth token"); + return; + } + } + + ServiceManager.ServiceInstance instance = serviceManager.createInstance( + serviceName, name, token, ctx.channel() + ); + + // Send success response + Message ack = new Message(Message.TYPE_INSTANCE_CREATED); + ack.setService(serviceName); + JsonObject ackPayload = new JsonObject(); + ackPayload.addProperty("success", true); + ackPayload.addProperty("uuid", instance.getUuid()); + ackPayload.addProperty("name", instance.getName()); + ackPayload.addProperty("hasToken", instance.hasToken()); + ack.setPayload(ackPayload); + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + + Log.i(TAG, "Instance created: " + instance.getUuid() + " (" + name + ")"); + } catch (Exception e) { + Log.e(TAG, "Failed to create instance", e); + sendError(ctx, "Failed to create instance: " + e.getMessage()); + } + } + + /** + * Handle join instance request from user client. + */ + private void handleJoinInstance(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + if (payload == null) { + sendError(ctx, "Missing payload"); + return; + } + + String serviceName = message.getService(); + String instanceUuid = getStringFromPayload(payload, "uuid", null); + if (instanceUuid == null || instanceUuid.isEmpty()) { + sendError(ctx, "Instance UUID is required"); + return; + } + String userId = getStringFromPayload(payload, "userId", "u" + System.currentTimeMillis()); + String token = getStringFromPayload(payload, "token", null); + + // Find the instance + ServiceManager.ServiceInstance instance = serviceManager.getInstance(instanceUuid); + if (instance == null || !instance.isOwnerConnected()) { + sendError(ctx, "Instance not found or not available"); + return; + } + + // Validate instance token + if (!instance.validateToken(token)) { + sendError(ctx, "Invalid instance token"); + return; + } + + // Register client to instance + serviceManager.registerClient(userId, serviceName, instanceUuid, ctx.channel()); + + // Send success response + Message ack = new Message(Message.TYPE_ACK); + ack.setService(serviceName); + JsonObject ackPayload = new JsonObject(); + ackPayload.addProperty("success", true); + ackPayload.addProperty("message", "Joined instance"); + ackPayload.addProperty("userId", userId); + ackPayload.addProperty("instanceUuid", instanceUuid); + ackPayload.addProperty("instanceName", instance.getName()); + ack.setPayload(ackPayload); + ctx.writeAndFlush(new TextWebSocketFrame(ack.toJson())); + + Log.i(TAG, "Client " + userId + " joined instance " + instanceUuid); + } catch (Exception e) { + Log.e(TAG, "Failed to join instance", e); + sendError(ctx, "Failed to join instance: " + e.getMessage()); + } + } + + /** + * Handle list instances request. + */ + private void handleListInstances(ChannelHandlerContext ctx, Message message) { + try { + String serviceName = message.getService(); + + List instances = serviceManager.getInstancesForService(serviceName); + + Message response = new Message(Message.TYPE_INSTANCE_LIST); + response.setService(serviceName); + JsonObject payload = new JsonObject(); + JsonArray arr = new JsonArray(); + for (ServiceManager.ServiceInstance inst : instances) { + arr.add(inst.toJson()); + } + payload.add("instances", arr); + response.setPayload(payload); + + ctx.writeAndFlush(new TextWebSocketFrame(response.toJson())); + } catch (Exception e) { + Log.e(TAG, "Failed to list instances", e); + sendError(ctx, "Failed to list instances: " + e.getMessage()); + } + } + + /** + * Handle reclaim instance request - allows owner to reconnect to their instance. + */ + private void handleReclaimInstance(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + if (payload == null) { + sendError(ctx, "Missing payload"); + return; + } + + String serviceName = message.getService(); + String uuid = getStringFromPayload(payload, "uuid", null); + String token = getStringFromPayload(payload, "token", null); + + if (uuid == null || uuid.isEmpty()) { + sendError(ctx, "Instance UUID is required"); + return; + } + + // Validate server auth + ServerConfig config = HttpHandler.getServerConfig(); + if (config != null && config.isAuthEnabled()) { + String authToken = getStringFromPayload(payload, "server_token", null); + if (!config.validateServerAuth(authToken)) { + sendError(ctx, "Invalid server auth token"); + return; + } + } + + // Find the instance and validate token + ServiceManager.ServiceInstance instance = serviceManager.getInstance(uuid); + if (instance == null) { + sendError(ctx, "Instance not found"); + return; + } + + // Validate instance token + if (!instance.validateToken(token)) { + sendError(ctx, "Invalid instance token"); + return; + } + + // Try to reclaim + boolean success = serviceManager.reclaimInstance(uuid, ctx.channel()); + + Message response = new Message(Message.TYPE_INSTANCE_RECLAIMED); + response.setService(serviceName); + JsonObject respPayload = new JsonObject(); + respPayload.addProperty("success", success); + if (success) { + respPayload.addProperty("uuid", uuid); + respPayload.addProperty("name", instance.getName()); + respPayload.addProperty("hasToken", instance.hasToken()); + respPayload.addProperty("userCount", instance.getUserCount()); + } else { + respPayload.addProperty("error", "Cannot reclaim instance (owner already connected or grace period expired)"); + } + response.setPayload(respPayload); + + ctx.writeAndFlush(new TextWebSocketFrame(response.toJson())); + + if (success) { + Log.i(TAG, "Instance reclaimed: " + uuid); + } + } catch (Exception e) { + Log.e(TAG, "Failed to reclaim instance", e); + sendError(ctx, "Failed to reclaim instance: " + e.getMessage()); + } + } + + /** + * Handle broadcast messages from service to all clients. + */ + private void handleBroadcast(ChannelHandlerContext ctx, Message message) { + try { + String service = message.getService(); + if (service == null) return; + + String json = message.toJson(); + + // Send to all clients of this service type + for (ServiceManager.ClientInfo client : serviceManager.getClientsByType(service)) { + if (client.getChannel() != null && client.getChannel().isActive() && client.getChannel() != ctx.channel()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + } catch (Exception e) { + Log.e(TAG, "Failed to broadcast message", e); + } + } + + /** + * Handle file request from service to specific client. + */ + private void handleFileRequest(ChannelHandlerContext ctx, Message message) { + try { + JsonObject payload = message.getPayload(); + String requestId = payload.get("requestId").getAsString(); + String fileId = payload.get("fileId").getAsString(); + String ownerId = payload.has("ownerId") ? payload.get("ownerId").getAsString() : null; + + // Find the file owner and send the request + if (ownerId != null) { + ServiceManager.ClientInfo owner = serviceManager.getClient(ownerId); + if (owner != null && owner.getChannel() != null && owner.getChannel().isActive()) { + // Forward the request to the owner + Message request = new Message(Message.TYPE_FILE_REQUEST); + request.setService(message.getService()); + JsonObject reqPayload = new JsonObject(); + reqPayload.addProperty("requestId", requestId); + reqPayload.addProperty("fileId", fileId); + request.setPayload(reqPayload); + owner.getChannel().writeAndFlush(new TextWebSocketFrame(request.toJson())); + return; + } + } + + // Owner not found + Log.w(TAG, "File owner not found: " + ownerId); + } catch (Exception e) { + Log.e(TAG, "Failed to handle file request", e); + } + } + + /** + * Handle instance-specific messages (user_join, file_add, etc.) + * These messages are broadcast to all other users in the same instance. + */ + private void handleInstanceMessage(ChannelHandlerContext ctx, Message message) { + try { + // Find which client/instance this message is from + ServiceManager.ClientInfo sender = serviceManager.getClientByChannel(ctx.channel()); + if (sender == null) { + Log.w(TAG, "Instance message from unknown client"); + return; + } + + String instanceUuid = sender.getInstanceUuid(); + String senderUserId = sender.getUserId(); + + // Broadcast to all other users in the same instance + String json = message.toJson(); + List instanceClients; + + if (instanceUuid != null) { + instanceClients = serviceManager.getClientsInInstance(instanceUuid); + } else { + // Fallback to service-based broadcasting for legacy clients + instanceClients = serviceManager.getClientsByType(sender.getClientType()); + } + + for (ServiceManager.ClientInfo client : instanceClients) { + // Don't send back to sender + if (client.getUserId().equals(senderUserId)) { + continue; + } + + if (client.getChannel() != null && client.getChannel().isActive()) { + client.getChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + + // Also send to the instance owner if any + if (instanceUuid != null) { + ServiceManager.ServiceInstance instance = serviceManager.getInstance(instanceUuid); + if (instance != null && instance.getOwnerChannel() != null && + instance.getOwnerChannel().isActive() && + instance.getOwnerChannel() != ctx.channel()) { + instance.getOwnerChannel().writeAndFlush(new TextWebSocketFrame(json)); + } + } + + Log.d(TAG, "Broadcast instance message: " + message.getType() + " to " + instanceClients.size() + " clients"); + } catch (Exception e) { + Log.e(TAG, "Failed to handle instance message", e); + } + } + + /** + * Safely get a string from JsonObject, handling null and JsonNull. + */ + private String getStringFromPayload(JsonObject payload, String key, String defaultValue) { + if (payload == null || !payload.has(key)) { + return defaultValue; + } + try { + if (payload.get(key).isJsonNull()) { + return defaultValue; + } + return payload.get(key).getAsString(); + } catch (Exception e) { + return defaultValue; + } + } + + private void sendError(ChannelHandlerContext ctx, String error) { + Message errorMsg = new Message(Message.TYPE_ERROR); + JsonObject payload = new JsonObject(); + payload.addProperty("message", error); // Client expects 'message' field + payload.addProperty("error", error); // Also include 'error' for compatibility + errorMsg.setPayload(payload); + ctx.writeAndFlush(new TextWebSocketFrame(errorMsg.toJson())); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + Log.i(TAG, "WebSocket channel disconnected"); + + // Clean up any pending requests for this service + cleanupPendingRequestsForChannel(ctx); + + serviceManager.onChannelDisconnect(ctx.channel()); + super.channelInactive(ctx); + } + + /** + * Clean up pending HTTP requests when a service client disconnects. + * This sends error responses to HTTP clients waiting for relay responses. + */ + private void cleanupPendingRequestsForChannel(ChannelHandlerContext ctx) { + // Find and fail all pending requests that were being handled by this channel + // The RequestManager should have a method to get requests by service + // For now, we rely on the timeout mechanism, but we can improve this + Log.d(TAG, "Cleaning up pending requests for disconnected channel"); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Log.e(TAG, "WebSocket error", cause); + ctx.close(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/service/WSTunService.java b/android/app/src/main/java/seven/lab/wstun/service/WSTunService.java new file mode 100644 index 0000000..6e0a83a --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/service/WSTunService.java @@ -0,0 +1,247 @@ +package seven.lab.wstun.service; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; + +import seven.lab.wstun.R; +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.server.LocalServiceManager; +import seven.lab.wstun.server.NettyServer; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.ui.MainActivity; + +/** + * Foreground service that runs the HTTP/WebSocket server. + */ +public class WSTunService extends Service { + + private static final String TAG = "WSTunService"; + private static final String CHANNEL_ID = "wstun_service"; + private static final int NOTIFICATION_ID = 1; + + public static final String ACTION_START = "seven.lab.wstun.START"; + public static final String ACTION_STOP = "seven.lab.wstun.STOP"; + + private final IBinder binder = new LocalBinder(); + private NettyServer server; + private ServerConfig config; + private ServiceManager serviceManager; + private boolean isRunning = false; + + private ServiceListener listener; + + public interface ServiceListener { + void onServerStarted(); + void onServerStopped(); + void onServiceChanged(); + void onError(String message); + } + + public class LocalBinder extends Binder { + public WSTunService getService() { + return WSTunService.this; + } + } + + @Override + public void onCreate() { + super.onCreate(); + createNotificationChannel(); + config = new ServerConfig(this); + serviceManager = new ServiceManager(); + serviceManager.setListener(new ServiceManager.ServiceChangeListener() { + @Override + public void onServiceAdded(ServiceManager.ServiceEntry service) { + if (listener != null) { + listener.onServiceChanged(); + } + } + + @Override + public void onServiceRemoved(ServiceManager.ServiceEntry service) { + if (listener != null) { + listener.onServiceChanged(); + } + } + }); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent != null) { + String action = intent.getAction(); + if (ACTION_START.equals(action)) { + startServer(); + } else if (ACTION_STOP.equals(action)) { + stopServer(); + stopSelf(); + } + } + return START_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + @Override + public void onDestroy() { + stopServer(); + super.onDestroy(); + } + + /** + * Start the HTTP server. + */ + public void startServer() { + if (isRunning) { + Log.w(TAG, "Server already running"); + return; + } + + try { + server = new NettyServer(this, config, serviceManager); + server.start(); + isRunning = true; + + // Start foreground + startForeground(NOTIFICATION_ID, createNotification()); + + if (listener != null) { + listener.onServerStarted(); + } + + Log.i(TAG, "Server started on port " + config.getPort()); + } catch (Exception e) { + Log.e(TAG, "Failed to start server", e); + if (listener != null) { + listener.onError("Failed to start server: " + e.getMessage()); + } + } + } + + /** + * Stop the HTTP server. + */ + public void stopServer() { + if (!isRunning) { + return; + } + + if (server != null) { + server.stop(); + server = null; + } + + isRunning = false; + stopForeground(true); + + if (listener != null) { + listener.onServerStopped(); + } + + Log.i(TAG, "Server stopped"); + } + + /** + * Check if server is running. + */ + public boolean isServerRunning() { + return isRunning; + } + + /** + * Get server port. + */ + public int getPort() { + return config.getPort(); + } + + /** + * Check if HTTPS is enabled. + */ + public boolean isHttpsEnabled() { + return config.isHttpsEnabled(); + } + + /** + * Get service manager. + */ + public ServiceManager getServiceManager() { + return serviceManager; + } + + /** + * Set service listener. + */ + public void setListener(ServiceListener listener) { + this.listener = listener; + } + + /** + * Kick a service by name. + */ + public void kickService(String serviceName) { + if (serviceManager != null) { + serviceManager.kickService(serviceName); + } + } + + /** + * Get the local service manager. + */ + public LocalServiceManager getLocalServiceManager() { + return server != null ? server.getLocalServiceManager() : null; + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_LOW + ); + channel.setDescription(getString(R.string.notification_channel_desc)); + + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); + } + } + } + + private Notification createNotification() { + Intent intent = new Intent(this, MainActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, intent, PendingIntent.FLAG_IMMUTABLE + ); + + Intent stopIntent = new Intent(this, WSTunService.class); + stopIntent.setAction(ACTION_STOP); + PendingIntent stopPendingIntent = PendingIntent.getService( + this, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE + ); + + String protocol = config.isHttpsEnabled() ? "HTTPS" : "HTTP"; + + return new NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(getString(R.string.notification_title)) + .setContentText(protocol + " server running on port " + config.getPort()) + .setSmallIcon(android.R.drawable.ic_menu_share) + .setContentIntent(pendingIntent) + .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPendingIntent) + .setOngoing(true) + .build(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/ConfigFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/ConfigFragment.java new file mode 100644 index 0000000..639c741 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/ConfigFragment.java @@ -0,0 +1,341 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.textfield.TextInputEditText; + +import seven.lab.wstun.R; +import seven.lab.wstun.config.ServerConfig; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment for server configuration. + */ +public class ConfigFragment extends Fragment { + + private WSTunService service; + private ServerConfig config; + + private TextInputEditText portInput; + private TextInputEditText corsOriginsInput; + private CheckBox httpsCheckbox; + private TextView certInfo; + private Button saveButton; + private CheckBox debugLogsCheckbox; + private TextView debugLogsUrl; + private Button viewLogsButton; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_config, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + config = new ServerConfig(requireContext()); + + portInput = view.findViewById(R.id.portInput); + corsOriginsInput = view.findViewById(R.id.corsOriginsInput); + httpsCheckbox = view.findViewById(R.id.httpsCheckbox); + certInfo = view.findViewById(R.id.certInfo); + saveButton = view.findViewById(R.id.saveButton); + debugLogsCheckbox = view.findViewById(R.id.debugLogsCheckbox); + debugLogsUrl = view.findViewById(R.id.debugLogsUrl); + viewLogsButton = view.findViewById(R.id.viewLogsButton); + + loadConfig(); + + httpsCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + certInfo.setVisibility(isChecked ? View.VISIBLE : View.GONE); + }); + + debugLogsCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + config.setDebugLogsEnabled(isChecked); + updateDebugLogsUrl(); + }); + + saveButton.setOnClickListener(v -> saveConfig()); + viewLogsButton.setOnClickListener(v -> showLogViewer()); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + updateUI(); + } + + private void loadConfig() { + portInput.setText(String.valueOf(config.getPort())); + httpsCheckbox.setChecked(config.isHttpsEnabled()); + certInfo.setVisibility(config.isHttpsEnabled() ? View.VISIBLE : View.GONE); + corsOriginsInput.setText(config.getCorsOrigins()); + debugLogsCheckbox.setChecked(config.isDebugLogsEnabled()); + updateDebugLogsUrl(); + } + + private void updateDebugLogsUrl() { + if (config.isDebugLogsEnabled()) { + String protocol = config.isHttpsEnabled() ? "https" : "http"; + String url = protocol + "://[device-ip]:" + config.getPort() + "/debug/logs"; + debugLogsUrl.setText("Access " + url + " in browser to view live server logs"); + } else { + debugLogsUrl.setText("Enable to access /debug/logs endpoint for live server logs"); + } + } + + private void saveConfig() { + // Validate port + String portStr = portInput.getText().toString().trim(); + int port; + try { + port = Integer.parseInt(portStr); + if (port < 1 || port > 65535) { + Toast.makeText(getContext(), "Port must be between 1 and 65535", Toast.LENGTH_SHORT).show(); + return; + } + } catch (NumberFormatException e) { + Toast.makeText(getContext(), "Invalid port number", Toast.LENGTH_SHORT).show(); + return; + } + + // Check if server is running + if (service != null && service.isServerRunning()) { + Toast.makeText(getContext(), "Stop server before changing configuration", Toast.LENGTH_SHORT).show(); + return; + } + + // Validate CORS origins + String corsOrigins = corsOriginsInput.getText().toString().trim(); + if (corsOrigins.isEmpty()) { + corsOrigins = "*"; + } + + // Save config + config.setPort(port); + config.setHttpsEnabled(httpsCheckbox.isChecked()); + config.setCorsOrigins(corsOrigins); + + Toast.makeText(getContext(), "Configuration saved", Toast.LENGTH_SHORT).show(); + } + + private void updateUI() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + boolean canEdit = service == null || !service.isServerRunning(); + portInput.setEnabled(canEdit); + httpsCheckbox.setEnabled(canEdit); + corsOriginsInput.setEnabled(canEdit); + saveButton.setEnabled(canEdit); + + updateDebugLogsUrl(); + }); + } + + // Log viewer thread and process + private Thread logViewerThread; + private Process logViewerProcess; + private volatile boolean logViewerRunning = false; + + // Track if user has scrolled manually (disable auto-scroll) + private volatile boolean userScrolling = false; + private String logFilter = ""; + + private void showLogViewer() { + // Create a dialog with a scrollable TextView for logs + android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(requireContext()); + builder.setTitle("Live Logs"); + + // Create main layout + android.widget.LinearLayout mainLayout = new android.widget.LinearLayout(requireContext()); + mainLayout.setOrientation(android.widget.LinearLayout.VERTICAL); + mainLayout.setPadding(16, 16, 16, 16); + + // Create filter input + android.widget.LinearLayout filterLayout = new android.widget.LinearLayout(requireContext()); + filterLayout.setOrientation(android.widget.LinearLayout.HORIZONTAL); + filterLayout.setGravity(android.view.Gravity.CENTER_VERTICAL); + + final android.widget.EditText filterInput = new android.widget.EditText(requireContext()); + filterInput.setHint("Filter keywords..."); + filterInput.setSingleLine(true); + filterInput.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + 0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + + final android.widget.CheckBox autoScrollCheckbox = new android.widget.CheckBox(requireContext()); + autoScrollCheckbox.setText("Auto-scroll"); + autoScrollCheckbox.setChecked(true); + + filterLayout.addView(filterInput); + filterLayout.addView(autoScrollCheckbox); + mainLayout.addView(filterLayout); + + // Create a scrollable TextView + final android.widget.ScrollView scrollView = new android.widget.ScrollView(requireContext()); + scrollView.setLayoutParams(new android.widget.LinearLayout.LayoutParams( + android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 600)); + + final TextView logTextView = new TextView(requireContext()); + logTextView.setTypeface(android.graphics.Typeface.MONOSPACE); + logTextView.setTextSize(10); + logTextView.setPadding(8, 8, 8, 8); + logTextView.setText("Starting log capture...\n"); + logTextView.setTextIsSelectable(true); + scrollView.addView(logTextView); + mainLayout.addView(scrollView); + + // Handle filter changes + filterInput.addTextChangedListener(new android.text.TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override + public void afterTextChanged(android.text.Editable s) { + logFilter = s.toString().toLowerCase(); + } + }); + + // Handle auto-scroll toggle + autoScrollCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + userScrolling = !isChecked; + }); + + // Detect manual scroll + scrollView.setOnTouchListener((v, event) -> { + if (event.getAction() == android.view.MotionEvent.ACTION_DOWN || + event.getAction() == android.view.MotionEvent.ACTION_MOVE) { + userScrolling = true; + autoScrollCheckbox.setChecked(false); + } + return false; + }); + + builder.setView(mainLayout); + builder.setNeutralButton("Clear", null); // Will set listener after show() + builder.setNegativeButton("Close", (dialog, which) -> { + stopLogViewer(); + dialog.dismiss(); + }); + + android.app.AlertDialog dialog = builder.create(); + dialog.setOnDismissListener(d -> stopLogViewer()); + dialog.show(); + + // Set clear button to not dismiss dialog + dialog.getButton(android.app.AlertDialog.BUTTON_NEUTRAL).setOnClickListener(v -> { + logTextView.setText(""); + }); + + // Start log capture thread + userScrolling = false; + logFilter = ""; + logViewerRunning = true; + logViewerThread = new Thread(() -> { + java.io.BufferedReader reader = null; + try { + // Clear logcat first and then start capturing + Runtime.getRuntime().exec("logcat -c").waitFor(); + logViewerProcess = Runtime.getRuntime().exec("logcat -v time *:D"); + reader = new java.io.BufferedReader( + new java.io.InputStreamReader(logViewerProcess.getInputStream())); + + String line; + final StringBuilder buffer = new StringBuilder(); + int lineCount = 0; + + while (logViewerRunning && (line = reader.readLine()) != null) { + // Apply filter + String currentFilter = logFilter; + if (currentFilter.isEmpty() || line.toLowerCase().contains(currentFilter)) { + buffer.append(line).append("\n"); + lineCount++; + } + + // Update UI every 5 lines + if (lineCount % 5 == 0 && buffer.length() > 0) { + final String newText = buffer.toString(); + buffer.setLength(0); + + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + // Save scroll position before update if not auto-scrolling + final int scrollY = scrollView.getScrollY(); + + logTextView.append(newText); + + // Limit text length to prevent memory issues + if (logTextView.getText().length() > 50000) { + String text = logTextView.getText().toString(); + logTextView.setText(text.substring(text.length() - 40000)); + } + + // Handle scroll position + if (userScrolling) { + // Restore scroll position when user is reading + scrollView.post(() -> scrollView.scrollTo(0, scrollY)); + } else { + // Auto-scroll to bottom + scrollView.post(() -> scrollView.fullScroll(View.FOCUS_DOWN)); + } + }); + } + } + } + + // Flush remaining buffer + if (buffer.length() > 0 && getActivity() != null) { + final String remaining = buffer.toString(); + getActivity().runOnUiThread(() -> logTextView.append(remaining)); + } + + } catch (Exception e) { + if (getActivity() != null) { + final String error = "Error: " + e.getMessage() + "\n"; + getActivity().runOnUiThread(() -> logTextView.append(error)); + } + } finally { + if (reader != null) { + try { reader.close(); } catch (Exception ignored) {} + } + if (logViewerProcess != null) { + logViewerProcess.destroy(); + } + } + }); + logViewerThread.setName("LogViewer"); + logViewerThread.start(); + } + + private void stopLogViewer() { + logViewerRunning = false; + if (logViewerProcess != null) { + logViewerProcess.destroy(); + logViewerProcess = null; + } + if (logViewerThread != null) { + logViewerThread.interrupt(); + logViewerThread = null; + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + stopLogViewer(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/InstalledServiceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServiceAdapter.java new file mode 100644 index 0000000..9bd8ae5 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServiceAdapter.java @@ -0,0 +1,140 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; + +/** + * Adapter for displaying installed services. + */ +public class InstalledServiceAdapter extends RecyclerView.Adapter { + + private List services = new ArrayList<>(); + private final ServiceActionListener listener; + + public interface ServiceActionListener { + void onEnableDisable(String serviceName, boolean enable); + void onUninstall(String serviceName); + } + + public static class ServiceItem { + public final String name; + public final String displayName; + public final String description; + public final boolean enabled; + public final boolean builtin; + public final int instanceCount; + + public ServiceItem(String name, String displayName, String description, + boolean enabled, boolean builtin, int instanceCount) { + this.name = name; + this.displayName = displayName != null ? displayName : name; + this.description = description; + this.enabled = enabled; + this.builtin = builtin; + this.instanceCount = instanceCount; + } + } + + public InstalledServiceAdapter(ServiceActionListener listener) { + this.listener = listener; + } + + public void setServices(List services) { + this.services = services; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_installed_service, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + ServiceItem item = services.get(position); + holder.bind(item, listener); + } + + @Override + public int getItemCount() { + return services.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView nameText; + private final TextView descText; + private final TextView statusText; + private final TextView instancesText; + private final Button enableButton; + private final Button uninstallButton; + + ViewHolder(View itemView) { + super(itemView); + nameText = itemView.findViewById(R.id.serviceNameText); + descText = itemView.findViewById(R.id.serviceDescText); + statusText = itemView.findViewById(R.id.statusText); + instancesText = itemView.findViewById(R.id.instancesText); + enableButton = itemView.findViewById(R.id.enableButton); + uninstallButton = itemView.findViewById(R.id.uninstallButton); + } + + void bind(ServiceItem item, ServiceActionListener listener) { + nameText.setText(item.displayName); + + if (item.description != null && !item.description.isEmpty()) { + descText.setText(item.description); + descText.setVisibility(View.VISIBLE); + } else { + descText.setVisibility(View.GONE); + } + + if (item.enabled) { + statusText.setText("Enabled"); + statusText.setTextColor(0xFF4CAF50); + enableButton.setText("Disable"); + } else { + statusText.setText("Disabled"); + statusText.setTextColor(0xFF9E9E9E); + enableButton.setText("Enable"); + } + + if (item.instanceCount > 0) { + instancesText.setText(item.instanceCount + " instance(s)"); + instancesText.setVisibility(View.VISIBLE); + } else { + instancesText.setVisibility(View.GONE); + } + + enableButton.setOnClickListener(v -> { + if (listener != null) { + listener.onEnableDisable(item.name, !item.enabled); + } + }); + + if (item.builtin) { + uninstallButton.setVisibility(View.GONE); + } else { + uninstallButton.setVisibility(View.VISIBLE); + uninstallButton.setOnClickListener(v -> { + if (listener != null) { + listener.onUninstall(item.name); + } + }); + } + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/InstalledServicesFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServicesFragment.java new file mode 100644 index 0000000..4566fce --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/InstalledServicesFragment.java @@ -0,0 +1,182 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.textfield.TextInputEditText; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import seven.lab.wstun.R; +import seven.lab.wstun.marketplace.InstalledService; +import seven.lab.wstun.server.LocalServiceManager; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment showing installed services. + */ +public class InstalledServicesFragment extends Fragment { + + private WSTunService service; + + private RecyclerView servicesRecyclerView; + private TextView emptyText; + private TextInputEditText filterInput; + + private InstalledServiceAdapter adapter; + private List allItems = new ArrayList<>(); + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_installed_services, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + servicesRecyclerView = view.findViewById(R.id.servicesRecyclerView); + emptyText = view.findViewById(R.id.emptyText); + filterInput = view.findViewById(R.id.filterInput); + + servicesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new InstalledServiceAdapter(new InstalledServiceAdapter.ServiceActionListener() { + @Override + public void onEnableDisable(String serviceName, boolean enable) { + if (service == null || service.getLocalServiceManager() == null) return; + + if (enable) { + service.getLocalServiceManager().enableService(serviceName); + } else { + service.getLocalServiceManager().disableService(serviceName, service.getServiceManager()); + } + refreshList(); + } + + @Override + public void onUninstall(String serviceName) { + if (service == null || service.getLocalServiceManager() == null) return; + + InstalledService svc = service.getLocalServiceManager().getInstalledService(serviceName); + if (svc != null && "builtin".equals(svc.getSource())) { + Toast.makeText(getContext(), "Cannot uninstall built-in service", Toast.LENGTH_SHORT).show(); + return; + } + + service.getLocalServiceManager().uninstallService(serviceName, service.getServiceManager()); + refreshList(); + Toast.makeText(getContext(), "Service uninstalled", Toast.LENGTH_SHORT).show(); + } + }); + servicesRecyclerView.setAdapter(adapter); + + filterInput.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override public void onTextChanged(CharSequence s, int start, int before, int count) {} + @Override + public void afterTextChanged(Editable s) { + filterServices(s.toString()); + } + }); + + refreshList(); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + refreshList(); + } + + @Override + public void onResume() { + super.onResume(); + refreshList(); + } + + private void filterServices(String query) { + if (query == null || query.isEmpty()) { + adapter.setServices(allItems); + } else { + String lowerQuery = query.toLowerCase(); + List filtered = new ArrayList<>(); + for (InstalledServiceAdapter.ServiceItem item : allItems) { + if (item.displayName.toLowerCase().contains(lowerQuery) || + item.name.toLowerCase().contains(lowerQuery)) { + filtered.add(item); + } + } + adapter.setServices(filtered); + } + updateEmptyState(); + } + + private void updateEmptyState() { + if (adapter.getItemCount() == 0) { + emptyText.setVisibility(View.VISIBLE); + servicesRecyclerView.setVisibility(View.GONE); + } else { + emptyText.setVisibility(View.GONE); + servicesRecyclerView.setVisibility(View.VISIBLE); + } + } + + private void refreshList() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + if (service == null || service.getLocalServiceManager() == null) { + allItems.clear(); + adapter.setServices(allItems); + emptyText.setVisibility(View.VISIBLE); + servicesRecyclerView.setVisibility(View.GONE); + return; + } + + LocalServiceManager lsm = service.getLocalServiceManager(); + ServiceManager sm = service.getServiceManager(); + + allItems.clear(); + Map installed = lsm.getInstalledServices(); + + for (Map.Entry entry : installed.entrySet()) { + String name = entry.getKey(); + InstalledService svc = entry.getValue(); + + int instanceCount = 0; + if (sm != null) { + instanceCount = sm.getInstanceCountForService(name); + } + + allItems.add(new InstalledServiceAdapter.ServiceItem( + name, + svc.getDisplayName(), + svc.getManifest() != null ? svc.getManifest().getDescription() : null, + svc.isEnabled(), + "builtin".equals(svc.getSource()), + instanceCount + )); + } + + // Apply current filter + String filter = filterInput.getText() != null ? filterInput.getText().toString() : ""; + filterServices(filter); + }); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/InstanceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/InstanceAdapter.java new file mode 100644 index 0000000..8119a31 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/InstanceAdapter.java @@ -0,0 +1,107 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; + +/** + * Adapter for displaying running instances (rooms). + */ +public class InstanceAdapter extends RecyclerView.Adapter { + + private List instances = new ArrayList<>(); + private final InstanceActionListener listener; + + public interface InstanceActionListener { + void onDestroy(InstanceItem instance); + } + + public static class InstanceItem { + public final String uuid; + public final String name; + public final String serviceName; + public final String serviceDisplayName; + public final int userCount; + + public InstanceItem(String uuid, String name, String serviceName, + String serviceDisplayName, int userCount) { + this.uuid = uuid; + this.name = name; + this.serviceName = serviceName; + this.serviceDisplayName = serviceDisplayName; + this.userCount = userCount; + } + + public String getUuid() { + return uuid; + } + } + + public InstanceAdapter(InstanceActionListener listener) { + this.listener = listener; + } + + public void setInstances(List instances) { + this.instances = instances; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_instance, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + InstanceItem item = instances.get(position); + holder.bind(item, listener); + } + + @Override + public int getItemCount() { + return instances.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView nameText; + private final TextView serviceText; + private final TextView usersText; + private final TextView uuidText; + private final Button destroyButton; + + ViewHolder(View itemView) { + super(itemView); + nameText = itemView.findViewById(R.id.instanceNameText); + serviceText = itemView.findViewById(R.id.serviceNameText); + usersText = itemView.findViewById(R.id.usersText); + uuidText = itemView.findViewById(R.id.uuidText); + destroyButton = itemView.findViewById(R.id.destroyButton); + } + + void bind(InstanceItem item, InstanceActionListener listener) { + nameText.setText(item.name); + serviceText.setText(item.serviceDisplayName); + usersText.setText(item.userCount + " user(s)"); + uuidText.setText(item.uuid); + + destroyButton.setOnClickListener(v -> { + if (listener != null) { + listener.onDestroy(item); + } + }); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/MainActivity.java b/android/app/src/main/java/seven/lab/wstun/ui/MainActivity.java new file mode 100644 index 0000000..ce5bd40 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/MainActivity.java @@ -0,0 +1,142 @@ +package seven.lab.wstun.ui; + +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import seven.lab.wstun.R; +import seven.lab.wstun.service.WSTunService; + +/** + * Main activity with tabs for server status and configuration. + * The Status tab contains 3 sub-tabs: Running Instances, Installed Services, Marketplace. + */ +public class MainActivity extends AppCompatActivity { + + private static final int PERMISSION_REQUEST_CODE = 100; + + private WSTunService service; + private boolean bound = false; + + private TabLayout tabLayout; + private ViewPager2 viewPager; + + private StatusFragment statusFragment; + private ConfigFragment configFragment; + + private final ServiceConnection connection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + WSTunService.LocalBinder localBinder = (WSTunService.LocalBinder) binder; + service = localBinder.getService(); + bound = true; + + // Notify fragments + if (statusFragment != null) { + statusFragment.onServiceConnected(service); + } + if (configFragment != null) { + configFragment.onServiceConnected(service); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + bound = false; + service = null; + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + + requestPermissions(); + initViews(); + bindService(); + } + + @Override + protected void onDestroy() { + if (bound) { + unbindService(connection); + bound = false; + } + super.onDestroy(); + } + + private void requestPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) + != PackageManager.PERMISSION_GRANTED) { + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.POST_NOTIFICATIONS}, + PERMISSION_REQUEST_CODE); + } + } + } + + private void initViews() { + tabLayout = findViewById(R.id.tabLayout); + viewPager = findViewById(R.id.viewPager); + + statusFragment = new StatusFragment(); + configFragment = new ConfigFragment(); + + viewPager.setAdapter(new FragmentStateAdapter(this) { + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return statusFragment; + } else { + return configFragment; + } + } + + @Override + public int getItemCount() { + return 2; + } + }); + + new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { + if (position == 0) { + tab.setText(R.string.tab_status); + } else { + tab.setText(R.string.tab_config); + } + }).attach(); + } + + private void bindService() { + Intent intent = new Intent(this, WSTunService.class); + bindService(intent, connection, Context.BIND_AUTO_CREATE); + } + + public WSTunService getWSTunService() { + return service; + } + + public boolean isServiceBound() { + return bound; + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceAdapter.java new file mode 100644 index 0000000..90222b7 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceAdapter.java @@ -0,0 +1,114 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; + +/** + * Adapter for displaying marketplace services. + */ +public class MarketplaceAdapter extends RecyclerView.Adapter { + + private List services = new ArrayList<>(); + private final InstallListener listener; + + public interface InstallListener { + void onInstall(String serviceName); + } + + public static class MarketplaceItem { + public final String name; + public final String displayName; + public final String description; + public final String version; + public final boolean installed; + + public MarketplaceItem(String name, String displayName, String description, + String version, boolean installed) { + this.name = name; + this.displayName = displayName != null ? displayName : name; + this.description = description; + this.version = version; + this.installed = installed; + } + } + + public MarketplaceAdapter(InstallListener listener) { + this.listener = listener; + } + + public void setServices(List services) { + this.services = services; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_marketplace_service, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + MarketplaceItem item = services.get(position); + holder.bind(item, listener); + } + + @Override + public int getItemCount() { + return services.size(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final TextView nameText; + private final TextView descText; + private final TextView versionText; + private final Button installButton; + + ViewHolder(View itemView) { + super(itemView); + nameText = itemView.findViewById(R.id.serviceNameText); + descText = itemView.findViewById(R.id.serviceDescText); + versionText = itemView.findViewById(R.id.versionText); + installButton = itemView.findViewById(R.id.installButton); + } + + void bind(MarketplaceItem item, InstallListener listener) { + nameText.setText(item.displayName); + + if (item.description != null && !item.description.isEmpty()) { + descText.setText(item.description); + descText.setVisibility(View.VISIBLE); + } else { + descText.setVisibility(View.GONE); + } + + versionText.setText("v" + item.version); + + if (item.installed) { + installButton.setText("Installed"); + installButton.setEnabled(false); + } else { + installButton.setText("Install"); + installButton.setEnabled(true); + installButton.setOnClickListener(v -> { + if (listener != null) { + listener.onInstall(item.name); + } + }); + } + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceFragment.java new file mode 100644 index 0000000..702475b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/MarketplaceFragment.java @@ -0,0 +1,205 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.textfield.TextInputEditText; +import com.google.gson.JsonObject; + +import java.util.ArrayList; +import java.util.List; + +import seven.lab.wstun.R; +import seven.lab.wstun.marketplace.MarketplaceService; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment for marketplace service browsing and installation. + */ +public class MarketplaceFragment extends Fragment { + + private WSTunService service; + + private TextInputEditText urlInput; + private Button fetchButton; + private ProgressBar progressBar; + private RecyclerView servicesRecyclerView; + private TextView emptyText; + private TextView errorText; + + private MarketplaceAdapter adapter; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_marketplace, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + urlInput = view.findViewById(R.id.marketplaceUrlInput); + fetchButton = view.findViewById(R.id.fetchButton); + progressBar = view.findViewById(R.id.progressBar); + servicesRecyclerView = view.findViewById(R.id.servicesRecyclerView); + emptyText = view.findViewById(R.id.emptyText); + errorText = view.findViewById(R.id.errorText); + + servicesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new MarketplaceAdapter(this::installService); + servicesRecyclerView.setAdapter(adapter); + + fetchButton.setOnClickListener(v -> fetchMarketplace()); + + // Load saved marketplace URL + if (service != null && service.getLocalServiceManager() != null) { + String savedUrl = service.getLocalServiceManager().getMarketplaceService().getMarketplaceUrl(); + if (savedUrl != null && !savedUrl.isEmpty()) { + urlInput.setText(savedUrl); + } + } + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + + if (urlInput != null && service.getLocalServiceManager() != null) { + String savedUrl = service.getLocalServiceManager().getMarketplaceService().getMarketplaceUrl(); + if (savedUrl != null && !savedUrl.isEmpty()) { + urlInput.setText(savedUrl); + } + } + } + + private void fetchMarketplace() { + if (service == null || service.getLocalServiceManager() == null) { + showError("Service not connected"); + return; + } + + String url = urlInput.getText() != null ? urlInput.getText().toString().trim() : ""; + if (url.isEmpty()) { + showError("Please enter a marketplace URL"); + return; + } + + hideError(); + showLoading(true); + + MarketplaceService marketplace = service.getLocalServiceManager().getMarketplaceService(); + marketplace.setMarketplaceUrl(url); + + marketplace.listMarketplace(url, new MarketplaceService.MarketplaceCallback>() { + @Override + public void onSuccess(List result) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + displayServices(result); + }); + } + + @Override + public void onError(String error) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + showError("Failed to fetch: " + error); + adapter.setServices(new ArrayList<>()); + }); + } + }); + } + + private void displayServices(List services) { + List items = new ArrayList<>(); + MarketplaceService marketplace = service.getLocalServiceManager().getMarketplaceService(); + + for (JsonObject svc : services) { + String name = svc.has("name") ? svc.get("name").getAsString() : null; + if (name == null) continue; + + String displayName = svc.has("displayName") ? svc.get("displayName").getAsString() : name; + String description = svc.has("description") ? svc.get("description").getAsString() : null; + String version = svc.has("version") ? svc.get("version").getAsString() : "1.0.0"; + boolean installed = marketplace.isInstalled(name); + + items.add(new MarketplaceAdapter.MarketplaceItem(name, displayName, description, version, installed)); + } + + adapter.setServices(items); + + if (items.isEmpty()) { + emptyText.setVisibility(View.VISIBLE); + servicesRecyclerView.setVisibility(View.GONE); + } else { + emptyText.setVisibility(View.GONE); + servicesRecyclerView.setVisibility(View.VISIBLE); + } + } + + private void installService(String serviceName) { + if (service == null || service.getLocalServiceManager() == null) { + showError("Service not connected"); + return; + } + + String url = urlInput.getText() != null ? urlInput.getText().toString().trim() : ""; + if (url.isEmpty()) { + showError("No marketplace URL"); + return; + } + + showLoading(true); + + MarketplaceService marketplace = service.getLocalServiceManager().getMarketplaceService(); + marketplace.installService(url, serviceName, new MarketplaceService.MarketplaceCallback() { + @Override + public void onSuccess(seven.lab.wstun.marketplace.InstalledService result) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + Toast.makeText(getContext(), "Service installed successfully", Toast.LENGTH_SHORT).show(); + fetchMarketplace(); // Refresh list + }); + } + + @Override + public void onError(String error) { + if (getActivity() == null) return; + getActivity().runOnUiThread(() -> { + showLoading(false); + showError("Install failed: " + error); + }); + } + }); + } + + private void showLoading(boolean show) { + progressBar.setVisibility(show ? View.VISIBLE : View.GONE); + fetchButton.setEnabled(!show); + } + + private void showError(String message) { + errorText.setText(message); + errorText.setVisibility(View.VISIBLE); + } + + private void hideError() { + errorText.setVisibility(View.GONE); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/RunningInstancesFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/RunningInstancesFragment.java new file mode 100644 index 0000000..d6ad09b --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/RunningInstancesFragment.java @@ -0,0 +1,126 @@ +package seven.lab.wstun.ui; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import seven.lab.wstun.R; +import seven.lab.wstun.marketplace.InstalledService; +import seven.lab.wstun.server.LocalServiceManager; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment showing running service instances (rooms). + */ +public class RunningInstancesFragment extends Fragment { + + private WSTunService service; + + private RecyclerView instancesRecyclerView; + private TextView emptyText; + + private InstanceAdapter adapter; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_running_instances, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + instancesRecyclerView = view.findViewById(R.id.servicesRecyclerView); + emptyText = view.findViewById(R.id.emptyText); + + instancesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new InstanceAdapter(instance -> { + if (service != null && service.getServiceManager() != null) { + service.getServiceManager().destroyInstance(instance.getUuid()); + updateInstanceList(); + } + }); + instancesRecyclerView.setAdapter(adapter); + + updateInstanceList(); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + updateInstanceList(); + } + + @Override + public void onResume() { + super.onResume(); + updateInstanceList(); + } + + public void updateInstanceList() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + if (service == null || service.getServiceManager() == null) { + adapter.setInstances(new ArrayList<>()); + emptyText.setVisibility(View.VISIBLE); + instancesRecyclerView.setVisibility(View.GONE); + return; + } + + LocalServiceManager lsm = service.getLocalServiceManager(); + ServiceManager sm = service.getServiceManager(); + + // Collect all instances across all services + List items = new ArrayList<>(); + + if (lsm != null) { + Map installed = lsm.getInstalledServices(); + for (String serviceName : installed.keySet()) { + List instances = sm.getInstancesForService(serviceName); + InstalledService svc = installed.get(serviceName); + String displayName = svc != null ? svc.getDisplayName() : serviceName; + + for (ServiceManager.ServiceInstance inst : instances) { + items.add(new InstanceAdapter.InstanceItem( + inst.getUuid(), + inst.getName(), + serviceName, + displayName, + inst.getUserCount() + )); + } + } + } + + adapter.setInstances(items); + + if (items.isEmpty()) { + emptyText.setVisibility(View.VISIBLE); + instancesRecyclerView.setVisibility(View.GONE); + } else { + emptyText.setVisibility(View.GONE); + instancesRecyclerView.setVisibility(View.VISIBLE); + } + }); + } + + // Renamed method to match StatusFragment expectations + public void updateServiceList() { + updateInstanceList(); + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/ServiceAdapter.java b/android/app/src/main/java/seven/lab/wstun/ui/ServiceAdapter.java new file mode 100644 index 0000000..3c94506 --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/ServiceAdapter.java @@ -0,0 +1,89 @@ +package seven.lab.wstun.ui; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import seven.lab.wstun.R; +import seven.lab.wstun.server.ServiceManager; + +/** + * RecyclerView adapter for displaying registered services. + */ +public class ServiceAdapter extends RecyclerView.Adapter { + + public interface KickCallback { + void onKick(ServiceManager.ServiceEntry service); + } + + private List services = new ArrayList<>(); + private final KickCallback callback; + private final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); + + public ServiceAdapter(KickCallback callback) { + this.callback = callback; + } + + public void setServices(List services) { + this.services = new ArrayList<>(services); + notifyDataSetChanged(); + } + + @NonNull + @Override + public ServiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_service, parent, false); + return new ServiceViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ServiceViewHolder holder, int position) { + ServiceManager.ServiceEntry service = services.get(position); + holder.bind(service); + } + + @Override + public int getItemCount() { + return services.size(); + } + + class ServiceViewHolder extends RecyclerView.ViewHolder { + private final TextView serviceName; + private final TextView serviceType; + private final TextView serviceEndpoint; + private final Button kickButton; + + ServiceViewHolder(View itemView) { + super(itemView); + serviceName = itemView.findViewById(R.id.serviceName); + serviceType = itemView.findViewById(R.id.serviceType); + serviceEndpoint = itemView.findViewById(R.id.serviceEndpoint); + kickButton = itemView.findViewById(R.id.kickButton); + } + + void bind(ServiceManager.ServiceEntry service) { + serviceName.setText(service.getName()); + serviceType.setText(service.getType() + " - Connected at " + + dateFormat.format(new Date(service.getRegisteredAt()))); + serviceEndpoint.setText("/" + service.getName() + "/main"); + + kickButton.setOnClickListener(v -> { + if (callback != null) { + callback.onKick(service); + } + }); + } + } +} diff --git a/android/app/src/main/java/seven/lab/wstun/ui/StatusFragment.java b/android/app/src/main/java/seven/lab/wstun/ui/StatusFragment.java new file mode 100644 index 0000000..467137e --- /dev/null +++ b/android/app/src/main/java/seven/lab/wstun/ui/StatusFragment.java @@ -0,0 +1,247 @@ +package seven.lab.wstun.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Collections; +import java.util.List; + +import seven.lab.wstun.R; +import seven.lab.wstun.server.ServiceManager; +import seven.lab.wstun.service.WSTunService; + +/** + * Fragment showing server status with nested tabs for: + * - Running Instances + * - Installed Services + * - Marketplace + */ +public class StatusFragment extends Fragment implements WSTunService.ServiceListener { + + private WSTunService service; + + private TextView statusText; + private TextView addressText; + private Button toggleButton; + + // Nested tabs + private TabLayout innerTabLayout; + private ViewPager2 innerViewPager; + + // Child fragments + private RunningInstancesFragment runningInstancesFragment; + private InstalledServicesFragment installedServicesFragment; + private MarketplaceFragment marketplaceFragment; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_status, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + + statusText = view.findViewById(R.id.statusText); + addressText = view.findViewById(R.id.addressText); + toggleButton = view.findViewById(R.id.toggleButton); + innerTabLayout = view.findViewById(R.id.innerTabLayout); + innerViewPager = view.findViewById(R.id.innerViewPager); + + // Set up nested tabs + setupInnerTabs(); + + toggleButton.setOnClickListener(v -> toggleServer()); + + updateUI(); + } + + private void setupInnerTabs() { + runningInstancesFragment = new RunningInstancesFragment(); + installedServicesFragment = new InstalledServicesFragment(); + marketplaceFragment = new MarketplaceFragment(); + + innerViewPager.setAdapter(new FragmentStateAdapter(this) { + @NonNull + @Override + public Fragment createFragment(int position) { + switch (position) { + case 0: + return runningInstancesFragment; + case 1: + return installedServicesFragment; + case 2: + return marketplaceFragment; + default: + return runningInstancesFragment; + } + } + + @Override + public int getItemCount() { + return 3; + } + }); + + new TabLayoutMediator(innerTabLayout, innerViewPager, (tab, position) -> { + switch (position) { + case 0: + tab.setText(R.string.tab_instances); + break; + case 1: + tab.setText(R.string.tab_services); + break; + case 2: + tab.setText(R.string.tab_marketplace); + break; + } + }).attach(); + } + + public void onServiceConnected(WSTunService service) { + this.service = service; + service.setListener(this); + updateUI(); + + // Pass service to child fragments + if (runningInstancesFragment != null) { + runningInstancesFragment.onServiceConnected(service); + } + if (installedServicesFragment != null) { + installedServicesFragment.onServiceConnected(service); + } + if (marketplaceFragment != null) { + marketplaceFragment.onServiceConnected(service); + } + } + + @Override + public void onResume() { + super.onResume(); + updateUI(); + // Refresh all child fragments when returning to the app + if (runningInstancesFragment != null) { + runningInstancesFragment.updateInstanceList(); + } + if (installedServicesFragment != null) { + installedServicesFragment.onResume(); + } + } + + private void toggleServer() { + if (service == null) return; + + if (service.isServerRunning()) { + Intent intent = new Intent(getContext(), WSTunService.class); + intent.setAction(WSTunService.ACTION_STOP); + requireContext().startService(intent); + } else { + Intent intent = new Intent(getContext(), WSTunService.class); + intent.setAction(WSTunService.ACTION_START); + requireContext().startForegroundService(intent); + } + } + + private void updateUI() { + if (getActivity() == null || !isAdded()) return; + + requireActivity().runOnUiThread(() -> { + boolean running = service != null && service.isServerRunning(); + + if (running) { + statusText.setText(R.string.server_running); + statusText.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); + toggleButton.setText(R.string.stop_server); + + String protocol = service.isHttpsEnabled() ? "https" : "http"; + String ip = getLocalIpAddress(); + String address = protocol + "://" + ip + ":" + service.getPort(); + addressText.setText(address); + addressText.setVisibility(View.VISIBLE); + } else { + statusText.setText(R.string.server_stopped); + statusText.setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + toggleButton.setText(R.string.start_server); + addressText.setVisibility(View.GONE); + } + }); + } + + private String getLocalIpAddress() { + try { + List interfaces = Collections.list(NetworkInterface.getNetworkInterfaces()); + for (NetworkInterface intf : interfaces) { + List addrs = Collections.list(intf.getInetAddresses()); + for (InetAddress addr : addrs) { + if (!addr.isLoopbackAddress()) { + String sAddr = addr.getHostAddress(); + if (sAddr != null && !sAddr.contains(":")) { + return sAddr; + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return "localhost"; + } + + @Override + public void onServerStarted() { + updateUI(); + // Pass service to child fragments now that server is ready + if (runningInstancesFragment != null) { + runningInstancesFragment.onServiceConnected(service); + } + if (installedServicesFragment != null) { + installedServicesFragment.onServiceConnected(service); + } + if (marketplaceFragment != null) { + marketplaceFragment.onServiceConnected(service); + } + } + + @Override + public void onServerStopped() { + updateUI(); + if (runningInstancesFragment != null) { + runningInstancesFragment.updateServiceList(); + } + } + + @Override + public void onServiceChanged() { + if (getActivity() != null && runningInstancesFragment != null) { + requireActivity().runOnUiThread(() -> runningInstancesFragment.updateServiceList()); + } + } + + @Override + public void onError(String message) { + if (getActivity() != null) { + requireActivity().runOnUiThread(() -> + Toast.makeText(getContext(), message, Toast.LENGTH_LONG).show() + ); + } + } +} diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100755 index 1cb7aa2..0000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/drawable/badge_background.xml b/android/app/src/main/res/drawable/badge_background.xml new file mode 100644 index 0000000..0b7dcf7 --- /dev/null +++ b/android/app/src/main/res/drawable/badge_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100755 index 8403758..0000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..ee0e042 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/android/app/src/main/res/layout/fragment_config.xml b/android/app/src/main/res/layout/fragment_config.xml new file mode 100644 index 0000000..71ea366 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_config.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + +``` + +### User Client (main.html) + +```html + + + + FileShare + + +

FileShare

+
Connecting...
+ +
+ + + + + +``` + +## Message Types + +### Standard Messages + +| Type | Direction | Description | +|------|-----------|-------------| +| `create_instance` | Host → Server | Create instance | +| `instance_created` | Server → Host | Instance created | +| `join_instance` | Client → Server | Join instance | +| `list_instances` | Client → Server | List instances | +| `instance_list` | Server → Client | Instance list | +| `kick_client` | Host → Server | Kick user | +| `kick` | Server → Client | You were kicked | +| `ack` | Server → Client | Operation acknowledged | +| `error` | Server → Client | Error occurred | +| `client_connected` | Server → Host | User joined | +| `client_disconnected` | Server → Host | User left | + +### Custom Messages + +Services can define their own message types. Just use `send()` or `broadcast()`: + +```javascript +// From user client +client.send('my_custom_type', { data: 'value' }); + +// From host to all users +host.broadcast('my_custom_type', { data: 'value' }); +``` + +## Error Handling + +Both host and client support error callbacks: + +```javascript +const client = WSTun.createInstanceClient({ + // ... + onError: (err) => { + console.error('Connection error:', err.message); + // Possible errors: + // - Invalid or missing server auth token + // - Invalid instance token + // - Instance not found + // - Connection failed + } +}); +``` + +## Tips + +1. **Generate unique user IDs**: Use `WSTun.generateId()` or your own UUID generator +2. **Handle disconnections**: Users may disconnect at any time +3. **Validate data**: Don't trust data from other clients +4. **Use instance tokens**: For private rooms, always set an instance token +5. **Broadcast state changes**: Keep all users in sync by broadcasting updates diff --git a/android/docs/marketplace.md b/android/docs/marketplace.md new file mode 100644 index 0000000..6143b92 --- /dev/null +++ b/android/docs/marketplace.md @@ -0,0 +1,377 @@ +# WSTun Marketplace + +## Overview + +The WSTun Marketplace allows users to discover, install, and manage services from external sources. This document explains how to create a marketplace server and publish services. + +## Marketplace API + +A marketplace is simply an HTTP server that provides a specific API. WSTun clients fetch service information and files from the marketplace URL. + +### API Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `{baseUrl}/list` | GET | List all available services | +| `{baseUrl}/service/{name}.json` | GET | Get service manifest | +| `{baseUrl}/service/{name}/{file}` | GET | Download service file | + +### Example Marketplace Structure + +``` +https://example.com/wstun/marketplace/ +├── list # Service list JSON +└── service/ + ├── myservice.json # Service manifest + └── myservice/ + ├── index.html # Service controller + └── main.html # User client +``` + +## Creating a Marketplace + +### 1. Service List (`/list`) + +Returns a JSON array of available services: + +```json +[ + { + "name": "myservice", + "displayName": "My Awesome Service", + "description": "A great service for doing things", + "version": "1.0.0", + "author": "Your Name" + }, + { + "name": "anotherservice", + "displayName": "Another Service", + "description": "Does other things", + "version": "2.1.0", + "author": "Someone Else" + } +] +``` + +### 2. Service Manifest (`/service/{name}.json`) + +Each service needs a manifest file that describes its structure: + +```json +{ + "name": "myservice", + "displayName": "My Awesome Service", + "description": "A great service for doing things", + "version": "1.0.0", + "author": "Your Name", + "icon": "https://example.com/icon.png", + "endpoints": [ + { + "path": "/service", + "file": "index.html", + "type": "service" + }, + { + "path": "/main", + "file": "main.html", + "type": "client" + } + ] +} +``` + +#### Manifest Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Unique service identifier (alphanumeric, lowercase) | +| `displayName` | No | Human-readable name | +| `description` | No | Short description of the service | +| `version` | No | Semantic version string | +| `author` | No | Author name or organization | +| `icon` | No | URL to service icon | +| `endpoints` | Yes | Array of endpoint definitions | + +#### Endpoint Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `path` | Yes | URL path (e.g., `/service`, `/main`) | +| `file` | Yes | Filename to serve for this path | +| `type` | Yes | `service` (controller) or `client` (user) | +| `contentType` | No | MIME type (auto-detected from extension if not set) | + +### 3. Service Files (`/service/{name}/{file}`) + +The actual HTML/JS files for your service. These should use `libwstun.js` for communication. + +## Example: Creating a Simple Service + +### 1. Create the Service Controller (`index.html`) + +```html + + + + + My Service - Controller + + +

My Service

+ +
+
+ + + + + +``` + +### 2. Create the User Client (`main.html`) + +```html + + + + + My Service + + +

My Service

+
Loading...
+ + +
+ + + + + +``` + +### 3. Create the Manifest (`myservice.json`) + +```json +{ + "name": "myservice", + "displayName": "My Custom Service", + "description": "A simple example service", + "version": "1.0.0", + "author": "Your Name", + "endpoints": [ + { + "path": "/service", + "file": "index.html", + "type": "service" + }, + { + "path": "/main", + "file": "main.html", + "type": "client" + } + ] +} +``` + +### 4. Update the Service List (`list`) + +```json +[ + { + "name": "myservice", + "displayName": "My Custom Service", + "description": "A simple example service", + "version": "1.0.0", + "author": "Your Name" + } +] +``` + +## Hosting a Marketplace + +### Option 1: Static File Server + +Host files on any static file server (Apache, Nginx, S3, GitHub Pages): + +``` +/marketplace/ + list # Service list JSON + service/ + myservice.json # Manifest + myservice/ + index.html # Controller + main.html # Client +``` + +Make sure CORS headers allow access from the WSTun server. + +### Option 2: Dynamic Server + +Create a dynamic server (Node.js, Python, etc.) that generates the list and serves files: + +```javascript +// Express.js example +const express = require('express'); +const app = express(); + +// Enable CORS +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + next(); +}); + +// Service list +app.get('/marketplace/list', (req, res) => { + res.json([ + { name: 'myservice', displayName: 'My Service', ... } + ]); +}); + +// Service manifest +app.get('/marketplace/service/:name.json', (req, res) => { + res.sendFile(`./services/${req.params.name}/manifest.json`); +}); + +// Service files +app.get('/marketplace/service/:name/:file', (req, res) => { + res.sendFile(`./services/${req.params.name}/${req.params.file}`); +}); + +app.listen(9090); +``` + +## Installing Services from Marketplace + +### Via Admin UI + +1. Open the WSTun server in a browser +2. Click "Service Manager" (or go to `/admin`) +3. Go to the "Marketplace" tab +4. Enter the marketplace URL +5. Click "Fetch" to see available services +6. Click "Install" on the desired service + +### Via API + +```javascript +// POST /_api/marketplace/install +fetch('/_api/marketplace/install', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + url: 'https://example.com/wstun/marketplace', + name: 'myservice' + }) +}); +``` + +## Best Practices + +1. **Use semantic versioning**: Makes it easy to track updates +2. **Write clear descriptions**: Help users understand what your service does +3. **Test thoroughly**: Ensure your service works with the WSTun server +4. **Include documentation**: Add comments or a README in your service +5. **Handle errors gracefully**: Show user-friendly error messages +6. **Use HTTPS**: Secure your marketplace server +7. **Add CORS headers**: Allow cross-origin requests from WSTun servers + +## Security Considerations + +1. **Validate service names**: Only alphanumeric and lowercase +2. **Sanitize file paths**: Prevent directory traversal attacks +3. **Review code**: Marketplace services can run arbitrary JavaScript +4. **Use HTTPS**: Encrypt data in transit +5. **Implement rate limiting**: Prevent abuse of your marketplace + +## Troubleshooting + +### "Failed to fetch marketplace" +- Check the marketplace URL is correct +- Ensure CORS headers are set +- Verify the `/list` endpoint returns valid JSON + +### "Failed to install service" +- Check the manifest is valid JSON +- Verify all endpoint files exist +- Check file permissions on the server + +### Service doesn't appear after install +- Refresh the service list +- Check the browser console for errors +- Verify the service files were downloaded correctly diff --git a/android/gradle.properties b/android/gradle.properties old mode 100755 new mode 100644 index 599d206..2e11322 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,6 +1,3 @@ -org.gradle.jvmargs=-Xmx4G -android.useAndroidX=true -android.enableJetifier=true -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +android.useAndroidX=true +android.nonTransitiveRClass=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties old mode 100755 new mode 100644 index 81fb745..62f495d --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..31296a5 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,207 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var)}», «## », «## */», «#%»; +# * compoundeli commands having a redirection after the closing «)»; +# * «else» with no «if» line at the same nesting level; +# * arrays. +# +# (2) This script passes all arguments to the gradle program as a single +# argument, but preserves internal quoting, so you can say +# +# gradlew "foo bar" baz +# +# and Gradle will receive two arguments: "foo bar" and "baz". +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't touch options #( + /?*) t=${arg#)}; t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # temporary args, so each arg winds up back in the position where + # it started, but possibly modified. + # + # NB: aass assignment://[[]= is used to discard the value + # temporarily args, so each arg winds up back in the position where + # it started, but possibly modified. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, GRADLE_OPTS, and GRADLE_USER_HOME are used to compute +# the final set of JVM options. +# * GRADLE_OPTS and GRADLE_USER_HOME are examined for their content because Gradle +# puts commonly-used JVM arguments there. +# * The user's JVM arguments or defaults are added to the java command's JVM arguments. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xeli" is not enabled or the shell is not bash. +if ! "$cygwin" && ! "$msys" ; then + exec "$JAVACMD" "$@" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle old mode 100755 new mode 100644 index ee56b97..97edfd1 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,29 +1,17 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version '8.2.1' apply false -} - -include ":app" +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "NodeBase" +include ':app' diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100755 index ad322bc..0000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100755 index e041d38..0000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100755 index 0b2d479..0000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100755 index 0b2d479..0000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100755 index b63de6e..0000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,609 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 331C80F3294D02FB00263BE5 /* RunnerTests.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80F3294D02FB00263BE5 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80EE294D02FB00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80F2294D02FB00263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80F3294D02FB00263BE5 /* RunnerTests.m */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 331C80F2294D02FB00263BE5 /* RunnerTests */, - 97C146EF1CF9000F007C117D /* Products */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80F0294D02FB00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80ED294D02FB00263BE5 /* Sources */, - 331C80EE294D02FB00263BE5 /* Frameworks */, - 331C80EF294D02FB00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80F6294D02FB00263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80F0294D02FB00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 97C146ED1CF9000F007C117D; - }; - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - 331C80F0294D02FB00263BE5 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80EF294D02FB00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80ED294D02FB00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80F6294D02FB00263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 331C80F8294D02FB00263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Debug; - }; - 331C80F9294D02FB00263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Release; - }; - 331C80FA294D02FB00263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80F8294D02FB00263BE5 /* Debug */, - 331C80F9294D02FB00263BE5 /* Release */, - 331C80FA294D02FB00263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index c4b79bd..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100755 index af0309c..0000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 2de4fc5..0000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 59c6d39..0000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100755 index af0309c..0000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.h b/ios/Runner/AppDelegate.h deleted file mode 100755 index a7bb489..0000000 --- a/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/ios/Runner/AppDelegate.m b/ios/Runner/AppDelegate.m deleted file mode 100755 index 569c492..0000000 --- a/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#import "AppDelegate.h" -#import "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index 1950fd8..0000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100755 index dc9ada4..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100755 index 7353c41..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100755 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100755 index 6ed2d93..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100755 index 4cd7b00..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100755 index fe73094..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100755 index 321773c..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100755 index 797d452..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100755 index 502f463..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100755 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100755 index 0ec3034..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100755 index e9f5fea..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100755 index 84ac32a..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100755 index 8953cba..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100755 index 0467bf1..0000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100755 index d08a4de..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100755 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100755 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100755 index 9da19ea..0000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100755 index 65a94b5..0000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100755 index 497371e..0000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100755 index bbb83ca..0000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100755 index 92b36e2..0000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Nodebase - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - nodebase - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/main.m b/ios/Runner/main.m deleted file mode 100755 index 4618607..0000000 --- a/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/ios/RunnerTests/RunnerTests.m b/ios/RunnerTests/RunnerTests.m deleted file mode 100755 index 478eaec..0000000 --- a/ios/RunnerTests/RunnerTests.m +++ /dev/null @@ -1,16 +0,0 @@ -#import -#import -#import - -@interface RunnerTests : XCTestCase - -@end - -@implementation RunnerTests - -- (void)testExample { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. -} - -@end diff --git a/lib/comp/app_runtime_tile.dart b/lib/comp/app_runtime_tile.dart deleted file mode 100755 index 92da317..0000000 --- a/lib/comp/app_runtime_tile.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:flutter/material.dart'; -import './util.dart'; -import '../ctrl/application_def.dart'; - -class AppRuntimeTile extends StatefulWidget { - const AppRuntimeTile({super.key, required this.process}); - - final IApplicationProcess process; - - @override - State createState() => _AppRuntimeTileState(); -} - -class _AppRuntimeTileState extends State { - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ListTile( - title: Text(widget.process.getName()), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("platform: ${widget.process.getPlatform()}"), - ], - ), - trailing: Wrap( - spacing: 2, - children: [ - IconButton(onPressed: () { - showInfo(context); - }, icon: const Icon(Icons.info_outline)), - IconButton(onPressed: () { - showConfirmDialog( - context, - "Stop Application", - "Do you confirm to stop the application \"${widget.process.getName()}\"?" - ).then((confirmed) { - if (!confirmed) return; - widget.process.stop().then((_) { - generateSnackBar( - context, - "Stopped application \"${widget.process.getName()}\"" - ); - }); - }); - }, icon: const Icon(Icons.stop)), - ], - ), - tileColor: const Color.fromARGB(255, 230, 255, 230), - ); - } - - void showInfo(BuildContext context) { - final cmd = widget.process.getCmd(); - final cmdMain = cmd[0]; - final cmdEntry = cmd.length > 1 ? "\n${cmd[1]}" : ""; - showDialog(context: context, builder: (_) => AlertDialog( - title: const Text("Application Info"), - shape: const BeveledRectangleBorder(), - content: SizedBox( - height: 200, - width: 300, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text("-- CMD -- "), - SelectableText("$cmdMain$cmdEntry"), - const Divider(), - const Text("-- ENV --"), - SelectableText(widget.process.getEnv().entries.map( - (kv) => "${kv.key} =\n${kv.value}" - ).toList().join("\n\n")), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("OK")), - ], - )); - } -} \ No newline at end of file diff --git a/lib/comp/app_tile.dart b/lib/comp/app_tile.dart deleted file mode 100755 index 77177c0..0000000 --- a/lib/comp/app_tile.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import './run_app_steps.dart'; -import './util.dart'; -import '../ctrl/nodebase.dart' as nodebase; - -class AppTile extends StatefulWidget { - const AppTile({ - super.key, - required this.name, - required this.version, - required this.platform, - this.defaultInstalled = false, - this.defaultRunning = false, - this.userDefined = false, - }); - - final String name; - final String version; - final String platform; - final bool defaultInstalled; - final bool defaultRunning; - final bool userDefined; - - @override - State createState() => _AppTileState(); -} - -class _AppTileState extends State { - AppTile? detect; - bool isInstalled = false; - - bool isDownloading = false; - bool isRunning = false; - bool isRemoving = false; - - void setInstall(bool installed) { - setState(() { - isInstalled = installed; - }); - } - - @override - void initState() { - super.initState(); - isInstalled = widget.defaultInstalled; - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final List actions = []; - if (detect != widget) { - isInstalled = widget.defaultInstalled; - isRunning = widget.defaultRunning; - detect = widget; - } - if (isInstalled) { - actions.add(IconButton(onPressed: isRemoving ? null : () { - showInfo(context); - }, icon: const Icon(Icons.info_outline))); - actions.add(IconButton(onPressed: isRemoving || isRunning ? null : () { - // TODO: if platform is not installed, pop up a dialog for installation - // list all available name - {versions} - // TODO: pop up a dialog for exec, env set up - // read exec, env from last run or default value - Map config = { - "name": widget.name, - "version": widget.version, - "platform": widget.platform, - }; - runAppInit(context, config).then((ok0) { - if (!ok0) return; - runAppStepCheckPlatform(context, config).then((ok1) { - if (!ok1) return; - runAppStepArgAndEnv(context, config).then((ok2) { - if (!ok2) return; - runAppStepReview(context, config).then((app) { - if (app == null) return; - saveAppConfig(config); - }); - }); - }); - }); - }, icon: const Icon(Icons.play_arrow))); - actions.add(IconButton(onPressed: isRemoving || isRunning ? null : () { - // TODO: check if running, no remove - // TODO: try ... catch ... - showConfirmDialog( - context, - "Remove Application", - "Do you confirm to remove the application \"${widget.name}-${widget.version}\"?" - ).then((confirmed) { - if (!confirmed) return; - isRemoving = true; - (() async { - try { - await nodebase.instance.platform.removeApplicationBinary( - widget.name, widget.version, widget.platform); - return true; - } catch (e) { - log("NodeBase [E] AppTile ... remove application \"${widget.name}-${widget.version}\" $e"); - return false; - } - })().then((ok) { - isRemoving = false; - if (!ok) return; - setInstall(false); - generateSnackBar(context, "Removed application: \"${widget.name}-${widget.version}\""); - }); - }); - }, icon: const Icon(Icons.delete_forever))); - } else { - actions.add(IconButton(onPressed: isDownloading ? null : () { - (() async { - final name = widget.name; - final version = widget.version; - final confirmed = await showConfirmDialog( - context, - "Install Application", - "Do you confirm to install the application \"$name-$version\"?" - ); - if (!confirmed) return "cancel"; - isDownloading = true; - final platform = nodebase.instance.platform; - try { - await platform.downloadApplicationMetaJson(name, version); - final json = await platform.readApplicationMetaJson(name, version); - final url = json["source"]; - await platform.downloadApplicationBinary(name, version, widget.platform, url); - return "ok"; - } catch (e) { - log("NodeBase [E] AppTile:install ${widget.name}-${widget.version} ... $e"); - return e.toString(); - } - })().then((r) { - isDownloading = false; - if (r == "cancel") return; - if (r == "ok") { - setInstall(true); - generateSnackBar(context, "Installed application: \"${widget.name}-${widget.version}\""); - } else { - generateSnackBar(context, "Failed to install application: \"${widget.name}-${widget.version}\""); - } - }); - }, icon: const Icon(Icons.download))); - } - return ListTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - ...(widget.userDefined ? [ - const Tooltip( - message: "Not in marketplace", - child: Icon(Icons.person_pin_circle_rounded, size: 12), - ), - ]: []), - Expanded(child: Text( - "${widget.name}-${widget.version}", - overflow: TextOverflow.ellipsis, - )), - ], - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("platform: ${widget.platform}") - ], - ), - trailing: Wrap( - spacing: 2, - children: actions, - ), - ); - } - - void showInfo(BuildContext context) { - final name = widget.name; - final version = widget.version; - showDialog(context: context, builder: (_) => AlertDialog( - title: const Text("Application Info"), - shape: const BeveledRectangleBorder(), - content: SizedBox( - height: 200, - width: 300, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text("-- Basic -- "), - SelectableText("$name-$version\n(${widget.platform})"), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("OK")), - ], - )); - } -} \ No newline at end of file diff --git a/lib/comp/download_tile.dart b/lib/comp/download_tile.dart deleted file mode 100755 index 619c1f0..0000000 --- a/lib/comp/download_tile.dart +++ /dev/null @@ -1,137 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import './util.dart'; -import '../util/event.dart' as event; -import '../ctrl/nodebase.dart' as nodebase; - -class DownloadTile extends StatefulWidget { - const DownloadTile({ - super.key, - required this.name, - required this.type, - required this.url, - required this.filename, - }); - - final String name; - final String type; - final String url; - final String filename; - - @override - State createState() => _DownloadTileState(); -} - -class _DownloadTileState extends State with TickerProviderStateMixin { - late AnimationController controller; - bool isDeterminate = false; - double progressValue = 0.0; - bool isCanceling = false; - - late StreamSubscription progress; - - void modeDeterminate(double value) => setState(() { - isDeterminate = true; - controller.stop(); - controller.value = value; - }); - void modeNonDeterminate() => setState(() { - isDeterminate = false; - controller - ..forward(from: 0) - ..repeat(); - }); - - @override - void initState() { - super.initState(); - controller = AnimationController( - vsync: this, - duration: const Duration(seconds: 1), - )..addListener(() { - setState(() {}); - }); - controller.repeat(); - - progress = event.platformToken.stream.listen((msg) { - final T = msg[0]; - if (T != "download") return; - final F = msg[3]; - if (F != widget.filename) return; - final V = msg[4]; - if (V == -99) { - modeNonDeterminate(); - } else if (V == -1) { - setState(() { - isCanceling = true; - }); - } else { - modeDeterminate(V + 0.0); - } - }); - } - - @override - void dispose() { - progress.cancel(); - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return ListTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text("download"), - Text( - "${widget.type} - ${widget.name}", - overflow: TextOverflow.ellipsis, - ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText( - widget.url, - maxLines: 1, - ), - LinearProgressIndicator( - value: controller.value, - semanticsLabel: 'Linear progress indicator', - ), - ], - ), - trailing: Wrap( - spacing: 2, - children: [ - IconButton(onPressed: isCanceling ? null :() { - (() async { - final confirmed = await showConfirmDialog( - context, - "Cancel Downloading", - "Do you confirm to cancel downloading for \"${widget.type} - ${widget.name}\"?" - ); - if (!confirmed) return false; - isCanceling = true; - await nodebase.instance.platform.downloadCancel(widget.filename); - // TODO: deal with if no network downloading - // event.platformToken.add(["download", widget.name, widget.url, widget.filename, -1]); - return true; - })().then((ok) { - isCanceling = false; - if (!ok) return; - generateSnackBar( - context, - "Canceled downloading for \"${widget.type} - ${widget.name}\"" - ); - }); - }, icon: const Icon(Icons.file_download_off)), - ], - ), - ); - } -} \ No newline at end of file diff --git a/lib/comp/platform_tile.dart b/lib/comp/platform_tile.dart deleted file mode 100755 index e9ad422..0000000 --- a/lib/comp/platform_tile.dart +++ /dev/null @@ -1,183 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter/material.dart'; -import './util.dart'; -import '../ctrl/nodebase.dart' as nodebase; - -class PlatformTile extends StatefulWidget { - const PlatformTile({ - super.key, - required this.name, - required this.version, - this.defaultInstalled = false, - this.defaultRunning = false, - this.userDefined = false, - }); - - final String name; - final String version; - final bool defaultInstalled; - final bool defaultRunning; - final bool userDefined; - - @override - State createState() => _AppTileState(); -} - -class _AppTileState extends State { - PlatformTile? detect; - bool isInstalled = false; - - bool isDownloading = false; - bool isRunning = false; - bool isRemoving = false; - - void setInstall(bool installed) { - setState(() { - isInstalled = installed; - }); - } - - @override - void initState() { - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final List actions = []; - if (detect != widget) { - isInstalled = widget.defaultInstalled; - isRunning = widget.defaultRunning; - detect = widget; - } - if (isInstalled) { - actions.add(IconButton(onPressed: isRemoving ? null : () { - showInfo(context); - }, icon: const Icon(Icons.info_outline))); - actions.add(IconButton(onPressed: isRemoving || isRunning ? null : () { - // TODO: check if running, no remove - // TODO: try ... catch ... - showConfirmDialog( - context, - "Remove Platform", - "Do you confirm to remove the platform \"${widget.name}-${widget.version}\"?" - ).then((confirmed) { - if (!confirmed) return; - isRemoving = true; - (() async { - try { - await nodebase.instance.platform.removePlatformBinary(widget.name, widget.version); - return true; - } catch(e) { - log("NodeBase [E] PlatformTile ... remove platform \"${widget.name}-${widget.version}\" $e"); - return false; - } - })().then((ok) { - isRemoving = false; - if (!ok) return; - setInstall(false); - generateSnackBar(context, "Removed platform: \"${widget.name}-${widget.version}\""); - }); - }); - }, icon: const Icon(Icons.delete_forever))); - } else { - actions.add(IconButton(onPressed: isDownloading ? null : () { - (() async { - final name = widget.name; - final version = widget.version; - final confirmed = await showConfirmDialog( - context, - "Install Platform", - "Do you confirm to install the platform \"$name-$version\"?" - ); - if (!confirmed) return "cancel"; - isDownloading = true; - final platform = nodebase.instance.platform; - try { - await platform.downloadPlatformMetaJson(name, version); - final json = await platform.readPlatformMetaJson(name, version); - final url = json["source"]; - await platform.downloadPlatformBinary(name, version, url); - return "ok"; - } catch (e) { - log("NodeBase [E] PlatformTile:install ${widget.name}-${widget.version} ... $e"); - return e.toString(); - } - })().then((r) { - isDownloading = false; - if (r == "cancel") return; - if (r == "ok") { - setInstall(true); - generateSnackBar(context, "Installed platform: \"${widget.name}-${widget.version}\""); - } else { - generateSnackBar(context, "Failed to install platform: \"${widget.name}-${widget.version}\""); - } - }); - }, icon: const Icon(Icons.download))); - } - return ListTile( - title: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - ...(widget.userDefined ? [ - const Tooltip( - message: "Not in marketplace", - child: Icon(Icons.person_pin_circle_rounded, size: 12), - ), - ]: []), - Expanded(child: Text( - "${widget.name}-${widget.version}", - overflow: TextOverflow.ellipsis, - )), - ], - ) - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("(${nodebase.instance.platform.getName()})") - ], - ), - trailing: Wrap( - spacing: 2, - children: actions, - ), - ); - } - - void showInfo(BuildContext context) { - final name = widget.name; - final version = widget.version; - showDialog(context: context, builder: (_) => AlertDialog( - title: const Text("Platform Info"), - shape: const BeveledRectangleBorder(), - content: SizedBox( - height: 200, - width: 300, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text("-- Basic -- "), - SelectableText("$name-$version\n(${nodebase.instance.platform.getName()})"), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("OK")), - ], - )); - } -} \ No newline at end of file diff --git a/lib/comp/run_app_steps.dart b/lib/comp/run_app_steps.dart deleted file mode 100755 index 7f987d1..0000000 --- a/lib/comp/run_app_steps.dart +++ /dev/null @@ -1,527 +0,0 @@ -import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:path/path.dart' as path; - -import '../ctrl/application_def.dart'; -import '../ctrl/nodebase.dart' as nodebase; -import './util.dart'; - -const defaultHintStyle = TextStyle(color: Color.fromARGB(128, 0, 0, 0)); - -class ArgInput extends StatelessWidget { - const ArgInput({ - super.key, - required this.onDelete, - required this.onChanged, - this.required = false, - this.placeholder = "", - this.initialValue = "", - }); - - final bool required; - final String placeholder; - final String initialValue; - final void Function(ArgInput)? onDelete; - final void Function(ArgInput, String)? onChanged; - - @override - Widget build(BuildContext context) { - return Row( - textDirection: TextDirection.rtl, - children: [ - ...(required ? [] : [IconButton( - onPressed: onDelete == null ? null : () => onDelete!(this), - icon: const Icon(Icons.close) - )]), - Expanded(child: TextFormField( - initialValue: initialValue, - onChanged: onChanged == null ? null : (val) => onChanged!(this, val), - decoration: InputDecoration( - hintText: placeholder, - hintStyle: defaultHintStyle - ), - )), - ], - ); - } - -} - -class EnvInput extends StatelessWidget { - const EnvInput({ - super.key, - required this.onDelete, - required this.onKeyChanged, - required this.onValChanged, - this.required = false, - this.placeholder = "", - this.initialKeyValue = "", - this.initialValValue = "", - }); - - final bool required; - final String placeholder; - final String initialKeyValue; - final String initialValValue; - final void Function(EnvInput)? onDelete; - final void Function(EnvInput, String)? onKeyChanged; - final void Function(EnvInput, String)? onValChanged; - - @override - Widget build(BuildContext context) { - return Row( - textDirection: TextDirection.rtl, - children: [ - ...(required ? [] : [IconButton( - onPressed: onDelete == null ? null : () => onDelete!(this), - icon: const Icon(Icons.close) - )]), - Expanded(child: TextFormField( - initialValue: initialValValue, - onChanged: onValChanged == null ? null : (val) => onValChanged!(this, val), - decoration: InputDecoration( - hintText: placeholder, - hintStyle: defaultHintStyle - ), - )), - Expanded(child: TextFormField( - initialValue: initialKeyValue, - onChanged: onKeyChanged == null ? null : (val) => onKeyChanged!(this, val), - )), - ], - ); - } - -} - -Future runAppInit(BuildContext context, Map config) async { - final name = config["name"] ?? ""; - final version = config["version"] ?? ""; - final platform = config["platform"] ?? ""; - if (name == "" || version == "" || platform == "") return false; - config["base"] = await nodebase.instance.platform.getApplicationBaseDir(name, version); - // meta.entryPoint[0] - final meta = await nodebase.instance.platform.readApplicationMetaJson(name, version); - config["availableEntryPoint"] = meta["entryPoint"] ?? []; - config["argRequire"] = meta["argRequire"] ?? []; - config["envRequire"] = meta["envRequire"] ?? []; - // savedConfig.arg, savedConfig.env - final savedConfig = await nodebase.instance.platform.readApplicationConfig(name, version); - - final lastArg = savedConfig["arg"] ?? []; - config["lastArg"] = []; - for(int i = 0, n = lastArg.length, m = config["argRequire"].length; i < n || i < m; i++) { - if (i < m && i < n) { - final one = config["argRequire"][i]; - config["lastArg"].add({ - "help": one["help"] ?? "", - "default": one["default"] ?? "", - "last": lastArg[i], - "required": one["required"] ?? true, - }); - } else if (i < m && i >= n) { - final one = config["argRequire"][i]; - config["lastArg"].add({ - "help": one["help"] ?? "", - "default": one["default"] ?? "", - "last": null, - "required": one["required"] ?? true, - }); - } else { - config["lastArg"].add({ - "help": "", - "default": "", - "last": lastArg[i], - "required": false, - }); - } - } - - final lastEnv = savedConfig["env"] ?? {}; - config["lastEnv"] = {}; - for(int i = 0, n = config["envRequire"].length; i < n; i++) { - final one = config["envRequire"][i]; - final name = one["name"]; - config["lastEnv"][name] = { - "name": name, - "help": one["help"] ?? "", - "default": one["default"], - "last": lastEnv[name] ?? "", - "required": one["required"] ?? false, - }; - } - for(final k in lastEnv.keys) { - final one = config["lastEnv"][k]; - if (one == null) { - config["lastEnv"][k] = { - "name": k, - "help": "", - "default": "", - "last": lastEnv[k], - "required": false, - }; - } - } - - config["lastEntryPoint"] = savedConfig["entryPoint"]; - config["lastExec"] = savedConfig["exec"]; - config["lastPlatformVersion"] = savedConfig["platformVersion"]; - // installed: {name: [version,...]} - final installed = await nodebase.instance.platform.listInstalledPlatformList(); - config["platformList"] = {}; - config["platformList"][platform] = installed[platform]; - return true; -} - -Future runAppStepCheckPlatform(BuildContext context, Map config) async { - final platform = config["platform"]; - if (config["platformList"][platform] == null) { - // no install, check available and guide to download - await downloadPlatform(context, config); - } else { - // by default select last one, guide user to select platform version - return await selectPlatform(context, config); - } - return false; -} -Future downloadPlatform(BuildContext context, Map config) async { - // TODO: show all; click one download and stop app running - final platform = config["platform"]; - generateSnackBar(context, "Dependency missing.\nNo available versions for \"$platform\"."); -} -Future> getPlatformExec(String name, String version) async { - Map meta = await nodebase.instance.platform.readPlatformMetaJson(name, version); - List r = []; - for (final one in meta["executable"]) { - r.add(one); - } - return r; -} -Future selectPlatform(BuildContext context, Map config) async { - final platform = config["platform"]; - final platformList = config["platformList"][platform]; - final List> dropdownItems = [ - const DropdownMenuItem(value: "-", child: Text("(not selected)")), - ]; - for (final value in platformList) { - dropdownItems.add(DropdownMenuItem(value: value, child: Text(value))); - } - String selected = "-"; - String selectedExec = "-"; - List execs = []; - bool firstTime = true; - bool ok = false; - await showDialog(context: context, builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - if (firstTime && selected == "-" && config["lastPlatformVersion"] != null) { - firstTime = false; - final lastSelected = config["lastPlatformVersion"]; - final lastSelectedExec = config["lastExec"]; - if (config["platformList"][platform].contains(lastSelected)) { - getPlatformExec(platform, lastSelected).then((list) { - selected = lastSelected; - execs = list; - if (list.contains(lastSelectedExec)) { - selectedExec = lastSelectedExec; - } - setState(() {}); - }); - } - } - return AlertDialog( - title: const Text("Select Platform"), - shape: const BeveledRectangleBorder(), - content: SizedBox( - height: 200, - width: 300, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text("Platform: $platform"), - DropdownButton( - value: selected, - items: dropdownItems, - onChanged: (val) { - if (val == null) return; - selected = val; - if (val == "-") { - execs = []; - setState(() {}); - return; - } - getPlatformExec(platform, selected).then((list) { - execs = list; - setState(() {}); - }); - }, - ), - ...(selected == "-" ? [] : [ - const Text("Exec"), - DropdownButton(items: [ - const DropdownMenuItem(value: "-", child: Text("(not selected)")), - ...execs.map((name) => DropdownMenuItem( - value: name, - child: Text(path.basename(name), overflow: TextOverflow.ellipsis), - )) - ], onChanged: (val) { - if (val == null) return; - selectedExec = val; - setState(() {}); - }, value: selectedExec), - ]), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel")), - ElevatedButton( - onPressed: selected == "-" || selectedExec == "-" ? null : () { - Navigator.of(context).pop(); - ok = true; - }, - child: const Text("Next")), - ], - ); - } - ); - } - ); - if (!ok) return false; - config["platformVersion"] = selected; - config["selectedExec"] = selectedExec; - config["exec"] = path.join( - await nodebase.instance.platform.getPlatformBaseDir(platform, selected), - selectedExec - ); - return true; -} - -ArgInput generateArgInput(List argItems, List arg, dynamic one, Function setState) { - return ArgInput( - onDelete: (self) { - final i = argItems.indexOf(self); - argItems.removeAt(i); - arg.removeAt(i); - setState(() {}); - }, onChanged: (self, val) { - final i = argItems.indexOf(self); - arg[i] = val; - }, - required: one["required"] ?? false, - placeholder: one["help"] ?? "", - initialValue: one["last"] ?? one["default"] ?? "" - ); -} -EnvInput generateEnvInput(List envItems, List envk, List envv, dynamic one, Function setState) { - return EnvInput( - onDelete: (self) { - final i = envItems.indexOf(self); - envItems.removeAt(i); - envk.removeAt(i); - envv.removeAt(i); - setState(() {}); - }, onKeyChanged: (self, val) { - final i = envItems.indexOf(self); - envk[i] = val; - }, onValChanged: (self, val) { - final i = envItems.indexOf(self); - envv[i] = val; - }, - required: one["required"] ?? false, - placeholder: one["help"] ?? "", - initialKeyValue: one["name"] ?? "", - initialValValue: one["last"] ?? one["default"] ?? "", - ); -} - -Future runAppStepArgAndEnv(BuildContext context, Map config) async { - final name = config["name"]; - final version = config["version"]; - final availableEntryPoint = config["availableEntryPoint"]; - final List> dropdownItems = [ - const DropdownMenuItem(value: "-", child: Text("(not selected)")), - ]; - for (final value in availableEntryPoint) { - dropdownItems.add(DropdownMenuItem(value: value, child: Text(value))); - } - String selected = config["lastEntryPoint"] ?? "-"; - List arg = []; - List envk = []; - List envv = []; - bool ok = false; - final List argItems = []; - final List envItems = []; - for (final _ in config["lastArg"]) { - argItems.add(null); - arg.add(""); - } - for (final _ in config["lastEnv"].values) { - envItems.add(null); - } - - await showDialog(context: context, builder: (context) { - return StatefulBuilder( - builder: (context, setState) { - int i = 0; - for (final one in config["lastArg"]) { - if (i >= argItems.length) break; - argItems[i] = generateArgInput(argItems, arg, one, setState); - arg[i] = one["last"] ?? one["default"] ?? ""; - i ++; - } - i = 0; - for (final one in config["lastEnv"].values) { - if (i >= envItems.length) break; - envItems[i] = generateEnvInput(envItems, envk, envv, one, setState); - envv[i] = one["name"] ?? ""; - envk[i] = one["last"] ?? one["default"] ?? ""; - i ++; - } - return AlertDialog( - title: const Text("Config Application"), - shape: const BeveledRectangleBorder(), - content: SizedBox( - height: 200, - width: 300, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Text("Entrypoint"), - DropdownButton( - value: selected, - items: dropdownItems, - onChanged: (val) { - if (val == null) return; - selected = val; - setState(() {}); - }, - ), - ...(selected == "-" ? []: [ - const Text("Arg"), - ...argItems, - TextButton(onPressed: () { - final newOne = generateArgInput(argItems, arg, {}, setState); - argItems.add(newOne); - arg.add(""); - setState(() {}); - }, child: const Text("+")), - const Text("Env"), - ...envItems, - TextButton(onPressed: () { - final newOne = generateEnvInput(envItems, envk, envv, {}, setState); - envItems.add(newOne); - envk.add(""); - envv.add(""); - setState(() {}); - }, child: const Text("+")), - ]), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel")), - ElevatedButton( - onPressed: selected == "-" ? null : () { - Navigator.of(context).pop(); - ok = true; - }, - child: const Text("Next")), - ], - ); - }); - }); - // TODO: check required arg, env - if (!ok) return false; - config["selectedEntryPoint"] = selected; - config["entryPoint"] = path.join( - await nodebase.instance.platform.getApplicationBaseDir(name, version), - selected - ); - - config["arg"] = arg; - - Map env = {}; - for (int i = 0, n = envk.length; i < n; i++) { - final k = envk[i]; - final v = envv[i]; - if (k == "" || v == "") continue; - env[k] = v; - } - config["env"] = env; - return true; -} - -Future runAppStepReview(BuildContext context, Map config) async { - final name = config["name"]; - final version = config["version"]; - final platform = config["platform"]; - final platformVersion = config["platformVersion"]; - final arg = config["arg"] ?? []; - final env = config["env"] ?? {}; - final entryPoint = config["entryPoint"]; // should not null - final exec = config["exec"]; // should not null - bool ok = false; - - await showDialog(context: context, builder: (_) => AlertDialog( - title: const Text("Application Info"), - shape: const BeveledRectangleBorder(), - content: SizedBox( - height: 200, - width: 300, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SelectableText("-- Application --\n" - "$name-$version\n\\-> ${path.basename(entryPoint)}\n\n" - "-- Platform --\n$platform-$platformVersion\n\\-> ${path.basename(exec)}\n\n" - "-- Config --\n" - "-> Arg\n${arg.isEmpty ? "(empty)" : arg.join("\n")}\n" - "-> Env\n${env.isEmpty ? "(empty)" : env.entries.map( - (item) => "${item.key} = ${item.value}").toList().join("\n")}" - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text("Cancel")), - ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - ok = true; - }, - child: const Text("Run")), - ], - )); - if (!ok) return null; - // TODO: mark platform is running - final app = nodebase.instance.application.startProcess( - "$name-$version", "$platform-$platformVersion", [exec, entryPoint, ...arg], env - ); - return app; -} - -Future saveAppConfig(Map config) async { - final name = config["name"]; - final version = config["version"]; - Map json = {}; - json["arg"] = config["arg"]; - json["env"] = config["env"]; - json["entryPoint"] = config["selectedEntryPoint"]; - json["exec"] = config["selectedExec"]; - json["platform"] = config["platform"]; - json["platformVersion"] = config["platformVersion"]; - await nodebase.instance.platform.writeApplicationConfig(name, version, json); -} \ No newline at end of file diff --git a/lib/comp/util.dart b/lib/comp/util.dart deleted file mode 100755 index cf23f99..0000000 --- a/lib/comp/util.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; - -SnackBar generateSnackBar(BuildContext context, String content, { - Duration duration = const Duration(seconds: 2) -}) { - final snackBar = SnackBar( - content: Text(content), - action: SnackBarAction( - label: "X", - onPressed: () { - // TODO: To safely refer to a widget's ancestor in its dispose() method, - // save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() - // in the widget's didChangeDependencies() method. - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - } - ), - duration: duration, - ); - ScaffoldMessenger.of(context).showSnackBar(snackBar); - return snackBar; -} - -Future showConfirmDialog(BuildContext context, String title, String content) async { - final ok = Completer(); - final dialog = AlertDialog( - title: Text(title), - shape: const BeveledRectangleBorder(), - content: Text(content), - actions: [ - TextButton(onPressed: () { - Navigator.of(context).pop(); - ok.complete(false); - }, child: const Text("Cancel")), - ElevatedButton(onPressed: () { - Navigator.of(context).pop(); - ok.complete(true); - }, child: const Text("Confirm")), - ], - ); - showDialog(context: context, builder: (_) => dialog); - return ok.future; -} diff --git a/lib/ctrl/application_def.dart b/lib/ctrl/application_def.dart deleted file mode 100755 index 957a93c..0000000 --- a/lib/ctrl/application_def.dart +++ /dev/null @@ -1,18 +0,0 @@ -abstract class IApplicationProcess { - Future start(); - Future stop(); - Future syncState(); - bool isDead(); - String getName(); - String getPlatform(); - List getCmd(); - Map getEnv(); -} - -abstract class IApplication { - IApplicationProcess startProcess(String name, String platform, List cmd, Map env); - void stopProcess(String name); - IApplicationProcess? getApp(String name); - List getRunningApp(); - void dispose(); -} \ No newline at end of file diff --git a/lib/ctrl/application_local.dart b/lib/ctrl/application_local.dart deleted file mode 100755 index 0f4a701..0000000 --- a/lib/ctrl/application_local.dart +++ /dev/null @@ -1,120 +0,0 @@ -// manage application runtime - -import 'dart:async'; - -import './application_def.dart'; - -import '../util/api.dart'; - -class _ApplicationProcess implements IApplicationProcess { - _ApplicationProcess({ - required this.name, - required this.cmd, - this.platform = "-", - this.env = const {}, - }); - - String name; - List cmd; - Map env; - String platform; - String state = "new"; - - @override - String getName() => name; - @override - String getPlatform() => platform; - @override - List getCmd() => cmd; - @override - Map getEnv() => env; - - @override - Future start() async { - await NodeBaseApi.apiAppStart(name, cmd, env: env); - } - - @override - Future stop() async { - await NodeBaseApi.apiAppStop(name); - } - - @override - Future syncState() async { - final obj = await NodeBaseApi.apiAppStatus(name); - final r = obj["state"]; - state = r; - return r; - } - - @override - bool isDead() => state == "dead"; -} - -class ApplicationLocal implements IApplication { - late StreamSubscription listener; - - ApplicationLocal({ - required this.baseDir - }) { - _connect(); - } - - void _connect() { - listener = NodeBaseApi.event.receiveBroadcastStream("app").listen((message) { - if (message is List) { - final name = message[0]; - final state = message[1]; - final app = runtime[name]; - if (app != null && app is _ApplicationProcess) { - app.state = state; - if (state == "dead") runtime.remove(name); - } - } - }); - } - - @override - IApplicationProcess startProcess( - String name, - String platform, - List cmd, - Map env - ) { - final app = _ApplicationProcess(name: name, cmd: cmd, env: env, platform: platform); - // XXX: if we want sandbox to run applications, - // on windows, MacOS, maybe consider cygwin, winehq, WSL and docker impl - // on Linux, Android, consider proot - app.start(); - runtime[name] = app; - return app; - } - - @override - void stopProcess(String name) { - final app = runtime[name]; - if (app == null) return; - runtime.remove(name); - app.stop(); - } - - @override - IApplicationProcess? getApp(String name) => runtime[name]; - @override - List getRunningApp() { - final List r = []; - runtime.forEach((name, app) { - if (!app.isDead()) r.add(app); - }); - return r; - } - - @override - void dispose() { - listener.cancel(); - runtime.forEach((_, app) => app.stop()); - } - - String baseDir; - Map runtime = {}; -} \ No newline at end of file diff --git a/lib/ctrl/configure.dart b/lib/ctrl/configure.dart deleted file mode 100755 index 67c632e..0000000 --- a/lib/ctrl/configure.dart +++ /dev/null @@ -1,28 +0,0 @@ -bool localMode = true; - -/* - * /nodebase.json - * /plm--.json - * /plm///(-).json - * /app--.json - * /app///(-).json - */ -String defaultPlatformBaseUrl = "https://raw.githubusercontent.com/wiki/dna2github/NodeBase/market/v1"; -//String defaultPlatformBaseUrl = "http://127.0.0.1:8000"; - -/* - * /app/running GET - * /app/start POST - * /app/stop POST - * /app/stat GET - * /config/nodebase GET - * /config/list/plm GET - * /config/list/installed_plm GET - * /config/install/plm/ POST - * /config/meta/plm/ GET - * /config/list/app GET - * /config/list/installed_app GET - * /config/install/app/ POST - * /config/meta/app/ GET - */ -String defaultRemoteBaseUrl = "http://127.0.0.1:8580"; \ No newline at end of file diff --git a/lib/ctrl/nodebase.dart b/lib/ctrl/nodebase.dart deleted file mode 100755 index 28e13e5..0000000 --- a/lib/ctrl/nodebase.dart +++ /dev/null @@ -1,70 +0,0 @@ -import 'dart:developer'; -import 'dart:io'; - -import '../ctrl/application_def.dart'; -import '../ctrl/application_local.dart'; -import '../ctrl/platform_def.dart'; -import '../ctrl/platform_local.dart'; -import '../util/fs.dart'; -import '../util/api.dart'; -import '../util/event.dart' as event; -import './configure.dart'; - -class NodeBaseController { - late IApplication application; - late IPlatform platform; - bool isSupported = false; - - Future initializeApp() async { - if (localMode) { - await initLocalMode(); - } else { - // TODO: add PlatformRemote and ApplicationRemote for remote mode - } - - event.initializeToken.add(true); - } - - Future initLocalMode() async { - final parts = (await NodeBaseApi.apiUtilGetArch()).split("|")[0].split("-"); - String os = parts[0]; - String arch = parts[1]; - final appBaseDir = await fsGetBaseDir(); - var baseUrl = defaultPlatformBaseUrl; - // TODO: read app config and get default base url - platform = PlatformLocal(baseUrl: baseUrl, baseDir: appBaseDir, os: os, arch: arch); - application = ApplicationLocal(baseDir: appBaseDir); - try { - // TODO: read user settings, app list, platform list - // get latest app, platform list from remote - final download = platform.downloadNodeBaseJson(); - if (!File(platform.getNodeBaseJsonFilename()).existsSync()) { - await download; - } - } catch (e) { - log("NodeBase [E] cannot download nodebase.json"); - } - isSupported = await platform.isSupported(); - // TODO: check platform version; if no change, skip download list - try { - final download = platform.downloadApplicationListJson(); - if (!File(platform.getApplicationListJsonFilename()).existsSync()) { - await download; - } - } catch(e) { - log("NodeBase [E] cannot download app-list.json"); - } - - try { - final download = platform.downloadPlatformListJson(); - if (!File(platform.getPlatformListJsonFilename()).existsSync()) { - await download; - } - } catch(e) { - log("NodeBase [E] cannot download plm-list.json"); - } - } - -} - -final instance = NodeBaseController(); \ No newline at end of file diff --git a/lib/ctrl/platform_def.dart b/lib/ctrl/platform_def.dart deleted file mode 100755 index 3bd7483..0000000 --- a/lib/ctrl/platform_def.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'dart:async'; - -abstract class IPlatform { - void changeBaseUrl(String url); - Future isSupported(); - String getName(); - String getNodeBaseJsonFilename(); - String getApplicationListJsonFilename(); - String getPlatformListJsonFilename(); - Future getApplicationBaseDir(String name, String version); - Future getPlatformBaseDir(String name, String version); - Future> readNodeBaseJson(); - Future> readApplicationListJson(); - Future> readPlatformListJson(); - Future> readApplicationMetaJson(String name, String version); - Future> readPlatformMetaJson(String name, String version); - Future downloadNodeBaseJson({StreamController? cancel}); - Future downloadApplicationListJson({StreamController? cancel}); - Future downloadPlatformListJson({StreamController? cancel}); - Future downloadApplicationMetaJson(String name, String version, {StreamController? cancel}); - Future downloadPlatformMetaJson(String name, String version, {StreamController? cancel}); - Future downloadApplicationBinary(String name, String version, String platform, String url, {StreamController? cancel}); - Future downloadPlatformBinary(String name, String version, String url, {StreamController? cancel}); - Future downloadCancel(String targetFilename); - Future>> listAvailableApplicationList(); - Future>> listInstalledApplicationList(); - Future> readApplicationConfig(String name, String version); - Future writeApplicationConfig(String name, String version, Map json); - Future removeApplicationBinary(String name, String version, String platform); - Future>> listAvailablePlatformList(); - Future>> listInstalledPlatformList(); - Future> listPlatformExecutableList(String name, String version); - Future> readPlatformConfig(String name, String version); - Future writePlatformConfig(String name, String version, Map json); - Future removePlatformBinary(String name, String version); -} \ No newline at end of file diff --git a/lib/ctrl/platform_local.dart b/lib/ctrl/platform_local.dart deleted file mode 100755 index 8cf472f..0000000 --- a/lib/ctrl/platform_local.dart +++ /dev/null @@ -1,540 +0,0 @@ -// manage platform like executable files, application files - -import 'dart:async'; -import 'dart:io'; -import 'package:path/path.dart' as path; - -import './platform_def.dart'; -import '../util/api.dart'; -import '../util/fs.dart'; -import '../util/event.dart' as event; - -/* - * == remote == - * /nodebase.json -> /workspace/tmp/nodebase.json - * - version - * - platform-- - * e.g. - * - version: 1.0 - * - platform-android-arm64: 20240101 - * - * /app--.json -> /workspace/tmp/app--.json - * - name - * - version - * /plm--.json -> /workspace/tmp/plm--.json - * - name - * - version - * /app///(-).json - * - url - * - sha256 - * - description - * - timestamp - * - entry - * /plm///(-).json - * - url - * - sha256 - * - description - * - timestamp - * - entry - * - * == local == - * /workspace/etc/nodebase/app.json - * - version - * - sources - * /workspace/etc/nodebase/sources/plm/().list - * /workspace/etc/nodebase/plm/all.list - * - name - * - version - * - source - * - meta_json - * /workspace/etc/nodebase/plm/(-).json - * - name - * - version - * - manifest (files, directories) - * - filename - * - type=exe,none - * /workspace/etc/nodebase/sources/app/().list - * /workspace/etc/nodebase/app/all.list - * - name - * - version - * - source - * - meta_json - * /workspace/etc/nodebase/app/(-).json - * - name - * - version - * - requirements - * - platform - * /workspace/etc/plm/(-)/... - * - name - * - version - * - source - * - executable - * - filename - * - type=exe,none - * /workspace/etc/app/(-)/... - * /workspace/app/... - * /workspace/plm/... - */ - -class _DownloadItem { - _DownloadItem({ - required this.action, - required this.cancel, - }); - - final Completer action; - final StreamController cancel; -} - -class PlatformLocal implements IPlatform { - PlatformLocal({ - required this.baseUrl, - required this.baseDir, - required this.os, - required this.arch}); - String baseUrl; - String baseDir; - String os; - String arch; - - final Map _downloadQueue = {}; - - @override - String getName() => "$os-$arch"; - - String _getEtcBaseDir() => path.join(baseDir, "workspace", "etc"); - String _getTmpBaseDir() => path.join(baseDir, "workspace", "tmp"); - String _getApplicationBaseDir() => path.join(baseDir, "workspace", "app"); - String _getPlatformBaseDir() => path.join(baseDir, "workspace", "plm"); - - @override - String getNodeBaseJsonFilename() => path.join(_getEtcBaseDir(), "nodebase", "nodebase.json"); - @override - String getApplicationListJsonFilename() => path.join(_getEtcBaseDir(), "nodebase", "app-$os-$arch.json"); - @override - String getPlatformListJsonFilename() => path.join(_getEtcBaseDir(), "nodebase", "plm-$os-$arch.json"); - - @override - void changeBaseUrl(String url) { baseUrl = url; } - Future isSupported() async { - final config = await readNodeBaseJson(); - return config.containsKey("platform-$os-$arch"); - } - - @override - Future getApplicationBaseDir(String name, String version) async { - return path.join(_getApplicationBaseDir(), await fsCalcStringHash("$name-$version")); - } - @override - Future getPlatformBaseDir(String name, String version) async { - return path.join(_getPlatformBaseDir(), await fsCalcStringHash("$name-$version")); - } - - @override - Future> readNodeBaseJson() async => - fsReadFileAsJson(path.join(_getEtcBaseDir(), "nodebase", "nodebase.json")); - @override - Future> readApplicationListJson() async => - fsReadFileAsJson(path.join(_getEtcBaseDir(), "nodebase", "app-$os-$arch.json")); - @override - Future> readPlatformListJson() async => - fsReadFileAsJson(path.join(_getEtcBaseDir(), "nodebase", "plm-$os-$arch.json")); - @override - Future> readApplicationMetaJson(String name, String version) async => - fsReadFileAsJson(path.join(_getEtcBaseDir(), "app", await fsCalcStringHash("$name-$version"), "meta.json")); - @override - Future> readPlatformMetaJson(String name, String version) async => - fsReadFileAsJson(path.join(_getEtcBaseDir(), "plm", await fsCalcStringHash("$name-$version"), "meta.json")); - - Future _downloadFile( - String name, - String url, - String targetFilename, - StreamController? cancel) async { - final signal = StreamController(); - cancel ??= StreamController(); - await fsGuaranteeDir(targetFilename); - event.platformToken.add(["download", name, url, targetFilename, 0]); - final download = fsProgressDownload(targetFilename, url, signal, cancel); - signal.stream.listen((message) { - final progress = message[2]; - final len = message[1]; - final cur = message[0]; - if (progress == -1) { - // cancel or error - event.platformToken.add(["download", name, url, targetFilename, -1]); - } else if (len == 0) { - if (cur == 0) event.platformToken.add(["download", name, url, targetFilename, -99]); - } else { - event.platformToken.add(["download", name, url, targetFilename, progress]); - } - }); - return download; - } - @override - Future downloadNodeBaseJson({StreamController? cancel}) async { - final tmpFilename = path.join(_getTmpBaseDir(), "nodebase.json"); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("nodebase.json", "$baseUrl/nodebase.json", tmpFilename, cancel); - final targetFilename = getNodeBaseJsonFilename(); - await fsGuaranteeDir(targetFilename); - final tmpFile = File(tmpFilename); - await tmpFile.copy(targetFilename); - await tmpFile.delete(); - action.complete(); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadApplicationListJson({StreamController? cancel}) async { - final tmpFilename = path.join(_getTmpBaseDir(), "app-$os-$arch.json"); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("app-$os-$arch.json", "$baseUrl/app-$os-$arch.json", tmpFilename, cancel); - final targetFilename = getApplicationListJsonFilename(); - await fsGuaranteeDir(targetFilename); - final tmpFile = File(tmpFilename); - await tmpFile.copy(targetFilename); - await tmpFile.delete(); - action.complete(); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadPlatformListJson({StreamController? cancel}) async { - final tmpFilename = path.join(_getTmpBaseDir(), "plm-$os-$arch.json"); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("plm-$os-$arch.json", "$baseUrl/plm-$os-$arch.json", tmpFilename, cancel); - final targetFilename = getPlatformListJsonFilename(); - await fsGuaranteeDir(targetFilename); - final tmpFile = File(tmpFilename); - await tmpFile.copy(targetFilename); - await tmpFile.delete(); - action.complete(); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadApplicationMetaJson(String name, String version, {StreamController? cancel}) async { - // python3 -c 'from hashlib import sha256; x="node-v20.11.0";print(x, sha256(x.encode("utf-8")).hexdigest())' - final hash = await fsCalcStringHash("$name-$version"); - final tmpFilename = path.join(_getTmpBaseDir(), "app-$hash-meta.json"); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("app-$name-$version.json", "$baseUrl/app/$os/$arch/$hash.json", tmpFilename, cancel); - final targetFilename = path.join(_getEtcBaseDir(), "app", hash, "meta.json"); - await fsGuaranteeDir(targetFilename); - final tmpFile = File(tmpFilename); - await tmpFile.copy(targetFilename); - await tmpFile.delete(); - action.complete(); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadPlatformMetaJson(String name, String version, {StreamController? cancel}) async { - final hash = await fsCalcStringHash("$name-$version"); - final tmpFilename = path.join(_getTmpBaseDir(), "plm-$hash-meta.json"); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("plm-$name-$version.json", "$baseUrl/plm/$os/$arch/$hash.json", tmpFilename, cancel); - final targetFilename = path.join(_getEtcBaseDir(), "plm", hash, "meta.json"); - await fsGuaranteeDir(targetFilename); - final tmpFile = File(tmpFilename); - await tmpFile.copy(targetFilename); - await tmpFile.delete(); - action.complete(); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadApplicationBinary(String name, String version, String platform, String url, {StreamController? cancel}) async { - final hash = await fsCalcStringHash("$name-$version"); - final baseName = path.basename(url); - final tmpFilename = path.join(_getTmpBaseDir(), baseName); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("app-$name-$version.bin", url, tmpFilename, cancel); - final tmpFile = File(tmpFilename); - Function(String, String)? unzipFn; - if (baseName.endsWith(".zip")) { - unzipFn = fsUnzipFiles; - } else if (baseName.endsWith(".tar")) { - unzipFn = fsUnzipTarFiles; - } else if (baseName.endsWith(".tar.xz")) { - unzipFn = fsUnzipXzTarFiles; - } else if (baseName.endsWith(".tar.gz") || baseName.endsWith(".tgz")) { - unzipFn = fsUnzipGzTarFiles; - } else if (baseName.endsWith(".tar.bz2") || baseName.endsWith(".tbz2")) { - unzipFn = fsUnzipBzTarFiles; - } - if (unzipFn == null) { - final targetFilename = path.join(_getApplicationBaseDir(), hash, baseName); - await fsGuaranteeDir(targetFilename); - await tmpFile.copy(targetFilename); - } else { - final targetFilename = path.join(_getApplicationBaseDir(), hash); - await fsMkdir(targetFilename); - await unzipFn(tmpFilename, targetFilename); - } - await tmpFile.delete(); - await _configListAdd("app-list.json", name, "$version:$platform"); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadPlatformBinary(String name, String version, String url, {StreamController? cancel}) async { - final hash = await fsCalcStringHash("$name-$version"); - final baseName = path.basename(url); - final tmpFilename = path.join(_getTmpBaseDir(), baseName); - final doing = _downloadQueue[tmpFilename]; - if (doing != null) return await doing.action.future; - final action = Completer(); - cancel ??= StreamController(); - _downloadQueue[tmpFilename] = _DownloadItem(action: action, cancel: cancel); - try { - await _downloadFile("plm-$name-$version.bin", url, tmpFilename, cancel); - final tmpFile = File(tmpFilename); - Function(String, String)? unzipFn; - if (baseName.endsWith(".zip")) { - unzipFn = fsUnzipFiles; - } else if (baseName.endsWith(".tar")) { - unzipFn = fsUnzipTarFiles; - } else if (baseName.endsWith(".tar.xz")) { - unzipFn = fsUnzipXzTarFiles; - } else if (baseName.endsWith(".tar.gz") || baseName.endsWith(".tgz")) { - unzipFn = fsUnzipGzTarFiles; - } else if (baseName.endsWith(".tar.bz2") || baseName.endsWith(".tbz2")) { - unzipFn = fsUnzipBzTarFiles; - } - if (unzipFn == null) { - final targetFilename = path.join(_getPlatformBaseDir(), hash, baseName); - await fsGuaranteeDir(targetFilename); - await tmpFile.copy(targetFilename); - await NodeBaseApi.apiUtilMarkExecutable(targetFilename); - } else { - final targetFilename = path.join(_getPlatformBaseDir(), hash); - await fsMkdir(targetFilename); - await unzipFn(tmpFilename, targetFilename); - for (final fname in await listPlatformExecutableList(name, version)) { - await NodeBaseApi.apiUtilMarkExecutable(path.join(targetFilename, fname)); - } - } - await tmpFile.delete(); - await _configListAdd("plm-list.json", name, version); - } catch (e) { - action.completeError(e); - } finally { - _downloadQueue.remove(tmpFilename); - } - } - @override - Future downloadCancel(String targetFilename) async { - final doing = _downloadQueue[targetFilename]; - if (doing == null) return; - doing.cancel.add(true); - await doing.action.future; - } - - Future _configListAdd(String target, String name, String version) async { - final filename = path.join(_getEtcBaseDir(), "nodebase", target); - final config = await fsReadFileAsJson(filename); - List list_ = config["items"] ?? []; - List list = []; - for (final one in list_) { - list.add(one.toString()); - } - final addone = "$name-$version"; - if (!list.contains(addone)) list.add(addone); - config["items"] = list; - await fsWriteFileAsJson(filename, config); - } - Future _configListRemove(String target, String name, String version) async { - final filename = path.join(_getEtcBaseDir(), "nodebase", target); - final config = await fsReadFileAsJson(filename); - List list_ = config["items"] ?? []; - List list = []; - for (final one in list_) { - list.add(one.toString()); - } - final addone = "$name-$version"; - int i = list.indexOf(addone); - if (i >= 0) { - list.removeAt(i); - } - config["items"] = list; - await fsWriteFileAsJson(filename, config); - } - - @override - Future>> listAvailableApplicationList() async { - // name-version:platform - final filename = path.join(_getEtcBaseDir(), "nodebase", "app-$os-$arch.json"); - final config = await fsReadFileAsJson(filename); - Map> r = {}; - for (final one in config["items"] ?? []) { - final onestr = one.toString(); - final i = onestr.indexOf('-'); - final name = onestr.substring(0, i); - final version = onestr.substring(i+1); - if (r.containsKey(name)) { - r[name]?.add(version); - } else { - r[name] = [version]; - } - } - return r; - } - @override - Future>> listInstalledApplicationList() async { - final filename = path.join(_getEtcBaseDir(), "nodebase", "app-list.json"); - final config = await fsReadFileAsJson(filename); - Map> r = {}; - for (final one in config["items"] ?? []) { - final onestr = one.toString(); - final i = onestr.indexOf('-'); - final name = onestr.substring(0, i); - final version = onestr.substring(i+1); - if (r.containsKey(name)) { - r[name]?.add(version); - } else { - r[name] = [version]; - } - } - return r; - } - @override - Future> readApplicationConfig(String name, String version) async { - final hash = await fsCalcStringHash("$name-$version"); - final filename = path.join(_getEtcBaseDir(), "app", hash, "config.json"); - return await fsReadFileAsJson(filename); - } - @override - Future writeApplicationConfig(String name, String version, Map json) async { - final hash = await fsCalcStringHash("$name-$version"); - final filename = path.join(_getEtcBaseDir(), "app", hash, "config.json"); - await fsWriteFileAsJson(filename, json); - } - @override - Future removeApplicationBinary(String name, String version, String platform) async { - final hash = await fsCalcStringHash("$name-$version"); - final dir = Directory(path.join(_getApplicationBaseDir(), hash)); - await _configListRemove("app-list.json", name, "$version:$platform"); - if (!dir.existsSync()) return; - await dir.delete(recursive: true); - } - - @override - Future>> listAvailablePlatformList() async { - final filename = path.join(_getEtcBaseDir(), "nodebase", "plm-$os-$arch.json"); - final config = await fsReadFileAsJson(filename); - Map> r = {}; - for (final one in config["items"] ?? []) { - final onestr = one.toString(); - final i = onestr.indexOf('-'); - final name = onestr.substring(0, i); - final version = onestr.substring(i+1); - if (r.containsKey(name)) { - r[name]?.add(version); - } else { - r[name] = [version]; - } - } - return r; - } - @override - Future>> listInstalledPlatformList() async { - // name-version - final filename = path.join(_getEtcBaseDir(), "nodebase", "plm-list.json"); - final config = await fsReadFileAsJson(filename); - Map> r = {}; - for (final one in config["items"] ?? []) { - final onestr = one.toString(); - final i = onestr.indexOf('-'); - final name = onestr.substring(0, i); - final version = onestr.substring(i+1); - if (r.containsKey(name)) { - r[name]?.add(version); - } else { - r[name] = [version]; - } - } - return r; - } - @override - Future> listPlatformExecutableList(String name, String version) async { - final meta = await readPlatformMetaJson(name, version); - List r = []; - for (final one in meta["executable"] ?? []) { - r.add(one.toString()); - } - return r; - } - @override - Future> readPlatformConfig(String name, String version) async { - final hash = await fsCalcStringHash("$name-$version"); - final filename = path.join(_getEtcBaseDir(), "plm", hash, "config.json"); - return await fsReadFileAsJson(filename); - } - @override - Future writePlatformConfig(String name, String version, Map json) async { - final hash = await fsCalcStringHash("$name-$version"); - final filename = path.join(_getEtcBaseDir(), "plm", hash, "config.json"); - await fsWriteFileAsJson(filename, json); - } - @override - Future removePlatformBinary(String name, String version) async { - final hash = await fsCalcStringHash("$name-$version"); - final dir = Directory(path.join(_getPlatformBaseDir(), hash)); - await _configListRemove("plm-list.json", name, version); - if (!dir.existsSync()) return; - // XXX: by default, on windows, MAX_PATh = 260, if too long, will fail - // ref: https://learn.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=registry - // ref: https://stackoverflow.com/questions/3277717/c-winapi-handling-long-file-paths-names (prefix \\?\) - await dir.delete(recursive: true); - } -} diff --git a/lib/main.dart b/lib/main.dart deleted file mode 100755 index fbe025f..0000000 --- a/lib/main.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import './page/splash.dart'; - -void main() { - runApp(const NodeBaseApp()); -} - -class NodeBaseApp extends StatelessWidget { - const NodeBaseApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'NodeBase', - theme: ThemeData( - // This is the theme of your application. - // - // TRY THIS: Try running your application with "flutter run". You'll see - // the application has a purple toolbar. Then, without quitting the app, - // try changing the seedColor in the colorScheme below to Colors.green - // and then invoke "hot reload" (save your changes or press the "hot - // reload" button in a Flutter-supported IDE, or press "r" if you used - // the command line to start the app). - // - // Notice that the counter didn't reset back to zero; the application - // state is not lost during the reload. To reset the state, use hot - // restart instead. - // - // This works for code too, not just values: Most code changes can be - // tested with just a hot reload. - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, - ), - home: const SplashPage(), - ); - } -} diff --git a/lib/page/not_supported.dart b/lib/page/not_supported.dart deleted file mode 100755 index e5cddd4..0000000 --- a/lib/page/not_supported.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import '../ctrl/nodebase.dart' as nodebase; - -class NotSupportedPage extends StatefulWidget { - const NotSupportedPage({super.key}); - - @override - State createState() => _NotSupportedPageState(); -} - -class _NotSupportedPageState extends State { - @override - Widget build(BuildContext context) { - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Image(image: AssetImage('asset/image/logo.png')), - const Text( - "\nNodeBase", - style: TextStyle(fontSize: 24, color: Color.fromARGB(128, 0, 0, 0)), - ), - Row( - children: [ - const Spacer(), - Row( - children: [ - Column( - children: [ - const Text( - "Sorry, we are not yet have", - style: TextStyle(color: Color.fromARGB(128, 0, 0, 0)), - ), - Row( - children: [ - const Text( - "support for ", - style: TextStyle(color: Color.fromARGB(128, 0, 0, 0)), - ), - Text( - "\"${nodebase.instance.platform.getName()}\"", - style: const TextStyle(color: Color.fromARGB(255, 255, 128, 128)), - ), - ], - ), - const Text(" "), - ], - ), - ] - ), - const Spacer(), - ], - ), - ElevatedButton(onPressed: () => { - if (Platform.isAndroid) { - SystemChannels.platform.invokeMethod('SystemNavigator.pop') - } else { - exit(0) - } - }, child: const Text("Exit")) - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/page/primay.dart b/lib/page/primay.dart deleted file mode 100755 index bff9cf2..0000000 --- a/lib/page/primay.dart +++ /dev/null @@ -1,362 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import '../util/api.dart'; -import '../util/event.dart' as event; -import '../ctrl/nodebase.dart' as nodebase; -import '../comp/app_runtime_tile.dart'; -import '../comp/download_tile.dart'; -import '../comp/app_tile.dart'; -import '../comp/platform_tile.dart'; - -class PrimaryPage extends StatefulWidget { - const PrimaryPage({super.key}); - - @override - State createState() => _PrimaryPageState(); -} - -class _PrimaryPageState extends State { - List runtimeList = []; - List downloadList = []; - List appList = []; - List plmList = []; - - late StreamSubscription downloadProgress; - late StreamSubscription applicationEvent; - - updateDependencyState() { - Map appRef = {}; - Map plmRef = {}; - for (final one in appList) { - appRef["${one.name}-${one.version}"] = 0; - } - for (final one in plmList) { - plmRef["${one.name}-${one.version}"] = 0; - } - for (final one in runtimeList) { - appRef[one.process.getName()] = (appRef[one.process.getName()] ?? 0) + 1; - plmRef[one.process.getPlatform()] = (plmRef[one.process.getName()] ?? 0) + 1; - } - for (int i = 0, n = appList.length; i < n; i++) { - final one = appList[i]; - final running = (appRef["${one.name}-${one.version}"] ?? 0) > 0; - if (running != one.defaultRunning) { - appList[i] = AppTile( - name: one.name, - version: one.version, - platform: one.platform, - defaultInstalled: one.defaultInstalled, - defaultRunning: running - ); - } - } - for (int i = 0, n = plmList.length; i < n; i++) { - final one = plmList[i]; - final running = (plmRef["${one.name}-${one.version}"] ?? 0) > 0; - if (running != one.defaultRunning) { - plmList[i] = PlatformTile( - name: one.name, - version: one.version, - defaultInstalled: one.defaultInstalled, - defaultRunning: running - ); - } - } - } - - @override - void initState() { - super.initState(); - - downloadProgress = event.platformToken.stream.listen((msg) { - final T = msg[0]; - if (T != "download") return; - final tnv = msg[1]; - if (tnv is! String) return; - if (!tnv.endsWith(".bin")) return; - final parts = tnv.substring(0, tnv.length - 4).split("-"); - // TODO: check parts.length - final type = parts[0] == "app" ? "application" : "platform"; - final name = "${parts[1]}-${parts[2]}"; - final url = msg[2]; - final filename = msg[3]; - final val = msg[4]; - try { - final mark = downloadList.firstWhere( - (one) => one.url == url && - one.filename == filename, - ); - if (val >= 1) { - final i = downloadList.indexOf(mark); - if (i >= 0) { - setState(() { - downloadList.removeAt(i); - }); - } - } else if (val == -1) { - // cancel or error - final i = downloadList.indexOf(mark); - if (i >= 0) { - setState(() { - downloadList.removeAt(i); - }); - } - } - } catch (e) { - if (val >= 0 && val < 1) { - setState(() { - downloadList.add(DownloadTile(name: name, type: type, url: url, filename: filename)); - }); - } - } - }); - - applicationEvent = NodeBaseApi.event.receiveBroadcastStream("app").listen((message) { - final cmd = message[0]; - final nameVersion = message[1]; - final app = nodebase.instance.application.getApp(nameVersion); - AppRuntimeTile? tile; - try { - tile = runtimeList.firstWhere((one) => one.process == app || one.process.getName() == nameVersion); - } catch(err) { - tile = null; - } - switch(cmd) { - case "start": - if (app == null || tile != null) return; - runtimeList.add(AppRuntimeTile(process: app)); - updateDependencyState(); - setState(() {}); - break; - case "stop": - if (tile == null) return; - final i = runtimeList.indexOf(tile); - runtimeList.removeAt(i); - updateDependencyState(); - setState(() {}); - break; - } - }); - - initPlatformList(); - initApplicationList(); - } - - @override - void dispose() { - downloadProgress.cancel(); - applicationEvent.cancel(); - super.dispose(); - } - - void initPlatformList() { - (() async { - final platform = nodebase.instance.platform; - return await Future.wait([ - platform.listAvailablePlatformList(), - platform.listInstalledPlatformList(), - ]); - })().then((list) { - final [listAp, listIp] = list; - final List r = []; - listIp.forEach((name, versions) { - for (final version in versions) { - if (listAp.containsKey(name) && listAp[name]!.contains(version)) continue; - final p = PlatformTile( - name: name, - version: version, - defaultInstalled: true, - userDefined: true - ); - r.add(p); - } - }); - listAp.forEach((name, versions) { - for (final version in versions) { - final installed = listIp.containsKey(name) && listIp[name]!.contains(version); - final p = PlatformTile( - name: name, - version: version, - defaultInstalled: installed, - userDefined: false - ); - r.add(p); - } - }); - // TODO: sort r and installed ones should at top - setState(() { - plmList.clear(); - plmList.addAll(r); - }); - }); - } - - void initApplicationList() { - (() async { - final platform = nodebase.instance.platform; - return await Future.wait([ - platform.listAvailableApplicationList(), - platform.listInstalledApplicationList(), - ]); - })().then((list) { - final [listAa, listIa] = list; - final List r = []; - listIa.forEach((name, vps) { - for (final vp in vps) { - if (listAa.containsKey(name) && listAa[name]!.contains(vp)) continue; - final i = vp.indexOf(':'); - final version = vp.substring(0, i); - final platform = vp.substring(i+1); - final a = AppTile( - name: name, - version: version, - defaultInstalled: true, - platform: platform, - userDefined: true - ); - r.add(a); - } - }); - listAa.forEach((name, vps) { - for (final vp in vps) { - final i = vp.indexOf(':'); - final version = vp.substring(0, i); - final platform = vp.substring(i+1); - final installed = listIa.containsKey(name) && listIa[name]!.contains(vp); - final a = AppTile( - name: name, - version: version, - defaultInstalled: installed, - platform: platform, - userDefined: false - ); - r.add(a); - } - }); - setState(() { - appList.clear(); - appList.addAll(r); - }); - }); - } - - List buildRunningView(BuildContext context) { - final List runningView = [ - const Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Wrap( - spacing: 2, - children: [ - Text(" "), - Icon(Icons.directions_run), - Text(" Running"), - ], - ), - ], - ), - ]; - if (runtimeList.isNotEmpty) { - runningView.add( - ListView.builder( - itemCount: runtimeList.length, - shrinkWrap: true, - itemBuilder: (BuildContext context, int index) => runtimeList[index] - ), - ); - } - if (downloadList.isNotEmpty) { - runningView.add( - ListView.builder( - itemCount: downloadList.length, - shrinkWrap: true, - itemBuilder: (BuildContext context, int index) => downloadList[index] - ), - ); - } - return runningView; - } - - List buildApplicationView(BuildContext context) { - final List appView = [ - const Divider(), - const Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Wrap( - spacing: 2, - children: [ - Text(" "), - Icon(Icons.extension_rounded), - Text(" Application"), - ], - ), - ], - ), - ]; - if (appList.isNotEmpty) { - appView.add( - ListView.builder( - itemCount: appList.length, - shrinkWrap: true, - itemBuilder: (BuildContext context, int index) => appList[index] - ), - ); - } - return appView; - } - - List buildPlatformView(BuildContext context) { - final List plmView = [ - const Divider(), - const Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Wrap( - spacing: 2, - children: [ - Text(" "), - Icon(Icons.apps), - Text(" Platform"), - ], - ), - ], - ), - ]; - if (plmList.isNotEmpty) { - plmView.add( - ListView.builder( - itemCount: plmList.length, - shrinkWrap: true, - itemBuilder: (BuildContext context, int index) => plmList[index] - ), - ); - } - return plmView; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - // TRY THIS: Try changing the color here to a specific color (to - // Colors.amber, perhaps?) and trigger a hot reload to see the AppBar - // change color while the other colors stay the same. - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - // Here we take the value from the MyHomePage object that was created by - // the App.build method, and use it to set our appbar title. - title: const Text("NodeBase"), - ), - body: SingleChildScrollView( - child: Column( - children: [ - ...buildRunningView(context), - ...buildApplicationView(context), - ...buildPlatformView(context), - ], - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/page/splash.dart b/lib/page/splash.dart deleted file mode 100755 index 1a3e60f..0000000 --- a/lib/page/splash.dart +++ /dev/null @@ -1,88 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import './not_supported.dart'; -import './primay.dart'; -import '../util/event.dart' as event; -import '../ctrl/nodebase.dart' as nodebase; - -class SplashPage extends StatefulWidget { - const SplashPage({super.key}); - - @override - State createState() => _SplashPageState(); -} - -class _SplashPageState extends State { - late StreamSubscription initListener; - Completer initCompleter = Completer(); - - @override - void initState() { - super.initState(); - nodebase.instance.initializeApp(); - initListener = event.initializeToken.stream.listen((event) { - if (!initCompleter.isCompleted) { - initCompleter.complete(); - } - }); - splashWait().then((_) { - if (nodebase.instance.isSupported) { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const PrimaryPage()), - (Route route) => false, - ); - } else { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute(builder: (context) => const NotSupportedPage()), - (Route route) => false, - ); - } - }); - // debug: Future.delayed(const Duration(seconds: 8)).then((_) => event.initializeToken.add(true)); - } - - @override - void dispose() { - initListener.cancel(); - super.dispose(); - } - - Future splashWait() async { - // at least, splash wait for N seconds, N = 3 - // at least, guarantee init complete - Future delay = Future.delayed(const Duration(seconds: 3)); - await Future.any([initCompleter.future, delay]); - if (initCompleter.isCompleted) { - await delay; - } else { - await initCompleter.future; - } - } - - @override - Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image(image: AssetImage('asset/image/logo.png')), - Text( - "\nNodeBase", - style: TextStyle(fontSize: 24, color: Color.fromARGB(128, 0, 0, 0)), - ), - Text( - "Connect the world with friends", - style: TextStyle(color: Color.fromARGB(65, 0, 0, 0)), - ), - ], - ), - ), - ); - } - -} diff --git a/lib/util/api.dart b/lib/util/api.dart deleted file mode 100755 index 8f2aaae..0000000 --- a/lib/util/api.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'dart:developer'; - -class NodeBaseApi { - static const api = MethodChannel('net.seven.nodebase/app'); - static const event = EventChannel('net.seven.nodebase/event'); - - static Future> apiUtilGetIPs() async { - /* { - : ["", "", ...] - } */ - try { - var json = await api.invokeMethod('util.ip'); - Map r = {}; - json.forEach((key, value) { - r[key.toString()] = value; - }); - return Future.value(r); - } catch (e) { - log("NodeBase [E] getIPs / ${e.toString()}"); - return {}; - } - } - - static Future apiUtilMarkExecutable(String filename) async { - try { - await api.invokeMethod( - 'util.file.executable', - {"filename": filename} - ); - } catch (e) { - log("NodeBase [E] utilMarkExecutable / ${e.toString()}"); - } - } - - static Future apiUtilGetArch() async { - try { - return await api.invokeMethod("util.arch"); - } catch(e) { - log("NodeBase [E] utilGetArch / ${e.toString()}"); - return ""; - } - } - - static Future apiUtilGetWorkspacePath() async { - try { - return await api.invokeMethod("util.workspace"); - } catch(e) { - log("NodeBase [E] utilGetWorkspacePath / ${e.toString()}"); - return ""; - } - } - - static Future> apiAppStatus(String app) async { - /* { - state: "none" | "new" | "running" | "dead" - } */ - try { - var json = await api.invokeMethod('app.stat', {"name": app}); - Map r = {}; - json.forEach((key, value) { - r[key.toString()] = value; - }); - return r; - } catch (e) { - log("NodeBase [E] appStatus / ${e.toString()}"); - return {}; - } - } - - static Future apiAppStart( - String app, - List cmd, - { Map env = const {} } - ) async { - try { - await api.invokeMethod( - 'app.start', {"name": app, "cmd": cmd, "env": env} - ); - } catch (e) { - log("NodeBase [E] appStart / ${e.toString()}"); - } - } - - static Future apiAppStop(String app) async { - try { - await api.invokeMethod( - 'app.stop', {"name": app} - ); - } catch (e) { - log("NodeBase [E] appStop / ${e.toString()}"); - } - } - - static Future apiAppOpenBrowser(String url) async { - try { - api.invokeMethod('util.browser.open', { - "url": url, - }); - } catch (e) { - log("NodeBase [E] appOpenBrowser / ${e.toString()}"); - } - } -} \ No newline at end of file diff --git a/lib/util/event.dart b/lib/util/event.dart deleted file mode 100755 index 70d1bcb..0000000 --- a/lib/util/event.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'dart:async'; - -final initializeToken = StreamController.broadcast(); -final platformToken = StreamController.broadcast(); \ No newline at end of file diff --git a/lib/util/fs.dart b/lib/util/fs.dart deleted file mode 100755 index aadfcf3..0000000 --- a/lib/util/fs.dart +++ /dev/null @@ -1,352 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:developer'; - -import 'package:archive/archive.dart'; -import 'package:crypto/crypto.dart'; -import 'package:path/path.dart' as path; - -import './api.dart'; - -// getApplicationDocumentsDirectory -> /data/data/app/... -// getExternalStorageDirectory -> /storage/sdcard-external/android/data/app/... - -var _appPath_ = ""; -Future get _appPath async { - if (_appPath_ != "") return _appPath_; - _appPath_ = await NodeBaseApi.apiUtilGetWorkspacePath(); - log("NodeBase [I] app path: $_appPath_"); - return _appPath_; -} - -Future fsGetBaseDir() async { - return await _appPath; -} - -Future fsReadFileAsString(filepath) async { - try { - final baseDir = await _appPath; - if (!filepath.startsWith(baseDir)) return ""; - final file = File(filepath); - String contents = await file.readAsString(); - return contents; - } catch (e) { - log("NodeBase [E] fsReadAppFileAsString ... ${e.toString()}"); - return ""; - } -} - -Future fsWriteFileAsString(filepath, contents) async { - final baseDir = await _appPath; - if (!filepath.startsWith(baseDir)) return; - final file = File(filepath); - file.writeAsString(contents); -} - -Future> fsReadFileAsJson(String filepath) async { - Map r = {}; - try { - final baseDir = await _appPath; - if (!filepath.startsWith(baseDir)) return r; - final text = await fsReadFileAsString(filepath); - r = jsonDecode(text); - } catch(e) { - log("NodeBase [E] fsReadFileAsString ... $e"); - } - return r; -} - -Future fsWriteFileAsJson(String filepath, Map config) async { - final f = File(filepath); - try { - await fsGuaranteeDir(f.path); - final text = jsonEncode(config); - await fsWriteFileAsString(filepath, text); - } catch(e) { - log("NodeBase [E] fsWriteFileAsString ... $e"); - } - return f; -} - -Future fsGetEntity(filepath) async { - final base = await _appPath; - final filename = path.join(base, filepath); - final T = await FileSystemEntity.type(filename); - if (T == FileSystemEntityType.notFound) { - return "notFound"; - } - if (T == FileSystemEntityType.link) { - return Link(filepath); - } - if (T == FileSystemEntityType.file) { - return File(filepath); - } - return Directory(filepath); -} - -Future fsMkdir(filepath) async { - final base = await _appPath; - return await Directory(path.join(base, filepath)).create(recursive: true); -} - -Future> fsLs(filepath) async { - final base = await _appPath; - final filename = path.join(base, filepath); - final list = []; - final T = await FileSystemEntity.type(filename); - if (T == FileSystemEntityType.notFound) { - } else if (T == FileSystemEntityType.link) { - // ignore files under link directory - list.add(Link(filename)); - } else if (T == FileSystemEntityType.file) { - list.add(File(filename)); - } else { - final dir = Directory(filename); - final entities = await dir.list(recursive: false, followLinks: false).toList(); - for (var entity in entities) { - list.add(entity); - } - } - return list; -} - -Future fsGetAppBaseDir(String app) async { - // TODO: check ".." in app - final base = await _appPath; - final appBaseDir = path.join(base, "apps", app); - return appBaseDir; -} - -Future fsRemoveApp(String app) async { - // TODO: check ".." in app - if (app == "") return false; - final base = await _appPath; - final appBaseDir = path.join(base, "apps", app); - final dir = Directory(appBaseDir); - if (await dir.exists()) { - await dir.delete(recursive: true); - } - return true; -} - -Future fsMoveApp(String app, String newname) async { - // TODO: check ".." in app and newname - if (app == "" || newname == "" || app == newname) return false; - final base = await _appPath; - final appBaseDir = path.join(base, "apps", app); - final newBaseDir = path.join(base, "app", newname); - final dir = Directory(appBaseDir); - if (await dir.exists()) { - await dir.rename(newBaseDir); - } - return true; -} - -Future fsZipFiles(String zipFilename, List files) async { - try { - // Create an empty archive - final archive = Archive(); - - for (final file in files) { - // Read the file bytes - final bytes = file.readAsBytesSync(); - - // Add the file to the archive - archive.addFile(ArchiveFile( - path.basename(file.path), // File name - bytes.length, // File size - bytes, // File data - )); - } - - // Encode the archive to Zip - final zipData = ZipEncoder().encode(archive); - - // Write the zipped bytes to a file - File(zipFilename) - ..createSync(recursive: true) // Create the file if it doesn't exist - ..writeAsBytesSync(zipData!); - } catch(e) { - log("NodeBase [E] fsZipFiles ... ${e.toString()}"); - } -} - -Future fsUnzipFiles(String zipFilename, String dstDir) async { - try { - final bytes = File(zipFilename).readAsBytesSync(); - // Decode the Zip archive - final archive = ZipDecoder().decodeBytes(bytes); - await fsUnzip(archive, dstDir); - } catch (e) { - log("NodeBase [E] fsUnzipFiles ... ${e.toString()}"); - } -} - -Future fsUnzipTarFiles(String tarFilename, String dstDir) async { - try { - final bytes = File(tarFilename).readAsBytesSync(); - // Decode the Zip archive - final archive = TarDecoder().decodeBytes(bytes); - await fsUnzip(archive, dstDir); - } catch (e) { - log("NodeBase [E] fsUnzipFiles ... ${e.toString()}"); - } -} - -Future fsUnzipXzTarFiles(String xztarFilename, String dstDir) async { - try { - final bytes = File(xztarFilename).readAsBytesSync(); - // Decode the Zip archive - final archive = TarDecoder().decodeBytes(XZDecoder().decodeBytes(bytes)); - await fsUnzip(archive, dstDir); - } catch (e) { - log("NodeBase [E] fsUnzipFiles ... ${e.toString()}"); - } -} - -Future fsUnzipGzTarFiles(String gztarFilename, String dstDir) async { - try { - final bytes = File(gztarFilename).readAsBytesSync(); - // Decode the Zip archive - final archive = TarDecoder().decodeBytes(GZipDecoder().decodeBytes(bytes)); - await fsUnzip(archive, dstDir); - } catch (e) { - log("NodeBase [E] fsUnzipFiles ... ${e.toString()}"); - } -} - -Future fsUnzipBzTarFiles(String bztarFilename, String dstDir) async { - try { - final bytes = File(bztarFilename).readAsBytesSync(); - // Decode the Zip archive - final archive = TarDecoder().decodeBytes(BZip2Decoder().decodeBytes(bytes)); - await fsUnzip(archive, dstDir); - } catch (e) { - log("NodeBase [E] fsUnzipFiles ... ${e.toString()}"); - } -} - -Future fsUnzip(Archive archive, String dstDir) async { - for (final file in archive) { - final filename = file.name; - if (file.isFile) { - final data = file.content as List; - File(path.join(dstDir, filename)) - ..createSync(recursive: true) - ..writeAsBytesSync(data); - } else { - Directory(path.join(dstDir, filename)) - .createSync(recursive: true); - } - } -} - -Future fsGuaranteeDir(String filename) async { - final dir = Directory(path.dirname(filename)); - if (!dir.existsSync()) { - await dir.create(recursive: true); - } -} - -Future fsDownload(String filename, String url) async { - var urlobj = Uri.parse(url); - var client = HttpClient(); - try { - final req = await client.getUrl(urlobj); - final res = await req.close(); - res.pipe(File(filename).openWrite()); - } finally { - client.close(); - } -} - -Future fsProgressDownload( - String filename, String url, - StreamController progressToken, - StreamController cancelToken) async { - // ref: filled by GPT 4 turbo and optimized - final httpClient = HttpClient(); - final done = Completer(); - bool canCancel = false; - try { - // Parse the URL - final uri = Uri.parse(url); - // Open a request - final request = await httpClient.getUrl(uri); - // Send the request - final response = await request.close(); - - // Check if the response is OK (status code 200) - if (response.statusCode == 200) { - // Get the total length of the file - final contentLength = response.contentLength; - int downloadedLength = 0; - - // Create a new file (overwrite if exists) - final file = File(filename); - final fileSink = file.openWrite(); - - // Listen for response data - final subscription = response.listen( - (List chunk) { - // Update the downloaded length - downloadedLength += chunk.length; - // Write the chunk to file - fileSink.add(chunk); - - // Calculate and print the download progress - double progressRate = contentLength == 0 ? 1 : (downloadedLength / contentLength); - progressToken.add([downloadedLength, contentLength, progressRate]); - log('NodeBase [D] fsProgressDownload ... progress ${(progressRate * 100).toStringAsFixed(2)}%'); - }, - onDone: () async { - // Close the fileSink to ensure all bytes are written - await fileSink.close(); - progressToken.add([-1, contentLength, 1]); - log('NodeBase [D] fsProgressDownload ... complete $filename'); - done.complete(); - }, - onError: (e) { - progressToken.add([-1, contentLength, -1]); - log('NodeBase [E] fsProgressDownload ... $e'); - done.complete('error'); - }, - cancelOnError: true, - ); - - canCancel = true; - cancelToken.stream.listen((_) { - progressToken.add([-1, contentLength, -1]); - subscription.cancel(); - fileSink.close(); - done.complete('cancel'); - httpClient.close(); - }); - } else { - log('NodeBase [E] fsProgressDownload ... http ${response.statusCode}'); - done.complete('error'); - } - } catch (e) { - log('NodeBase [E] fsProgressDownload ... $e - $url'); - done.complete('error'); - } finally { - await done.future; - httpClient.close(); - await progressToken.close(); - // if no stream listener, it will hang there - if (canCancel) await cancelToken.close(); - } -} - -Future fsCalcHash(String filename) async { - final f = File(filename); - if (!f.existsSync()) return ""; - final digest = await sha256.bind(f.openRead()).first; - return digest.toString(); -} - -Future fsCalcStringHash(String text) async { - final digest = sha256.convert(utf8.encode(text)); - return digest.toString(); -} diff --git a/lib/util/remote.dart b/lib/util/remote.dart deleted file mode 100755 index f5ca3bc..0000000 --- a/lib/util/remote.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -Future?> remoteGet(String url) async { - HttpClient client = HttpClient(); - try { - final req = await client.getUrl(Uri.parse(url)); - final response = await req.close(); - if (response.statusCode / 200 == 2) { - final stream = await response.transform(utf8.decoder).toList(); - final ret = jsonDecode(stream.first) as Map; - return ret; - } - throw "request failure"; - } catch (e) { - return null; - } finally { - client.close(); - } -} - -Future?> remotePost(String url, Map data) async { - HttpClient client = HttpClient(); - try { - final req = await client.postUrl(Uri.parse(url)); - req.headers.set("Content-Type", "application/json"); - req.write(data); - final response = await req.close(); - if (response.statusCode / 200 == 2) { - final stream = await response.transform(utf8.decoder).toList(); - final ret = jsonDecode(stream.first) as Map; - return ret; - } - throw "request failure"; - } catch (e) { - return null; - } finally { - client.close(); - } -} \ No newline at end of file diff --git a/linux/.gitignore b/linux/.gitignore deleted file mode 100755 index c7ea17f..0000000 --- a/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt deleted file mode 100755 index 40114da..0000000 --- a/linux/CMakeLists.txt +++ /dev/null @@ -1,145 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "nodebase") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "net.seven.nodebase.nodebase") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Define the application target. To change its name, change BINARY_NAME above, -# not the value here, or `flutter run` will no longer work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt deleted file mode 100755 index 27860e8..0000000 --- a/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/linux/flutter/flutter_dart.h b/linux/flutter/flutter_dart.h deleted file mode 100755 index 052913b..0000000 --- a/linux/flutter/flutter_dart.h +++ /dev/null @@ -1,463 +0,0 @@ -#ifndef _FLUTTER_DART_ -#define _FLUTTER_DART_ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -static FlEventChannel *eventHandler = nullptr; - -enum NodeAppSTAT { - BORN, READY, RUNNING, DEAD -}; - -class NodeAppMonitor { -public: - NodeAppMonitor(const std::string &name, const std::vector &cmd, const std::map &env) { - this->name = name; - this->cmd = cmd; - this->env = env; - this->stat = NodeAppSTAT::BORN; - this->curpid = -1; - this->pth = std::thread(NodeAppMonitor::Run, this); - } - ~NodeAppMonitor() { - this->Stop(); - if (this->pth.joinable()) this->pth.join(); - } - - void Start() { - this->stat = NodeAppSTAT::READY; - int argN = this->cmd.size(), envN = this->env.size(); - int status = 0; - const char *cmd = this->cmd.at(0).c_str(); - const char *args[argN+1], *env[envN+1]; - const char **cur; - - cur = args; - for (auto i = this->cmd.begin(); i != this->cmd.end(); i++) { - *cur = i->c_str(); - ++ cur; - } - *cur = nullptr; - - int envI = 0; - std::string envRaw[envN]; - for (auto i = this->env.begin(); i != this->env.end(); i++) { - envRaw[envI] = std::string(i->first) + "=" + std::string(i->second); - env[envI] = envRaw[envI].c_str(); - ++ envI; - } - env[envI] = nullptr; - - pid_t chpid = this->_startProcess(cmd, args, env); - if (chpid <= 0) { - this->stat = NodeAppSTAT::DEAD; - printf( - "NodeAppMonitor [E] \"%s\" start failure on CreateProcess.", - this->name.c_str() - ); - return; - } - this->curpid = chpid; - this->stat = NodeAppSTAT::RUNNING; - - if (eventHandler) { - g_autoptr(FlValue) message = fl_value_new_list(); - g_autoptr(GError) error = NULL; - fl_value_append(message, fl_value_new_string ("start")); - fl_value_append(message, fl_value_new_string (this->name.c_str())); - fl_event_channel_send(eventHandler, message, NULL, &error); - } - - waitpid(chpid, &status, 0); - this->curpid = -1; - this->stat = NodeAppSTAT::DEAD; - - if (eventHandler) { - g_autoptr(FlValue) message = fl_value_new_list(); - g_autoptr(GError) error = NULL; - fl_value_append(message, fl_value_new_string ("stop")); - fl_value_append(message, fl_value_new_string (this->name.c_str())); - fl_event_channel_send(eventHandler, message, NULL, &error); - } - } - - void Stop() { - if (!this->IsRunning()) return; - if (this->curpid <= 0) return; - int status = 0; - this->_stopProcessTree(this->curpid, SIGTERM); - waitpid(this->curpid, &status, 0); - } - - NodeAppMonitor* Restart() { - this->Stop(); - return new NodeAppMonitor(this->name, this->cmd, this->env); - } - void toJSON(FlValue* map) { - std::string rstat = "none"; - switch (this->stat) { - case NodeAppSTAT::RUNNING: - rstat = "running"; - break; - case NodeAppSTAT::DEAD: - rstat = "dead"; - break; - case NodeAppSTAT::READY: - rstat = "new"; - break; - case NodeAppSTAT::BORN: - default: - break; - } - g_autoptr(FlValue) statval = fl_value_new_string(rstat.c_str()); - fl_value_set_string(map, "stat", statval); - } - - bool IsRunning() { return this->stat == NodeAppSTAT::RUNNING; } - bool IsDead() { return this->stat == NodeAppSTAT::DEAD; } - - std::string GetName() { return this->name; } - std::vector GetCmd() { return this->cmd; } - std::map GetEnv() { return this->env; } -private: - static void Run(NodeAppMonitor* app) { - app->Start(); - } - - pid_t _startProcess(const char *cmd, const char **args, const char **env) { - pid_t pid = fork(); - if (pid == 0) { - execve(cmd, (char * const *)args, (char * const *)env); - exit(-1); - } else if (pid > 0){ - return pid; - } else { - return -1; - } - } - - bool _stopProcessTree(pid_t pid, int signal) { - DIR *dir; - struct dirent *entry; - char path[PATH_MAX]; - FILE *fp; - pid_t child_pid; - - // Open the /proc directory - dir = opendir("/proc"); - if (!dir) return false; - - // Iterate over each entry in /proc - while ((entry = readdir(dir)) != NULL) { - // Check if the entry is a directory and starts with a digit (representing a PID) - if (entry->d_type == DT_DIR && entry->d_name[0] >= '0' && entry->d_name[0] <= '9') { - sprintf(path, "/proc/%s/stat", entry->d_name); - fp = fopen(path, "r"); - if (fp) { - // Read the process information from the stat file - if (fscanf(fp, "%*d %*s %*c %d", &child_pid) == 1) { - // Check if the process is a child of the target process - if (child_pid == pid) { - // Kill the child process and its subprocesses recursively - this->_stopProcessTree(atoi(entry->d_name), signal); - } - } - fclose(fp); - } - } - } - - closedir(dir); - - // Kill the target process - kill(pid, signal); - return true; - } - -private: - NodeAppSTAT stat; - std::string name; - std::vector cmd; - std::map env; - std::thread pth; - pid_t curpid; -}; - -static std::map services; -static std::mutex service_lock; - -void appStart(const std::string &name, const std::vector &cmd, const std::map &env) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - NodeAppMonitor *app; - if (app_ != services.end()) { - app = app_->second; - services.erase(name); - delete app; - } - app = new NodeAppMonitor(name, cmd, env); - // TODO: if (!app) {} // memory allocate failure - services.insert({name, app}); -} -void appStop(const std::string &name) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - if (app_ == services.end()) return; - NodeAppMonitor *app = app_->second; - app->Stop(); -} -bool appRestart(const std::string &name) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - if (app_ == services.end()) return false; - NodeAppMonitor *app = app_->second; - app->Stop(); - NodeAppMonitor *newapp = app->Restart(); - if (newapp == nullptr) { - return false; - } - delete app; - services.insert({name, newapp}); - return true; -} -void appStat(const std::string &name, FlValue *map) { - auto app_ = services.find(name); - if (app_ == services.end()) return; - NodeAppMonitor *app = app_->second; - app->toJSON(map); -} - -void utilBrowserOpen(const std::string &url) { - if (url.rfind("http:", 0) != 0 && url.rfind("https:", 0) != 0) return; - pid_t pid = fork(); - if (pid == -1) { - fprintf(stderr, "cannot open url (fork failed)\n"); - return; - } else if (pid == 0) { - execlp("xdg-open", "xdg-open", url.c_str(), (char*)nullptr); - fprintf(stderr, "cannot open url (execlp failed)\n"); - exit(-1); - } -} - -bool utilFileMarkExecutable(const std::string &filename) { - struct stat fileStat; - if (stat(filename.c_str(), &fileStat)) {printf("%o\n", fileStat.st_mode);return false;} - mode_t newPerms = fileStat.st_mode | S_IXUSR; - if (chmod(filename.c_str(), newPerms)) return false; - return true; -} - -std::string utilGetArch() { - std::string arch("linux-"); - struct utsname uts; - if (uname(&uts) < 0) { - arch += "unknown"; - } else if (strcmp("x86_64", uts.machine) == 0 || strcmp("ia64", uts.machine) == 0) { - arch += "x64"; - } else if (strcmp("x86", uts.machine) == 0 || strcmp("i386", uts.machine) == 0 || strcmp("i686", uts.machine) == 0) { - arch += "x86"; - } else if (strcmp("aarch64", uts.machine) == 0 || strcmp("arm64", uts.machine) == 0 || strcmp("armv8l", uts.machine) == 0) { - arch += "arm64"; - } else if (strcmp("arm", uts.machine) == 0 || strcmp("armv7l", uts.machine) == 0) { - arch += "arm"; - } else { - arch += "unknown"; - } - return arch; -} - -std::string utilWorkspaceBaseDir() { - char buf[PATH_MAX]; - size_t len = readlink("/proc/self/exe", buf, sizeof(buf)); - if (len <= 0) { - buf[0] = '\0'; - } else { - dirname(buf); - } - return std::string(buf); -} - -bool _convertIpBinary2String(struct sockaddr *addr, char* buf, size_t buf_len) { - void *binAddr; - if (addr->sa_family == AF_INET) { - binAddr = &((struct sockaddr_in *)addr)->sin_addr; - } else if (addr->sa_family == AF_INET6) { - binAddr = &((struct sockaddr_in6 *)addr)->sin6_addr; - } else { - return false; - } - if (!inet_ntop(addr->sa_family, binAddr, buf, buf_len)) return false; - return true; -} -bool utilGetIps(std::map> &map) { - struct ifaddrs *ifaddr, *cur; - const char *ifa_name; - char addrBuf[INET6_ADDRSTRLEN]; - if (getifaddrs(&ifaddr)) return false; - for (cur = ifaddr; cur != NULL; cur = cur->ifa_next) { - if (!cur->ifa_addr) continue; - ifa_name = cur->ifa_name; - if (!_convertIpBinary2String(cur->ifa_addr, addrBuf, INET6_ADDRSTRLEN)) continue; - std::string name = std::string(ifa_name); - std::string ipval = std::string(addrBuf); - auto list_ = map.find(name); - if (list_ == map.end()) { - std::vector newlist; - newlist.push_back(ipval); - map.insert({name, newlist}); - } else { - list_->second.push_back(ipval); - } - } - return true; -} - -#define RETURN_BADARG_ERR(x) { fl_method_call_respond_error(method_call, "BAD_ARGS", "Invalid argument type for '" #x "'", nullptr, nullptr); return; } -void InitMethodChannel(FlView* flutter_instance) { - const static char *channel_name = "net.seven.nodebase/app"; - g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); - - auto channel = fl_method_channel_new( - fl_engine_get_binary_messenger(fl_view_get_engine(flutter_instance)), - channel_name, - FL_METHOD_CODEC(codec) - ); - fl_method_channel_set_method_call_handler( - channel, - [](FlMethodChannel *channel, FlMethodCall *method_call, gpointer user_data) { - g_autoptr(FlMethodResponse) response = nullptr; - const gchar *method_name = fl_method_call_get_name(method_call); - if (strcmp("app.stat", method_name) == 0) { - FlValue *args = fl_method_call_get_args(method_call); - if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP || fl_value_get_length(args) < 1) RETURN_BADARG_ERR(app.stat); - FlValue *name_ = fl_value_lookup_string(args, "name"); - if (fl_value_get_type(name_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.stat); - std::string name = std::string(fl_value_get_string(name_)); - g_autoptr(FlValue) appstat = fl_value_new_map(); - appStat(name, appstat); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(appstat)); - } else if (strcmp("app.start", method_name) == 0) { - FlValue *args = fl_method_call_get_args(method_call); - if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP || fl_value_get_length(args) < 3) RETURN_BADARG_ERR(app.start); - FlValue *name_ = fl_value_lookup_string(args, "name"); - if (fl_value_get_type(name_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.start); - FlValue *cmd_ = fl_value_lookup_string(args, "cmd"); - if (fl_value_get_type(cmd_) != FL_VALUE_TYPE_LIST) RETURN_BADARG_ERR(app.start); - FlValue *env_ = fl_value_lookup_string(args, "env"); - if (fl_value_get_type(env_) != FL_VALUE_TYPE_MAP) env_ = nullptr; - std::string name = std::string(fl_value_get_string(name_)); - - std::vector cmd; - for (size_t i = 0; i < fl_value_get_length(cmd_); i++) { - FlValue *cmdi = fl_value_get_list_value(cmd_, i); - if (fl_value_get_type(cmdi) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.start); - cmd.push_back(std::string(fl_value_get_string(cmdi))); - } - - std::map env; - for (size_t i = 0; env_ && i < fl_value_get_length(env_); i++) { - FlValue *envk_ = fl_value_get_map_key(env_, i); - if (fl_value_get_type(envk_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.start); - FlValue *envv_ = fl_value_get_map_value(env_, i); - if (fl_value_get_type(envv_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.start); - std::string envk = std::string(fl_value_get_string(envk_)); - std::string envv = std::string(fl_value_get_string(envv_)); - env.insert({envk, envv}); - } - - appStart(name, cmd, env); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); - } else if (strcmp("app.restart", method_name) == 0) { - FlValue *args = fl_method_call_get_args(method_call); - if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP || fl_value_get_length(args) < 1) RETURN_BADARG_ERR(app.restart); - FlValue *name_ = fl_value_lookup_string(args, "name"); - if (fl_value_get_type(name_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.restart); - std::string name = std::string(fl_value_get_string(name_)); - appRestart(name); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); - } else if (strcmp("app.stop", method_name) == 0) { - FlValue *args = fl_method_call_get_args(method_call); - if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP || fl_value_get_length(args) < 1) RETURN_BADARG_ERR(app.stop); - FlValue *name_ = fl_value_lookup_string(args, "name"); - if (fl_value_get_type(name_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(app.stop); - std::string name = std::string(fl_value_get_string(name_)); - appStop(name); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); - } else if (strcmp("util.ip", method_name) == 0) { - std::map> iptblmap; - utilGetIps(iptblmap); - g_autoptr(FlValue) iptbl = fl_value_new_map(); - for (auto i = iptblmap.begin(); i != iptblmap.end(); i++) { - g_autoptr(FlValue) list = fl_value_new_list(); - for (auto j = i->second.begin(); j != i->second.end(); j++) { - g_autoptr(FlValue) ip = fl_value_new_string(j->c_str()); - fl_value_append(list, ip); - } - fl_value_set_string(iptbl, i->first.c_str(), list); - } - response = FL_METHOD_RESPONSE(fl_method_success_response_new(iptbl)); - } else if (strcmp("util.file.executable", method_name) == 0) { - FlValue *args = fl_method_call_get_args(method_call); - if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP || fl_value_get_length(args) < 1) RETURN_BADARG_ERR(util.file.executable); - FlValue *filename_ = fl_value_lookup_string(args, "filename"); - if (fl_value_get_type(filename_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(util.file.executable); - std::string filename = std::string(fl_value_get_string(filename_)); - g_autoptr(FlValue) val = fl_value_new_bool(utilFileMarkExecutable(filename)); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(val)); - } else if (strcmp("util.browser.open", method_name) == 0) { - FlValue *args = fl_method_call_get_args(method_call); - if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP || fl_value_get_length(args) < 1) RETURN_BADARG_ERR(util.browser.open); - FlValue *url_ = fl_value_lookup_string(args, "url"); - if (fl_value_get_type(url_) != FL_VALUE_TYPE_STRING) RETURN_BADARG_ERR(util.browser.open); - std::string url = std::string(fl_value_get_string(url_)); - utilBrowserOpen(url); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); - } else if (strcmp("util.arch", method_name) == 0) { - g_autoptr(FlValue) val = fl_value_new_string(utilGetArch().c_str()); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(val)); - } else if (strcmp("util.workspace", method_name) == 0) { - g_autoptr(FlValue) val = fl_value_new_string(utilWorkspaceBaseDir().c_str()); - response = FL_METHOD_RESPONSE(fl_method_success_response_new(val)); - } else { - response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); - } - fl_method_call_respond(method_call, response, nullptr); - }, - g_strdup("custom_channel"), g_free - ); -} -#undef RETURN_BADARG_ERR - -void InitEventChannel(FlView* flutter_instance) { - const static char *channel_name = "net.seven.nodebase/event"; - g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); - eventHandler = fl_event_channel_new( - fl_engine_get_binary_messenger(fl_view_get_engine(flutter_instance)), - channel_name, - FL_METHOD_CODEC(codec) - ); -} -#endif // _FLUTTER_DART_ diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100755 index e71a16d..0000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100755 index e0f0a47..0000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100755 index 2e1de87..0000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc deleted file mode 100755 index 4340ffc..0000000 --- a/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/linux/my_application.cc b/linux/my_application.cc deleted file mode 100755 index 0b3d394..0000000 --- a/linux/my_application.cc +++ /dev/null @@ -1,108 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" -#include "flutter/flutter_dart.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "nodebase"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "nodebase"); - } - - gtk_window_set_default_size(window, 360, 720); - gtk_window_set_resizable(window, FALSE); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - InitMethodChannel(view); - InitEventChannel(view); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/linux/my_application.h b/linux/my_application.h deleted file mode 100755 index 8f20fb5..0000000 --- a/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100755 index d4e0569..0000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100755 index f022c34..0000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100755 index f022c34..0000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1 +0,0 @@ -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100755 index cccf817..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,10 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { -} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100755 index 6118882..0000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,695 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* nodebase.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "nodebase.app"; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* nodebase.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* nodebase.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nodebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nodebase"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nodebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nodebase"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/nodebase.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/nodebase"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 3dd3ce4..0000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100755 index 59c6d39..0000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100755 index fc6bf80..0000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100755 index 553a135..0000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index 8d4e7cb..0000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100755 index 82b6f9d..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100755 index 13b35eb..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100755 index 0a3f5fa..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100755 index bdb5722..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100755 index f083318..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100755 index 326c0e7..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100755 index 2f1632c..0000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100755 index 797e9e0..0000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100755 index e3e5ade..0000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = nodebase - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = net.seven.nodebase.nodebase - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 net.seven.nodebase. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100755 index b398823..0000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100755 index d93e5dc..0000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100755 index fb4d7d3..0000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100755 index 51d0967..0000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100755 index 3733c1a..0000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100755 index ab30cba..0000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100755 index 04336df..0000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100755 index ba12981..0000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FlutterMacOS -import Cocoa -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100755 index 9b644bd..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,273 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: "direct main" - description: - name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.4.10" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.0" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.18.0" - convert: - dependency: transitive - description: - name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.1" - crypto: - dependency: "direct main" - description: - name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.6" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.1" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.1" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - intl: - dependency: "direct main" - description: - name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.18.1" - js: - dependency: transitive - description: - name: js - sha256: "4186c61b32f99e60f011f7160e32c89a758ae9b1d0c6d28e2c02ef0382300e2b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.7.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.flutter-io.cn" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.16+1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.8.0" - meta: - dependency: transitive - description: - name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.11.0" - path: - dependency: "direct main" - description: - name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.9.0" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.7.4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.10.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.11.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.6.1" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.2" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.flutter-io.cn" - source: hosted - version: "13.0.0" -sdks: - dart: ">=3.2.4 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml deleted file mode 100755 index fd9b726..0000000 --- a/pubspec.yaml +++ /dev/null @@ -1,98 +0,0 @@ -name: nodebase -description: "Running Node.js application over Wifi and share with your friends." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 - -environment: - sdk: '>=3.2.4 <4.0.0' - -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. -dependencies: - flutter: - sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - path: ^1.8.3 - archive: ^3.4.10 - flutter_localizations: - sdk: flutter - intl: any - crypto: ^3.0.3 - -dev_dependencies: - flutter_test: - sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^3.0.1 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - assets: - - asset/image/logo.png - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/test/util_fs_test.dart b/test/util_fs_test.dart deleted file mode 100755 index 0575923..0000000 --- a/test/util_fs_test.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as path; -import 'package:nodebase/util/fs.dart'; - -void main() { - test('hash functions (file, string) should work correctly', () async { - final baseDir = path.dirname(Platform.script.toFilePath()); - final testfile = path.join(baseDir, 'asset', 'image', 'logo.png'); - expect(await fsCalcHash(testfile), "9c3cfce62af42b32b3ad94e40594869065fcf936dff200a7cc042198d218fcec"); - expect(await fsCalcStringHash("logo.png"), "ab211233b6576dbb0f8b5826447eeac61e2a833a99ac5d788fbc1a174c3c6ce5"); - }); -} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100755 index d6c7473..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:nodebase/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} diff --git a/web/favicon.png b/web/favicon.png deleted file mode 100755 index 8aaa46a..0000000 Binary files a/web/favicon.png and /dev/null differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png deleted file mode 100755 index b749bfe..0000000 Binary files a/web/icons/Icon-192.png and /dev/null differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png deleted file mode 100755 index 88cfd48..0000000 Binary files a/web/icons/Icon-512.png and /dev/null differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png deleted file mode 100755 index eb9b4d7..0000000 Binary files a/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png deleted file mode 100755 index d69c566..0000000 Binary files a/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/web/index.html b/web/index.html deleted file mode 100755 index 83cde70..0000000 --- a/web/index.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - - - - - - - - - - - nodebase - - - - - - - - - - diff --git a/web/manifest.json b/web/manifest.json deleted file mode 100755 index 8f6a3dc..0000000 --- a/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "nodebase", - "short_name": "nodebase", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "Running Node.js application over Wifi and share with your friends.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/windows/.gitignore b/windows/.gitignore deleted file mode 100755 index ec4098a..0000000 --- a/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt deleted file mode 100755 index 99b14a3..0000000 --- a/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(nodebase LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "nodebase") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt deleted file mode 100755 index efb62eb..0000000 --- a/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc deleted file mode 100755 index 8b6d468..0000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void RegisterPlugins(flutter::PluginRegistry* registry) { -} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h deleted file mode 100755 index dc139d8..0000000 --- a/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake deleted file mode 100755 index b93c4c3..0000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt deleted file mode 100755 index 2041a04..0000000 --- a/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc deleted file mode 100755 index 0f2758d..0000000 --- a/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "net.seven.nodebase" "\0" - VALUE "FileDescription", "nodebase" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "nodebase" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 net.seven.nodebase. All rights reserved." "\0" - VALUE "OriginalFilename", "nodebase.exe" "\0" - VALUE "ProductName", "nodebase" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_dart.h b/windows/runner/flutter_dart.h deleted file mode 100755 index 145282d..0000000 --- a/windows/runner/flutter_dart.h +++ /dev/null @@ -1,521 +0,0 @@ -#ifndef _FLUTTER_DART_ -#define _FLUTTER_DART_ -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include "utils.h" - -class NodeAppMonitor; -class NodeBaseEventChannelHandler; -void utilEventPostMessage(std::string&& name, flutter::EncodableValue&& val); - -enum NodeAppSTAT { - BORN, READY, RUNNING, DEAD -}; - -class NodeAppMonitor { -public: - NodeAppMonitor(const std::string &name, const std::vector &cmd, const std::map &env) { - this->name = name; - this->cmd = cmd; - this->env = env; - this->stat = NodeAppSTAT::BORN; - ZeroMemory(&this->pi, sizeof(this->pi)); - this->pth = std::thread(NodeAppMonitor::Run, this); - } - ~NodeAppMonitor() { - this->Stop(); - if (this->pth.joinable()) this->pth.join(); - } - - void Start() { - this->stat = NodeAppSTAT::READY; - STARTUPINFO si; - SecureZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - SecureZeroMemory(&this->pi, sizeof(this->pi)); - std::wstring prog = Utf8ToUtf16((*(this->cmd.begin())).c_str()); - std::wstring cmdLine = this->BuildCommandLine(); - std::wstring envStr = this->BuildEnvironment(); - // ref: https://forums.codeguru.com/showthread.php?514716-std-string-to-LPSTR - LPWSTR cmdLineAdapter = &cmdLine.front(); - if (!CreateProcess( - prog.c_str(), - cmdLineAdapter, - nullptr, - nullptr, - TRUE, - CREATE_UNICODE_ENVIRONMENT, - this->env.size() == 0 ? nullptr : (LPVOID)(envStr.c_str()), - nullptr, - &si, - &pi) - ) { - this->stat = NodeAppSTAT::DEAD; - printf( - "NodeAppMonitor [E] \"%s\" start failure on CreateProcess.", - this->name.c_str() - ); - return; - } - this->stat = NodeAppSTAT::RUNNING; - - { - flutter::EncodableList r; - r.push_back(flutter::EncodableValue("start")); - r.push_back(flutter::EncodableValue(name)); - utilEventPostMessage("app", flutter::EncodableValue(r)); - } - - WaitForSingleObject( this->pi.hProcess, INFINITE ); - this->stat = NodeAppSTAT::DEAD; - CloseHandle( this->pi.hProcess ); - CloseHandle( this->pi.hThread ); - - { - flutter::EncodableList r; - r.push_back(flutter::EncodableValue("stop")); - r.push_back(flutter::EncodableValue(name)); - utilEventPostMessage("app", flutter::EncodableValue(r)); - } - } - - void Stop() { - if (!this->IsRunning()) return; - if (!this->pi.hProcess) return; - TerminateProcess(this->pi.hProcess, 0); - this->stat = NodeAppSTAT::DEAD; - const DWORD r = WaitForSingleObject(this->pi.hProcess, 500); - if (r == WAIT_OBJECT_0) { - // TODO: ok - } else { - printf( - "NodeAppMonitor [E] \"%s\" stop failure on TerminateProcess.", - this->name.c_str() - ); - // TODO: failured - } - CloseHandle(this->pi.hProcess); - CloseHandle(this->pi.hThread); - } - - NodeAppMonitor* restart() { - this->Stop(); - return new NodeAppMonitor(this->name, this->cmd, this->env); - } - flutter::EncodableValue toJSON() { - std::string rstat = "none"; - switch (this->stat) { - case NodeAppSTAT::RUNNING: - rstat = "running"; - break; - case NodeAppSTAT::DEAD: - rstat = "dead"; - break; - case NodeAppSTAT::READY: - rstat = "new"; - break; - case NodeAppSTAT::BORN: - default: - break; - } - flutter::EncodableMap r = { - {flutter::EncodableValue("state"), flutter::EncodableValue(rstat)}, - }; - return flutter::EncodableValue(r); - } - - bool IsRunning() { return this->stat == NodeAppSTAT::RUNNING; } - bool IsDead() { return this->stat == NodeAppSTAT::DEAD; } - - std::string GetName() { return this->name; } - std::vector GetCmd() { return this->cmd; } - std::map GetEnv() { return this->env; } -private: - static void Run(NodeAppMonitor* app) { - app->Start(); - } - - std::wstring BuildCommandLine() { - std::wostringstream os; - // process command line to deal with space and special characters - // e.g. convert 'a "\b c' into '"a ""\\b c"' - for (auto i = this->cmd.begin(); i != this->cmd.end(); i++) { - std::wstring one = Utf8ToUtf16(i->c_str()); - this->replaceAllW(one, L"\\", L"\\\\"); - this->replaceAllW(one, L"\"", L"\"\""); - os << L'"' << one << L'"' << L' '; - } - return os.str(); - } - - std::wstring BuildEnvironment() { - std::wostringstream os; - // TODO+XXX: process env to deal with space and special characters - for (auto i = this->env.begin(); i != this->env.end(); i++) { - os << Utf8ToUtf16(i->first.c_str()) << L'='; - os << Utf8ToUtf16(i->second.c_str()) << L'\x00'; - } - os << L'\x00'; - return os.str(); - } - - // ref: https://stackoverflow.com/questions/2896600/how-to-replace-all-occurrences-of-a-character-in-string - int replaceAllW(std::wstring& str, const std::wstring& from, const std::wstring& to) { - int count = 0; - size_t start_pos = 0; - while((start_pos = str.find(from, start_pos)) != std::wstring::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); - count++; - } - return count; - } - -private: - NodeAppSTAT stat; - std::string name; - std::vector cmd; - std::map env; - std::thread pth; - PROCESS_INFORMATION pi; -}; - -// ref: https://stackoverflow.com/questions/24764477/network-adapter-information-in-c -// ref: https://learn.microsoft.com/zh-cn/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses?redirectedfrom=MSDN -static int getIPAdresses(flutter::EncodableMap& out) { - int count = 0; - - char buf[32*1024]; - ULONG pAddresses_len = sizeof(buf); - IP_ADAPTER_ADDRESSES * pAddresses = (IP_ADAPTER_ADDRESSES *)buf; - DWORD dwRetVal = GetAdaptersAddresses( - AF_UNSPEC, - GAA_FLAG_INCLUDE_PREFIX, - nullptr, - pAddresses, - &pAddresses_len - ); - if (dwRetVal == NO_ERROR) { - IP_ADAPTER_ADDRESSES *pCurrAddresses = pAddresses; - out.clear(); - while (pCurrAddresses) { - int n = (int)(pCurrAddresses->PhysicalAddressLength); - if (n > 0) { - // pCurrAddresses->AdapterName is in a format like {xxxx-yyyy-zzzz...} - flutter::EncodableValue name(Utf8FromUtf16(pCurrAddresses->FriendlyName)); - flutter::EncodableList arr; - for (IP_ADAPTER_UNICAST_ADDRESS* pUnicast = pCurrAddresses->FirstUnicastAddress; pUnicast != NULL; pUnicast = pUnicast->Next) { - wchar_t ipAddress[INET6_ADDRSTRLEN] = {0}; - void* pAddr = nullptr; - - // Determine the family of the IP address - if (pUnicast->Address.lpSockaddr->sa_family == AF_INET) { - // It's an IPv4 address - pAddr = &((struct sockaddr_in*)pUnicast->Address.lpSockaddr)->sin_addr; - } else if (pUnicast->Address.lpSockaddr->sa_family == AF_INET6) { - // It's an IPv6 address - pAddr = &((struct sockaddr_in6*)pUnicast->Address.lpSockaddr)->sin6_addr; - } - - // Convert the binary IP address to a string - if (pAddr) { - InetNtop(pUnicast->Address.lpSockaddr->sa_family, pAddr, ipAddress, sizeof(ipAddress)); - arr.push_back(flutter::EncodableValue(Utf8FromUtf16(ipAddress))); - } - } - out.insert_or_assign(name, arr); - } - pCurrAddresses = pCurrAddresses->Next; - count++; - } - } else { - // TODO: handle error - count = -1; - } - return count; -} - -class NodeBaseEventChannelHandler { -public: - NodeBaseEventChannelHandler(std::string&& channel_name, flutter::FlutterEngine* flutter_instance) { - auto event_channel = - std::make_unique>( - flutter_instance->messenger(), channel_name, - &flutter::StandardMethodCodec::GetInstance()); - - auto event_channel_handler = std::make_unique< - flutter::StreamHandlerFunctions>( - [this]( - const flutter::EncodableValue* arguments, - std::unique_ptr>&& events - ) -> std::unique_ptr> { - std::string name = EncodableValue2String(arguments); - this->sink.insert_or_assign(name, std::move(events)); - return nullptr; - }, - [this](const flutter::EncodableValue* arguments) - -> std::unique_ptr> { - // TODO: replace as find and erase - std::string name = EncodableValue2String(arguments); - this->sink.insert_or_assign(name, nullptr); - return nullptr; - }); - event_channel->SetStreamHandler(std::move(event_channel_handler)); - } - - void postMessage(std::string&& name, flutter::EncodableValue message) { - auto target_ = this->sink.find(name); - if (target_ == this->sink.end()) return; - if (!target_->second) return; - //auto target = target_->second; - target_->second->Success(message); - } -private: - std::string EncodableValue2String(const flutter::EncodableValue* val) { - if (!val || val->IsNull()) { - return std::string(""); - } - - if (std::holds_alternative(*val)) { - return std::get(*val); - } - - return std::string(""); - } -private: - std::map>> sink; -}; - -static std::map services; -static std::mutex service_lock; -static std::unique_ptr eventHandler; - -void utilEventPostMessage(std::string&& name, flutter::EncodableValue&& val) { - if (eventHandler == nullptr) return; - /* - * XXX: annoying warning but no non-hacking solution yet, - * need help from `fml` namespace - * general idea: - * - when init channels, get current thread id - * - using the thread id to get task runner - * - do post task here - * [eventHandler=eventHandler.get(),name=std::move(name),val]() {...}; - * - * The 'net.seven.nodebase/event' channel sent a message from native to Flutter on - * a non-platform thread. Platform channel messages must be sent on the platform - * thread. Failure to do so may result in data loss or crashes, and must be fixed - * in the plugin or application code creating that channel. - */ - eventHandler->postMessage(std::move(name), val); -} - -void appStart(const std::string &name, const std::vector &cmd, const std::map &env) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - NodeAppMonitor *app; - if (app_ != services.end()) { - app = app_->second; - services.erase(name); - delete app; - } - app = new NodeAppMonitor(name, cmd, env); - // TODO: if (!app) {} // memory allocate failure - services.insert_or_assign(name, app); -} -void appStop(const std::string &name) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - if (app_ == services.end()) return; - NodeAppMonitor *app = app_->second; - app->Stop(); -} -bool appRestart(const std::string &name) { - std::lock_guard guard(service_lock); - auto app_ = services.find(name); - if (app_ == services.end()) return false; - NodeAppMonitor *app = app_->second; - app->Stop(); - NodeAppMonitor *newapp = app->restart(); - if (newapp == nullptr) { - return false; - } - delete app; - services.insert_or_assign(name, newapp); - return true; -} -flutter::EncodableValue appStat(const std::string &name) { - auto app_ = services.find(name); - if (app_ == services.end()) return flutter::EncodableValue(flutter::EncodableMap()); - NodeAppMonitor *app = app_->second; - return app->toJSON(); -} -flutter::EncodableValue utilGetIPs() { - flutter::EncodableMap ips; - getIPAdresses(ips); - return flutter::EncodableValue(ips); -} -void utilBrowserOpen(const std::string &url) { - if (url.rfind("http:", 0) != 0 && url.rfind("https:", 0) != 0) return; - ShellExecuteA(0, 0, url.c_str(), 0, 0 , SW_SHOW ); -} -flutter::EncodableValue utilGetArch() { - std::string arch("windows-"); - SYSTEM_INFO si; - // XXX: GetSystemInfo(&si) for old windows, maybe we can use LoadLibrary for test - GetNativeSystemInfo(&si); - switch(si.wProcessorArchitecture) { - // compatible with GetSystemInfo - case PROCESSOR_ARCHITECTURE_AMD64: - arch += "x64|x86"; - break; - case PROCESSOR_ARCHITECTURE_INTEL: - arch += "x86"; - break; - // new - case PROCESSOR_ARCHITECTURE_ARM: - arch += "arm"; - break; - case PROCESSOR_ARCHITECTURE_ARM64: - arch += "arm64|arm"; - break; - case PROCESSOR_ARCHITECTURE_IA64: - arch += "x64|x86"; - break; - } - return flutter::EncodableValue(arch); -} -flutter::EncodableValue utilWorkspaceBaseDir() { - TCHAR buf[1024]; - if (!GetModuleFileName(NULL, buf, 1024)) return flutter::EncodableValue(std::string("")); - buf[1023] = 0; - TCHAR* last = wcsrchr(buf, L'\\'); - if (last == nullptr || last == buf || last == &(buf[1]) || last == &(buf[2])) { - // not found, buf = "\\", buf = "\\\\", buf = "c:\\" - return flutter::EncodableValue(std::string("")); - } - last[0] = 0; - return flutter::EncodableValue(Utf8FromUtf16(buf)); -} - -#define RETURN_BADARG_ERR(x) { result->Error("BAD_ARGS", "Invalid argument type for '" #x "'"); return; } -void InitMethodChannel(flutter::FlutterEngine* flutter_instance) { - // name your channel - const static std::string channel_name("net.seven.nodebase/app"); - - auto channel = - std::make_unique>( - flutter_instance->messenger(), channel_name, - &flutter::StandardMethodCodec::GetInstance()); - - channel->SetMethodCallHandler( - [](const flutter::MethodCall& call, - std::unique_ptr> result) { - - const auto* args = std::get_if(call.arguments()); - if (call.method_name().compare("app.stat") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.stat); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.stat); - std::string name = std::get(name_->second); - result->Success(appStat(name)); - } - else if (call.method_name().compare("app.start") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.start); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.start); - auto cmd_ = args->find(flutter::EncodableValue("cmd")); - if (cmd_ == args->end()) RETURN_BADARG_ERR(app.start); - auto env_ = args->find(flutter::EncodableValue("env")); - if (env_ == args->end()) RETURN_BADARG_ERR(app.start); - std::string name = std::get(name_->second); - - flutter::EncodableList cmdRaw = std::get(cmd_->second); - std::vector cmd; - cmd.reserve(cmdRaw.size()); - for (const auto &val : cmdRaw) { - cmd.push_back(std::get(val)); - } - - flutter::EncodableMap envRaw = std::get(env_->second); - std::map env; - for (const auto &val : envRaw) { - env.insert_or_assign( - std::get(val.first), - std::get(val.second) - ); - } - appStart(name, cmd, env); - result->Success(); - } - else if (call.method_name().compare("app.restart") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.restart); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.restart); - std::string name = std::get(name_->second); - if (!appRestart(name)) { - std::ostringstream msg; - msg << "Restart failed for app \"" << name << "\""; - result->Error("FAILURE", msg.str()); - return; - } - result->Success(); - } - else if (call.method_name().compare("app.stop") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(app.stop); - auto name_ = args->find(flutter::EncodableValue("name")); - if (name_ == args->end()) RETURN_BADARG_ERR(app.stop); - std::string name = std::get(name_->second); - appStop(name); - result->Success(); - } - else if (call.method_name().compare("util.ip") == 0) { - result->Success(utilGetIPs()); - } - else if (call.method_name().compare("util.file.executable") == 0) { - // in windows, we may not need to implement this; - // PE file can be executable directly without additional flag by default - /* - if (args == nullptr) RETURN_BADARG_ERR(util.file.executable); - auto name = args->find(flutter::EncodableValue("filename"))->second; - if (name.IsNull()) RETURN_BADARG_ERR(util.file.executable); - ... - */ - result->Success(); - } - else if (call.method_name().compare("util.browser.open") == 0) { - if (args == nullptr) RETURN_BADARG_ERR(util.browser.open); - auto url_ = args->find(flutter::EncodableValue("url")); - if (url_ == args->end()) RETURN_BADARG_ERR(util.browser.open); - std::string url = std::get(url_->second); - utilBrowserOpen(url); - result->Success(); - } - else if (call.method_name().compare("util.arch") == 0) { - result->Success(utilGetArch()); - } - else if (call.method_name().compare("util.workspace") == 0) { - result->Success(utilWorkspaceBaseDir()); - } - else { - result->NotImplemented(); - } - }); -} -#undef RETURN_BADARG_ERR - -void InitEventChannel(flutter::FlutterEngine* flutter_instance) { - eventHandler = std::make_unique( - std::string("net.seven.nodebase/event"), flutter_instance); -} -#endif // _FLUTTER_DART_ \ No newline at end of file diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp deleted file mode 100755 index 5015640..0000000 --- a/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,82 +0,0 @@ -// XXX ugly include, what a pity is windows.h uses winsock.h by default -// should define winsock2.h before windows.h -#include -#include -#include -#pragma comment(lib, "Ws2_32.lib") -#pragma comment(lib, "IPHLPAPI.lib") -#include -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" -#include "flutter_dart.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - InitMethodChannel(flutter_controller_->engine()); - InitEventChannel(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h deleted file mode 100755 index 28c2383..0000000 --- a/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp deleted file mode 100755 index 30682ea..0000000 --- a/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(360, 720); - if (!window.Create(L"nodebase", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/windows/runner/resource.h b/windows/runner/resource.h deleted file mode 100755 index ddc7f3e..0000000 --- a/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico deleted file mode 100755 index c04e20c..0000000 Binary files a/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest deleted file mode 100755 index fbff8fc..0000000 --- a/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,21 +0,0 @@ - - - - - PerMonitorV2 - true - - - - - - - - - - - - - - - diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp deleted file mode 100755 index 968e5ee..0000000 --- a/windows/runner/utils.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length <= 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} - -std::wstring Utf8ToUtf16(const char* utf8_string) { - // ref: filled by chatGPT 4 turbo - if (utf8_string == nullptr) { - return std::wstring(); // Return an empty wstring if the input is null. - } - - // Calculate the length of the resulting wide string. - int wide_char_length = MultiByteToWideChar( - CP_UTF8, // Source string is in UTF-8 - 0, // No flags - utf8_string, // Source UTF-8 string - -1, // The string is null-terminated - nullptr, // No output buffer since we're calculating the length - 0 // Request length calculation - ); - - if (wide_char_length == 0) { - // Handle the error, could be due to an invalid UTF-8 sequence. - // GetLastError() can be used to get more information. - return std::wstring(); - } - - // Allocate a buffer for the wide string. - std::wstring utf16_string(wide_char_length, L'\0'); - - // Now convert the UTF-8 string to UTF-16. - int convert_result = MultiByteToWideChar( - CP_UTF8, // Source string is in UTF-8 - 0, // No flags - utf8_string, // Source UTF-8 string - -1, // The string is null-terminated - &utf16_string[0], // Output buffer for the wide string - wide_char_length // Size of the output buffer - ); - - if (convert_result == 0) { - // Handle the error, could be due to an invalid UTF-8 sequence. - // GetLastError() can be used to get more information. - return std::wstring(); - } - - // The length includes the null terminator, so we resize to remove it. - utf16_string.resize(wide_char_length - 1); - - return utf16_string; -} \ No newline at end of file diff --git a/windows/runner/utils.h b/windows/runner/utils.h deleted file mode 100755 index 13e37c8..0000000 --- a/windows/runner/utils.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); -std::wstring Utf8ToUtf16(const char* utf8_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp deleted file mode 100755 index 1b2adca..0000000 --- a/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW ^ (WS_THICKFRAME | WS_MAXIMIZEBOX), - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h deleted file mode 100755 index 49b847f..0000000 --- a/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_