updated control access
This commit is contained in:
@@ -83,3 +83,34 @@
|
||||
[2025-10-16 00:06:02 +0300] [288324] [INFO] Worker exiting (pid: 288324)
|
||||
[2025-10-16 00:06:02 +0300] [288325] [INFO] Worker exiting (pid: 288325)
|
||||
[2025-10-16 00:06:03 +0300] [288316] [INFO] Shutting down: Master
|
||||
[2025-10-16 02:34:31 +0300] [299414] [INFO] Starting gunicorn 23.0.0
|
||||
[2025-10-16 02:34:31 +0300] [299414] [INFO] Listening at: http://0.0.0.0:8781 (299414)
|
||||
[2025-10-16 02:34:31 +0300] [299414] [INFO] Using worker: sync
|
||||
[2025-10-16 02:34:31 +0300] [299414] [INFO] Trasabilitate Application server is ready. Listening on: [('0.0.0.0', 8781)]
|
||||
[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:31 +0300] [299432] [INFO] Booting worker with pid: 299432
|
||||
[2025-10-16 02:34:31 +0300] [299432] [INFO] Worker spawned (pid: 299432)
|
||||
[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:31 +0300] [299438] [INFO] Booting worker with pid: 299438
|
||||
[2025-10-16 02:34:31 +0300] [299438] [INFO] Worker spawned (pid: 299438)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299439] [INFO] Booting worker with pid: 299439
|
||||
[2025-10-16 02:34:32 +0300] [299439] [INFO] Worker spawned (pid: 299439)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299440] [INFO] Booting worker with pid: 299440
|
||||
[2025-10-16 02:34:32 +0300] [299440] [INFO] Worker spawned (pid: 299440)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299441] [INFO] Booting worker with pid: 299441
|
||||
[2025-10-16 02:34:32 +0300] [299441] [INFO] Worker spawned (pid: 299441)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299442] [INFO] Booting worker with pid: 299442
|
||||
[2025-10-16 02:34:32 +0300] [299442] [INFO] Worker spawned (pid: 299442)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299443] [INFO] Booting worker with pid: 299443
|
||||
[2025-10-16 02:34:32 +0300] [299443] [INFO] Worker spawned (pid: 299443)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299444] [INFO] Booting worker with pid: 299444
|
||||
[2025-10-16 02:34:32 +0300] [299444] [INFO] Worker spawned (pid: 299444)
|
||||
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||
[2025-10-16 02:34:32 +0300] [299445] [INFO] Booting worker with pid: 299445
|
||||
[2025-10-16 02:34:32 +0300] [299445] [INFO] Worker spawned (pid: 299445)
|
||||
|
||||
121
old code/migrate_external_db.py
Normal file
121
old code/migrate_external_db.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to add modules column to external database and migrate existing users
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import mariadb
|
||||
|
||||
def migrate_external_database():
|
||||
"""Add modules column to external database and update existing users"""
|
||||
try:
|
||||
# Read external database configuration from instance folder
|
||||
config_file = os.path.join(os.path.dirname(__file__), 'instance/external_server.conf')
|
||||
if not os.path.exists(config_file):
|
||||
print("External database configuration file not found at instance/external_server.conf")
|
||||
return False
|
||||
|
||||
with open(config_file, 'r') as f:
|
||||
lines = f.read().strip().split('\n')
|
||||
|
||||
# Parse the config file format "key=value"
|
||||
config = {}
|
||||
for line in lines:
|
||||
if '=' in line and not line.strip().startswith('#'):
|
||||
key, value = line.split('=', 1)
|
||||
config[key.strip()] = value.strip()
|
||||
|
||||
host = config.get('server_domain', 'localhost')
|
||||
port = int(config.get('port', '3306'))
|
||||
database = config.get('database_name', '')
|
||||
user = config.get('username', '')
|
||||
password = config.get('password', '')
|
||||
|
||||
if not all([host, database, user, password]):
|
||||
print("Missing required database configuration values.")
|
||||
return False
|
||||
|
||||
print(f"Connecting to external database: {host}:{port}/{database}")
|
||||
|
||||
# Connect to external database
|
||||
conn = mariadb.connect(
|
||||
user=user,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
database=database
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if users table exists
|
||||
cursor.execute("SHOW TABLES LIKE 'users'")
|
||||
if not cursor.fetchone():
|
||||
print("Users table not found in external database.")
|
||||
conn.close()
|
||||
return False
|
||||
|
||||
# Check if modules column already exists
|
||||
cursor.execute("DESCRIBE users")
|
||||
columns = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
if 'modules' not in columns:
|
||||
print("Adding modules column to users table...")
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
|
||||
print("Modules column added successfully.")
|
||||
else:
|
||||
print("Modules column already exists.")
|
||||
|
||||
# Get current users and convert their roles
|
||||
cursor.execute("SELECT id, username, role FROM users")
|
||||
users = cursor.fetchall()
|
||||
|
||||
role_mapping = {
|
||||
'superadmin': ('superadmin', None),
|
||||
'administrator': ('admin', None),
|
||||
'admin': ('admin', None),
|
||||
'quality': ('manager', '["quality"]'),
|
||||
'warehouse': ('manager', '["warehouse"]'),
|
||||
'warehouse_manager': ('manager', '["warehouse"]'),
|
||||
'scan': ('worker', '["quality"]'),
|
||||
'etichete': ('manager', '["labels"]'),
|
||||
'quality_manager': ('manager', '["quality"]'),
|
||||
'quality_worker': ('worker', '["quality"]'),
|
||||
}
|
||||
|
||||
print(f"Migrating {len(users)} users...")
|
||||
|
||||
for user_id, username, old_role in users:
|
||||
if old_role in role_mapping:
|
||||
new_role, modules_json = role_mapping[old_role]
|
||||
|
||||
cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?",
|
||||
(new_role, modules_json, user_id))
|
||||
|
||||
print(f" {username}: {old_role} -> {new_role} with modules {modules_json}")
|
||||
else:
|
||||
print(f" {username}: Unknown role '{old_role}', keeping as-is")
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print("External database migration completed successfully!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error migrating external database: {e}")
|
||||
return False
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("External Database Migration for Simplified 4-Tier Permission System")
|
||||
print("=" * 70)
|
||||
|
||||
success = migrate_external_database()
|
||||
|
||||
if success:
|
||||
print("\n✅ Migration completed successfully!")
|
||||
print("\nUsers can now log in with the new simplified permission system.")
|
||||
print("Role structure: superadmin → admin → manager → worker")
|
||||
print("Modules: quality, warehouse, labels")
|
||||
else:
|
||||
print("\n❌ Migration failed. Please check the error messages above.")
|
||||
172
old code/migrate_permissions.py
Executable file
172
old code/migrate_permissions.py
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script to convert from complex permission system to simplified 4-tier system
|
||||
This script will:
|
||||
1. Add 'modules' column to users table
|
||||
2. Convert existing roles to new 4-tier system
|
||||
3. Assign appropriate modules based on old roles
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add the app directory to Python path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
def get_db_connections():
|
||||
"""Get both internal SQLite and external database connections"""
|
||||
connections = {}
|
||||
|
||||
# Internal SQLite database
|
||||
internal_db_path = os.path.join(os.path.dirname(__file__), 'instance/users.db')
|
||||
if os.path.exists(internal_db_path):
|
||||
connections['internal'] = sqlite3.connect(internal_db_path)
|
||||
print(f"Connected to internal SQLite database: {internal_db_path}")
|
||||
|
||||
# External database (try to connect using existing method)
|
||||
try:
|
||||
import mariadb
|
||||
|
||||
# Read external database configuration
|
||||
config_file = os.path.join(os.path.dirname(__file__), '../external_database_settings')
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file, 'r') as f:
|
||||
lines = f.read().strip().split('\n')
|
||||
if len(lines) >= 5:
|
||||
host = lines[0].strip()
|
||||
port = int(lines[1].strip())
|
||||
database = lines[2].strip()
|
||||
user = lines[3].strip()
|
||||
password = lines[4].strip()
|
||||
|
||||
conn = mariadb.connect(
|
||||
user=user,
|
||||
password=password,
|
||||
host=host,
|
||||
port=port,
|
||||
database=database
|
||||
)
|
||||
connections['external'] = conn
|
||||
print(f"Connected to external MariaDB database: {host}:{port}/{database}")
|
||||
except Exception as e:
|
||||
print(f"Could not connect to external database: {e}")
|
||||
|
||||
return connections
|
||||
|
||||
def role_mapping():
|
||||
"""Map old roles to new 4-tier system"""
|
||||
return {
|
||||
# Old role -> (new_role, modules)
|
||||
'superadmin': ('superadmin', []), # All modules by default
|
||||
'administrator': ('admin', []), # All modules by default
|
||||
'admin': ('admin', []), # All modules by default
|
||||
'quality': ('manager', ['quality']),
|
||||
'warehouse': ('manager', ['warehouse']),
|
||||
'warehouse_manager': ('manager', ['warehouse']),
|
||||
'scan': ('worker', ['quality']), # Assume scan users are quality workers
|
||||
'etichete': ('manager', ['labels']),
|
||||
'quality_manager': ('manager', ['quality']),
|
||||
'quality_worker': ('worker', ['quality']),
|
||||
}
|
||||
|
||||
def migrate_database(conn, db_type):
|
||||
"""Migrate a specific database"""
|
||||
cursor = conn.cursor()
|
||||
|
||||
print(f"Migrating {db_type} database...")
|
||||
|
||||
# Check if users table exists
|
||||
if db_type == 'internal':
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
|
||||
else: # external/MariaDB
|
||||
cursor.execute("SHOW TABLES LIKE 'users'")
|
||||
|
||||
if not cursor.fetchone():
|
||||
print(f"No users table found in {db_type} database")
|
||||
return
|
||||
|
||||
# Check if modules column already exists
|
||||
try:
|
||||
if db_type == 'internal':
|
||||
cursor.execute("PRAGMA table_info(users)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
else: # external/MariaDB
|
||||
cursor.execute("DESCRIBE users")
|
||||
columns = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
if 'modules' not in columns:
|
||||
print(f"Adding modules column to {db_type} database...")
|
||||
if db_type == 'internal':
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
|
||||
else: # external/MariaDB
|
||||
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
|
||||
else:
|
||||
print(f"Modules column already exists in {db_type} database")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking/adding modules column in {db_type}: {e}")
|
||||
return
|
||||
|
||||
# Get current users
|
||||
cursor.execute("SELECT id, username, role FROM users")
|
||||
users = cursor.fetchall()
|
||||
|
||||
print(f"Found {len(users)} users in {db_type} database")
|
||||
|
||||
# Convert roles and assign modules
|
||||
mapping = role_mapping()
|
||||
updates = []
|
||||
|
||||
for user_id, username, old_role in users:
|
||||
if old_role in mapping:
|
||||
new_role, modules = mapping[old_role]
|
||||
modules_json = json.dumps(modules) if modules else None
|
||||
updates.append((new_role, modules_json, user_id, username))
|
||||
print(f" {username}: {old_role} -> {new_role} with modules {modules}")
|
||||
else:
|
||||
print(f" {username}: Unknown role '{old_role}', keeping as-is")
|
||||
|
||||
# Apply updates
|
||||
for new_role, modules_json, user_id, username in updates:
|
||||
try:
|
||||
cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?",
|
||||
(new_role, modules_json, user_id))
|
||||
print(f" Updated {username} successfully")
|
||||
except Exception as e:
|
||||
print(f" Error updating {username}: {e}")
|
||||
|
||||
conn.commit()
|
||||
print(f"Migration completed for {db_type} database")
|
||||
|
||||
def main():
|
||||
"""Main migration function"""
|
||||
print("Starting migration to simplified 4-tier permission system...")
|
||||
print("="*60)
|
||||
|
||||
connections = get_db_connections()
|
||||
|
||||
if not connections:
|
||||
print("No database connections available. Please check your configuration.")
|
||||
return
|
||||
|
||||
for db_type, conn in connections.items():
|
||||
try:
|
||||
migrate_database(conn, db_type)
|
||||
print()
|
||||
except Exception as e:
|
||||
print(f"Error migrating {db_type} database: {e}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
print("Migration completed!")
|
||||
print("\nNew role structure:")
|
||||
print("- superadmin: Full system access")
|
||||
print("- admin: Full app access (except role_permissions and download_extension)")
|
||||
print("- manager: Module-based access (can have multiple modules)")
|
||||
print("- worker: Limited module access (one module only)")
|
||||
print("\nAvailable modules: quality, warehouse, labels")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
111
old code/test_permissions.py
Normal file
111
old code/test_permissions.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for the new simplified 4-tier permission system
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
||||
|
||||
from permissions_simple import check_access, validate_user_modules, get_user_accessible_pages
|
||||
|
||||
def test_permission_system():
|
||||
"""Test the new permission system with various scenarios"""
|
||||
print("Testing Simplified 4-Tier Permission System")
|
||||
print("=" * 50)
|
||||
|
||||
# Test cases: (role, modules, page, expected_result)
|
||||
test_cases = [
|
||||
# Superadmin tests
|
||||
('superadmin', [], 'dashboard', True),
|
||||
('superadmin', [], 'role_permissions', True),
|
||||
('superadmin', [], 'quality', True),
|
||||
('superadmin', [], 'warehouse', True),
|
||||
|
||||
# Admin tests
|
||||
('admin', [], 'dashboard', True),
|
||||
('admin', [], 'role_permissions', False), # Restricted for admin
|
||||
('admin', [], 'download_extension', False), # Restricted for admin
|
||||
('admin', [], 'quality', True),
|
||||
('admin', [], 'warehouse', True),
|
||||
|
||||
# Manager tests
|
||||
('manager', ['quality'], 'quality', True),
|
||||
('manager', ['quality'], 'quality_reports', True),
|
||||
('manager', ['quality'], 'warehouse', False), # No warehouse module
|
||||
('manager', ['warehouse'], 'warehouse', True),
|
||||
('manager', ['warehouse'], 'quality', False), # No quality module
|
||||
('manager', ['quality', 'warehouse'], 'quality', True), # Multiple modules
|
||||
('manager', ['quality', 'warehouse'], 'warehouse', True),
|
||||
|
||||
# Worker tests
|
||||
('worker', ['quality'], 'quality', True),
|
||||
('worker', ['quality'], 'quality_reports', False), # Workers can't access reports
|
||||
('worker', ['quality'], 'warehouse', False), # No warehouse module
|
||||
('worker', ['warehouse'], 'move_orders', True),
|
||||
('worker', ['warehouse'], 'create_locations', False), # Workers can't create locations
|
||||
|
||||
# Invalid role test
|
||||
('invalid_role', ['quality'], 'quality', False),
|
||||
]
|
||||
|
||||
print("Testing access control:")
|
||||
print("-" * 30)
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for role, modules, page, expected in test_cases:
|
||||
result = check_access(role, modules, page)
|
||||
status = "PASS" if result == expected else "FAIL"
|
||||
print(f"{status}: {role:12} {str(modules):20} {page:18} -> {result} (expected {expected})")
|
||||
|
||||
if result == expected:
|
||||
passed += 1
|
||||
else:
|
||||
failed += 1
|
||||
|
||||
print(f"\nResults: {passed} passed, {failed} failed")
|
||||
|
||||
# Test module validation
|
||||
print("\nTesting module validation:")
|
||||
print("-" * 30)
|
||||
|
||||
validation_tests = [
|
||||
('superadmin', ['quality'], True), # Superadmin can have any modules
|
||||
('admin', ['warehouse'], True), # Admin can have any modules
|
||||
('manager', ['quality'], True), # Manager can have one module
|
||||
('manager', ['quality', 'warehouse'], True), # Manager can have multiple modules
|
||||
('manager', [], False), # Manager must have at least one module
|
||||
('worker', ['quality'], True), # Worker can have one module
|
||||
('worker', ['quality', 'warehouse'], False), # Worker cannot have multiple modules
|
||||
('worker', [], False), # Worker must have exactly one module
|
||||
('invalid_role', ['quality'], False), # Invalid role
|
||||
]
|
||||
|
||||
for role, modules, expected in validation_tests:
|
||||
is_valid, error_msg = validate_user_modules(role, modules)
|
||||
status = "PASS" if is_valid == expected else "FAIL"
|
||||
print(f"{status}: {role:12} {str(modules):20} -> {is_valid} (expected {expected})")
|
||||
if error_msg:
|
||||
print(f" Error: {error_msg}")
|
||||
|
||||
# Test accessible pages for different users
|
||||
print("\nTesting accessible pages:")
|
||||
print("-" * 30)
|
||||
|
||||
user_tests = [
|
||||
('superadmin', []),
|
||||
('admin', []),
|
||||
('manager', ['quality']),
|
||||
('manager', ['warehouse']),
|
||||
('worker', ['quality']),
|
||||
('worker', ['warehouse']),
|
||||
]
|
||||
|
||||
for role, modules in user_tests:
|
||||
accessible_pages = get_user_accessible_pages(role, modules)
|
||||
print(f"{role:12} {str(modules):20} -> {len(accessible_pages)} pages: {', '.join(accessible_pages[:5])}{'...' if len(accessible_pages) > 5 else ''}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_permission_system()
|
||||
65
old code/tray/src/qz/communication/H4J_HidUtilities.java
Executable file
65
old code/tray/src/qz/communication/H4J_HidUtilities.java
Executable file
@@ -0,0 +1,65 @@
|
||||
package qz.communication;
|
||||
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.hid4java.HidDevice;
|
||||
import org.hid4java.HidManager;
|
||||
import org.hid4java.HidServices;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class H4J_HidUtilities {
|
||||
private static final HidServices service = HidManager.getHidServices();
|
||||
|
||||
public static List<HidDevice> getHidDevices() {
|
||||
return service.getAttachedHidDevices();
|
||||
}
|
||||
|
||||
public static JSONArray getHidDevicesJSON() throws JSONException {
|
||||
List<HidDevice> devices = getHidDevices();
|
||||
JSONArray devicesJSON = new JSONArray();
|
||||
|
||||
HashSet<String> unique = new HashSet<>();
|
||||
for(HidDevice device : devices) {
|
||||
JSONObject deviceJSON = new JSONObject();
|
||||
|
||||
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||
.put("productId", UsbUtil.toHexString(device.getProductId()))
|
||||
.put("usagePage", UsbUtil.toHexString((short)device.getUsagePage()))
|
||||
.put("serial", device.getSerialNumber())
|
||||
.put("manufacturer", device.getManufacturer())
|
||||
.put("product", device.getProduct());
|
||||
|
||||
String uid = String.format("v%sp%su%ss%s", deviceJSON.optString("vendorId"), deviceJSON.optString("productId"), deviceJSON.optString("usagePage"), deviceJSON.optString("serial"));
|
||||
if (!unique.contains(uid)) {
|
||||
devicesJSON.put(deviceJSON);
|
||||
unique.add(uid);
|
||||
}
|
||||
}
|
||||
|
||||
return devicesJSON;
|
||||
}
|
||||
|
||||
public static HidDevice findDevice(DeviceOptions dOpts) {
|
||||
if (dOpts.getVendorId() == null) {
|
||||
throw new IllegalArgumentException("Vendor ID cannot be null");
|
||||
}
|
||||
if (dOpts.getProductId() == null) {
|
||||
throw new IllegalArgumentException("Product ID cannot be null");
|
||||
}
|
||||
|
||||
List<HidDevice> devices = getHidDevices();
|
||||
for(HidDevice device : devices) {
|
||||
if (device.isVidPidSerial(dOpts.getVendorId(), dOpts.getProductId(), dOpts.getSerial())
|
||||
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage() == device.getUsagePage())) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
154
old code/tray/src/qz/communication/PJHA_HidIO.java
Executable file
154
old code/tray/src/qz/communication/PJHA_HidIO.java
Executable file
@@ -0,0 +1,154 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import purejavahidapi.HidDevice;
|
||||
import purejavahidapi.HidDeviceInfo;
|
||||
import purejavahidapi.InputReportListener;
|
||||
import purejavahidapi.PureJavaHidApi;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
import java.io.IOException;
|
||||
import java.util.Vector;
|
||||
|
||||
public class PJHA_HidIO implements DeviceIO {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PJHA_HidIO.class);
|
||||
|
||||
private HidDeviceInfo deviceInfo;
|
||||
private HidDevice device;
|
||||
|
||||
private static final int BUFFER_SIZE = 32;
|
||||
private Vector<byte[]> dataBuffer;
|
||||
private boolean streaming;
|
||||
private DeviceOptions dOpts;
|
||||
private SocketConnection websocket;
|
||||
|
||||
public PJHA_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this(PJHA_HidUtilities.findDevice(dOpts), dOpts, websocket);
|
||||
}
|
||||
|
||||
private PJHA_HidIO(HidDeviceInfo deviceInfo, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this.dOpts = dOpts;
|
||||
this.websocket = websocket;
|
||||
if (deviceInfo == null) {
|
||||
throw new DeviceException("HID device could not be found");
|
||||
}
|
||||
|
||||
this.deviceInfo = deviceInfo;
|
||||
|
||||
dataBuffer = new Vector<byte[]>() {
|
||||
@Override
|
||||
public synchronized boolean add(byte[] e) {
|
||||
while(this.size() >= BUFFER_SIZE) {
|
||||
this.remove(0);
|
||||
}
|
||||
return super.add(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void open() throws DeviceException {
|
||||
if (!isOpen()) {
|
||||
try {
|
||||
device = PureJavaHidApi.openDevice(deviceInfo);
|
||||
device.setInputReportListener(new InputReportListener() {
|
||||
@Override
|
||||
public void onInputReport(HidDevice source, byte id, byte[] data, int len) {
|
||||
byte[] dataCopy = new byte[len];
|
||||
System.arraycopy(data, 0, dataCopy, 0, len);
|
||||
dataBuffer.add(dataCopy);
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(IOException ex) {
|
||||
throw new DeviceException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return device != null;
|
||||
}
|
||||
|
||||
public void setStreaming(boolean active) {
|
||||
streaming = active;
|
||||
}
|
||||
|
||||
public boolean isStreaming() {
|
||||
return streaming;
|
||||
}
|
||||
|
||||
public String getVendorId() {
|
||||
return UsbUtil.toHexString(deviceInfo.getVendorId());
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return UsbUtil.toHexString(deviceInfo.getProductId());
|
||||
}
|
||||
|
||||
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
|
||||
byte[] response = new byte[responseSize];
|
||||
if (dataBuffer.isEmpty()) {
|
||||
return new byte[0]; //no data received yet
|
||||
}
|
||||
|
||||
byte[] latestData = dataBuffer.remove(0);
|
||||
if (SystemUtilities.isWindows()) {
|
||||
//windows missing the leading byte
|
||||
System.arraycopy(latestData, 0, response, 1, Math.min(responseSize - 1, latestData.length));
|
||||
} else {
|
||||
System.arraycopy(latestData, 0, response, 0, Math.min(responseSize - 1, latestData.length));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public void sendData(byte[] data, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
|
||||
int wrote = device.setOutputReport(reportId, data, data.length);
|
||||
if (wrote == -1) {
|
||||
throw new DeviceException("Failed to write to device");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getFeatureReport(int responseSize, Byte unused) throws DeviceException {
|
||||
byte[] response = new byte[responseSize];
|
||||
int read = device.getFeatureReport(response, responseSize);
|
||||
if (read == -1) {
|
||||
throw new DeviceException("Failed to read from device");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
|
||||
if (reportId == null) { reportId = (byte)0x00; }
|
||||
int wrote = device.setFeatureReport(reportId, data, data.length);
|
||||
|
||||
if (wrote == -1) {
|
||||
throw new DeviceException("Failed to write to device");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
setStreaming(false);
|
||||
// Remove orphaned reference
|
||||
websocket.removeDevice(dOpts);
|
||||
if (isOpen()) {
|
||||
try {
|
||||
device.setInputReportListener(null);
|
||||
device.close();
|
||||
}
|
||||
catch(IllegalStateException e) {
|
||||
log.warn("Device already closed");
|
||||
}
|
||||
}
|
||||
|
||||
device = null;
|
||||
}
|
||||
|
||||
}
|
||||
50
old code/tray/src/qz/communication/PJHA_HidListener.java
Executable file
50
old code/tray/src/qz/communication/PJHA_HidListener.java
Executable file
@@ -0,0 +1,50 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import purejavahidapi.DeviceRemovalListener;
|
||||
import purejavahidapi.HidDevice;
|
||||
import qz.ws.PrintSocketClient;
|
||||
import qz.ws.StreamEvent;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
|
||||
public class PJHA_HidListener implements DeviceListener, DeviceRemovalListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PJHA_HidListener.class);
|
||||
|
||||
private Session session;
|
||||
private HidDevice device;
|
||||
|
||||
|
||||
public PJHA_HidListener(Session session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
public void setDevice(HidDevice device) {
|
||||
this.device = device;
|
||||
device.setDeviceRemovalListener(this);
|
||||
}
|
||||
|
||||
private StreamEvent createStreamAction(HidDevice device, String action) {
|
||||
return new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
|
||||
.withData("vendorId", UsbUtil.toHexString(device.getHidDeviceInfo().getVendorId()))
|
||||
.withData("productId", UsbUtil.toHexString(device.getHidDeviceInfo().getProductId()))
|
||||
.withData("actionType", action);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (device != null) {
|
||||
device.setDeviceRemovalListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeviceRemoval(HidDevice device) {
|
||||
log.debug("Device detached: {}", device.getHidDeviceInfo().getProductString());
|
||||
PrintSocketClient.sendStream(session, createStreamAction(device, "Device Detached"), this);
|
||||
}
|
||||
}
|
||||
56
old code/tray/src/qz/communication/PJHA_HidUtilities.java
Executable file
56
old code/tray/src/qz/communication/PJHA_HidUtilities.java
Executable file
@@ -0,0 +1,56 @@
|
||||
package qz.communication;
|
||||
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import purejavahidapi.HidDeviceInfo;
|
||||
import purejavahidapi.PureJavaHidApi;
|
||||
|
||||
import javax.usb.util.UsbUtil;
|
||||
import java.util.List;
|
||||
|
||||
public class PJHA_HidUtilities {
|
||||
|
||||
public static JSONArray getHidDevicesJSON() throws JSONException {
|
||||
List<HidDeviceInfo> devices = PureJavaHidApi.enumerateDevices();
|
||||
JSONArray devicesJSON = new JSONArray();
|
||||
|
||||
for(HidDeviceInfo device : devices) {
|
||||
JSONObject deviceJSON = new JSONObject();
|
||||
|
||||
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||
.put("productId", UsbUtil.toHexString(device.getProductId()))
|
||||
.put("usagePage", UsbUtil.toHexString(device.getUsagePage()))
|
||||
.put("serial", device.getSerialNumberString())
|
||||
.put("manufacturer", device.getManufacturerString())
|
||||
.put("product", device.getProductString());
|
||||
|
||||
devicesJSON.put(deviceJSON);
|
||||
}
|
||||
|
||||
return devicesJSON;
|
||||
}
|
||||
|
||||
public static HidDeviceInfo findDevice(DeviceOptions dOpts) {
|
||||
if (dOpts.getVendorId() == null) {
|
||||
throw new IllegalArgumentException("Vendor ID cannot be null");
|
||||
}
|
||||
if (dOpts.getProductId() == null) {
|
||||
throw new IllegalArgumentException("Product ID cannot be null");
|
||||
}
|
||||
|
||||
|
||||
List<HidDeviceInfo> devList = PureJavaHidApi.enumerateDevices();
|
||||
for(HidDeviceInfo device : devList) {
|
||||
if (device.getVendorId() == dOpts.getVendorId().shortValue() && device.getProductId() == dOpts.getProductId().shortValue()
|
||||
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage().shortValue() == device.getUsagePage())
|
||||
&& (dOpts.getSerial() == null || dOpts.getSerial().equals(device.getSerialNumberString()))) {
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
297
old code/tray/src/qz/communication/SerialIO.java
Executable file
297
old code/tray/src/qz/communication/SerialIO.java
Executable file
@@ -0,0 +1,297 @@
|
||||
package qz.communication;
|
||||
|
||||
import jssc.*;
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.ByteArrayBuilder;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.DeviceUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author Tres
|
||||
*/
|
||||
public class SerialIO implements DeviceListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SerialIO.class);
|
||||
|
||||
// Timeout to wait before giving up on reading the specified amount of bytes
|
||||
private static final int TIMEOUT = 1200;
|
||||
|
||||
private String portName;
|
||||
private SerialPort port;
|
||||
private SerialOptions serialOpts;
|
||||
|
||||
private ByteArrayBuilder data = new ByteArrayBuilder();
|
||||
|
||||
private SocketConnection websocket;
|
||||
|
||||
|
||||
/**
|
||||
* Controller for serial communications
|
||||
*
|
||||
* @param portName Port name to open, such as "COM1" or "/dev/tty0/"
|
||||
*/
|
||||
public SerialIO(String portName, SocketConnection websocket) {
|
||||
this.portName = portName;
|
||||
this.websocket = websocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the specified port name.
|
||||
*
|
||||
* @param opts Parsed serial options
|
||||
* @return Boolean indicating success.
|
||||
* @throws SerialPortException If the port fails to open.
|
||||
*/
|
||||
public boolean open(SerialOptions opts) throws SerialPortException {
|
||||
if (isOpen()) {
|
||||
log.warn("Serial port [{}] is already open", portName);
|
||||
return false;
|
||||
}
|
||||
|
||||
port = new SerialPort(portName);
|
||||
port.openPort();
|
||||
|
||||
serialOpts = new SerialOptions();
|
||||
setOptions(opts);
|
||||
|
||||
return port.isOpened();
|
||||
}
|
||||
|
||||
public void applyPortListener(SerialPortEventListener listener) throws SerialPortException {
|
||||
port.addEventListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Boolean indicating if port is currently open.
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return port != null && port.isOpened();
|
||||
}
|
||||
|
||||
public String processSerialEvent(SerialPortEvent event) {
|
||||
SerialOptions.ResponseFormat format = serialOpts.getResponseFormat();
|
||||
|
||||
try {
|
||||
// Receive data
|
||||
if (event.isRXCHAR()) {
|
||||
data.append(port.readBytes(event.getEventValue(), TIMEOUT));
|
||||
|
||||
String response = null;
|
||||
if (format.isBoundNewline()) {
|
||||
//process as line delimited
|
||||
|
||||
// check for CR AND NL
|
||||
Integer endIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r', '\n'});
|
||||
int delimSize = 2;
|
||||
|
||||
// check for CR OR NL
|
||||
if(endIdx == null) {
|
||||
endIdx = min(
|
||||
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r'}),
|
||||
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\n'}));
|
||||
delimSize = 1;
|
||||
}
|
||||
if (endIdx != null) {
|
||||
log.trace("Reading newline-delimited response");
|
||||
byte[] output = new byte[endIdx];
|
||||
System.arraycopy(data.getByteArray(), 0, output, 0, endIdx);
|
||||
String buffer = new String(output, format.getEncoding());
|
||||
|
||||
if (!buffer.isEmpty()) {
|
||||
//send non-empty string
|
||||
response = buffer;
|
||||
}
|
||||
|
||||
data.clearRange(0, endIdx + delimSize);
|
||||
}
|
||||
} else if (format.getBoundStart() != null && format.getBoundStart().length > 0) {
|
||||
//process as formatted response
|
||||
Integer startIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundStart());
|
||||
|
||||
if (startIdx != null) {
|
||||
int startOffset = startIdx + format.getBoundStart().length;
|
||||
|
||||
int copyLength = 0;
|
||||
int endIdx = 0;
|
||||
|
||||
if (format.getBoundEnd() != null && format.getBoundEnd().length > 0) {
|
||||
//process as bounded response
|
||||
Integer boundEnd = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundEnd(), startIdx);
|
||||
|
||||
if (boundEnd != null) {
|
||||
log.trace("Reading bounded response");
|
||||
|
||||
copyLength = boundEnd - startOffset;
|
||||
endIdx = boundEnd + 1;
|
||||
if (format.isIncludeStart()) {
|
||||
//also include the ending bytes
|
||||
copyLength += format.getBoundEnd().length;
|
||||
}
|
||||
}
|
||||
} else if (format.getFixedWidth() > 0) {
|
||||
//process as fixed length prefixed response
|
||||
log.trace("Reading fixed length prefixed response");
|
||||
|
||||
copyLength = format.getFixedWidth();
|
||||
endIdx = startOffset + format.getFixedWidth();
|
||||
} else if (format.getLength() != null) {
|
||||
//process as dynamic formatted response
|
||||
SerialOptions.ByteParam lengthParam = format.getLength();
|
||||
|
||||
if (data.getLength() > startOffset + lengthParam.getIndex() + lengthParam.getLength()) { //ensure there's length bytes to read
|
||||
log.trace("Reading dynamic formatted response");
|
||||
|
||||
int expectedLength = ByteUtilities.parseBytes(data.getByteArray(), startOffset + lengthParam.getIndex(), lengthParam.getLength(), lengthParam.getEndian());
|
||||
log.trace("Found length byte, expecting {} bytes", expectedLength);
|
||||
|
||||
startOffset += lengthParam.getIndex() + lengthParam.getLength(); // don't include the length byte(s) in the response
|
||||
copyLength = expectedLength;
|
||||
endIdx = startOffset + copyLength;
|
||||
|
||||
if (format.getCrc() != null) {
|
||||
SerialOptions.ByteParam crcParam = format.getCrc();
|
||||
|
||||
log.trace("Expecting {} crc bytes", crcParam.getLength());
|
||||
int expand = crcParam.getIndex() + crcParam.getLength();
|
||||
|
||||
//include crc in copy
|
||||
copyLength += expand;
|
||||
endIdx += expand;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
//process as header formatted raw response - high risk of lost data, likely unintended settings
|
||||
log.warn("Reading header formatted raw response, are you missing an rx option?");
|
||||
|
||||
copyLength = data.getLength() - startOffset;
|
||||
endIdx = data.getLength();
|
||||
}
|
||||
|
||||
|
||||
if (copyLength > 0 && data.getLength() >= endIdx) {
|
||||
log.debug("Response format readable, starting copy");
|
||||
|
||||
if (format.isIncludeStart()) {
|
||||
//increase length to account for header bytes and bump offset back to include in copy
|
||||
copyLength += (startOffset - startIdx);
|
||||
startOffset = startIdx;
|
||||
}
|
||||
|
||||
byte[] responseData = new byte[copyLength];
|
||||
System.arraycopy(data.getByteArray(), startOffset, responseData, 0, copyLength);
|
||||
|
||||
response = new String(responseData, format.getEncoding());
|
||||
data.clearRange(startIdx, endIdx);
|
||||
}
|
||||
}
|
||||
} else if (format.getFixedWidth() > 0) {
|
||||
if (data.getLength() >= format.getFixedWidth()) {
|
||||
//process as fixed width response
|
||||
log.trace("Reading fixed length response");
|
||||
|
||||
byte[] output = new byte[format.getFixedWidth()];
|
||||
System.arraycopy(data.getByteArray(), 0, output, 0, format.getFixedWidth());
|
||||
|
||||
response = StringUtils.newStringUtf8(output);
|
||||
data.clearRange(0, format.getFixedWidth());
|
||||
}
|
||||
} else {
|
||||
//no processing, return raw
|
||||
log.trace("Reading raw response");
|
||||
|
||||
response = new String(data.getByteArray(), format.getEncoding());
|
||||
data.clear();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
catch(SerialPortException e) {
|
||||
log.error("Exception occurred while reading data from port.", e);
|
||||
}
|
||||
catch(SerialPortTimeoutException e) {
|
||||
log.error("Timeout occurred waiting for port to respond.", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and caches the properties as to not set them every data call
|
||||
*
|
||||
* @throws SerialPortException If the properties fail to set
|
||||
*/
|
||||
private void setOptions(SerialOptions opts) throws SerialPortException {
|
||||
if (opts == null) { return; }
|
||||
|
||||
SerialOptions.PortSettings ps = opts.getPortSettings();
|
||||
if (ps != null && !ps.equals(serialOpts.getPortSettings())) {
|
||||
log.debug("Applying new port settings");
|
||||
port.setParams(ps.getBaudRate(), ps.getDataBits(), ps.getStopBits(), ps.getParity());
|
||||
port.setFlowControlMode(ps.getFlowControl());
|
||||
serialOpts.setPortSettings(ps);
|
||||
}
|
||||
|
||||
SerialOptions.ResponseFormat rf = opts.getResponseFormat();
|
||||
if (rf != null) {
|
||||
log.debug("Applying new response formatting");
|
||||
serialOpts.setResponseFormat(rf);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the port parameters and writes the buffered data to the serial port.
|
||||
*/
|
||||
public void sendData(JSONObject params, SerialOptions opts) throws JSONException, IOException, SerialPortException {
|
||||
if (opts != null) {
|
||||
setOptions(opts);
|
||||
}
|
||||
|
||||
log.debug("Sending data over [{}]", portName);
|
||||
port.writeBytes(DeviceUtilities.getDataBytes(params, serialOpts.getPortSettings().getEncoding()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the serial port, if open.
|
||||
*
|
||||
* @throws SerialPortException If the port fails to close.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
// Remove orphaned reference
|
||||
websocket.removeSerialPort(portName);
|
||||
|
||||
if (!isOpen()) {
|
||||
log.warn("Serial port [{}] is not open.", portName);
|
||||
}
|
||||
|
||||
try {
|
||||
boolean closed = port.closePort();
|
||||
if (closed) {
|
||||
log.info("Serial port [{}] closed successfully.", portName);
|
||||
} else {
|
||||
// Handle ambiguity in JSSCs API
|
||||
throw new SerialPortException(portName, "closePort", "Port not closed");
|
||||
}
|
||||
} catch(SerialPortException e) {
|
||||
log.warn("Serial port [{}] was not closed properly.", portName);
|
||||
}
|
||||
|
||||
port = null;
|
||||
portName = null;
|
||||
}
|
||||
|
||||
private Integer min(Integer a, Integer b) {
|
||||
if (a == null) { return b; }
|
||||
if (b == null) { return a; }
|
||||
return Math.min(a, b);
|
||||
}
|
||||
|
||||
}
|
||||
350
old code/tray/src/qz/communication/SerialOptions.java
Executable file
350
old code/tray/src/qz/communication/SerialOptions.java
Executable file
@@ -0,0 +1,350 @@
|
||||
package qz.communication;
|
||||
|
||||
import jssc.SerialPort;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.DeviceUtilities;
|
||||
import qz.utils.LoggerUtilities;
|
||||
import qz.utils.SerialUtilities;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SerialOptions {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SerialOptions.class);
|
||||
|
||||
private static final String DEFAULT_BEGIN = "0x0002";
|
||||
private static final String DEFAULT_END = "0x000D";
|
||||
|
||||
private PortSettings portSettings = null;
|
||||
private ResponseFormat responseFormat = null;
|
||||
|
||||
/**
|
||||
* Creates an empty/default options object
|
||||
*/
|
||||
public SerialOptions() {
|
||||
portSettings = new PortSettings();
|
||||
responseFormat = new ResponseFormat();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the provided JSON object into relevant SerialPort constants
|
||||
*/
|
||||
public SerialOptions(JSONObject serialOpts, boolean isOpening) {
|
||||
if (serialOpts == null) { return; }
|
||||
|
||||
//only apply port settings if opening or explicitly set in a send data call
|
||||
if (isOpening || serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl")) {
|
||||
portSettings = new PortSettings();
|
||||
|
||||
if (!serialOpts.isNull("baudRate")) {
|
||||
try { portSettings.baudRate = SerialUtilities.parseBaudRate(serialOpts.getString("baudRate")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "baudRate", serialOpts.opt("baudRate")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("dataBits")) {
|
||||
try { portSettings.dataBits = SerialUtilities.parseDataBits(serialOpts.getString("dataBits")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "dataBits", serialOpts.opt("dataBits")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("stopBits")) {
|
||||
try { portSettings.stopBits = SerialUtilities.parseStopBits(serialOpts.getString("stopBits")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "stopBits", serialOpts.opt("stopBits")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("parity")) {
|
||||
try { portSettings.parity = SerialUtilities.parseParity(serialOpts.getString("parity")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "parity", serialOpts.opt("parity")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("flowControl")) {
|
||||
try { portSettings.flowControl = SerialUtilities.parseFlowControl(serialOpts.getString("flowControl")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "flowControl", serialOpts.opt("flowControl")); }
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("encoding") && !serialOpts.optString("encoding").isEmpty()) {
|
||||
try { portSettings.encoding = Charset.forName(serialOpts.getString("encoding")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "encoding", serialOpts.opt("encoding")); }
|
||||
}
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("rx")) {
|
||||
responseFormat = new ResponseFormat();
|
||||
//Make the response encoding default to the port encoding. If this is removed it will default to UTF-8
|
||||
responseFormat.encoding = portSettings.encoding;
|
||||
|
||||
JSONObject respOpts = serialOpts.optJSONObject("rx");
|
||||
if (respOpts != null) {
|
||||
if (!respOpts.isNull("start")) {
|
||||
try {
|
||||
JSONArray startBits = respOpts.getJSONArray("start");
|
||||
ArrayList<Byte> bytes = new ArrayList<>();
|
||||
for(int i = 0; i < startBits.length(); i++) {
|
||||
byte[] charByte = DeviceUtilities.characterBytes(startBits.getString(i), responseFormat.encoding);
|
||||
for(byte b : charByte) { bytes.add(b); }
|
||||
}
|
||||
responseFormat.boundStart = ArrayUtils.toPrimitive(bytes.toArray(new Byte[0]));
|
||||
}
|
||||
catch(JSONException e) {
|
||||
try { responseFormat.boundStart = DeviceUtilities.characterBytes(respOpts.getString("start"), responseFormat.encoding); }
|
||||
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "string", "start", respOpts.opt("start")); }
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("includeHeader")) {
|
||||
try { responseFormat.includeStart = respOpts.getBoolean("includeHeader"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "includeHeader", respOpts.opt("includeHeader")); }
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("end")) {
|
||||
try { responseFormat.boundEnd = DeviceUtilities.characterBytes(respOpts.getString("end"), responseFormat.encoding); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "end", respOpts.opt("end")); }
|
||||
|
||||
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||
log.warn("End bound set without start bound defined");
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("untilNewline")) {
|
||||
try { responseFormat.boundNewline = respOpts.getBoolean("untilNewline"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "untilNewline", respOpts.opt("untilNewline")); }
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("width")) {
|
||||
try { responseFormat.fixedWidth = respOpts.getInt("width"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", respOpts.opt("width")); }
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("lengthBytes")) {
|
||||
try {
|
||||
JSONObject lengthOpts = respOpts.optJSONObject("lengthBytes");
|
||||
responseFormat.length = new ByteParam();
|
||||
|
||||
if (lengthOpts != null) {
|
||||
if (!lengthOpts.isNull("index")) {
|
||||
try { responseFormat.length.index = lengthOpts.getInt("index"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.index", lengthOpts.opt("index")); }
|
||||
}
|
||||
|
||||
if (!lengthOpts.isNull("length")) {
|
||||
try { responseFormat.length.length = lengthOpts.getInt("length"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.length", lengthOpts.opt("length")); }
|
||||
}
|
||||
|
||||
if (!lengthOpts.isNull("endian")) {
|
||||
try { responseFormat.length.endian = ByteUtilities.Endian.valueOf(lengthOpts.getString("endian").toUpperCase(Locale.ENGLISH)); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "string", "lengthBytes.endian", lengthOpts.opt("endian")); }
|
||||
}
|
||||
} else {
|
||||
responseFormat.length.index = respOpts.getInt("lengthBytes");
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes", respOpts.opt("lengthBytes")); }
|
||||
|
||||
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||
log.warn("Length byte(s) defined without start bound defined");
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("crcBytes")) {
|
||||
try {
|
||||
JSONObject crcOpts = respOpts.optJSONObject("crcBytes");
|
||||
responseFormat.crc = new ByteParam();
|
||||
|
||||
if (crcOpts != null) {
|
||||
if (!crcOpts.isNull("index")) {
|
||||
try { responseFormat.crc.index = crcOpts.getInt("index"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.index", crcOpts.opt("index")); }
|
||||
}
|
||||
|
||||
if (!crcOpts.isNull("length")) {
|
||||
try { responseFormat.crc.length = crcOpts.getInt("length"); }
|
||||
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.length", crcOpts.opt("length")); }
|
||||
}
|
||||
} else {
|
||||
responseFormat.crc.length = respOpts.getInt("crcBytes");
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "crcBytes", respOpts.opt("crcBytes")); }
|
||||
|
||||
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||
log.warn("CRC byte(s) defined without start bound defined");
|
||||
}
|
||||
}
|
||||
|
||||
if (!respOpts.isNull("encoding") && !respOpts.optString("encoding").isEmpty()) {
|
||||
try { responseFormat.encoding = Charset.forName(respOpts.getString("encoding")); }
|
||||
catch(JSONException | IllegalArgumentException e) { LoggerUtilities.optionWarn(log, "charset", "encoding", respOpts.opt("encoding")); }
|
||||
}
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "rx", serialOpts.opt("rx"));
|
||||
}
|
||||
} else if (isOpening) {
|
||||
// legacy support - only applies on port open
|
||||
responseFormat = new ResponseFormat();
|
||||
|
||||
// legacy start only supports string, not an array
|
||||
if (!serialOpts.isNull("start")) {
|
||||
responseFormat.boundStart = DeviceUtilities.characterBytes(serialOpts.optString("start", DEFAULT_BEGIN), responseFormat.encoding);
|
||||
} else {
|
||||
responseFormat.boundStart = DeviceUtilities.characterBytes(DEFAULT_BEGIN, responseFormat.encoding);
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("end")) {
|
||||
responseFormat.boundEnd = DeviceUtilities.characterBytes(serialOpts.optString("end", DEFAULT_END), responseFormat.encoding);
|
||||
} else {
|
||||
responseFormat.boundEnd = DeviceUtilities.characterBytes(DEFAULT_END, responseFormat.encoding);
|
||||
}
|
||||
|
||||
if (!serialOpts.isNull("width")) {
|
||||
try {
|
||||
responseFormat.fixedWidth = serialOpts.getInt("width");
|
||||
if (responseFormat.boundEnd.length > 0) {
|
||||
log.warn("Combining 'width' property with 'end' property has undefined behavior and should not be used");
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", serialOpts.opt("width")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PortSettings getPortSettings() {
|
||||
return portSettings;
|
||||
}
|
||||
|
||||
public ResponseFormat getResponseFormat() {
|
||||
return responseFormat;
|
||||
}
|
||||
|
||||
public void setPortSettings(PortSettings portSettings) {
|
||||
this.portSettings = portSettings;
|
||||
}
|
||||
|
||||
public void setResponseFormat(ResponseFormat responseFormat) {
|
||||
this.responseFormat = responseFormat;
|
||||
}
|
||||
|
||||
public class PortSettings {
|
||||
|
||||
private Charset encoding = Charset.forName("UTF-8");
|
||||
private int baudRate = SerialPort.BAUDRATE_9600;
|
||||
private int dataBits = SerialPort.DATABITS_8;
|
||||
private int stopBits = SerialPort.STOPBITS_1;
|
||||
private int parity = SerialPort.PARITY_NONE;
|
||||
private int flowControl = SerialPort.FLOWCONTROL_NONE;
|
||||
|
||||
|
||||
public Charset getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public int getBaudRate() {
|
||||
return baudRate;
|
||||
}
|
||||
|
||||
public int getDataBits() {
|
||||
return dataBits;
|
||||
}
|
||||
|
||||
public int getStopBits() {
|
||||
return stopBits;
|
||||
}
|
||||
|
||||
public int getParity() {
|
||||
return parity;
|
||||
}
|
||||
|
||||
public int getFlowControl() {
|
||||
return flowControl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o instanceof PortSettings) {
|
||||
PortSettings that = (PortSettings)o;
|
||||
|
||||
return getEncoding().equals(that.getEncoding()) &&
|
||||
getBaudRate() == that.getBaudRate() &&
|
||||
getDataBits() == that.getDataBits() &&
|
||||
getStopBits() == that.getStopBits() &&
|
||||
getParity() == that.getParity() &&
|
||||
getFlowControl() == that.getFlowControl();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ResponseFormat {
|
||||
|
||||
private Charset encoding = Charset.forName("UTF-8"); //Response charset
|
||||
private byte[] boundStart; //Character(s) denoting start of new response
|
||||
private byte[] boundEnd; //Character denoting end of a response
|
||||
private boolean boundNewline; //If the response should be split on \r?\n
|
||||
private int fixedWidth; //Fixed length response bounds
|
||||
private ByteParam length; //Info about the data length byte(s)
|
||||
private ByteParam crc; //Info about the data crc byte(s)
|
||||
private boolean includeStart; //If the response headers should be sent as well
|
||||
|
||||
|
||||
public Charset getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
public byte[] getBoundStart() {
|
||||
return boundStart;
|
||||
}
|
||||
|
||||
public byte[] getBoundEnd() {
|
||||
return boundEnd;
|
||||
}
|
||||
|
||||
public int getFixedWidth() {
|
||||
return fixedWidth;
|
||||
}
|
||||
|
||||
public ByteParam getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public ByteParam getCrc() {
|
||||
return crc;
|
||||
}
|
||||
|
||||
public boolean isIncludeStart() {
|
||||
return includeStart;
|
||||
}
|
||||
|
||||
public boolean isBoundNewline() {
|
||||
return boundNewline;
|
||||
}
|
||||
}
|
||||
|
||||
public class ByteParam {
|
||||
|
||||
private int index = 0;
|
||||
private int length = 1;
|
||||
private ByteUtilities.Endian endian = ByteUtilities.Endian.BIG;
|
||||
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public ByteUtilities.Endian getEndian() {
|
||||
return endian;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
100
old code/tray/src/qz/communication/SocketIO.java
Executable file
100
old code/tray/src/qz/communication/SocketIO.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.DeviceUtilities;
|
||||
import qz.utils.NetworkUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class SocketIO implements DeviceListener {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(SocketIO.class);
|
||||
|
||||
private String host;
|
||||
private int port;
|
||||
private Charset encoding;
|
||||
|
||||
private Socket socket;
|
||||
private DataOutputStream dataOut;
|
||||
private DataInputStream dataIn;
|
||||
|
||||
private SocketConnection websocket;
|
||||
|
||||
public SocketIO(String host, int port, Charset encoding, SocketConnection websocket) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.encoding = encoding;
|
||||
this.websocket = websocket;
|
||||
}
|
||||
|
||||
public boolean open() throws IOException {
|
||||
socket = new Socket(host, port);
|
||||
socket.setSoTimeout(NetworkUtilities.SOCKET_TIMEOUT);
|
||||
dataOut = new DataOutputStream(socket.getOutputStream());
|
||||
dataIn = new DataInputStream(socket.getInputStream());
|
||||
|
||||
return socket.isConnected();
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return socket.isConnected();
|
||||
}
|
||||
|
||||
public void sendData(JSONObject params) throws JSONException, IOException {
|
||||
log.debug("Sending data over [{}:{}]", host, port);
|
||||
dataOut.write(DeviceUtilities.getDataBytes(params, encoding));
|
||||
dataOut.flush();
|
||||
}
|
||||
|
||||
public String processSocketResponse() throws IOException {
|
||||
byte[] response = new byte[1024];
|
||||
ArrayList<Byte> fullResponse = new ArrayList<>();
|
||||
do {
|
||||
int size = dataIn.read(response);
|
||||
for(int i = 0; i < size; i++) {
|
||||
fullResponse.add(response[i]);
|
||||
}
|
||||
}
|
||||
while(dataIn.available() > 0);
|
||||
if(fullResponse.size() > 0) {
|
||||
return new String(ArrayUtils.toPrimitive(fullResponse.toArray(new Byte[0])), encoding);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// Remove orphaned reference
|
||||
websocket.removeNetworkSocket(String.format("%s:%s", host, port));
|
||||
|
||||
try {
|
||||
dataOut.close();
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not close socket output stream", e);
|
||||
}
|
||||
try {
|
||||
socket.close();
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not close socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
}
|
||||
153
old code/tray/src/qz/communication/UsbIO.java
Executable file
153
old code/tray/src/qz/communication/UsbIO.java
Executable file
@@ -0,0 +1,153 @@
|
||||
package qz.communication;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.UsbUtilities;
|
||||
import qz.ws.SocketConnection;
|
||||
|
||||
import javax.usb.*;
|
||||
import javax.usb.util.UsbUtil;
|
||||
|
||||
public class UsbIO implements DeviceIO {
|
||||
private static final Logger log = LogManager.getLogger(UsbIO.class);
|
||||
private UsbDevice device;
|
||||
private UsbInterface iface;
|
||||
|
||||
private boolean streaming;
|
||||
|
||||
private DeviceOptions dOpts;
|
||||
private SocketConnection websocket;
|
||||
|
||||
public UsbIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||
this.dOpts = dOpts;
|
||||
this.websocket = websocket;
|
||||
UsbDevice device = UsbUtilities.findDevice(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue());
|
||||
if (device == null) {
|
||||
throw new DeviceException("USB device could not be found");
|
||||
}
|
||||
if (dOpts.getInterfaceId() == null) {
|
||||
throw new IllegalArgumentException("Device interface cannot be null");
|
||||
}
|
||||
this.iface = device.getActiveUsbConfiguration().getUsbInterface(dOpts.getInterfaceId());
|
||||
if (iface == null) {
|
||||
throw new DeviceException(String.format("Could not find USB interface matching [ vendorId: '%s', productId: '%s', interface: '%s' ]",
|
||||
"0x" + UsbUtil.toHexString(dOpts.getVendorId()),
|
||||
"0x" + UsbUtil.toHexString(dOpts.getProductId()),
|
||||
"0x" + UsbUtil.toHexString(dOpts.getInterfaceId())));
|
||||
}
|
||||
this.device = device;
|
||||
|
||||
}
|
||||
|
||||
public void open() throws DeviceException {
|
||||
try {
|
||||
iface.claim(new UsbInterfacePolicy() {
|
||||
@Override
|
||||
public boolean forceClaim(UsbInterface usbInterface) {
|
||||
// Releases kernel driver for systems that auto-claim usb devices
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(UsbException e) {
|
||||
throw new DeviceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return iface.isClaimed();
|
||||
}
|
||||
|
||||
public void setStreaming(boolean active) {
|
||||
streaming = active;
|
||||
}
|
||||
|
||||
public boolean isStreaming() {
|
||||
return streaming;
|
||||
}
|
||||
|
||||
public String getVendorId() {
|
||||
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idVendor());
|
||||
}
|
||||
|
||||
public String getProductId() {
|
||||
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idProduct());
|
||||
}
|
||||
|
||||
public String getInterface() {
|
||||
return UsbUtil.toHexString(iface.getUsbInterfaceDescriptor().iInterface());
|
||||
}
|
||||
|
||||
public byte[] readData(int responseSize, Byte endpoint) throws DeviceException {
|
||||
try {
|
||||
byte[] response = new byte[responseSize];
|
||||
exchangeData(endpoint, response);
|
||||
return response;
|
||||
}
|
||||
catch(UsbException e) {
|
||||
throw new DeviceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendData(byte[] data, Byte endpoint) throws DeviceException {
|
||||
try {
|
||||
exchangeData(endpoint, data);
|
||||
}
|
||||
catch(UsbException e) {
|
||||
throw new DeviceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
|
||||
throw new DeviceException("USB feature reports are not supported");
|
||||
}
|
||||
|
||||
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
|
||||
throw new DeviceException("USB feature reports are not supported");
|
||||
}
|
||||
|
||||
/**
|
||||
* Data will be sent to or received from the open usb device, depending on the {@code endpoint} used.
|
||||
*
|
||||
* @param endpoint Endpoint on the usb device interface to pass data across
|
||||
* @param data Byte array of data to send, or to be written from a receive
|
||||
*/
|
||||
private synchronized void exchangeData(Byte endpoint, byte[] data) throws UsbException, DeviceException {
|
||||
if (endpoint == null) {
|
||||
throw new IllegalArgumentException("Interface endpoint cannot be null");
|
||||
}
|
||||
|
||||
UsbEndpoint usbEndpoint = iface.getUsbEndpoint(endpoint);
|
||||
if(usbEndpoint == null) {
|
||||
throw new DeviceException(String.format("Could not find USB endpoint matching [ endpoint: '%s' ]",
|
||||
"0x" + UsbUtil.toHexString(endpoint)));
|
||||
}
|
||||
UsbPipe pipe = usbEndpoint.getUsbPipe();
|
||||
if (!pipe.isOpen()) { pipe.open(); }
|
||||
|
||||
try {
|
||||
pipe.syncSubmit(data);
|
||||
}
|
||||
finally {
|
||||
if(pipe != null) {
|
||||
pipe.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
setStreaming(false);
|
||||
// Remove orphaned reference
|
||||
websocket.removeDevice(dOpts);
|
||||
if (iface.isClaimed()) {
|
||||
try {
|
||||
iface.release();
|
||||
}
|
||||
catch(UsbException e) {
|
||||
log.error("Unable to close USB device", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
28
old code/tray/src/qz/communication/WinspoolEx.java
Executable file
28
old code/tray/src/qz/communication/WinspoolEx.java
Executable file
@@ -0,0 +1,28 @@
|
||||
package qz.communication;
|
||||
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import com.sun.jna.platform.win32.Winspool;
|
||||
import com.sun.jna.win32.W32APIOptions;
|
||||
|
||||
/**
|
||||
* TODO: Remove when JNA 5.14.0+ is bundled
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface WinspoolEx extends Winspool {
|
||||
WinspoolEx INSTANCE = Native.load("Winspool.drv", WinspoolEx.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||
|
||||
int JOB_CONTROL_NONE = 0x00000000; // Perform no additional action.
|
||||
int JOB_CONTROL_PAUSE = 0x00000001; // Pause the print job.
|
||||
int JOB_CONTROL_RESUME = 0x00000002; // Resume a paused print job.
|
||||
int JOB_CONTROL_CANCEL = 0x00000003; // Delete a print job.
|
||||
int JOB_CONTROL_RESTART = 0x00000004; // Restart a print job.
|
||||
int JOB_CONTROL_DELETE = 0x00000005; // Delete a print job.
|
||||
int JOB_CONTROL_SENT_TO_PRINTER = 0x00000006; // Used by port monitors to signal that a print job has been sent to the printer. This value SHOULD NOT be used remotely.
|
||||
int JOB_CONTROL_LAST_PAGE_EJECTED = 0x00000007; // Used by language monitors to signal that the last page of a print job has been ejected from the printer. This value SHOULD NOT be used remotely.
|
||||
int JOB_CONTROL_RETAIN = 0x00000008; // Keep the print job in the print queue after it prints.
|
||||
int JOB_CONTROL_RELEASE = 0x00000009; // Release the print job, undoing the effect of a JOB_CONTROL_RETAIN action.
|
||||
|
||||
boolean SetJob(WinNT.HANDLE hPrinter, int JobId, int Level, Pointer pJob, int Command);
|
||||
}
|
||||
11
old code/tray/src/qz/exception/InvalidRawImageException.java
Executable file
11
old code/tray/src/qz/exception/InvalidRawImageException.java
Executable file
@@ -0,0 +1,11 @@
|
||||
package qz.exception;
|
||||
|
||||
public class InvalidRawImageException extends Exception {
|
||||
public InvalidRawImageException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public InvalidRawImageException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
}
|
||||
3
old code/tray/src/qz/exception/MissingArgException.java
Executable file
3
old code/tray/src/qz/exception/MissingArgException.java
Executable file
@@ -0,0 +1,3 @@
|
||||
package qz.exception;
|
||||
|
||||
public class MissingArgException extends Exception {}
|
||||
10
old code/tray/src/qz/exception/NullCommandException.java
Executable file
10
old code/tray/src/qz/exception/NullCommandException.java
Executable file
@@ -0,0 +1,10 @@
|
||||
package qz.exception;
|
||||
|
||||
public class NullCommandException extends javax.print.PrintException {
|
||||
public NullCommandException() {
|
||||
super();
|
||||
}
|
||||
public NullCommandException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
7
old code/tray/src/qz/exception/NullPrintServiceException.java
Executable file
7
old code/tray/src/qz/exception/NullPrintServiceException.java
Executable file
@@ -0,0 +1,7 @@
|
||||
package qz.exception;
|
||||
|
||||
public class NullPrintServiceException extends javax.print.PrintException {
|
||||
public NullPrintServiceException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
413
old code/tray/src/qz/installer/Installer.java
Executable file
413
old code/tray/src/qz/installer/Installer.java
Executable file
@@ -0,0 +1,413 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.auth.Certificate;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.installer.certificate.*;
|
||||
import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
|
||||
import qz.installer.provision.ProvisionInstaller;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.WebsocketPorts;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
import static qz.installer.certificate.KeyPairWrapper.Type.CA;
|
||||
import static qz.utils.FileUtilities.*;
|
||||
|
||||
/**
|
||||
* Cross-platform wrapper for install steps
|
||||
* - Used by CommandParser via command line
|
||||
* - Used by PrintSocketServer at startup to ensure SSL is functioning
|
||||
*/
|
||||
public abstract class Installer {
|
||||
protected static final Logger log = LogManager.getLogger(Installer.class);
|
||||
|
||||
// Silence prompts within our control
|
||||
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
|
||||
public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";
|
||||
|
||||
WebsocketPorts websocketPorts;
|
||||
|
||||
public enum PrivilegeLevel {
|
||||
USER,
|
||||
SYSTEM
|
||||
}
|
||||
|
||||
public abstract Installer removeLegacyStartup();
|
||||
public abstract Installer addAppLauncher();
|
||||
public abstract Installer addStartupEntry();
|
||||
public abstract Installer addSystemSettings();
|
||||
public abstract Installer removeSystemSettings();
|
||||
public abstract void spawn(List<String> args) throws Exception;
|
||||
|
||||
public abstract void setDestination(String destination);
|
||||
public abstract String getDestination();
|
||||
|
||||
private static Installer instance;
|
||||
|
||||
public static Installer getInstance() {
|
||||
if(instance == null) {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
instance = new WindowsInstaller();
|
||||
break;
|
||||
case MAC:
|
||||
instance = new MacInstaller();
|
||||
break;
|
||||
default:
|
||||
instance = new LinuxInstaller();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void install(String destination, boolean silent) throws Exception {
|
||||
IS_SILENT |= silent; // preserve environmental variable if possible
|
||||
getInstance();
|
||||
if (destination != null) {
|
||||
instance.setDestination(destination);
|
||||
}
|
||||
install();
|
||||
}
|
||||
|
||||
public static boolean preinstall() {
|
||||
getInstance();
|
||||
log.info("Fixing runtime permissions...");
|
||||
instance.setJrePermissions(SystemUtilities.getAppPath().toString());
|
||||
log.info("Stopping running instances...");
|
||||
return TaskKiller.killAll();
|
||||
}
|
||||
|
||||
public static void install() throws Exception {
|
||||
getInstance();
|
||||
log.info("Installing to {}", instance.getDestination());
|
||||
instance.removeLibs()
|
||||
.removeProvisioning()
|
||||
.deployApp()
|
||||
.removeLegacyStartup()
|
||||
.removeLegacyFiles()
|
||||
.addSharedDirectory()
|
||||
.addAppLauncher()
|
||||
.addStartupEntry()
|
||||
.invokeProvisioning(Phase.INSTALL)
|
||||
.addSystemSettings();
|
||||
}
|
||||
|
||||
public static void uninstall() {
|
||||
log.info("Stopping running instances...");
|
||||
TaskKiller.killAll();
|
||||
getInstance();
|
||||
log.info("Uninstalling from {}", instance.getDestination());
|
||||
instance.removeSharedDirectory()
|
||||
.removeSystemSettings()
|
||||
.removeCerts()
|
||||
.invokeProvisioning(Phase.UNINSTALL);
|
||||
}
|
||||
|
||||
public Installer deployApp() throws IOException {
|
||||
Path src = SystemUtilities.getAppPath();
|
||||
Path dest = Paths.get(getDestination());
|
||||
|
||||
if(!Files.exists(dest)) {
|
||||
Files.createDirectories(dest);
|
||||
}
|
||||
|
||||
// Delete the JDK blindly
|
||||
FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile());
|
||||
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
|
||||
FileUtils.copyDirectory(src.toFile(), dest.toFile(), false);
|
||||
FileUtilities.setPermissionsRecursively(dest, false);
|
||||
// Fix permissions for provisioned files
|
||||
FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ?
|
||||
dest.resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||
dest.resolve(PROVISION_DIR), false);
|
||||
if(!SystemUtilities.isWindows()) {
|
||||
setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall");
|
||||
setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE);
|
||||
return setJrePermissions(getDestination());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
private Installer setJrePermissions(String dest) {
|
||||
File jreLocation = new File(dest, JRE_LOCATION);
|
||||
File jreBin = new File(jreLocation, "bin");
|
||||
File jreLib = new File(jreLocation, "lib");
|
||||
|
||||
// Set jre/bin/java and friends executable
|
||||
File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory());
|
||||
if(files != null) {
|
||||
for(File file : files) {
|
||||
file.setExecutable(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Set jspawnhelper executable
|
||||
new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void setExecutable(String relativePath) {
|
||||
new File(getDestination(), relativePath).setExecutable(true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicitly purge libs to notify system cache per https://github.com/qzind/tray/issues/662
|
||||
*/
|
||||
public Installer removeLibs() {
|
||||
String[] dirs = { "libs" };
|
||||
for (String dir : dirs) {
|
||||
try {
|
||||
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
|
||||
} catch(IOException ignore) {}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer cleanupLegacyLogs(int rolloverCount) {
|
||||
// Convert old < 2.2.3 log file format
|
||||
Path logLocation = USER_DIR;
|
||||
int oldIndex = 0;
|
||||
int newIndex = 0;
|
||||
File oldFile;
|
||||
do {
|
||||
// Old: debug.log.1
|
||||
oldFile = logLocation.resolve("debug.log." + ++oldIndex).toFile();
|
||||
if(oldFile.exists()) {
|
||||
// New: debug.1.log
|
||||
File newFile;
|
||||
do {
|
||||
newFile = logLocation.resolve("debug." + ++newIndex + ".log").toFile();
|
||||
} while(newFile.exists());
|
||||
|
||||
oldFile.renameTo(newFile);
|
||||
log.info("Migrated log file {} to new location {}", oldFile, newFile);
|
||||
}
|
||||
} while(oldFile.exists() || oldIndex <= rolloverCount);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeLegacyFiles() {
|
||||
ArrayList<String> dirs = new ArrayList<>();
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
HashMap<String, String> move = new HashMap<>();
|
||||
|
||||
// QZ Tray 2.0 files
|
||||
dirs.add("demo/js/3rdparty");
|
||||
dirs.add("utils");
|
||||
dirs.add("auth");
|
||||
files.add("demo/js/qz-websocket.js");
|
||||
files.add("windows-icon.ico");
|
||||
|
||||
// QZ Tray 2.2.3-SNAPSHOT accidentally wrote certs in the wrong place
|
||||
dirs.add("ssl");
|
||||
|
||||
// QZ Tray 2.1 files
|
||||
if(SystemUtilities.isMac()) {
|
||||
// Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure
|
||||
dirs.add("demo");
|
||||
dirs.add("libs");
|
||||
files.add(PROPS_FILE + ".jar");
|
||||
files.add("LICENSE.txt");
|
||||
files.add("uninstall");
|
||||
move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties");
|
||||
}
|
||||
|
||||
dirs.forEach(dir -> {
|
||||
try {
|
||||
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
|
||||
} catch(IOException ignore) {}
|
||||
});
|
||||
|
||||
files.forEach(file -> {
|
||||
new File(instance.getDestination() + File.separator + file).delete();
|
||||
});
|
||||
|
||||
move.forEach((src, dest) -> {
|
||||
try {
|
||||
FileUtils.moveFile(new File(instance.getDestination() + File.separator + src),
|
||||
new File(instance.getDestination() + File.separator + dest));
|
||||
} catch(IOException ignore) {}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addSharedDirectory() {
|
||||
try {
|
||||
Files.createDirectories(SHARED_DIR);
|
||||
FileUtilities.setPermissionsRecursively(SHARED_DIR, true);
|
||||
Path ssl = Paths.get(SHARED_DIR.toString(), "ssl");
|
||||
Files.createDirectories(ssl);
|
||||
FileUtilities.setPermissionsRecursively(ssl, true);
|
||||
|
||||
log.info("Created shared directory: {}", SHARED_DIR);
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not create shared directory: {}", SHARED_DIR);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeSharedDirectory() {
|
||||
try {
|
||||
FileUtils.deleteDirectory(SHARED_DIR.toFile());
|
||||
log.info("Deleted shared directory: {}", SHARED_DIR);
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not delete shared directory: {}", SHARED_DIR);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, and if needed generates an SSL for the system
|
||||
*/
|
||||
public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception {
|
||||
CertificateManager certificateManager = new CertificateManager(forceNew, hostNames);
|
||||
boolean needsInstall = certificateManager.needsInstall();
|
||||
try {
|
||||
// Check that the CA cert is installed
|
||||
X509Certificate caCert = certificateManager.getKeyPair(CA).getCert();
|
||||
NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance();
|
||||
|
||||
if (forceNew || needsInstall) {
|
||||
// Remove installed certs per request (usually the desktop installer, or failure to write properties)
|
||||
// Skip if running from IDE, this may accidentally remove sandboxed certs
|
||||
if(SystemUtilities.isJar()) {
|
||||
List<String> matchingCerts = installer.find();
|
||||
installer.remove(matchingCerts);
|
||||
}
|
||||
installer.install(caCert);
|
||||
FirefoxCertificateInstaller.install(caCert, hostNames);
|
||||
} else {
|
||||
// Make sure the certificate is recognized by the system
|
||||
if(caCert == null) {
|
||||
log.info("CA cert is empty, skipping installation checks. This is normal for trusted/3rd-party SSL certificates.");
|
||||
} else {
|
||||
File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
|
||||
CertificateManager.writeCert(caCert, tempCert); // temp cert
|
||||
if (!installer.verify(tempCert)) {
|
||||
installer.install(caCert);
|
||||
FirefoxCertificateInstaller.install(caCert, hostNames);
|
||||
}
|
||||
if(!tempCert.delete()) {
|
||||
tempCert.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e);
|
||||
}
|
||||
|
||||
// Add provisioning steps that come after certgen
|
||||
if(SystemUtilities.isAdmin()) {
|
||||
invokeProvisioning(Phase.CERTGEN);
|
||||
}
|
||||
|
||||
return certificateManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove matching certs from user|system, then Firefox
|
||||
*/
|
||||
public Installer removeCerts() {
|
||||
// System certs
|
||||
NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance();
|
||||
instance.remove(instance.find());
|
||||
// Firefox certs
|
||||
FirefoxCertificateInstaller.uninstall();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add user-specific settings
|
||||
* Note: See override usage for platform-specific tasks
|
||||
*/
|
||||
public Installer addUserSettings() {
|
||||
// Check for whitelisted certificates in <install>/whitelist/
|
||||
Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR);
|
||||
if(Files.exists(whiteList) && Files.isDirectory(whiteList)) {
|
||||
for(File file : whiteList.toFile().listFiles()) {
|
||||
try {
|
||||
Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath()));
|
||||
if (!cert.isSaved()) {
|
||||
FileUtilities.addToCertList(ALLOW_FILE, file);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("Could not add {} to {}", file, ALLOW_FILE, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Installer invokeProvisioning(Phase phase) {
|
||||
try {
|
||||
Path provisionPath = SystemUtilities.isMac() ?
|
||||
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||
Paths.get(getDestination()).resolve(PROVISION_DIR);
|
||||
ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath);
|
||||
provisionInstaller.invoke(phase);
|
||||
|
||||
// Special case for custom websocket ports
|
||||
if(phase == Phase.INSTALL) {
|
||||
websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeProvisioning() {
|
||||
try {
|
||||
Path provisionPath = SystemUtilities.isMac() ?
|
||||
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||
Paths.get(getDestination()).resolve(PROVISION_DIR);
|
||||
FileUtils.deleteDirectory(provisionPath.toFile());
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred removing provision directory", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public static Properties persistProperties(File oldFile, Properties newProps) {
|
||||
if(oldFile.exists()) {
|
||||
Properties oldProps = new Properties();
|
||||
try(Reader reader = new FileReader(oldFile)) {
|
||||
oldProps.load(reader);
|
||||
for(String key : PERSIST_PROPS) {
|
||||
if (oldProps.containsKey(key)) {
|
||||
String value = oldProps.getProperty(key);
|
||||
log.info("Preserving {}={} for install", key, value);
|
||||
newProps.put(key, value);
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Warning, an error occurred reading the old properties file {}", oldFile, e);
|
||||
}
|
||||
}
|
||||
return newProps;
|
||||
}
|
||||
|
||||
public void spawn(String ... args) throws Exception {
|
||||
spawn(new ArrayList(Arrays.asList(args)));
|
||||
}
|
||||
}
|
||||
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
@@ -0,0 +1,371 @@
|
||||
package qz.installer;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.UnixUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
|
||||
public class LinuxInstaller extends Installer {
|
||||
protected static final Logger log = LogManager.getLogger(LinuxInstaller.class);
|
||||
|
||||
public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop";
|
||||
public static final String STARTUP_DIR = "/etc/xdg/autostart/";
|
||||
public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME;
|
||||
public static final String APP_DIR = "/usr/share/applications/";
|
||||
public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME;
|
||||
public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules";
|
||||
public static final String[] CHROME_POLICY_DIRS = {"/etc/chromium/policies/managed", "/etc/opt/chrome/policies/managed" };
|
||||
public static final String CHROME_POLICY = "{ \"URLAllowlist\": [\"" + DATA_DIR + "://*\"] }";
|
||||
|
||||
private String destination = "/opt/" + PROPS_FILE;
|
||||
private String sudoer;
|
||||
|
||||
public LinuxInstaller() {
|
||||
super();
|
||||
sudoer = getSudoer();
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public Installer addAppLauncher() {
|
||||
addLauncher(APP_LAUNCHER, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addStartupEntry() {
|
||||
addLauncher(STARTUP_LAUNCHER, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
private void addLauncher(String location, boolean isStartup) {
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
// Dynamic fields
|
||||
fieldMap.put("%DESTINATION%", destination);
|
||||
fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE));
|
||||
fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE));
|
||||
fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "%u");
|
||||
|
||||
File launcher = new File(location);
|
||||
try {
|
||||
FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class);
|
||||
launcher.setReadable(true, false);
|
||||
launcher.setExecutable(true, false);
|
||||
} catch(IOException e) {
|
||||
log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e);
|
||||
}
|
||||
}
|
||||
|
||||
public Installer removeLegacyStartup() {
|
||||
log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE);
|
||||
// assume users are in /home
|
||||
String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE};
|
||||
for(File file : new File("/home").listFiles()) {
|
||||
if (file.isDirectory()) {
|
||||
File userStart = new File(file.getPath() + "/.config/autostart");
|
||||
if (userStart.exists() && userStart.isDirectory()) {
|
||||
for (String shortcutName : shortcutNames) {
|
||||
File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop");
|
||||
if(legacyStartup.exists()) {
|
||||
legacyStartup.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addSystemSettings() {
|
||||
// Legacy Ubuntu versions only: Patch Unity to show the System Tray
|
||||
if(UnixUtilities.isUbuntu()) {
|
||||
ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
|
||||
|
||||
if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
|
||||
ShellUtilities.execute("nohup", "unity", "-panel");
|
||||
}
|
||||
|
||||
if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) {
|
||||
ShellUtilities.execute("nohup", "unity", "-2d");
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome protocol handler
|
||||
for (String policyDir : CHROME_POLICY_DIRS) {
|
||||
log.info("Installing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
|
||||
try {
|
||||
FileUtilities.setPermissionsParentally(Files.createDirectories(Paths.get(policyDir)), false);
|
||||
} catch(IOException e) {
|
||||
log.warn("An error occurred creating {}", policyDir);
|
||||
}
|
||||
|
||||
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(policy.toFile()))){
|
||||
writer.write(CHROME_POLICY);
|
||||
policy.toFile().setReadable(true, false);
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Unable to write chrome policy: {} ({}:launch will fail)", policy, DATA_DIR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// USB permissions
|
||||
try {
|
||||
File udev = new File(UDEV_RULES);
|
||||
if (udev.exists()) {
|
||||
udev.delete();
|
||||
}
|
||||
FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class);
|
||||
// udev rules should be -rw-r--r--
|
||||
udev.setReadable(true, false);
|
||||
ShellUtilities.execute("udevadm", "control", "--reload-rules");
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e);
|
||||
}
|
||||
|
||||
// Cleanup incorrectly placed files
|
||||
File badFirefoxJs = new File("/usr/bin/defaults/pref/" + PROPS_FILE + ".js");
|
||||
File badFirefoxCfg = new File("/usr/bin/" + PROPS_FILE + ".cfg");
|
||||
|
||||
if(badFirefoxCfg.exists()) {
|
||||
log.info("Removing incorrectly placed Firefox configuration {}, {}...", badFirefoxJs, badFirefoxCfg);
|
||||
badFirefoxCfg.delete();
|
||||
new File("/usr/bin/defaults").delete();
|
||||
}
|
||||
|
||||
// Cleanup incorrectly placed files
|
||||
File badFirefoxPolicy = new File("/usr/bin/distribution/policies.json");
|
||||
if(badFirefoxPolicy.exists()) {
|
||||
log.info("Removing incorrectly placed Firefox policy {}", badFirefoxPolicy);
|
||||
badFirefoxPolicy.delete();
|
||||
// Delete the distribution folder too, as long as it's empty
|
||||
File badPolicyFolder = badFirefoxPolicy.getParentFile();
|
||||
if(badPolicyFolder.isDirectory() && badPolicyFolder.listFiles().length == 0) {
|
||||
badPolicyFolder.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
log.info("Cleaning up any remaining files...");
|
||||
new File(destination + File.separator + "install").delete();
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer removeSystemSettings() {
|
||||
// Chrome protocol handler
|
||||
for (String policyDir : CHROME_POLICY_DIRS) {
|
||||
log.info("Removing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
|
||||
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
|
||||
policy.toFile().delete();
|
||||
}
|
||||
|
||||
// USB permissions
|
||||
File udev = new File(UDEV_RULES);
|
||||
if (udev.exists()) {
|
||||
udev.delete();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
// Environmental variables for spawning a task using sudo. Order is important.
|
||||
static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" };
|
||||
|
||||
/**
|
||||
* Spawns the process as the underlying regular user account, preserving the environment
|
||||
*/
|
||||
public void spawn(List<String> args) throws Exception {
|
||||
if(!SystemUtilities.isAdmin()) {
|
||||
// Not admin, just run as the existing user
|
||||
ShellUtilities.execute(args.toArray(new String[args.size()]));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user's environment from dbus, etc
|
||||
HashMap<String, String> env = getUserEnv(sudoer);
|
||||
if(env.size() == 0) {
|
||||
throw new Exception("Unable to get dbus info; can't spawn instance");
|
||||
}
|
||||
|
||||
// Prepare the environment
|
||||
String[] envp = new String[env.size() + ShellUtilities.envp.length];
|
||||
int i = 0;
|
||||
// Keep existing env
|
||||
for(String keep : ShellUtilities.envp) {
|
||||
envp[i++] = keep;
|
||||
}
|
||||
for(String key :env.keySet()) {
|
||||
envp[i++] = String.format("%s=%s", key, env.get(key));
|
||||
}
|
||||
|
||||
// Concat "sudo|su", sudoer, "nohup", args
|
||||
ArrayList<String> argsList = sudoCommand(sudoer, true, args);
|
||||
|
||||
// Spawn
|
||||
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
|
||||
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a command to help running as another user using "sudo" or "su"
|
||||
*/
|
||||
public static ArrayList<String> sudoCommand(String sudoer, boolean async, List<String> cmds) {
|
||||
ArrayList<String> sudo = new ArrayList<>();
|
||||
if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) {
|
||||
throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer));
|
||||
}
|
||||
if(ShellUtilities.execute("which", "sudo") // check if sudo exists
|
||||
|| ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login
|
||||
// Pass directly into "sudo"
|
||||
log.info("Guessing that this system prefers \"sudo\" over \"su\".");
|
||||
sudo.add("sudo");
|
||||
|
||||
// Add calling user
|
||||
sudo.add("-E"); // preserve environment
|
||||
sudo.add("-u");
|
||||
sudo.add(sudoer);
|
||||
|
||||
// Add "background" task support
|
||||
if(async) {
|
||||
sudo.add("nohup");
|
||||
}
|
||||
if(cmds != null && cmds.size() > 0) {
|
||||
// Add additional commands
|
||||
sudo.addAll(cmds);
|
||||
}
|
||||
} else {
|
||||
// Build and escape for "su"
|
||||
log.info("Guessing that this system prefers \"su\" over \"sudo\".");
|
||||
sudo.add("su");
|
||||
|
||||
// Add calling user
|
||||
sudo.add(sudoer);
|
||||
|
||||
sudo.add("-c");
|
||||
|
||||
// Add "background" task support
|
||||
if(async) {
|
||||
sudo.add("nohup");
|
||||
}
|
||||
if(cmds != null && cmds.size() > 0) {
|
||||
// Add additional commands
|
||||
sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\""));
|
||||
}
|
||||
}
|
||||
return sudo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the most likely non-root user account that the installer is running from
|
||||
*/
|
||||
private static String getSudoer() {
|
||||
String sudoer = ShellUtilities.executeRaw("logname").trim();
|
||||
if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
|
||||
sudoer = System.getenv("SUDO_USER");
|
||||
}
|
||||
return sudoer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses two common POSIX techniques for testing if the provided user account exists
|
||||
*/
|
||||
private static boolean userExists(String user) {
|
||||
return ShellUtilities.execute("id", "-u", user) ||
|
||||
ShellUtilities.execute("getent", "passwd", user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to extract user environment variables from the dbus process to
|
||||
* allow starting a graphical application as the current user.
|
||||
*
|
||||
* If this fails, items such as the user's desktop theme may not be known to Java
|
||||
* at runtime resulting in the Swing L&F instead of the Gtk L&F.
|
||||
*/
|
||||
private static HashMap<String, String> getUserEnv(String matchingUser) {
|
||||
if(!SystemUtilities.isAdmin()) {
|
||||
throw new UnsupportedOperationException("Administrative access is required");
|
||||
}
|
||||
|
||||
String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
|
||||
|
||||
ArrayList<String> pids = new ArrayList<>();
|
||||
for(String dbusMatch : dbusMatches) {
|
||||
pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n")));
|
||||
}
|
||||
|
||||
HashMap<String, String> env = new HashMap<>();
|
||||
HashMap<String, String> tempEnv = new HashMap<>();
|
||||
ArrayList<String> toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS));
|
||||
for(String pid : pids) {
|
||||
if(pid.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String[] vars;
|
||||
if(SystemUtilities.isSolaris()) {
|
||||
// Use pargs -e $$ to get environment
|
||||
log.info("Reading environment info from [pargs, -e, {}]", pid);
|
||||
String pargs = ShellUtilities.executeRaw("pargs", "-e", pid);
|
||||
vars = pargs.split("\\r?\\n");
|
||||
String delim = "]: ";
|
||||
for(int i = 0; i < vars.length; i++) {
|
||||
if(vars[i].contains(delim)) {
|
||||
vars[i] = vars[i].substring(vars[i].indexOf(delim) + delim.length()).trim();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Assume /proc/$$/environ
|
||||
String environ = String.format("/proc/%s/environ", pid);
|
||||
String delim = Pattern.compile("\0").pattern();
|
||||
log.info("Reading environment info from {}", environ);
|
||||
vars = new String(Files.readAllBytes(Paths.get(environ))).split(delim);
|
||||
}
|
||||
for(String var : vars) {
|
||||
String[] parts = var.split("=", 2);
|
||||
if(parts.length == 2) {
|
||||
String key = parts[0].trim();
|
||||
String val = parts[1].trim();
|
||||
if(toExport.contains(key)) {
|
||||
tempEnv.put(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("An unexpected error occurred obtaining dbus info", e);
|
||||
}
|
||||
|
||||
// Only add vars for the current user
|
||||
if(matchingUser.trim().equals(tempEnv.get("USER"))) {
|
||||
env.putAll(tempEnv);
|
||||
} else {
|
||||
log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid);
|
||||
}
|
||||
|
||||
// Use gtk theme
|
||||
if(env.containsKey("XDG_CURRENT_DESKTOP") && !env.containsKey("GNOME_DESKTOP_SESSION_ID")) {
|
||||
if(env.get("XDG_CURRENT_DESKTOP").toLowerCase(Locale.ENGLISH).contains("gnome")) {
|
||||
env.put("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated");
|
||||
}
|
||||
}
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
}
|
||||
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
@@ -0,0 +1,125 @@
|
||||
package qz.installer;
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
|
||||
public class MacInstaller extends Installer {
|
||||
protected static final Logger log = LogManager.getLogger(MacInstaller.class);
|
||||
private static final String PACKAGE_NAME = getPackageName();
|
||||
public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME);
|
||||
private String destination = "/Applications/" + ABOUT_TITLE + ".app";
|
||||
|
||||
public Installer addAppLauncher() {
|
||||
// not needed; registered when "QZ Tray.app" is copied
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addStartupEntry() {
|
||||
File dest = new File(LAUNCH_AGENT_PATH);
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
// Dynamic fields
|
||||
fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME);
|
||||
fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE));
|
||||
fieldMap.put("%PARAM%", "--honorautostart");
|
||||
|
||||
try {
|
||||
FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
|
||||
// Disable service until reboot
|
||||
if(SystemUtilities.isMac()) {
|
||||
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Unable to write startup file: {}", dest, e);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public Installer addSystemSettings() {
|
||||
// Chrome protocol handler
|
||||
String plist = "/Library/Preferences/com.google.Chrome.plist";
|
||||
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) {
|
||||
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public Installer removeSystemSettings() {
|
||||
// Remove startup entry
|
||||
File dest = new File(LAUNCH_AGENT_PATH);
|
||||
dest.delete();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes legacy (<= 2.0) startup entries
|
||||
*/
|
||||
public Installer removeLegacyStartup() {
|
||||
log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
|
||||
String script = "tell application \"System Events\" to delete "
|
||||
+ "every login item where name is \"" + ABOUT_TITLE + "\""
|
||||
+ " or name is \"" + PROPS_FILE + ".jar\"";
|
||||
|
||||
// Run on background thread in case System Events is hung or slow to respond
|
||||
final String finalScript = script;
|
||||
new Thread(() -> {
|
||||
ShellUtilities.executeAppleScript(finalScript);
|
||||
}).run();
|
||||
return this;
|
||||
}
|
||||
|
||||
public static String getPackageName() {
|
||||
String packageName;
|
||||
String[] parts = ABOUT_URL.split("\\W");
|
||||
if (parts.length >= 2) {
|
||||
// Parse io.qz.qz-print from Constants
|
||||
packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE);
|
||||
} else {
|
||||
// Fallback on something sane
|
||||
packageName = "local." + PROPS_FILE;
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void spawn(List<String> args) throws Exception {
|
||||
if(SystemUtilities.isAdmin()) {
|
||||
// macOS unconventionally uses "$USER" during its install process
|
||||
String sudoer = System.getenv("USER");
|
||||
if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) {
|
||||
// Fallback, should only fire via Terminal + sudo
|
||||
sudoer = ShellUtilities.executeRaw("logname").trim();
|
||||
}
|
||||
// Start directly without waitFor(...), avoids deadlocking
|
||||
Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
|
||||
} else {
|
||||
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.installer;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
|
||||
import static qz.common.Constants.PROPS_FILE;
|
||||
|
||||
public class TaskKiller {
|
||||
protected static final Logger log = LogManager.getLogger(TaskKiller.class);
|
||||
private static final String[] JAR_NAMES = {
|
||||
PROPS_FILE + ".jar",
|
||||
"qz.App", // v2.2.0...
|
||||
"qz.ws.PrintSocketServer" // v2.0.0...v2.1.6
|
||||
};
|
||||
private static final String[] KILL_PID_CMD_POSIX = { "kill", "-9" };
|
||||
private static final String[] KILL_PID_CMD_WIN32 = { "taskkill.exe", "/F", "/PID" };
|
||||
private static final String[] KILL_PID_CMD = SystemUtilities.isWindows() ? KILL_PID_CMD_WIN32 : KILL_PID_CMD_POSIX;
|
||||
|
||||
/**
|
||||
* Kills all QZ Tray processes, being careful not to kill itself
|
||||
*/
|
||||
public static boolean killAll() {
|
||||
boolean success = true;
|
||||
|
||||
// Disable service until reboot
|
||||
if(SystemUtilities.isMac()) {
|
||||
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
|
||||
}
|
||||
|
||||
// Use jcmd to get all java processes
|
||||
HashSet<Integer> pids = findPidsJcmd();
|
||||
if(!SystemUtilities.isWindows()) {
|
||||
// Fallback to pgrep, needed for macOS (See JDK-8319589, JDK-8197387)
|
||||
pids.addAll(findPidsPgrep());
|
||||
} else if(WindowsUtilities.isSystemAccount()) {
|
||||
// Fallback to powershell, needed for Windows
|
||||
pids.addAll(findPidsPwsh());
|
||||
}
|
||||
|
||||
// Careful not to kill ourselves ;)
|
||||
pids.remove(SystemUtilities.getProcessId());
|
||||
|
||||
// Kill each PID
|
||||
String[] killPid = new String[KILL_PID_CMD.length + 1];
|
||||
System.arraycopy(KILL_PID_CMD, 0, killPid, 0, KILL_PID_CMD.length);
|
||||
for (Integer pid : pids) {
|
||||
killPid[killPid.length - 1] = pid.toString();
|
||||
success = success && ShellUtilities.execute(killPid);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private static Path getJcmdPath() throws IOException {
|
||||
Path jcmd;
|
||||
if(SystemUtilities.isWindows()) {
|
||||
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd.exe");
|
||||
} else if (SystemUtilities.isMac()) {
|
||||
jcmd = SystemUtilities.getJarParentPath().resolve("../PlugIns/Java.runtime/Contents/Home/bin/jcmd");
|
||||
} else {
|
||||
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd");
|
||||
}
|
||||
if(!jcmd.toFile().exists()) {
|
||||
log.error("Could not find {}", jcmd);
|
||||
throw new IOException("Could not find jcmd, we can't use it for detecting running instances");
|
||||
}
|
||||
return jcmd;
|
||||
}
|
||||
|
||||
|
||||
static final String[] PWSH_QUERY = { "powershell.exe", "-Command", "\"(Get-CimInstance Win32_Process -Filter \\\"Name = 'java.exe' OR Name = 'javaw.exe'\\\").Where({$_.CommandLine -like '*%s*'}).ProcessId\"" };
|
||||
|
||||
/**
|
||||
* Leverage powershell.exe when run as SYSTEM to workaround https://github.com/qzind/tray/issues/1360
|
||||
* TODO: Remove when jcmd is patched to work as SYSTEM account
|
||||
*/
|
||||
private static HashSet<Integer> findPidsPwsh() {
|
||||
HashSet<Integer> foundPids = new HashSet<>();
|
||||
|
||||
for(String jarName : JAR_NAMES) {
|
||||
String[] pwshQuery = PWSH_QUERY.clone();
|
||||
int lastIndex = pwshQuery.length - 1;
|
||||
// Format the last element to contain the jarName
|
||||
pwshQuery[lastIndex] = String.format(pwshQuery[lastIndex], jarName);
|
||||
String stdout = ShellUtilities.executeRaw(pwshQuery);
|
||||
String[] lines = stdout.split("\\s*\\r?\\n");
|
||||
for(String line : lines) {
|
||||
if(line.trim().isEmpty()) {
|
||||
// Don't try to process blank lines
|
||||
continue;
|
||||
}
|
||||
|
||||
int pid = parsePid(line);
|
||||
if (pid >= 0) {
|
||||
foundPids.add(pid);
|
||||
} else {
|
||||
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use pgrep to fetch all PIDs to workaround https://github.com/openjdk/jdk/pull/25824
|
||||
* TODO: Remove when jcmd is patched to work properly on macOS
|
||||
*/
|
||||
private static HashSet<Integer> findPidsPgrep() {
|
||||
HashSet<Integer> foundPids = new HashSet<>();
|
||||
|
||||
for(String jarName : JAR_NAMES) {
|
||||
String stdout = ShellUtilities.executeRaw("pgrep", "-f", jarName);
|
||||
String[] lines = stdout.split("\\s*\\r?\\n");
|
||||
for(String line : lines) {
|
||||
if(line.trim().isEmpty()) {
|
||||
// Don't try to process blank lines
|
||||
continue;
|
||||
}
|
||||
|
||||
int pid = parsePid(line);
|
||||
if (pid >= 0) {
|
||||
foundPids.add(pid);
|
||||
} else {
|
||||
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Uses jcmd to fetch all PIDs that match this product
|
||||
*/
|
||||
private static HashSet<Integer> findPidsJcmd() {
|
||||
HashSet<Integer> foundPids = new HashSet<>();
|
||||
|
||||
String stdout;
|
||||
String[] lines;
|
||||
try {
|
||||
stdout = ShellUtilities.executeRaw(getJcmdPath().toString(), "-l");
|
||||
if(stdout == null) {
|
||||
log.error("Error calling '{}' {}", getJcmdPath(), "-l");
|
||||
return foundPids;
|
||||
}
|
||||
lines = stdout.split("\\r?\\n");
|
||||
} catch(Exception e) {
|
||||
log.error(e);
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
for(String line : lines) {
|
||||
if (line.trim().isEmpty()) {
|
||||
// Don't try to process blank lines
|
||||
continue;
|
||||
}
|
||||
// e.g. "35446 C:\Program Files\QZ Tray\qz-tray.jar"
|
||||
String[] parts = line.split(" ", 2);
|
||||
int pid = parsePid(parts);
|
||||
if (pid >= 0) {
|
||||
String args = parseArgs(parts);
|
||||
if (args == null) {
|
||||
log.warn("Found PID value '{}' but no args to match. Full line: '{}', Full output: '{}'", pid, line, stdout);
|
||||
continue;
|
||||
}
|
||||
for(String jarName : JAR_NAMES) {
|
||||
if (args.contains(jarName)) {
|
||||
foundPids.add(pid);
|
||||
break; // continue parent loop
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||
}
|
||||
}
|
||||
|
||||
return foundPids;
|
||||
}
|
||||
|
||||
// Returns the second index of a String[], trimmed
|
||||
private static String parseArgs(String[] input) {
|
||||
if(input != null) {
|
||||
if(input.length == 2) {
|
||||
return input[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parses an int value form the first index of a String[], returning -1 if something went wrong
|
||||
private static int parsePid(String[] input) {
|
||||
if(input != null) {
|
||||
if(input.length == 2) {
|
||||
return parsePid(input[0]);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Parses an int value form the provided string, returning -1 if something went wrong
|
||||
private static int parsePid(String input) {
|
||||
String pidString = input.trim();
|
||||
if(StringUtils.isNumeric(pidString)) {
|
||||
return Integer.parseInt(pidString);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer;
|
||||
|
||||
import com.sun.jna.platform.win32.*;
|
||||
import mslinks.ShellLink;
|
||||
import mslinks.ShellLinkException;
|
||||
import mslinks.ShellLinkHelper;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
import qz.ws.PrintSocketServer;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
import static qz.installer.WindowsSpecialFolders.*;
|
||||
import static com.sun.jna.platform.win32.WinReg.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class WindowsInstaller extends Installer {
|
||||
protected static final Logger log = LogManager.getLogger(WindowsInstaller.class);
|
||||
private String destination = getDefaultDestination();
|
||||
private String destinationExe = getDefaultDestination() + File.separator + PROPS_FILE + ".exe";
|
||||
|
||||
public void setDestination(String destination) {
|
||||
this.destination = destination;
|
||||
this.destinationExe = destination + File.separator + PROPS_FILE + ".exe";
|
||||
}
|
||||
|
||||
/**
|
||||
* Cycles through registry keys removing legacy (<= 2.0) startup entries
|
||||
*/
|
||||
public Installer removeLegacyStartup() {
|
||||
log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE);
|
||||
for (String user : Advapi32Util.registryGetKeys(HKEY_USERS)) {
|
||||
WindowsUtilities.deleteRegValue(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ABOUT_TITLE);
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.deleteQuietly(new File(STARTUP + File.separator + ABOUT_TITLE + ".lnk"));
|
||||
} catch(Win32Exception ignore) {}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addAppLauncher() {
|
||||
try {
|
||||
// Delete old 2.0 launcher
|
||||
FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
|
||||
Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
|
||||
loc.toFile().mkdirs();
|
||||
String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
|
||||
String exe = destination + File.separator + PROPS_FILE+ ".exe";
|
||||
log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe);
|
||||
ShellLinkHelper.createLink(exe, lnk);
|
||||
} catch(ShellLinkException | IOException | Win32Exception e) {
|
||||
log.warn("Could not create launcher", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addStartupEntry() {
|
||||
try {
|
||||
String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk";
|
||||
String exe = destination + File.separator + PROPS_FILE+ ".exe";
|
||||
log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe);
|
||||
ShellLink link = ShellLinkHelper.createLink(exe, lnk).getLink();
|
||||
link.setCMDArgs("--honorautostart"); // honors auto-start preferences
|
||||
} catch(ShellLinkException | IOException | Win32Exception e) {
|
||||
log.warn("Could not create startup launcher", e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
public Installer removeSystemSettings() {
|
||||
// Cleanup registry
|
||||
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE);
|
||||
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE);
|
||||
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR);
|
||||
// Chrome protocol handler
|
||||
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
|
||||
// Deprecated Chrome protocol handler
|
||||
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR));
|
||||
|
||||
// Cleanup launchers
|
||||
for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP, COMMON_STARTUP, RECENT }) {
|
||||
try {
|
||||
new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete();
|
||||
// Since 2.1, start menus use subfolder
|
||||
if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) {
|
||||
FileUtils.deleteQuietly(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
|
||||
FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE));
|
||||
}
|
||||
} catch(InvalidPathException | IOException | Win32Exception ignore) {}
|
||||
}
|
||||
|
||||
// Cleanup firewall rules
|
||||
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Installer addSystemSettings() {
|
||||
/**
|
||||
* TODO: Upgrade JNA!
|
||||
* 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64
|
||||
* However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding
|
||||
* against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward
|
||||
* for support and troubleshooting.
|
||||
*/
|
||||
|
||||
// Mime-type support e.g. qz:launch
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE));
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", "");
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe));
|
||||
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe));
|
||||
|
||||
/// Uninstall info
|
||||
String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION));
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe");
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL );
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL);
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString());
|
||||
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024);
|
||||
|
||||
// Chrome protocol handler
|
||||
WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
|
||||
|
||||
// Firewall rules
|
||||
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
|
||||
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE),
|
||||
"dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp");
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Installer addUserSettings() {
|
||||
// Whitelist loopback for IE/Edge
|
||||
if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) {
|
||||
log.warn("Could not whitelist loopback connections for IE, Edge");
|
||||
}
|
||||
|
||||
try {
|
||||
// Intranet settings; uncheck "include sites not listed in other zones"
|
||||
String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
|
||||
String value = "Flags";
|
||||
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
|
||||
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
|
||||
// remove value using bitwise XOR
|
||||
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16);
|
||||
}
|
||||
|
||||
// Legacy Edge loopback support
|
||||
key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
|
||||
value = "AllowLocalhostLoopback";
|
||||
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
|
||||
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
|
||||
// remove value using bitwise OR
|
||||
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e);
|
||||
}
|
||||
return super.addUserSettings();
|
||||
}
|
||||
|
||||
public static String getDefaultDestination() {
|
||||
String path = System.getenv("ProgramW6432");
|
||||
if (path == null || path.trim().isEmpty()) {
|
||||
path = System.getenv("ProgramFiles");
|
||||
}
|
||||
return path + File.separator + ABOUT_TITLE;
|
||||
}
|
||||
|
||||
public String getDestination() {
|
||||
return destination;
|
||||
}
|
||||
|
||||
public void spawn(List<String> args) throws Exception {
|
||||
if(SystemUtilities.isAdmin()) {
|
||||
log.warn("Spawning as user isn't implemented; starting process with elevation instead");
|
||||
}
|
||||
ShellUtilities.execute(args.toArray(new String[args.size()]));
|
||||
}
|
||||
}
|
||||
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer;
|
||||
|
||||
import com.sun.jna.platform.win32.*;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
/**
|
||||
* Windows XP-compatible special folder's wrapper for JNA
|
||||
*
|
||||
*/
|
||||
public enum WindowsSpecialFolders {
|
||||
ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools),
|
||||
STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup),
|
||||
ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData),
|
||||
RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder),
|
||||
CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning),
|
||||
COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools),
|
||||
COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup),
|
||||
PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData),
|
||||
PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop),
|
||||
PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments),
|
||||
COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites),
|
||||
COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic),
|
||||
COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks),
|
||||
COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures),
|
||||
COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms),
|
||||
COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu),
|
||||
COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup),
|
||||
COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates),
|
||||
COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos),
|
||||
COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder),
|
||||
CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder),
|
||||
CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder),
|
||||
COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies),
|
||||
DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop),
|
||||
DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop),
|
||||
COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder),
|
||||
FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites),
|
||||
FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts),
|
||||
HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History),
|
||||
INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder),
|
||||
INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache),
|
||||
LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData),
|
||||
MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents),
|
||||
MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music),
|
||||
MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures),
|
||||
MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos),
|
||||
NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood),
|
||||
NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder),
|
||||
PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents),
|
||||
PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder),
|
||||
PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood),
|
||||
PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile),
|
||||
PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles),
|
||||
PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86),
|
||||
PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon),
|
||||
PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86),
|
||||
PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs),
|
||||
RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent),
|
||||
RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir),
|
||||
RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir),
|
||||
SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo),
|
||||
START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu),
|
||||
STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup),
|
||||
SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System),
|
||||
SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86),
|
||||
TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates),
|
||||
WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows);
|
||||
|
||||
private int csidl;
|
||||
private Guid.GUID guid;
|
||||
WindowsSpecialFolders(int csidl, Guid.GUID guid) {
|
||||
this.csidl = csidl;
|
||||
this.guid = guid;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
if(WindowsUtilities.isWindowsXP()) {
|
||||
return Shell32Util.getSpecialFolderPath(csidl, false);
|
||||
}
|
||||
return Shell32Util.getKnownFolderPath(guid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getPath();
|
||||
}
|
||||
}
|
||||
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=%ABOUT_TITLE%
|
||||
Exec="%COMMAND%" %PARAM%
|
||||
Path=%DESTINATION%
|
||||
Icon=%DESTINATION%/%LINUX_ICON%
|
||||
MimeType=application/x-qz;x-scheme-handler/qz;
|
||||
Terminal=false
|
||||
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
@@ -0,0 +1,2 @@
|
||||
# %ABOUT_TITLE% usb override settings
|
||||
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
|
||||
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key><string>%PACKAGE_NAME%</string>
|
||||
<key>KeepAlive</key>
|
||||
<dict>
|
||||
<key>SuccessfulExit</key><false/>
|
||||
<key>AfterInitialDemand</key><false/>
|
||||
</dict>
|
||||
<key>RunAtLoad</key><true/>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>%COMMAND%</string>
|
||||
<string>%PARAM%</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.*;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bouncycastle.asn1.*;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x509.*;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.OperatorException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import static qz.installer.certificate.KeyPairWrapper.Type.*;
|
||||
|
||||
public class CertificateChainBuilder {
|
||||
public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" };
|
||||
|
||||
private static int KEY_SIZE = 2048;
|
||||
public static int CA_CERT_AGE = 7305; // 20 years
|
||||
public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176
|
||||
|
||||
private String[] hostNames;
|
||||
|
||||
public CertificateChainBuilder(String ... hostNames) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
if(hostNames.length > 0) {
|
||||
this.hostNames = hostNames;
|
||||
} else {
|
||||
this.hostNames = DEFAULT_HOSTNAMES;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException {
|
||||
KeyPair keyPair = createRsaKey();
|
||||
|
||||
X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE, hostNames);
|
||||
|
||||
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1))
|
||||
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
|
||||
|
||||
// Signing
|
||||
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
|
||||
X509CertificateHolder certHolder = builder.build(sign);
|
||||
|
||||
// Convert to java-friendly format
|
||||
return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException {
|
||||
KeyPair sslKeyPair = createRsaKey();
|
||||
X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE, hostNames);
|
||||
|
||||
JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();
|
||||
|
||||
builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert()))
|
||||
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
|
||||
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment))
|
||||
.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}))
|
||||
.addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames))
|
||||
.addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic()));
|
||||
|
||||
// Signing
|
||||
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey());
|
||||
X509CertificateHolder certHolder = builder.build(sign);
|
||||
|
||||
// Convert to java-friendly format
|
||||
return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
private static KeyPair createRsaKey() throws GeneralSecurityException {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
|
||||
private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) {
|
||||
String cn = hostNames.length > 0? hostNames[0]:DEFAULT_HOSTNAMES[0];
|
||||
X500Name name = new X500NameBuilder()
|
||||
.addRDN(BCStyle.C, Constants.ABOUT_COUNTRY)
|
||||
.addRDN(BCStyle.ST, Constants.ABOUT_STATE)
|
||||
.addRDN(BCStyle.L, Constants.ABOUT_CITY)
|
||||
.addRDN(BCStyle.O, Constants.ABOUT_COMPANY)
|
||||
.addRDN(BCStyle.OU, Constants.ABOUT_COMPANY)
|
||||
.addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL)
|
||||
.addRDN(BCStyle.CN, cn)
|
||||
.build();
|
||||
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
|
||||
Calendar notBefore = Calendar.getInstance(Locale.ENGLISH);
|
||||
Calendar notAfter = Calendar.getInstance(Locale.ENGLISH);
|
||||
notBefore.add(Calendar.DAY_OF_YEAR, -1);
|
||||
notAfter.add(Calendar.DAY_OF_YEAR, age - 1);
|
||||
|
||||
SystemUtilities.swapLocale();
|
||||
X509v3CertificateBuilder x509builder = new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic());
|
||||
SystemUtilities.restoreLocale();
|
||||
return x509builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname
|
||||
*/
|
||||
private static GeneralNames buildSan(String ... hostNames) {
|
||||
GeneralName[] gn = new GeneralName[hostNames.length];
|
||||
for (int i = 0; i < hostNames.length; i++) {
|
||||
int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName;
|
||||
gn[i] = new GeneralName(gnType, hostNames[i]);
|
||||
}
|
||||
return GeneralNames.getInstance(new DERSequence(gn));
|
||||
}
|
||||
|
||||
private static boolean isIp(String ip) {
|
||||
try {
|
||||
String[] split = ip.split("\\.");
|
||||
if (split.length != 4) return false;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
int p = Integer.parseInt(split[i]);
|
||||
if (p > 255 || p < 0) return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception ignore) {}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
@@ -0,0 +1,478 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
|
||||
import org.bouncycastle.asn1.x500.RDN;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMKeyPair;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.operator.OperatorException;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ArgValue;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.MacUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.*;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.utils.FileUtilities.*;
|
||||
import static qz.installer.certificate.KeyPairWrapper.Type.*;
|
||||
|
||||
/**
|
||||
* Stores and maintains reading and writing of certificate related files
|
||||
*/
|
||||
public class CertificateManager {
|
||||
static List<Path> SAVE_LOCATIONS = new ArrayList<>();
|
||||
static {
|
||||
// Workaround for JDK-8266929
|
||||
// See also https://github.com/qzind/tray/issues/814
|
||||
SystemUtilities.clearAlgorithms();
|
||||
|
||||
// Skip shared location if running from IDE or build directory
|
||||
// Prevents corrupting the version installed per https://github.com/qzind/tray/issues/1200
|
||||
if(SystemUtilities.isJar() && SystemUtilities.isInstalled()) {
|
||||
// Skip install location if running from sandbox (must remain sealed)
|
||||
if(!SystemUtilities.isMac() || !MacUtilities.isSandboxed()) {
|
||||
SAVE_LOCATIONS.add(SystemUtilities.getJarParentPath());
|
||||
}
|
||||
SAVE_LOCATIONS.add(SHARED_DIR);
|
||||
}
|
||||
SAVE_LOCATIONS.add(USER_DIR);
|
||||
}
|
||||
private static final Logger log = LogManager.getLogger(CertificateManager.class);
|
||||
|
||||
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
|
||||
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
|
||||
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
|
||||
private static int DEFAULT_PASSWORD_BITS = 100;
|
||||
|
||||
private boolean needsInstall;
|
||||
private SslContextFactory.Server sslContextFactory;
|
||||
private KeyPairWrapper sslKeyPair;
|
||||
private KeyPairWrapper caKeyPair;
|
||||
|
||||
private Properties properties;
|
||||
private char[] password;
|
||||
|
||||
/**
|
||||
* For internal certs
|
||||
*/
|
||||
public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
caKeyPair = new KeyPairWrapper(CA);
|
||||
|
||||
if (!forceNew) {
|
||||
// order is important: ssl, ca
|
||||
properties = loadProperties(sslKeyPair, caKeyPair);
|
||||
}
|
||||
|
||||
if(properties == null) {
|
||||
log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them...");
|
||||
|
||||
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
|
||||
caKeyPair = cb.createCaCert();
|
||||
sslKeyPair = cb.createSslCert(caKeyPair);
|
||||
|
||||
// Create CA
|
||||
properties = createKeyStore(CA)
|
||||
.writeCert(CA)
|
||||
.writeKeystore(null, CA);
|
||||
|
||||
// Create SSL
|
||||
properties = createKeyStore(SSL)
|
||||
.writeCert(SSL)
|
||||
.writeKeystore(properties, SSL);
|
||||
|
||||
// Save properties
|
||||
saveProperties();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For trusted PEM-formatted certs
|
||||
*/
|
||||
public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
needsInstall = false;
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
|
||||
// Assumes ssl/privkey.pem, ssl/fullchain.pem
|
||||
properties = createTrustedKeystore(trustedPemKey, trustedPemCert)
|
||||
.writeKeystore(properties, SSL);
|
||||
|
||||
// Save properties
|
||||
saveProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* For trusted PKCS12-formatted certs
|
||||
*/
|
||||
public CertificateManager(File pkcs12File, char[] password) throws Exception {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
needsInstall = false;
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
|
||||
// Assumes direct pkcs12 import
|
||||
this.password = password;
|
||||
sslKeyPair.init(pkcs12File, password);
|
||||
|
||||
// Save it back, but to a location we can find
|
||||
properties = writeKeystore(null, SSL);
|
||||
|
||||
// Save properties
|
||||
saveProperties();
|
||||
}
|
||||
|
||||
public void renewCertChain(String ... hostNames) throws Exception {
|
||||
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
|
||||
sslKeyPair = cb.createSslCert(caKeyPair);
|
||||
createKeyStore(SSL).writeKeystore(properties, SSL);
|
||||
reloadSslContextFactory();
|
||||
}
|
||||
|
||||
public KeyPairWrapper getSslKeyPair() {
|
||||
return sslKeyPair;
|
||||
}
|
||||
|
||||
public KeyPairWrapper getCaKeyPair() {
|
||||
return caKeyPair;
|
||||
}
|
||||
|
||||
public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) {
|
||||
switch(type) {
|
||||
case SSL:
|
||||
return sslKeyPair;
|
||||
case CA:
|
||||
default:
|
||||
return caKeyPair;
|
||||
}
|
||||
}
|
||||
|
||||
public KeyPairWrapper getKeyPair(String alias) {
|
||||
for(KeyPairWrapper.Type type : KeyPairWrapper.Type.values()) {
|
||||
if (KeyPairWrapper.getAlias(type).equalsIgnoreCase(alias)) {
|
||||
return getKeyPair(type);
|
||||
}
|
||||
}
|
||||
return getKeyPair(KeyPairWrapper.Type.CA);
|
||||
}
|
||||
|
||||
public Properties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
private char[] getPassword() {
|
||||
if (password == null) {
|
||||
if(caKeyPair != null && caKeyPair.getPassword() != null) {
|
||||
// Reuse existing
|
||||
password = caKeyPair.getPassword();
|
||||
} else {
|
||||
// Create new
|
||||
BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom());
|
||||
password = bi.toString(16).toCharArray();
|
||||
log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password));
|
||||
}
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
public SslContextFactory.Server configureSslContextFactory() {
|
||||
sslContextFactory = new SslContextFactory.Server();
|
||||
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
|
||||
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
|
||||
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
|
||||
return sslContextFactory;
|
||||
}
|
||||
|
||||
public void reloadSslContextFactory() throws Exception {
|
||||
if(isSslActive()) {
|
||||
sslContextFactory.reload(sslContextFactory -> {
|
||||
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
|
||||
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
|
||||
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
|
||||
});
|
||||
} else {
|
||||
log.warn("SSL isn't active, can't reload");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSslActive() {
|
||||
return sslContextFactory != null;
|
||||
}
|
||||
|
||||
public boolean needsInstall() {
|
||||
return needsInstall;
|
||||
}
|
||||
|
||||
public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException {
|
||||
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||
KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT);
|
||||
keyStore.load(null, password);
|
||||
|
||||
List<X509Certificate> chain = new ArrayList<>();
|
||||
chain.add(keyPair.getCert());
|
||||
|
||||
// Add ca to ssl cert chain
|
||||
if (keyPair.getType() == SSL) {
|
||||
chain.add(caKeyPair.getCert());
|
||||
}
|
||||
keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null);
|
||||
keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()]));
|
||||
keyPair.init(keyStore, getPassword());
|
||||
return this;
|
||||
}
|
||||
|
||||
public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception {
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
sslKeyPair.init(p12Store, password.toCharArray());
|
||||
return this;
|
||||
}
|
||||
|
||||
public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception {
|
||||
sslKeyPair = new KeyPairWrapper(SSL);
|
||||
|
||||
// Private Key
|
||||
PEMParser pem = new PEMParser(new FileReader(pemKey));
|
||||
Object parsedObject = pem.readObject();
|
||||
|
||||
PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject;
|
||||
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
|
||||
KeyFactory factory = KeyFactory.getInstance("RSA");
|
||||
PrivateKey key = factory.generatePrivate(privateKeySpec);
|
||||
|
||||
List<X509Certificate> certs = new ArrayList<>();
|
||||
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
|
||||
if(certHolder != null) {
|
||||
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
// Certificate
|
||||
pem = new PEMParser(new FileReader(pemCert));
|
||||
while((certHolder = (X509CertificateHolder)pem.readObject()) != null) {
|
||||
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||
}
|
||||
|
||||
// Keystore
|
||||
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||
ks.load(null);
|
||||
|
||||
for (int i = 0; i < certs.size(); i++) {
|
||||
ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i));
|
||||
}
|
||||
|
||||
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||
keyStore.load(null);
|
||||
keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()]));
|
||||
|
||||
sslKeyPair.init(keyStore, getPassword());
|
||||
return this;
|
||||
}
|
||||
|
||||
public static void writeCert(X509Certificate data, File dest) throws IOException {
|
||||
// PEMWriter doesn't always clear the file, explicitly delete it, see issue #796
|
||||
if(dest.exists()) {
|
||||
dest.delete();
|
||||
}
|
||||
JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data);
|
||||
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE)));
|
||||
writer.writeObject(cert.generate());
|
||||
writer.close();
|
||||
FileUtilities.inheritParentPermissions(dest.toPath());
|
||||
log.info("Wrote Cert: \"{}\"", dest);
|
||||
}
|
||||
|
||||
public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException {
|
||||
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||
File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION);
|
||||
|
||||
writeCert(keyPair.getCert(), certFile);
|
||||
FileUtilities.inheritParentPermissions(certFile.toPath());
|
||||
if(keyPair.getType() == CA) {
|
||||
needsInstall = true;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException {
|
||||
File sslDir = getWritableLocation("ssl");
|
||||
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||
|
||||
File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION);
|
||||
keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword());
|
||||
FileUtilities.inheritParentPermissions(keyFile.toPath());
|
||||
log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile);
|
||||
|
||||
if (props == null) {
|
||||
props = new Properties();
|
||||
}
|
||||
props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString());
|
||||
props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword()));
|
||||
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
|
||||
|
||||
if (keyPair.getType() == SSL) {
|
||||
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
|
||||
}
|
||||
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
public static File getWritableLocation(String ... suffixes) throws IOException {
|
||||
// Get an array of preferred directories
|
||||
ArrayList<Path> locs = new ArrayList<>();
|
||||
|
||||
if (suffixes.length == 0) {
|
||||
locs.addAll(SAVE_LOCATIONS);
|
||||
// Last, fallback on a directory we won't ever see again :/
|
||||
locs.add(TEMP_DIR);
|
||||
} else {
|
||||
// Same as above, but with suffixes added (usually "ssl"), skipping the install location
|
||||
for(Path saveLocation : SAVE_LOCATIONS) {
|
||||
if(!saveLocation.equals(SystemUtilities.getJarParentPath())) {
|
||||
locs.add(Paths.get(saveLocation.toString(), suffixes));
|
||||
}
|
||||
}
|
||||
// Last, fallback on a directory we won't ever see again :/
|
||||
locs.add(Paths.get(TEMP_DIR.toString(), suffixes));
|
||||
}
|
||||
|
||||
// Find a suitable write location
|
||||
File path;
|
||||
for(Path loc : locs) {
|
||||
if (loc == null) continue;
|
||||
boolean isPreferred = locs.indexOf(loc) == 0;
|
||||
path = loc.toFile();
|
||||
path.mkdirs();
|
||||
if (path.canWrite()) {
|
||||
log.debug("Writing to {}", loc);
|
||||
if(!isPreferred) {
|
||||
log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc);
|
||||
}
|
||||
return path;
|
||||
} else {
|
||||
log.debug("Can't write to {}, trying the next...", loc);
|
||||
}
|
||||
}
|
||||
throw new IOException("Can't find a suitable write location. SSL will fail.");
|
||||
}
|
||||
|
||||
public static Properties loadProperties(KeyPairWrapper... keyPairs) {
|
||||
log.info("Try to find SSL properties file...");
|
||||
|
||||
|
||||
Properties props = null;
|
||||
for(Path loc : SAVE_LOCATIONS) {
|
||||
if (loc == null) continue;
|
||||
try {
|
||||
for(KeyPairWrapper keyPair : keyPairs) {
|
||||
props = loadKeyPair(keyPair, loc, props);
|
||||
}
|
||||
// We've loaded without Exception, return
|
||||
log.info("Found {}/{}.properties", loc, Constants.PROPS_FILE);
|
||||
return props;
|
||||
} catch(Exception ignore) {
|
||||
log.warn("Properties couldn't be loaded at {}, trying fallback...", loc, ignore);
|
||||
}
|
||||
}
|
||||
log.info("Could not get SSL properties from file.");
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception {
|
||||
Properties props;
|
||||
|
||||
if (existing == null) {
|
||||
FileInputStream fis = null;
|
||||
try {
|
||||
props = new Properties();
|
||||
props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties")));
|
||||
} finally {
|
||||
if(fis != null) fis.close();
|
||||
}
|
||||
} else {
|
||||
props = existing;
|
||||
}
|
||||
|
||||
String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix()));
|
||||
String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), "");
|
||||
|
||||
if(ks == null || ks.trim().isEmpty()) {
|
||||
if(keyPair.getType() == SSL) {
|
||||
throw new IOException("Missing wss.keystore entry");
|
||||
} else {
|
||||
// CA is only needed for internal certs, return
|
||||
return props;
|
||||
}
|
||||
}
|
||||
File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks);
|
||||
if (ksFile.exists()) {
|
||||
keyPair.init(ksFile, pw.toCharArray());
|
||||
return props;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void saveProperties() throws IOException {
|
||||
File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties");
|
||||
Installer.persistProperties(propsFile, properties); // checks for props from previous install
|
||||
properties.store(new FileOutputStream(propsFile), null);
|
||||
FileUtilities.inheritParentPermissions(propsFile.toPath());
|
||||
log.info("Successfully created SSL properties file: {}", propsFile);
|
||||
}
|
||||
|
||||
public static boolean emailMatches(X509Certificate cert) {
|
||||
return emailMatches(cert, false);
|
||||
}
|
||||
|
||||
public static boolean emailMatches(X509Certificate cert, boolean quiet) {
|
||||
try {
|
||||
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
|
||||
RDN[] emailNames = x500name.getRDNs(BCStyle.E);
|
||||
for(RDN emailName : emailNames) {
|
||||
AttributeTypeAndValue first = emailName.getFirst();
|
||||
if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) {
|
||||
if(!quiet) {
|
||||
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
if(!quiet) {
|
||||
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x509.GeneralName;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.naming.InvalidNameException;
|
||||
import javax.naming.ldap.LdapName;
|
||||
import javax.naming.ldap.Rdn;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.*;
|
||||
|
||||
import static qz.utils.FileUtilities.*;
|
||||
|
||||
public class ExpiryTask extends TimerTask {
|
||||
private static final Logger log = LogManager.getLogger(CertificateManager.class);
|
||||
public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute
|
||||
public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour
|
||||
private static final int DEFAULT_GRACE_PERIOD_DAYS = 5;
|
||||
private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED}
|
||||
|
||||
public enum CertProvider {
|
||||
INTERNAL(Constants.ABOUT_COMPANY + ".*"),
|
||||
LETS_ENCRYPT("Let's Encrypt.*"),
|
||||
CA_CERT_ORG("CA Cert Signing.*"),
|
||||
UNKNOWN;
|
||||
String[] patterns;
|
||||
CertProvider(String ... regexPattern) {
|
||||
this.patterns = regexPattern;
|
||||
}
|
||||
}
|
||||
|
||||
private Timer timer;
|
||||
private CertificateManager certificateManager;
|
||||
private String[] hostNames;
|
||||
private CertProvider certProvider;
|
||||
|
||||
public ExpiryTask(CertificateManager certificateManager) {
|
||||
super();
|
||||
this.certificateManager = certificateManager;
|
||||
this.hostNames = parseHostNames();
|
||||
this.certProvider = findCertProvider();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// Check for expiration
|
||||
ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert());
|
||||
switch(state) {
|
||||
case EXPIRING:
|
||||
case EXPIRED:
|
||||
log.info("Certificate ExpiryState {}, renewing/reloading...", state);
|
||||
switch(certProvider) {
|
||||
case INTERNAL:
|
||||
if(renewInternalCert()) {
|
||||
getExpiry();
|
||||
}
|
||||
break;
|
||||
case CA_CERT_ORG:
|
||||
case LETS_ENCRYPT:
|
||||
if(renewExternalCert(certProvider)) {
|
||||
getExpiry();
|
||||
}
|
||||
break;
|
||||
case UNKNOWN:
|
||||
default:
|
||||
log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider);
|
||||
}
|
||||
case VALID:
|
||||
default:
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean renewInternalCert() {
|
||||
try {
|
||||
log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias());
|
||||
certificateManager.renewCertChain(hostNames);
|
||||
log.info("New SSL certificate created. Reloading SslContextFactory...");
|
||||
certificateManager.reloadSslContextFactory();
|
||||
log.info("Reloaded SSL successfully.");
|
||||
return true;
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Could not reload SSL certificate", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public ExpiryState getExpiry() {
|
||||
return getExpiry(certificateManager.getSslKeyPair().getCert());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD.
|
||||
* GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours
|
||||
*/
|
||||
public static ExpiryState getExpiry(X509Certificate cert) {
|
||||
// Invalid
|
||||
if (cert == null) {
|
||||
log.error("Can't check for expiration, certificate is missing.");
|
||||
return ExpiryState.EXPIRED;
|
||||
}
|
||||
|
||||
Date expireDate = cert.getNotAfter();
|
||||
Calendar now = Calendar.getInstance(Locale.ENGLISH);
|
||||
Calendar expires = Calendar.getInstance(Locale.ENGLISH);
|
||||
expires.setTime(expireDate);
|
||||
|
||||
// Expired
|
||||
if (now.after(expires)) {
|
||||
log.info("SSL certificate has expired {}. It must be renewed immediately.", SystemUtilities.toISO(expireDate));
|
||||
return ExpiryState.EXPIRED;
|
||||
}
|
||||
|
||||
// Expiring
|
||||
expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS);
|
||||
if (now.after(expires)) {
|
||||
log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, SystemUtilities.toISO(expireDate));
|
||||
return ExpiryState.EXPIRING;
|
||||
}
|
||||
|
||||
// Valid
|
||||
int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000);
|
||||
log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, SystemUtilities.toISO(expireDate));
|
||||
return ExpiryState.VALID;
|
||||
}
|
||||
|
||||
public void schedule() {
|
||||
schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
|
||||
}
|
||||
|
||||
public void schedule(int delayMillis, int freqMillis) {
|
||||
if(timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
}
|
||||
timer = new Timer();
|
||||
timer.scheduleAtFixedRate(this, delayMillis, freqMillis);
|
||||
}
|
||||
|
||||
public String[] parseHostNames() {
|
||||
return parseHostNames(certificateManager.getSslKeyPair().getCert());
|
||||
}
|
||||
|
||||
public CertProvider findCertProvider() {
|
||||
return findCertProvider(certificateManager.getSslKeyPair().getCert());
|
||||
}
|
||||
|
||||
public static CertProvider findCertProvider(X509Certificate cert) {
|
||||
// Internal certs use CN=localhost, trust email instead
|
||||
if (CertificateManager.emailMatches(cert)) {
|
||||
return CertProvider.INTERNAL;
|
||||
}
|
||||
|
||||
String providerDN;
|
||||
|
||||
// check registered patterns to classify certificate
|
||||
if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) {
|
||||
String cn = null;
|
||||
try {
|
||||
// parse issuer's DN
|
||||
LdapName ldapName = new LdapName(providerDN);
|
||||
for(Rdn rdn : ldapName.getRdns()) {
|
||||
if(rdn.getType().equalsIgnoreCase("CN")) {
|
||||
cn = (String)rdn.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// compare cn to our pattern
|
||||
if(cn != null) {
|
||||
for(CertProvider provider : CertProvider.values()) {
|
||||
for(String pattern : provider.patterns) {
|
||||
if (cn.matches(pattern)) {
|
||||
log.warn("Cert issuer detected as {}", provider.name());
|
||||
return provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(InvalidNameException ignore) {}
|
||||
}
|
||||
|
||||
log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires");
|
||||
return CertProvider.UNKNOWN;
|
||||
}
|
||||
|
||||
public static String[] parseHostNames(X509Certificate cert) {
|
||||
// Cache the SAN hosts for recreation
|
||||
List<String> hostNameList = new ArrayList<>();
|
||||
try {
|
||||
Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
|
||||
if (altNames != null) {
|
||||
for(List<?> altName : altNames) {
|
||||
if(altName.size()< 1) continue;
|
||||
switch((Integer)altName.get(0)) {
|
||||
case GeneralName.dNSName:
|
||||
case GeneralName.iPAddress:
|
||||
Object data = altName.get(1);
|
||||
if (data instanceof String) {
|
||||
hostNameList.add(((String)data));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("getSubjectAlternativeNames is null?");
|
||||
}
|
||||
log.debug("Parsed hostNames: {}", String.join(", ", hostNameList));
|
||||
} catch(CertificateException e) {
|
||||
log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead");
|
||||
}
|
||||
return hostNameList.toArray(new String[hostNameList.size()]);
|
||||
}
|
||||
|
||||
public boolean renewExternalCert(CertProvider externalProvider) {
|
||||
switch(externalProvider) {
|
||||
case LETS_ENCRYPT:
|
||||
return renewLetsEncryptCert(externalProvider);
|
||||
case CA_CERT_ORG:
|
||||
default:
|
||||
log.error("Cert renewal for {} is not implemented", externalProvider);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean renewLetsEncryptCert(CertProvider externalProvider) {
|
||||
try {
|
||||
File storagePath = CertificateManager.getWritableLocation("ssl");
|
||||
|
||||
// cerbot is much simpler than acme, let's use it
|
||||
Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config");
|
||||
log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root);
|
||||
List<String> cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly"));
|
||||
|
||||
cmds.add("--standalone");
|
||||
|
||||
cmds.add("--config-dir");
|
||||
String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString();
|
||||
cmds.add(config);
|
||||
|
||||
cmds.add("--logs-dir");
|
||||
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString());
|
||||
|
||||
cmds.add("--work-dir");
|
||||
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString());
|
||||
|
||||
// append dns names
|
||||
for(String hostName : hostNames) {
|
||||
cmds.add("-d");
|
||||
cmds.add(hostName);
|
||||
}
|
||||
|
||||
if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) {
|
||||
// Assume the cert is stored in a folder called "letsencrypt/config/live/<domain>"
|
||||
Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem");
|
||||
Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required
|
||||
certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile());
|
||||
log.info("Files imported, converted and saved. Reloading SslContextFactory...");
|
||||
certificateManager.reloadSslContextFactory();
|
||||
log.info("Reloaded SSL successfully.");
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more.");
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error renewing/reloading LetsEncrypt cert", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import qz.common.Constants;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyStore;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* Wrap handling of X509Certificate, PrivateKey and KeyStore conversion
|
||||
*/
|
||||
public class KeyPairWrapper {
|
||||
public enum Type {CA, SSL}
|
||||
|
||||
private Type type;
|
||||
private PrivateKey key;
|
||||
private char[] password;
|
||||
private X509Certificate cert;
|
||||
private KeyStore keyStore; // for SSL
|
||||
|
||||
public KeyPairWrapper(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) {
|
||||
this.type = type;
|
||||
this.key = keyPair.getPrivate();
|
||||
this.cert = cert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from disk
|
||||
*/
|
||||
public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException {
|
||||
KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
|
||||
keyStore.load(new FileInputStream(keyFile), password);
|
||||
init(keyStore, password);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load from memory
|
||||
*/
|
||||
public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException {
|
||||
this.keyStore = keyStore;
|
||||
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password);
|
||||
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param);
|
||||
// the entry we assume is always wrong for pkcs12 imports, search for it instead
|
||||
if(entry == null) {
|
||||
Enumeration<String> enumerator = keyStore.aliases();
|
||||
while(enumerator.hasMoreElements()) {
|
||||
String alias = enumerator.nextElement();
|
||||
if(keyStore.isKeyEntry(alias)) {
|
||||
this.password = password;
|
||||
this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey();
|
||||
this.cert = (X509Certificate)keyStore.getCertificate(alias);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new GeneralSecurityException("Could not initialize the KeyStore for internal use");
|
||||
}
|
||||
|
||||
this.password = password;
|
||||
this.key = entry.getPrivateKey();
|
||||
this.cert = (X509Certificate)keyStore.getCertificate(getAlias());
|
||||
}
|
||||
|
||||
public X509Certificate getCert() {
|
||||
return cert;
|
||||
}
|
||||
|
||||
public PrivateKey getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getPasswordString() {
|
||||
return new String(password);
|
||||
}
|
||||
|
||||
public char[] getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public static String getAlias(Type type) {
|
||||
switch(type) {
|
||||
case SSL:
|
||||
return Constants.PROPS_FILE; // "qz-tray"
|
||||
case CA:
|
||||
default:
|
||||
return "root-ca";
|
||||
}
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return getAlias(getType());
|
||||
}
|
||||
|
||||
public String propsPrefix() {
|
||||
switch(type) {
|
||||
case SSL:
|
||||
return "wss";
|
||||
case CA:
|
||||
default:
|
||||
return "ca";
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public KeyStore getKeyStore() {
|
||||
return keyStore;
|
||||
}
|
||||
}
|
||||
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.asn1.DEROctetString;
|
||||
import org.bouncycastle.asn1.x509.Extension;
|
||||
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import qz.auth.X509Constants;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ByteUtilities;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.UnixUtilities;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static qz.installer.Installer.PrivilegeLevel.*;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public class LinuxCertificateInstaller extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(LinuxCertificateInstaller.class);
|
||||
private static final String CA_CERTIFICATES = "/usr/local/share/ca-certificates/";
|
||||
private static final String CA_CERTIFICATE_NAME = Constants.PROPS_FILE + "-root.crt"; // e.g. qz-tray-root.crt
|
||||
private static final String PK11_KIT_ID = "pkcs11:id=";
|
||||
|
||||
private static String[] NSSDB_URLS = {
|
||||
// Conventional cert store
|
||||
"sql:" + System.getenv("HOME") + "/.pki/nssdb/",
|
||||
|
||||
// Snap-specific cert stores
|
||||
"sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb/",
|
||||
"sql:" + System.getenv("HOME") + "/snap/brave/current/.pki/nssdb/",
|
||||
"sql:" + System.getenv("HOME") + "/snap/opera/current/.pki/nssdb/",
|
||||
"sql:" + System.getenv("HOME") + "/snap/opera-beta/current/.pki/nssdb/"
|
||||
};
|
||||
|
||||
private Installer.PrivilegeLevel certType;
|
||||
|
||||
public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
findCertutil();
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
return certType;
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel certType) {
|
||||
this.certType = certType;
|
||||
if (this.certType == SYSTEM) {
|
||||
log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch.");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(List<String> idList) {
|
||||
boolean success = true;
|
||||
if(certType == SYSTEM) {
|
||||
boolean first = distrustUsingUpdateCaCertificates(idList);
|
||||
boolean second = distrustUsingTrustAnchor(idList);
|
||||
success = first || second;
|
||||
} else {
|
||||
for(String nickname : idList) {
|
||||
for(String nssdb : NSSDB_URLS) {
|
||||
success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname);
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> find() {
|
||||
ArrayList<String> nicknames = new ArrayList<>();
|
||||
if(certType == SYSTEM) {
|
||||
nicknames = findUsingTrustAnchor();
|
||||
nicknames.addAll(findUsingUsingUpdateCaCert());
|
||||
} else {
|
||||
try {
|
||||
for(String nssdb : NSSDB_URLS) {
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", nssdb, "-L"});
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while((line = in.readLine()) != null) {
|
||||
if (line.startsWith(Constants.ABOUT_COMPANY + " ")) {
|
||||
nicknames.add(Constants.ABOUT_COMPANY);
|
||||
break; // Stop reading input; nicknames can't appear more than once
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Could not get certificate nicknames", e);
|
||||
}
|
||||
}
|
||||
return nicknames;
|
||||
}
|
||||
|
||||
public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed
|
||||
|
||||
public boolean add(File certFile) {
|
||||
boolean success = true;
|
||||
|
||||
if(certType == SYSTEM) {
|
||||
// Attempt two common methods for installing the SSL certificate
|
||||
File systemCertFile;
|
||||
boolean first = (systemCertFile = trustUsingUpdateCaCertificates(certFile)) != null;
|
||||
boolean second = trustUsingTrustAnchor(systemCertFile, certFile);
|
||||
success = first || second;
|
||||
} else if(certType == USER) {
|
||||
// Install certificate to local profile using "certutil"
|
||||
for(String nssdb : NSSDB_URLS) {
|
||||
String[] parts = nssdb.split(":", 2);
|
||||
if (parts.length > 1) {
|
||||
File folder = new File(parts[1]);
|
||||
// If .pki/nssdb doesn't exist yet, don't create it! Per https://github.com/qzind/tray/issues/1003
|
||||
if(folder.exists() && folder.isDirectory()) {
|
||||
if (!ShellUtilities.execute("certutil", "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath())) {
|
||||
log.warn("Something went wrong creating {}. HTTPS will fail on certain browsers which depend on it.", nssdb);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
private boolean findCertutil() {
|
||||
boolean installed = ShellUtilities.execute("which", "certutil");
|
||||
if (!installed) {
|
||||
if (certType == SYSTEM && promptCertutil()) {
|
||||
if(UnixUtilities.isUbuntu() || UnixUtilities.isDebian()) {
|
||||
installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
|
||||
} else if(UnixUtilities.isFedora()) {
|
||||
installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools");
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!installed) {
|
||||
log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on certain browsers which depend on it.");
|
||||
}
|
||||
return installed;
|
||||
}
|
||||
|
||||
private boolean promptCertutil() {
|
||||
// Assume silent or headless installs want certutil
|
||||
if(Installer.IS_SILENT || GraphicsEnvironment.isHeadless()) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
SystemUtilities.setSystemLookAndFeel(true);
|
||||
return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?");
|
||||
} catch(Throwable ignore) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common technique for installing system-wide certificates on Debian-based systems (Ubuntu, etc.)
|
||||
*
|
||||
* This technique is only known to work for select browsers, such as Epiphany. Browsers such as
|
||||
* Firefox and Chromium require different techniques.
|
||||
*
|
||||
* @return Full path to the destination file if successful, otherwise <code>null</code>
|
||||
*/
|
||||
private File trustUsingUpdateCaCertificates(File certFile) {
|
||||
if(hasUpdateCaCertificatesCommand()) {
|
||||
File destFile = new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME);
|
||||
log.debug("Copying SYSTEM SSL certificate {} to {}", certFile.getPath(), destFile.getPath());
|
||||
try {
|
||||
if (new File(CA_CERTIFICATES).isDirectory()) {
|
||||
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
|
||||
FileUtils.copyFile(certFile, destFile, false);
|
||||
if (destFile.isFile()) {
|
||||
// Attempt "update-ca-certificates" (Debian)
|
||||
if (!ShellUtilities.execute("update-ca-certificates")) {
|
||||
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
|
||||
} else {
|
||||
return destFile;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("{} is not a valid directory, skipping", CA_CERTIFICATES);
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Error copying SYSTEM SSL certificate file", e);
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate install using \"update-ca-certificates\", command missing or invalid");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common technique for installing system-wide certificates on Fedora-based systems
|
||||
*
|
||||
* Uses first existing non-null file provided
|
||||
*/
|
||||
private boolean trustUsingTrustAnchor(File ... certFiles) {
|
||||
if (hasTrustAnchorCommand()) {
|
||||
for(File certFile : certFiles) {
|
||||
if (certFile == null || !certFile.exists()) {
|
||||
continue;
|
||||
}
|
||||
// Install certificate to system using "trust anchor" (Fedora)
|
||||
if (ShellUtilities.execute("trust", "anchor", "--store", certFile.getPath())) {
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate install using \"trust anchor\", command missing or invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean distrustUsingUpdateCaCertificates(List<String> paths) {
|
||||
if(hasUpdateCaCertificatesCommand()) {
|
||||
boolean deleted = false;
|
||||
for(String path : paths) {
|
||||
// Process files only; not "trust anchor" URIs
|
||||
if(!path.startsWith(PK11_KIT_ID)) {
|
||||
File certFile = new File(path);
|
||||
if (certFile.isFile() && certFile.delete()) {
|
||||
deleted = true;
|
||||
} else {
|
||||
log.warn("SYSTEM SSL certificate {} does not exist, skipping", certFile.getPath());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Attempt "update-ca-certificates" (Debian)
|
||||
if(deleted) {
|
||||
if (ShellUtilities.execute("update-ca-certificates")) {
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate removal using \"update-ca-certificates\", command missing or invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean distrustUsingTrustAnchor(List<String> idList) {
|
||||
if(hasTrustAnchorCommand()) {
|
||||
for(String id : idList) {
|
||||
// only remove by id
|
||||
if (id.startsWith(PK11_KIT_ID) && !ShellUtilities.execute("trust", "anchor", "--remove", id)) {
|
||||
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping SYSTEM SSL certificate removal using \"trust anchor\", command missing or invalid");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for the presence of a QZ certificate in known locations (e.g. /usr/local/share/ca-certificates/
|
||||
* and return the path if found
|
||||
*/
|
||||
private ArrayList<String> findUsingUsingUpdateCaCert() {
|
||||
ArrayList<String> found = new ArrayList<>();
|
||||
File[] systemCertFiles = { new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME) };
|
||||
for(File file : systemCertFiles) {
|
||||
if(file.isFile()) {
|
||||
found.add(file.getPath());
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find QZ installed certificates in the "trust anchor" by searching by email.
|
||||
*
|
||||
* The "trust" utility identifies certificates as URIs:
|
||||
* Example:
|
||||
* pkcs11:id=%7C%5D%02%84%13%D4%CC%8A%9B%81%CE%17%1C%2E%29%1E%9C%48%63%42;type=cert
|
||||
* ... which is an encoded version of the cert's SubjectKeyIdentifier field
|
||||
* To identify a match:
|
||||
* 1. Extract all trusted certificates and look for a familiar email address
|
||||
* 2. If found, construct and store a "trust" compatible URI as the nickname
|
||||
*/
|
||||
private ArrayList<String> findUsingTrustAnchor() {
|
||||
ArrayList<String> uris = new ArrayList<>();
|
||||
File tempFile = null;
|
||||
try {
|
||||
// Temporary location for system certificates
|
||||
tempFile = File.createTempFile("trust-extract-for-qz-", ".pem");
|
||||
// Delete before use: "trust extract" requires an empty file
|
||||
tempFile.delete();
|
||||
if(ShellUtilities.execute("trust", "extract", "--format", "pem-bundle", tempFile.getPath())) {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(tempFile));
|
||||
String line;
|
||||
StringBuilder base64 = new StringBuilder();
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if(line.startsWith(X509Constants.BEGIN_CERT)) {
|
||||
// Beginning of a new certificate
|
||||
base64.setLength(0);
|
||||
} else if(line.startsWith(X509Constants.END_CERT)) {
|
||||
// End of the existing certificate
|
||||
byte[] certBytes = Base64.decode(base64.toString());
|
||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||
X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||
if(CertificateManager.emailMatches(cert, true)) {
|
||||
byte[] extensionValue = cert.getExtensionValue(Extension.subjectKeyIdentifier.getId());
|
||||
byte[] octets = DEROctetString.getInstance(extensionValue).getOctets();
|
||||
SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets);
|
||||
byte[] keyIdentifier = subjectKeyIdentifier.getKeyIdentifier();
|
||||
String hex = ByteUtilities.bytesToHex(keyIdentifier, true);
|
||||
String uri = PK11_KIT_ID + hex.replaceAll("(.{2})", "%$1") + ";type=cert";
|
||||
log.info("Found matching cert: {}", uri);
|
||||
|
||||
uris.add(uri);
|
||||
}
|
||||
} else {
|
||||
base64.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
reader.close();
|
||||
}
|
||||
} catch(IOException | CertificateException e) {
|
||||
log.warn("An error occurred finding preexisting \"trust anchor\" certificates", e);
|
||||
} finally {
|
||||
if(tempFile != null && !tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
return uris;
|
||||
}
|
||||
|
||||
private boolean hasUpdateCaCertificatesCommand() {
|
||||
return ShellUtilities.execute("which", "update-ca-certificates");
|
||||
}
|
||||
|
||||
private boolean hasTrustAnchorCommand() {
|
||||
return ShellUtilities.execute("trust", "anchor", "--help");
|
||||
}
|
||||
}
|
||||
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ShellUtilities;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MacCertificateInstaller extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(MacCertificateInstaller.class);
|
||||
|
||||
public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db
|
||||
public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain";
|
||||
private String certStore;
|
||||
|
||||
public MacCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
}
|
||||
|
||||
public boolean add(File certFile) {
|
||||
if (certStore.equals(USER_STORE)) {
|
||||
// This will prompt the user
|
||||
return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath());
|
||||
} else {
|
||||
return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(List<String> idList) {
|
||||
boolean success = true;
|
||||
for (String certId : idList) {
|
||||
success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> find() {
|
||||
ArrayList<String> hashList = new ArrayList<>();
|
||||
try {
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-a", "-e", Constants.ABOUT_EMAIL, "-Z", certStore});
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
if (line.contains("SHA-1") && line.contains(":")) {
|
||||
hashList.add(line.split(":", 2)[1].trim());
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not get certificate list", e);
|
||||
}
|
||||
return hashList;
|
||||
}
|
||||
|
||||
public boolean verify(File certFile) {
|
||||
return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath());
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||
if (type == Installer.PrivilegeLevel.USER) {
|
||||
certStore = USER_STORE;
|
||||
} else {
|
||||
certStore = SYSTEM_STORE;
|
||||
}
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
if (certStore == USER_STORE) {
|
||||
return Installer.PrivilegeLevel.USER;
|
||||
} else {
|
||||
return Installer.PrivilegeLevel.SYSTEM;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(NativeCertificateInstaller.class);
|
||||
protected static NativeCertificateInstaller instance;
|
||||
|
||||
public static NativeCertificateInstaller getInstance() {
|
||||
return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER);
|
||||
}
|
||||
public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
|
||||
if (instance == null) {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
instance = new WindowsCertificateInstaller(type);
|
||||
break;
|
||||
case MAC:
|
||||
instance = new MacCertificateInstaller(type);
|
||||
break;
|
||||
case LINUX:
|
||||
default:
|
||||
instance = new LinuxCertificateInstaller(type);
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a certificate from memory
|
||||
*/
|
||||
public boolean install(X509Certificate cert) {
|
||||
File certFile = null;
|
||||
try {
|
||||
certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
|
||||
JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert);
|
||||
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE)));
|
||||
writer.writeObject(generator.generate());
|
||||
writer.close();
|
||||
|
||||
return install(certFile);
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not install cert from temp file", e);
|
||||
} finally {
|
||||
if(certFile != null && !certFile.delete()) {
|
||||
certFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a certificate from disk
|
||||
*/
|
||||
public boolean install(File certFile) {
|
||||
String helper = instance.getClass().getSimpleName();
|
||||
String store = instance.getInstallType().name();
|
||||
if(SystemUtilities.isJar()) {
|
||||
if (remove(find())) {
|
||||
log.info("Certificate removed from {} store using {}", store, helper);
|
||||
} else {
|
||||
log.warn("Could not remove certificate from {} store using {}", store, helper);
|
||||
}
|
||||
} else {
|
||||
log.info("Skipping {} store certificate removal, IDE detected.", store, helper);
|
||||
}
|
||||
if (add(certFile)) {
|
||||
log.info("Certificate added to {} store using {}", store, helper);
|
||||
return true;
|
||||
} else {
|
||||
log.warn("Could not install certificate to {} store using {}", store, helper);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract boolean add(File certFile);
|
||||
public abstract boolean remove(List<String> idList);
|
||||
public abstract List<String> find();
|
||||
public abstract boolean verify(File certFile);
|
||||
public abstract void setInstallType(Installer.PrivilegeLevel certType);
|
||||
public abstract Installer.PrivilegeLevel getInstallType();
|
||||
}
|
||||
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.Structure;
|
||||
import com.sun.jna.platform.win32.Kernel32Util;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import com.sun.jna.win32.StdCallLibrary;
|
||||
import com.sun.jna.win32.W32APIOptions;
|
||||
import org.bouncycastle.cert.X509CertificateHolder;
|
||||
import org.bouncycastle.openssl.PEMParser;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
|
||||
import java.io.*;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
public class WindowsCertificateInstaller extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(WindowsCertificateInstaller.class);
|
||||
private WinCrypt.HCERTSTORE store;
|
||||
private byte[] certBytes;
|
||||
private Installer.PrivilegeLevel certType;
|
||||
|
||||
public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
}
|
||||
|
||||
public boolean add(File certFile) {
|
||||
log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType);
|
||||
try {
|
||||
|
||||
byte[] bytes = getCertBytes(certFile);
|
||||
Pointer pointer = new Memory(bytes.length);
|
||||
pointer.write(0, bytes, 0, bytes.length);
|
||||
|
||||
boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore(
|
||||
openStore(),
|
||||
WinCrypt.X509_ASN_ENCODING,
|
||||
pointer,
|
||||
bytes.length,
|
||||
Crypt32.CERT_STORE_ADD_REPLACE_EXISTING,
|
||||
null
|
||||
);
|
||||
if(!success) {
|
||||
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
|
||||
closeStore();
|
||||
|
||||
return success;
|
||||
} catch(IOException e) {
|
||||
log.warn("An error occurred installing the certificate", e);
|
||||
} finally {
|
||||
certBytes = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private byte[] getCertBytes(File certFile) throws IOException {
|
||||
if(certBytes == null) {
|
||||
PEMParser pem = new PEMParser(new FileReader(certFile));
|
||||
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
|
||||
certBytes = certHolder.getEncoded();
|
||||
}
|
||||
return certBytes;
|
||||
}
|
||||
|
||||
private WinCrypt.HCERTSTORE openStore() {
|
||||
if(store == null) {
|
||||
store = openStore(certType);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
private void closeStore() {
|
||||
if(store != null && closeStore(store)) {
|
||||
store = null;
|
||||
} else {
|
||||
log.warn("Unable to close {} cert store", certType);
|
||||
}
|
||||
}
|
||||
|
||||
private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) {
|
||||
log.info("Opening {} store using Crypt32...", certType);
|
||||
|
||||
WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore(
|
||||
Crypt32.CERT_STORE_PROV_SYSTEM,
|
||||
0,
|
||||
null,
|
||||
certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE,
|
||||
"ROOT"
|
||||
);
|
||||
if(store == null) {
|
||||
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
private static boolean closeStore(WinCrypt.HCERTSTORE certStore) {
|
||||
boolean isClosed = Crypt32.INSTANCE.CertCloseStore(
|
||||
certStore, 0
|
||||
);
|
||||
if(!isClosed) {
|
||||
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
return isClosed;
|
||||
}
|
||||
|
||||
public boolean remove(List<String> ignore) {
|
||||
boolean success = true;
|
||||
|
||||
WinCrypt.CERT_CONTEXT hCertContext;
|
||||
WinCrypt.CERT_CONTEXT pPrevCertContext = null;
|
||||
while(true) {
|
||||
hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore(
|
||||
openStore(),
|
||||
WinCrypt.X509_ASN_ENCODING,
|
||||
0,
|
||||
Crypt32.CERT_FIND_SUBJECT_STR,
|
||||
Constants.ABOUT_EMAIL,
|
||||
pPrevCertContext);
|
||||
|
||||
if(hCertContext == null) {
|
||||
break;
|
||||
}
|
||||
|
||||
pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext);
|
||||
|
||||
if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) {
|
||||
log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL);
|
||||
} else {
|
||||
log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError()));
|
||||
}
|
||||
}
|
||||
|
||||
closeStore();
|
||||
return success;
|
||||
}
|
||||
|
||||
public List<String> find() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||
this.certType = type;
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
return certType;
|
||||
}
|
||||
|
||||
public boolean verify(File certFile) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
md.update(getCertBytes(certFile));
|
||||
WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest());
|
||||
WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore(
|
||||
openStore(),
|
||||
WinCrypt.X509_ASN_ENCODING,
|
||||
0,
|
||||
Crypt32.CERT_FIND_SHA1_HASH,
|
||||
thumbPrint,
|
||||
null);
|
||||
|
||||
return cert != null;
|
||||
} catch(IOException | NoSuchAlgorithmException e) {
|
||||
log.warn("An error occurred verifying the cert is installed: {}", certFile, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used
|
||||
*/
|
||||
interface Crypt32 extends StdCallLibrary {
|
||||
int CERT_SYSTEM_STORE_CURRENT_USER = 65536;
|
||||
int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072;
|
||||
int CERT_STORE_PROV_SYSTEM = 10;
|
||||
int CERT_STORE_ADD_REPLACE_EXISTING = 3;
|
||||
int CERT_FIND_SUBJECT_STR = 524295;
|
||||
int CERT_FIND_SHA1_HASH = 65536;
|
||||
|
||||
Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||
|
||||
WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
|
||||
boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
|
||||
boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext);
|
||||
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
|
||||
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
|
||||
boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext);
|
||||
boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
|
||||
WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
|
||||
}
|
||||
|
||||
// Polyfill from JNA5+
|
||||
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||
public static class WinCrypt {
|
||||
public static int X509_ASN_ENCODING = 0x00000001;
|
||||
public static class HCERTSTORE extends WinNT.HANDLE {
|
||||
public HCERTSTORE() {}
|
||||
public HCERTSTORE(Pointer p) {
|
||||
super(p);
|
||||
}
|
||||
}
|
||||
public static class CERT_CONTEXT extends WinNT.HANDLE {
|
||||
public CERT_CONTEXT() {}
|
||||
public CERT_CONTEXT(Pointer p) {
|
||||
super(p);
|
||||
}
|
||||
}
|
||||
public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB {
|
||||
// Wrap the constructor for code readability
|
||||
public DATA_BLOB() {
|
||||
super();
|
||||
}
|
||||
public DATA_BLOB(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Command Line technique for installing certificates on Windows
|
||||
* Fallback class for when JNA is not available (e.g. Windows on ARM)
|
||||
*/
|
||||
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||
public class WindowsCertificateInstallerCli extends NativeCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(WindowsCertificateInstallerCli.class);
|
||||
private Installer.PrivilegeLevel certType;
|
||||
|
||||
public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
|
||||
setInstallType(certType);
|
||||
}
|
||||
|
||||
public boolean add(File certFile) {
|
||||
if (WindowsUtilities.isWindowsXP()) return false;
|
||||
if (certType == Installer.PrivilegeLevel.USER) {
|
||||
// This will prompt the user
|
||||
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
|
||||
} else {
|
||||
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(List<String> idList) {
|
||||
if (WindowsUtilities.isWindowsXP()) return false;
|
||||
boolean success = true;
|
||||
for (String certId : idList) {
|
||||
if (certType == Installer.PrivilegeLevel.USER) {
|
||||
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId);
|
||||
} else {
|
||||
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId);
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of serials, if found
|
||||
*/
|
||||
public List<String> find() {
|
||||
ArrayList<String> serialList = new ArrayList<>();
|
||||
try {
|
||||
Process p;
|
||||
if (certType == Installer.PrivilegeLevel.USER) {
|
||||
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"});
|
||||
} else {
|
||||
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"});
|
||||
}
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = in.readLine()) != null) {
|
||||
if (line.contains("================")) {
|
||||
// First line is serial
|
||||
String serial = parseNextLine(in);
|
||||
if (serial != null) {
|
||||
// Second line is issuer
|
||||
String issuer = parseNextLine(in);
|
||||
if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) {
|
||||
serialList.add(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
in.close();
|
||||
} catch(Exception e) {
|
||||
log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY);
|
||||
}
|
||||
return serialList;
|
||||
}
|
||||
|
||||
public boolean verify(File certFile) {
|
||||
return verifyCert(certFile);
|
||||
}
|
||||
|
||||
public static boolean verifyCert(File certFile) {
|
||||
// -user also will check the root store
|
||||
String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false);
|
||||
if(!dwErrorStatus.isEmpty()) {
|
||||
String[] parts = dwErrorStatus.split("[\r\n\\s]+");
|
||||
for(String part : parts) {
|
||||
if(part.startsWith("dwErrorStatus=")) {
|
||||
log.info("Certificate validity says {}", part);
|
||||
String[] status = part.split("=", 2);
|
||||
if (status.length == 2) {
|
||||
return status[1].trim().equals("0");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("Unable to determine certificate validity, you'll be prompted on startup");
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||
this.certType = type;
|
||||
}
|
||||
|
||||
public Installer.PrivilegeLevel getInstallType() {
|
||||
return certType;
|
||||
}
|
||||
|
||||
private static String parseNextLine(BufferedReader reader) throws IOException {
|
||||
String data = reader.readLine();
|
||||
if (data != null) {
|
||||
String[] split = data.split(":", 2);
|
||||
if (split.length == 2) {
|
||||
return split[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
class ConflictingPolicyException extends Exception {
|
||||
ConflictingPolicyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import com.sun.jna.platform.win32.WinReg;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.Installer;
|
||||
import qz.installer.certificate.CertificateManager;
|
||||
import qz.installer.certificate.firefox.locator.AppAlias;
|
||||
import qz.installer.certificate.firefox.locator.AppInfo;
|
||||
import qz.installer.certificate.firefox.locator.AppLocator;
|
||||
import qz.utils.JsonWriter;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* Installs the Firefox Policy file via Enterprise Policy, Distribution Policy file or AutoConfig, depending on OS & version
|
||||
*/
|
||||
public class FirefoxCertificateInstaller {
|
||||
protected static final Logger log = LogManager.getLogger(FirefoxCertificateInstaller.class);
|
||||
|
||||
/**
|
||||
* Versions are for Mozilla's official Firefox release.
|
||||
* 3rd-party/clones may adopt Enterprise Policy support under
|
||||
* different version numbers, adapt as needed.
|
||||
*/
|
||||
private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0");
|
||||
private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0");
|
||||
private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0");
|
||||
public static final Version FIREFOX_RESTART_VERSION = Version.valueOf("60.0.0");
|
||||
|
||||
public static final String LINUX_GLOBAL_POLICY_LOCATION = "/etc/firefox/policies/policies.json";
|
||||
public static final String LINUX_SNAP_CERT_LOCATION = "/etc/firefox/policies/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; // See https://github.com/mozilla/policy-templates/issues/936
|
||||
public static final String LINUX_GLOBAL_CERT_LOCATION = "/usr/lib/mozilla/certificates/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION;
|
||||
private static String DISTRIBUTION_ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
|
||||
private static String DISTRIBUTION_INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\", \"" + LINUX_SNAP_CERT_LOCATION + "\" ] } } }";
|
||||
private static String DISTRIBUTION_REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }";
|
||||
|
||||
public static final String DISTRIBUTION_POLICY_LOCATION = "distribution/policies.json";
|
||||
public static final String DISTRIBUTION_MAC_POLICY_LOCATION = "Contents/Resources/" + DISTRIBUTION_POLICY_LOCATION;
|
||||
|
||||
public static final String POLICY_AUDIT_MESSAGE = "Enterprise policy installed by " + Constants.ABOUT_TITLE + " on " + SystemUtilities.timeStamp();
|
||||
|
||||
public static void install(X509Certificate cert, String ... hostNames) {
|
||||
// Blindly install Firefox enterprise policies to the system (macOS, Windows)
|
||||
ArrayList<AppAlias.Alias> enterpriseFailed = new ArrayList<>();
|
||||
for(AppAlias.Alias alias : AppAlias.FIREFOX.getAliases()) {
|
||||
boolean success = false;
|
||||
try {
|
||||
if(alias.isEnterpriseReady() && !hasEnterprisePolicy(alias, false)) {
|
||||
log.info("Installing Firefox enterprise certificate policy for {}", alias);
|
||||
success = installEnterprisePolicy(alias, false);
|
||||
}
|
||||
} catch(ConflictingPolicyException e) {
|
||||
log.warn("Conflict found installing {} enterprise cert support. We'll fallback on the distribution policy instead", alias.getName(), e);
|
||||
}
|
||||
if(!success) {
|
||||
enterpriseFailed.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// Search for installed instances
|
||||
ArrayList<AppInfo> foundApps = AppLocator.getInstance().locate(AppAlias.FIREFOX);
|
||||
ArrayList<Path> processPaths = null;
|
||||
|
||||
for(AppInfo appInfo : foundApps) {
|
||||
boolean success = false;
|
||||
if (honorsPolicy(appInfo)) {
|
||||
if((SystemUtilities.isWindows()|| SystemUtilities.isMac()) && !enterpriseFailed.contains(appInfo.getAlias())) {
|
||||
// Enterprise policy was already installed
|
||||
success = true;
|
||||
} else {
|
||||
log.info("Installing Firefox distribution policy for {}", appInfo);
|
||||
success = installDistributionPolicy(appInfo, cert);
|
||||
}
|
||||
} else {
|
||||
log.info("Installing Firefox auto-config script for {}", appInfo);
|
||||
try {
|
||||
String certData = Base64.getEncoder().encodeToString(cert.getEncoded());
|
||||
success = LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames);
|
||||
}
|
||||
catch(CertificateEncodingException e) {
|
||||
log.warn("Unable to install auto-config script for {}", appInfo, e);
|
||||
}
|
||||
}
|
||||
if(success) {
|
||||
issueRestartWarning(processPaths = AppLocator.getRunningPaths(foundApps, processPaths), appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void uninstall() {
|
||||
ArrayList<AppInfo> appList = AppLocator.getInstance().locate(AppAlias.FIREFOX);
|
||||
for(AppInfo appInfo : appList) {
|
||||
if(honorsPolicy(appInfo)) {
|
||||
if(SystemUtilities.isWindows() || SystemUtilities.isMac()) {
|
||||
log.info("Skipping uninstall of Firefox enterprise root certificate policy for {}", appInfo);
|
||||
} else {
|
||||
try {
|
||||
File policy = appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toFile();
|
||||
if(policy.exists()) {
|
||||
JsonWriter.write(appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toString(), DISTRIBUTION_INSTALL_CERT_POLICY, false, true);
|
||||
}
|
||||
} catch(IOException | JSONException e) {
|
||||
log.warn("Unable to remove Firefox policy for {}", appInfo, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("Uninstalling Firefox auto-config script for {}", appInfo);
|
||||
LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean honorsPolicy(AppInfo appInfo) {
|
||||
if (appInfo.getVersion() == null) {
|
||||
log.warn("Firefox-compatible browser found {}, but no version information is available", appInfo);
|
||||
return false;
|
||||
}
|
||||
if(SystemUtilities.isWindows()) {
|
||||
return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION);
|
||||
} else if (SystemUtilities.isMac()) {
|
||||
return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION);
|
||||
} else {
|
||||
return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an alternative Firefox policy (e.g. registry, plist user or system) is installed
|
||||
*/
|
||||
private static boolean hasEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) throws ConflictingPolicyException {
|
||||
if(SystemUtilities.isWindows()) {
|
||||
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));
|
||||
Integer foundPolicy = WindowsUtilities.getRegInt(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots");
|
||||
if(foundPolicy != null) {
|
||||
return foundPolicy == 1;
|
||||
}
|
||||
} else if(SystemUtilities.isMac()) {
|
||||
String policyLocation = "/Library/Preferences/";
|
||||
if(userOnly) {
|
||||
policyLocation = System.getProperty("user.home") + policyLocation;
|
||||
}
|
||||
String policesEnabled = ShellUtilities.executeRaw(new String[] { "defaults", "read", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled"}, true);
|
||||
String foundPolicy = ShellUtilities.executeRaw(new String[] {"defaults", "read", policyLocation + alias.getBundleId(), "Certificates"}, true);
|
||||
if(!policesEnabled.isEmpty() && !foundPolicy.isEmpty()) {
|
||||
// Policies exist, decide how to proceed
|
||||
if(policesEnabled.trim().equals("1") && foundPolicy.contains("ImportEnterpriseRoots = 1;")) {
|
||||
return true;
|
||||
}
|
||||
throw new ConflictingPolicyException(String.format("%s enterprise policy conflict at %s: %s", alias.getName(), policyLocation + alias.getBundleId(), foundPolicy));
|
||||
}
|
||||
} else {
|
||||
// Linux alternate policy not yet supported
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install policy to distribution/policies.json
|
||||
*/
|
||||
public static boolean installDistributionPolicy(AppInfo app, X509Certificate cert) {
|
||||
Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? DISTRIBUTION_MAC_POLICY_LOCATION:DISTRIBUTION_POLICY_LOCATION);
|
||||
String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? DISTRIBUTION_ENTERPRISE_ROOT_POLICY:DISTRIBUTION_INSTALL_CERT_POLICY;
|
||||
|
||||
// Special handling for snaps
|
||||
if(app.getPath().toString().startsWith("/snap")) {
|
||||
log.info("Snap detected, installing policy file to global location instead: {}", LINUX_GLOBAL_POLICY_LOCATION);
|
||||
jsonPath = Paths.get(LINUX_GLOBAL_POLICY_LOCATION);
|
||||
}
|
||||
|
||||
try {
|
||||
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
|
||||
// Linux lacks the concept of "enterprise roots", we'll write it to a known location instead
|
||||
writeCertFile(cert, LINUX_SNAP_CERT_LOCATION); // so that the snap can read from it
|
||||
writeCertFile(cert, LINUX_GLOBAL_CERT_LOCATION); // default location for non-snaps
|
||||
}
|
||||
|
||||
File jsonFile = jsonPath.toFile();
|
||||
|
||||
// Make sure we can traverse and read
|
||||
File distribution = jsonFile.getParentFile();
|
||||
distribution.mkdirs();
|
||||
distribution.setReadable(true, false);
|
||||
distribution.setExecutable(true, false);
|
||||
|
||||
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
|
||||
// Delete previous policy
|
||||
JsonWriter.write(jsonPath.toString(), DISTRIBUTION_REMOVE_CERT_POLICY, false, true);
|
||||
}
|
||||
|
||||
JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false);
|
||||
|
||||
// Make sure ew can read
|
||||
jsonFile.setReadable(true, false);
|
||||
return true;
|
||||
} catch(JSONException | IOException e) {
|
||||
log.warn("Could not install distribution policy {} to {}", jsonPolicy, jsonPath.toString(), e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean installEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) {
|
||||
if(SystemUtilities.isWindows()) {
|
||||
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));;
|
||||
WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "Comment", POLICY_AUDIT_MESSAGE);
|
||||
return WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots", 1);
|
||||
} else if(SystemUtilities.isMac()) {
|
||||
String policyLocation = "/Library/Preferences/";
|
||||
if(userOnly) {
|
||||
policyLocation = System.getProperty("user.home") + policyLocation;
|
||||
}
|
||||
return ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled", "-bool", "TRUE"}, true) &&
|
||||
ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "Certificates", "-dict", "ImportEnterpriseRoots", "-bool", "TRUE",
|
||||
"Comment", "-string", POLICY_AUDIT_MESSAGE}, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean issueRestartWarning(ArrayList<Path> runningPaths, AppInfo appInfo) {
|
||||
boolean firefoxIsRunning = runningPaths.contains(appInfo.getExePath());
|
||||
|
||||
// Edge case for detecting if snap is running, since we can't compare the exact path easily
|
||||
for(Path runningPath : runningPaths) {
|
||||
if(runningPath.startsWith("/snap/")) {
|
||||
firefoxIsRunning = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (firefoxIsRunning) {
|
||||
if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) {
|
||||
try {
|
||||
Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired");
|
||||
return true;
|
||||
}
|
||||
catch(Exception ignore) {}
|
||||
} else {
|
||||
log.warn("{} must be restarted manually for changes to take effect", appInfo);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void writeCertFile(X509Certificate cert, String location) throws IOException {
|
||||
File certFile = new File(location);
|
||||
|
||||
// Make sure we can traverse and read
|
||||
File certs = new File(location).getParentFile();
|
||||
certs.mkdirs();
|
||||
certs.setReadable(true, false);
|
||||
certs.setExecutable(true, false);
|
||||
File mozilla = certs.getParentFile();
|
||||
mozilla.setReadable(true, false);
|
||||
mozilla.setExecutable(true, false);
|
||||
|
||||
// Make sure we can read
|
||||
CertificateManager.writeCert(cert, certFile);
|
||||
certFile.setReadable(true, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate.firefox;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.certificate.CertificateChainBuilder;
|
||||
import qz.installer.certificate.firefox.locator.AppInfo;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Path;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Legacy Firefox Certificate installer
|
||||
*
|
||||
* For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc.
|
||||
*/
|
||||
public class LegacyFirefoxCertificateInstaller {
|
||||
private static final Logger log = LogManager.getLogger(CertificateChainBuilder.class);
|
||||
|
||||
private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in";
|
||||
private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg";
|
||||
private static final String PREFS_FILE = Constants.PROPS_FILE + ".js";
|
||||
private static final String PREFS_DIR = "defaults/pref";
|
||||
private static final String MAC_PREFIX = "Contents/Resources";
|
||||
|
||||
public static boolean installAutoConfigScript(AppInfo appInfo, String certData, String ... hostNames) {
|
||||
try {
|
||||
if(appInfo.getPath().toString().equals("/usr/bin")) {
|
||||
throw new Exception("Preventing install to root location");
|
||||
}
|
||||
writePrefsFile(appInfo);
|
||||
writeParsedConfig(appInfo, certData, false, hostNames);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
log.warn("Error installing auto-config support for {}", appInfo, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean uninstallAutoConfigScript(AppInfo appInfo) {
|
||||
try {
|
||||
writeParsedConfig(appInfo, "", true);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
log.warn("Error uninstalling auto-config support for {}", appInfo, e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException {
|
||||
Path dir = appInfo.getPath();
|
||||
if (SystemUtilities.isMac()) {
|
||||
dir = dir.resolve(MAC_PREFIX);
|
||||
}
|
||||
for (String path : paths) {
|
||||
dir = dir.resolve(path);
|
||||
}
|
||||
File file = dir.toFile();
|
||||
|
||||
if(mkdirs) file.mkdirs();
|
||||
if(file.exists() && file.isDirectory() && file.canWrite()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
throw new IOException(String.format("Directory does not exist or is not writable: %s", file));
|
||||
}
|
||||
|
||||
public static void deleteFile(File parent, String ... paths) {
|
||||
if(parent != null) {
|
||||
String toDelete = parent.getPath();
|
||||
for (String path : paths) {
|
||||
toDelete += File.separator + path;
|
||||
}
|
||||
File deleteFile = new File(toDelete);
|
||||
if (!deleteFile.exists()) {
|
||||
} else if (new File(toDelete).delete()) {
|
||||
log.info("Deleted old file: {}", toDelete);
|
||||
} else {
|
||||
log.warn("Could not delete old file: {}", toDelete);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void writePrefsFile(AppInfo app) throws Exception {
|
||||
File prefsDir = tryWrite(app, true, PREFS_DIR);
|
||||
deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version
|
||||
|
||||
// first check that there aren't other prefs files
|
||||
String pref = "general.config.filename";
|
||||
for (File file : prefsDir.listFiles()) {
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
if(line.contains(pref) && !line.contains(CFG_FILE)) {
|
||||
throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line));
|
||||
}
|
||||
}
|
||||
} catch(IOException ignore) {}
|
||||
}
|
||||
|
||||
// write out the new prefs file
|
||||
File prefsFile = new File(prefsDir, PREFS_FILE);
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile));
|
||||
String[] data = {
|
||||
String.format("pref('%s', '%s');", pref, CFG_FILE),
|
||||
"pref('general.config.obscure_value', 0);"
|
||||
};
|
||||
for (String line : data) {
|
||||
writer.write(line + "\n");
|
||||
}
|
||||
writer.close();
|
||||
prefsFile.setReadable(true, false);
|
||||
}
|
||||
|
||||
private static void writeParsedConfig(AppInfo appInfo, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{
|
||||
if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES;
|
||||
|
||||
File cfgDir = tryWrite(appInfo, false);
|
||||
deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version
|
||||
File dest = new File(cfgDir.getPath(), CFG_FILE);
|
||||
|
||||
HashMap<String, String> fieldMap = new HashMap<>();
|
||||
// Dynamic fields
|
||||
fieldMap.put("%CERT_DATA%", certData);
|
||||
fieldMap.put("%COMMON_NAME%", hostNames[0]);
|
||||
fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
|
||||
fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : "");
|
||||
fieldMap.put("%UNINSTALL%", "" + uninstall);
|
||||
|
||||
FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
|
||||
dest.setReadable(true, false);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// Firefox AutoConfig Certificate Installer for Legacy Firefox versions
|
||||
// This is part of the QZ Tray application
|
||||
//
|
||||
var serviceObserver = {
|
||||
observe: function observe(aSubject, aTopic, aData) {
|
||||
// Get NSS certdb object
|
||||
var certdb = getCertDB();
|
||||
|
||||
if (needsUninstall()) {
|
||||
deleteCertificate();
|
||||
unregisterProtocol();
|
||||
} else if (needsCert()) {
|
||||
deleteCertificate();
|
||||
installCertificate();
|
||||
registerProtocol();
|
||||
}
|
||||
|
||||
// Compares the timestamp embedded in this script against that stored in the browser's about:config
|
||||
function needsCert() {
|
||||
try {
|
||||
return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%";
|
||||
} catch(notfound) {}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Installs the embedded base64 certificate into the browser
|
||||
function installCertificate() {
|
||||
certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%");
|
||||
pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%");
|
||||
}
|
||||
|
||||
// Deletes the certificate, if it exists
|
||||
function deleteCertificate() {
|
||||
var certs = certdb.getCerts();
|
||||
var enumerator = certs.getEnumerator();
|
||||
while (enumerator.hasMoreElements()) {
|
||||
var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert);
|
||||
if (cert.containsEmailAddress("%ABOUT_EMAIL%")) {
|
||||
try {
|
||||
certdb.deleteCertificate(cert);
|
||||
} catch (ignore) {}
|
||||
}
|
||||
}
|
||||
pref("%PROPS_FILE%.installer.timestamp", "-1");
|
||||
}
|
||||
|
||||
// Register the specified protocol to open with the specified application
|
||||
function registerProtocol() {
|
||||
// Only register if platform needs it (e.g. macOS)
|
||||
var trayApp = "%APP_PATH%";
|
||||
if (!trayApp) { return; }
|
||||
try {
|
||||
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
|
||||
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
|
||||
|
||||
var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
|
||||
file.initWithPath(trayApp);
|
||||
|
||||
var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp);
|
||||
lhandler.executable = file;
|
||||
lhandler.name = "%PROPS_FILE%";
|
||||
|
||||
var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%");
|
||||
protocol.preferredApplicationHandler = lhandler;
|
||||
protocol.preferredAction = 2; // useHelperApp
|
||||
protocol.alwaysAskBeforeHandling = false;
|
||||
hservice.store(protocol);
|
||||
} catch(ignore) {}
|
||||
}
|
||||
|
||||
// De-register the specified protocol from opening with the specified application
|
||||
function unregisterProtocol() {
|
||||
// Only register if platform needs it (e.g. macOS)
|
||||
var trayApp = "%APP_PATH%";
|
||||
if (!trayApp) { return; }
|
||||
try {
|
||||
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
|
||||
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
|
||||
hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%"));
|
||||
} catch(ignore) {}
|
||||
}
|
||||
|
||||
// Get certdb object
|
||||
function getCertDB() {
|
||||
// Import certificate using NSS certdb API (http://tinyurl.com/x509certdb)
|
||||
var id = "@mozilla.org/security/x509certdb;1";
|
||||
var db1 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB);
|
||||
var db2 = db1;
|
||||
try {
|
||||
db2 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB2);
|
||||
} catch(ignore) {}
|
||||
return db2;
|
||||
}
|
||||
|
||||
// The certificate to import (automatically generated by desktop installer)
|
||||
function getCertData() {
|
||||
return "%CERT_DATA%";
|
||||
}
|
||||
|
||||
// Whether or not an uninstall should occur, flagged by the installer/uninstaller
|
||||
function needsUninstall() {
|
||||
try {
|
||||
if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch(notfound) {
|
||||
return false;
|
||||
}
|
||||
return %UNINSTALL%;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Services.obs.addObserver(serviceObserver, "profile-after-change", false);
|
||||
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
@@ -0,0 +1,91 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum AppAlias {
|
||||
// Tor Browser intentionally excluded; Tor's proxy blocks localhost connections
|
||||
FIREFOX(
|
||||
new Alias("Mozilla", "Mozilla Firefox", "org.mozilla.firefox", true),
|
||||
new Alias("Mozilla", "Firefox Developer Edition", "org.mozilla.firefoxdeveloperedition", true),
|
||||
new Alias("Mozilla", "Firefox Nightly", "org.mozilla.nightly", true),
|
||||
new Alias("Mozilla", "SeaMonkey", "org.mozilla.seamonkey", false),
|
||||
new Alias("Waterfox", "Waterfox", "net.waterfox.waterfoxcurrent", true),
|
||||
new Alias("Waterfox", "Waterfox Classic", "org.waterfoxproject.waterfox classic", false),
|
||||
new Alias("Mozilla", "Pale Moon", "org.mozilla.palemoon", false),
|
||||
// IceCat is technically enterprise ready, but not officially distributed for macOS, Windows
|
||||
new Alias("Mozilla", "IceCat", "org.gnu.icecat", false)
|
||||
);
|
||||
Alias[] aliases;
|
||||
AppAlias(Alias... aliases) {
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
public Alias[] getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
public static Alias findAlias(AppAlias appAlias, String appName, boolean stripVendor) {
|
||||
if (appName != null) {
|
||||
for (Alias alias : appAlias.aliases) {
|
||||
if (appName.toLowerCase(Locale.ENGLISH).matches(alias.getName(stripVendor).toLowerCase(Locale.ENGLISH))) {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class Alias {
|
||||
private String vendor;
|
||||
private String name;
|
||||
private String bundleId;
|
||||
private boolean enterpriseReady;
|
||||
private String posix;
|
||||
|
||||
public Alias(String vendor, String name, String bundleId, boolean enterpriseReady) {
|
||||
this.name = name;
|
||||
this.vendor = vendor;
|
||||
this.bundleId = bundleId;
|
||||
this.enterpriseReady = enterpriseReady;
|
||||
this.posix = getName(true).replaceAll(" ", "").toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
public String getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove vendor prefix if exists
|
||||
*/
|
||||
public String getName(boolean stripVendor) {
|
||||
if(stripVendor && "Mozilla".equals(vendor) && name.startsWith(vendor)) {
|
||||
return name.substring(vendor.length()).trim();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getBundleId() {
|
||||
return bundleId;
|
||||
}
|
||||
|
||||
public String getPosix() {
|
||||
return posix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the app is known to recognizes enterprise policies, such as GPO
|
||||
*/
|
||||
public boolean isEnterpriseReady() {
|
||||
return enterpriseReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
|
||||
|
||||
/**
|
||||
* Container class for installed app information
|
||||
*/
|
||||
public class AppInfo {
|
||||
private AppAlias.Alias alias;
|
||||
private Path path;
|
||||
private Path exePath;
|
||||
private Version version;
|
||||
|
||||
public AppInfo(Alias alias, Path exePath, String version) {
|
||||
this.alias = alias;
|
||||
this.path = exePath.getParent();
|
||||
this.exePath = exePath;
|
||||
this.version = parseVersion(version);
|
||||
}
|
||||
|
||||
public AppInfo(Alias alias, Path path, Path exePath, String version) {
|
||||
this.alias = alias;
|
||||
this.path = path;
|
||||
this.exePath = exePath;
|
||||
this.version = parseVersion(version);
|
||||
}
|
||||
|
||||
public AppInfo(Alias alias, Path exePath) {
|
||||
this.alias = alias;
|
||||
this.path = exePath.getParent();
|
||||
this.exePath = exePath;
|
||||
}
|
||||
|
||||
public Alias getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public String getName(boolean stripVendor) {
|
||||
return alias.getName(stripVendor);
|
||||
}
|
||||
|
||||
public Path getExePath() {
|
||||
return exePath;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = parseVersion(version);
|
||||
}
|
||||
|
||||
public void setVersion(Version version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
private static Version parseVersion(String version) {
|
||||
try {
|
||||
// Ensure < 3 octets (e.g. "56.0") doesn't failing
|
||||
while(version.split("\\.").length < 3) {
|
||||
version = version + ".0";
|
||||
}
|
||||
return Version.valueOf(version);
|
||||
} catch(Exception ignore1) {
|
||||
// Catch poor formatting (e.g. "97.0a1"), try to use major version only
|
||||
if(version.split("\\.").length > 0) {
|
||||
try {
|
||||
String[] tryFix = version.split("\\.");
|
||||
return Version.valueOf(tryFix[0] + ".0.0-unknown");
|
||||
} catch(Exception ignore2) {}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(o instanceof AppInfo && o != null && path != null) {
|
||||
return path.equals(((AppInfo)o).getPath());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return alias + " " + path;
|
||||
}
|
||||
}
|
||||
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
@@ -0,0 +1,87 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
public abstract class AppLocator {
|
||||
protected static final Logger log = LogManager.getLogger(AppLocator.class);
|
||||
|
||||
private static AppLocator INSTANCE = getPlatformSpecificAppLocator();
|
||||
|
||||
public abstract ArrayList<AppInfo> locate(AppAlias appAlias);
|
||||
public abstract ArrayList<Path> getPidPaths(ArrayList<String> pids);
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public ArrayList<String> getPids(String ... processNames) {
|
||||
return getPids(new ArrayList<>(Arrays.asList(processNames)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Linux, Mac
|
||||
*/
|
||||
public ArrayList<String> getPids(ArrayList<String> processNames) {
|
||||
String[] response;
|
||||
ArrayList<String> pidList = new ArrayList<>();
|
||||
|
||||
if(processNames.contains("firefox") && !(SystemUtilities.isWindows() || SystemUtilities.isMac())) {
|
||||
processNames.add("MainThread"); // Workaround Firefox 79 https://github.com/qzind/tray/issues/701
|
||||
processNames.add("GeckoMain"); // Workaround Firefox 94 https://bugzilla.mozilla.org/show_bug.cgi?id=1742606
|
||||
}
|
||||
|
||||
if (processNames.size() == 0) return pidList;
|
||||
|
||||
// Quoting handled by the command processor (e.g. pgrep -x "myapp|my app" is perfectly valid)
|
||||
String data = ShellUtilities.executeRaw("pgrep", "-x", String.join("|", processNames));
|
||||
|
||||
//Splitting an empty string results in a 1 element array, this is not what we want
|
||||
if (!data.isEmpty()) {
|
||||
response = data.split("\\s*\\r?\\n");
|
||||
Collections.addAll(pidList, response);
|
||||
}
|
||||
|
||||
return pidList;
|
||||
}
|
||||
|
||||
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList) {
|
||||
return getRunningPaths(appList, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the path to the running executables matching on <code>AppInfo.getExePath</code>
|
||||
* This is resource intensive; if a non-null <code>cache</code> is provided, it will return that instead
|
||||
*/
|
||||
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList, ArrayList<Path> cache) {
|
||||
if(cache == null) {
|
||||
ArrayList<String> appNames = new ArrayList<>();
|
||||
for(AppInfo app : appList) {
|
||||
String exeName = app.getExePath().getFileName().toString();
|
||||
if (!appNames.contains(exeName)) appNames.add(exeName);
|
||||
}
|
||||
cache = INSTANCE.getPidPaths(INSTANCE.getPids(appNames));
|
||||
}
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
public static AppLocator getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static AppLocator getPlatformSpecificAppLocator() {
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
return new WindowsAppLocator();
|
||||
case MAC:
|
||||
return new MacAppLocator();
|
||||
default:
|
||||
return new LinuxAppLocator();
|
||||
}
|
||||
}
|
||||
}
|
||||
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
@@ -0,0 +1,159 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.utils.UnixUtilities;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class LinuxAppLocator extends AppLocator {
|
||||
private static final Logger log = LogManager.getLogger(LinuxAppLocator.class);
|
||||
|
||||
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||
|
||||
// Workaround for calling "firefox --version" as sudo
|
||||
String[] env = appendPaths("HOME=/tmp");
|
||||
|
||||
// Search for matching executable in all path values
|
||||
aliasLoop:
|
||||
for(AppAlias.Alias alias : appAlias.aliases) {
|
||||
// Add non-standard app search locations (e.g. Fedora)
|
||||
for (String dirname : appendPaths(alias.getPosix(), "/usr/lib/$/bin", "/usr/lib64/$/bin", "/usr/lib/$", "/usr/lib64/$")) {
|
||||
Path path = Paths.get(dirname, alias.getPosix());
|
||||
if (Files.isRegularFile(path) && Files.isExecutable(path)) {
|
||||
log.info("Found {} {}: {}, investigating...", alias.getVendor(), alias.getName(true), path);
|
||||
try {
|
||||
File file = path.toFile().getCanonicalFile(); // fix symlinks
|
||||
if(file.getPath().endsWith("/snap")) {
|
||||
// Ubuntu 22.04+ ships Firefox as a snap
|
||||
// Snaps are read-only and are symlinks back to /usr/bin/snap
|
||||
// Reset the executable back to /snap/bin/firefox to get proper version information
|
||||
file = path.toFile();
|
||||
}
|
||||
if(file.getPath().endsWith(".sh")) {
|
||||
// Legacy Ubuntu likes to use .../firefox/firefox.sh, return .../firefox/firefox instead
|
||||
log.info("Found an '.sh' file: {}, removing file extension: {}", file, file = new File(FilenameUtils.removeExtension(file.getPath())));
|
||||
}
|
||||
String contentType = Files.probeContentType(file.toPath());
|
||||
if(contentType == null) {
|
||||
// Fallback to commandline per https://bugs.openjdk.org/browse/JDK-8188228
|
||||
contentType = ShellUtilities.executeRaw("file", "--mime-type", "--brief", file.getPath()).trim();
|
||||
}
|
||||
if(contentType != null && contentType.endsWith("/x-shellscript")) {
|
||||
if(UnixUtilities.isFedora()) {
|
||||
// Firefox's script is full of variables and not parsable, fallback to /usr/lib64/$, etc
|
||||
log.info("Found shell script at {}, but we're on Fedora, so we'll look in some known locations instead.", file.getPath());
|
||||
continue;
|
||||
}
|
||||
// Debian and Arch like to place a stub script directly in /usr/bin/
|
||||
// TODO: Split into a function; possibly recurse on search paths
|
||||
log.info("{} bin was expected but script found... Reading...", appAlias.name());
|
||||
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
if(line.startsWith("exec") && line.contains(alias.getPosix())) {
|
||||
String[] parts = line.split(" ");
|
||||
// Get the app name after "exec"
|
||||
if (parts.length > 1) {
|
||||
log.info("Found a familiar line '{}', using '{}'", line, parts[1]);
|
||||
Path p = Paths.get(parts[1]);
|
||||
String exec = parts[1];
|
||||
// Handle edge-case for esr release
|
||||
if(!p.isAbsolute()) {
|
||||
// Script doesn't contain the full path, go deeper
|
||||
exec = Paths.get(dirname, exec).toFile().getCanonicalPath();
|
||||
log.info("Calculated full bin path {}", exec);
|
||||
}
|
||||
// Make sure it actually exists
|
||||
if(!(file = new File(exec)).exists()) {
|
||||
log.warn("Sorry, we couldn't detect the real path of {}. Skipping...", appAlias.name());
|
||||
continue aliasLoop;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.close();
|
||||
} else {
|
||||
log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file);
|
||||
}
|
||||
AppInfo appInfo = new AppInfo(alias, file.toPath());
|
||||
if(file.getPath().startsWith("/snap/")) {
|
||||
// Ubuntu 22.04+ uses snaps, fallback to a sane "path" value
|
||||
String snapPath = file.getPath(); // e.g. /snap/bin/firefox
|
||||
snapPath = snapPath.replaceFirst("/bin/", "/");
|
||||
snapPath += "/current";
|
||||
appInfo.setPath(Paths.get(snapPath));
|
||||
}
|
||||
|
||||
appList.add(appInfo);
|
||||
|
||||
// Call "--version" on executable to obtain version information
|
||||
Process p = Runtime.getRuntime().exec(new String[] {file.getPath(), "--version" }, env);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String version = reader.readLine();
|
||||
reader.close();
|
||||
if (version != null) {
|
||||
log.info("We obtained version info: {}, but we'll need to parse it", version);
|
||||
if(version.contains(" ")) {
|
||||
String[] split = version.split(" ");
|
||||
String parsed = split[split.length - 1];
|
||||
String stripped = parsed.replaceAll("[^\\d.]", "");
|
||||
appInfo.setVersion(stripped);
|
||||
if(!parsed.equals(stripped)) {
|
||||
// Add the meta data back (e.g. "esr")
|
||||
appInfo.getVersion().setBuildMetadata(parsed.replaceAll("[\\d.]", ""));
|
||||
}
|
||||
} else {
|
||||
appInfo.setVersion(version.trim());
|
||||
}
|
||||
}
|
||||
break;
|
||||
} catch(Exception e) {
|
||||
log.warn("Something went wrong getting app info for {} {}", alias.getVendor(), alias.getName(true), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||
ArrayList<Path> pathList = new ArrayList<>();
|
||||
|
||||
for(String pid : pids) {
|
||||
try {
|
||||
pathList.add(Paths.get("/proc/", pid, !SystemUtilities.isSolaris() ? "/exe" : "/path/a.out").toRealPath());
|
||||
} catch(IOException e) {
|
||||
log.warn("Process {} vanished", pid);
|
||||
}
|
||||
}
|
||||
|
||||
return pathList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PATH value with provided paths appended, replacing "$" with POSIX app name
|
||||
* Useful for strange Firefox install locations (e.g. Fedora)
|
||||
*
|
||||
* Usage: appendPaths("firefox", "/usr/lib64");
|
||||
*
|
||||
*/
|
||||
private static String[] appendPaths(String posix, String ... prefixes) {
|
||||
String newPath = System.getenv("PATH");
|
||||
for (String prefix : prefixes) {
|
||||
newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix);
|
||||
}
|
||||
return newPath.split(File.pathSeparator);
|
||||
}
|
||||
}
|
||||
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
@@ -0,0 +1,168 @@
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.ptr.IntByReference;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import qz.utils.ShellUtilities;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class MacAppLocator extends AppLocator{
|
||||
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
|
||||
|
||||
private static String[] BLACKLISTED_PATHS = new String[]{"/Volumes/", "/.Trash/", "/Applications (Parallels)/" };
|
||||
|
||||
/**
|
||||
* Helper class for finding key/value siblings from the DDM
|
||||
*/
|
||||
private enum SiblingNode {
|
||||
NAME("_name"),
|
||||
PATH("path"),
|
||||
VERSION("version");
|
||||
|
||||
private String key;
|
||||
private boolean wants;
|
||||
|
||||
SiblingNode(String key) {
|
||||
this.key = key;
|
||||
this.wants = false;
|
||||
}
|
||||
|
||||
private boolean isKey(Node node) {
|
||||
if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||
Document doc;
|
||||
|
||||
try {
|
||||
// system_profile benchmarks about 30% better than lsregister
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp);
|
||||
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||
// don't let the <!DOCTYPE> fail parsing per https://github.com/qzind/tray/issues/809
|
||||
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||
doc = dbf.newDocumentBuilder().parse(p.getInputStream());
|
||||
} catch(IOException | ParserConfigurationException | SAXException e) {
|
||||
log.warn("Could not retrieve app listing for {}", appAlias.name(), e);
|
||||
return appList;
|
||||
}
|
||||
doc.normalizeDocument();
|
||||
|
||||
NodeList nodeList = doc.getElementsByTagName("dict");
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
NodeList dict = nodeList.item(i).getChildNodes();
|
||||
HashMap<SiblingNode, String> foundApp = new HashMap<>();
|
||||
for (int j = 0; j < dict.getLength(); j++) {
|
||||
Node node = dict.item(j);
|
||||
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||
for (SiblingNode sibling : SiblingNode.values()) {
|
||||
if (sibling.wants) {
|
||||
foundApp.put(sibling, node.getTextContent());
|
||||
sibling.wants = false;
|
||||
break;
|
||||
} else if(sibling.isKey(node)) {
|
||||
sibling.wants = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AppAlias.Alias alias;
|
||||
if((alias = AppAlias.findAlias(appAlias, foundApp.get(SiblingNode.NAME), true)) != null) {
|
||||
appList.add(new AppInfo(alias, Paths.get(foundApp.get(SiblingNode.PATH)),
|
||||
getExePath(foundApp.get(SiblingNode.PATH)), foundApp.get(SiblingNode.VERSION)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove blacklisted paths
|
||||
Iterator<AppInfo> appInfoIterator = appList.iterator();
|
||||
while(appInfoIterator.hasNext()) {
|
||||
AppInfo appInfo = appInfoIterator.next();
|
||||
for(String listEntry : BLACKLISTED_PATHS) {
|
||||
if (appInfo.getPath() != null && appInfo.getPath().toString().contains(listEntry)) {
|
||||
appInfoIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||
ArrayList<Path> processPaths = new ArrayList();
|
||||
for (String pid : pids) {
|
||||
Pointer buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE);
|
||||
SystemB.INSTANCE.proc_pidpath(Integer.parseInt(pid), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE);
|
||||
processPaths.add(Paths.get(buf.getString(0).trim()));
|
||||
}
|
||||
return processPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate executable path by parsing Contents/Info.plist
|
||||
*/
|
||||
private static Path getExePath(String appPath) {
|
||||
Path path = Paths.get(appPath).toAbsolutePath().normalize();
|
||||
Path plist = path.resolve("Contents/Info.plist");
|
||||
Document doc;
|
||||
try {
|
||||
if(!plist.toFile().exists()) {
|
||||
log.warn("Could not locate plist file for {}: {}", appPath, plist);
|
||||
return null;
|
||||
}
|
||||
// Convert potentially binary plist files to XML
|
||||
Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp);
|
||||
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
|
||||
} catch(IOException | ParserConfigurationException | SAXException e) {
|
||||
log.warn("Could not parse plist file for {}: {}", appPath, appPath, e);
|
||||
return null;
|
||||
}
|
||||
doc.normalizeDocument();
|
||||
|
||||
boolean upNext = false;
|
||||
NodeList nodeList = doc.getElementsByTagName("dict");
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
NodeList dict = nodeList.item(i).getChildNodes();
|
||||
for(int j = 0; j < dict.getLength(); j++) {
|
||||
Node node = dict.item(j);
|
||||
if ("key".equals(node.getNodeName()) && node.getTextContent().equals("CFBundleExecutable")) {
|
||||
upNext = true;
|
||||
} else if (upNext && "string".equals(node.getNodeName())) {
|
||||
return path.resolve("Contents/MacOS/" + node.getTextContent());
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private interface SystemB extends Library {
|
||||
SystemB INSTANCE = Native.load("System", SystemB.class);
|
||||
int PROC_ALL_PIDS = 1;
|
||||
int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
|
||||
int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen);
|
||||
int proc_listpids(int type, int typeinfo, int[] buffer, int buffersize);
|
||||
int proc_pidpath(int pid, Pointer buffer, int buffersize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.certificate.firefox.locator;
|
||||
|
||||
import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
import com.sun.jna.platform.win32.Psapi;
|
||||
import com.sun.jna.platform.win32.Tlhelp32;
|
||||
import com.sun.jna.platform.win32.WinNT;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
|
||||
import qz.utils.WindowsUtilities;
|
||||
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
|
||||
|
||||
public class WindowsAppLocator extends AppLocator{
|
||||
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
|
||||
|
||||
private static String REG_TEMPLATE = "Software\\%s%s\\%s%s";
|
||||
|
||||
@Override
|
||||
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||
for (Alias alias : appAlias.aliases) {
|
||||
if (alias.getVendor() != null) {
|
||||
String[] suffixes = new String[]{ "", " ESR"};
|
||||
String[] prefixes = new String[]{ "", "WOW6432Node\\"};
|
||||
for (String suffix : suffixes) {
|
||||
for (String prefix : prefixes) {
|
||||
String key = String.format(REG_TEMPLATE, prefix, alias.getVendor(), alias.getName(), suffix);
|
||||
AppInfo appInfo = getAppInfo(alias, key, suffix);
|
||||
if (appInfo != null && !appList.contains(appInfo)) {
|
||||
appList.add(appInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return appList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ArrayList<String> getPids(ArrayList<String> processNames) {
|
||||
ArrayList<String> pidList = new ArrayList<>();
|
||||
|
||||
if (processNames.isEmpty()) return pidList;
|
||||
|
||||
Tlhelp32.PROCESSENTRY32 pe32 = new Tlhelp32.PROCESSENTRY32();
|
||||
pe32.dwSize = new WinNT.DWORD(pe32.size());
|
||||
|
||||
// Fetch a snapshot of all processes
|
||||
WinNT.HANDLE hSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinNT.DWORD(0));
|
||||
if (hSnapshot.equals(WinNT.INVALID_HANDLE_VALUE)) {
|
||||
log.warn("Process snapshot has invalid handle");
|
||||
return pidList;
|
||||
}
|
||||
|
||||
if (Kernel32.INSTANCE.Process32First(hSnapshot, pe32)) {
|
||||
do {
|
||||
String processName = Native.toString(pe32.szExeFile);
|
||||
if(processNames.contains(processName.toLowerCase(Locale.ENGLISH))) {
|
||||
pidList.add(pe32.th32ProcessID.toString());
|
||||
}
|
||||
} while (Kernel32.INSTANCE.Process32Next(hSnapshot, pe32));
|
||||
}
|
||||
|
||||
Kernel32.INSTANCE.CloseHandle(hSnapshot);
|
||||
return pidList;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||
ArrayList<Path> pathList = new ArrayList<>();
|
||||
|
||||
for(String pid : pids) {
|
||||
WinNT.HANDLE hProcess = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, Integer.parseInt(pid));
|
||||
if (hProcess == null) {
|
||||
log.warn("Handle for PID {} is missing, skipping.", pid);
|
||||
continue;
|
||||
}
|
||||
|
||||
int bufferSize = WinNT.MAX_PATH;
|
||||
Pointer buffer = new Memory(bufferSize * Native.WCHAR_SIZE);
|
||||
|
||||
if (Psapi.INSTANCE.GetModuleFileNameEx(hProcess, null, buffer, bufferSize) == 0) {
|
||||
log.warn("Full path to PID {} is empty, skipping.", pid);
|
||||
Kernel32.INSTANCE.CloseHandle(hProcess);
|
||||
continue;
|
||||
}
|
||||
|
||||
Kernel32.INSTANCE.CloseHandle(hProcess);
|
||||
pathList.add(Paths.get(Native.WCHAR_SIZE == 1 ?
|
||||
buffer.getString(0) :
|
||||
buffer.getWideString(0)));
|
||||
}
|
||||
return pathList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a proprietary Firefox-only technique for getting "PathToExe" registry value
|
||||
*/
|
||||
private static AppInfo getAppInfo(Alias alias, String key, String suffix) {
|
||||
String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion");
|
||||
if (version != null) {
|
||||
version = version.split(" ")[0]; // chop off (x86 ...)
|
||||
if (!suffix.isEmpty()) {
|
||||
if (key.endsWith(suffix)) {
|
||||
key = key.substring(0, key.length() - suffix.length());
|
||||
}
|
||||
version = version + suffix;
|
||||
}
|
||||
String exePath = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe");
|
||||
|
||||
if (exePath != null) {
|
||||
// SemVer: Replace spaces in suffixes with dashes
|
||||
version = version.replaceAll(" ", "-");
|
||||
return new AppInfo(alias, Paths.get(exePath), version);
|
||||
} else {
|
||||
log.warn("Couldn't locate \"PathToExe\" for \"{}\" in \"{}\", skipping", alias.getName(), key);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
@@ -0,0 +1,161 @@
|
||||
package qz.installer.provision;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.Phase;
|
||||
import qz.build.provision.params.types.Script;
|
||||
import qz.build.provision.params.types.Software;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.provision.invoker.*;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static qz.common.Constants.*;
|
||||
import static qz.utils.FileUtilities.*;
|
||||
|
||||
public class ProvisionInstaller {
|
||||
protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class);
|
||||
private ArrayList<Step> steps;
|
||||
|
||||
static {
|
||||
// Populate variables for scripting environment
|
||||
ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE,
|
||||
"APP_VERSION", VERSION,
|
||||
"APP_ABBREV", PROPS_FILE,
|
||||
"APP_VENDOR", ABOUT_COMPANY,
|
||||
"APP_VENDOR_ABBREV", DATA_DIR,
|
||||
"APP_ARCH", SystemUtilities.getArch(),
|
||||
"APP_OS", SystemUtilities.getOs(),
|
||||
"APP_DIR", SystemUtilities.getAppPath(),
|
||||
"APP_USER_DIR", USER_DIR,
|
||||
"APP_SHARED_DIR", SHARED_DIR);
|
||||
}
|
||||
|
||||
public ProvisionInstaller(Path relativePath) throws IOException, JSONException {
|
||||
this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile());
|
||||
}
|
||||
|
||||
public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException {
|
||||
if(!jsonFile.exists()) {
|
||||
log.info("Provision file not found '{}', skipping", jsonFile);
|
||||
this.steps = new ArrayList<>();
|
||||
return;
|
||||
}
|
||||
this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Package private for internal testing only
|
||||
* Assumes files located in ./resources/ subdirectory
|
||||
*/
|
||||
ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException {
|
||||
this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Package private for internal testing only
|
||||
* Assumes files located in ./resources/ subdirectory
|
||||
*/
|
||||
ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException {
|
||||
this.steps = parse(jsonData, relativeClass);
|
||||
}
|
||||
|
||||
public void invoke(Phase phase) {
|
||||
for(Step step : this.steps) {
|
||||
if(phase == null || step.getPhase() == phase) {
|
||||
try {
|
||||
invokeStep(step);
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("[PROVISION] Provisioning step failed '{}'", step, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void invoke() {
|
||||
invoke(null);
|
||||
}
|
||||
|
||||
private static ArrayList<Step> parse(String jsonData, Object relativeObject) throws JSONException {
|
||||
return parse(new JSONArray(jsonData), relativeObject);
|
||||
}
|
||||
|
||||
private boolean invokeStep(Step step) throws Exception {
|
||||
if(Os.matchesHost(step.getOs())) {
|
||||
log.info("[PROVISION] Invoking step '{}'", step.toString());
|
||||
} else {
|
||||
log.info("[PROVISION] Skipping step for different OS '{}'", step.toString());
|
||||
return false;
|
||||
}
|
||||
|
||||
Invokable invoker;
|
||||
switch(step.getType()) {
|
||||
case CA:
|
||||
invoker = new CaInvoker(step, PropertyInvoker.getProperties(step));
|
||||
break;
|
||||
case CERT:
|
||||
invoker = new CertInvoker(step);
|
||||
break;
|
||||
case CONF:
|
||||
invoker = new ConfInvoker(step);
|
||||
break;
|
||||
case SCRIPT:
|
||||
invoker = new ScriptInvoker(step);
|
||||
break;
|
||||
case SOFTWARE:
|
||||
invoker = new SoftwareInvoker(step);
|
||||
break;
|
||||
case REMOVER:
|
||||
invoker = new RemoverInvoker(step);
|
||||
break;
|
||||
case RESOURCE:
|
||||
invoker = new ResourceInvoker(step);
|
||||
break;
|
||||
case PREFERENCE:
|
||||
invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step));
|
||||
break;
|
||||
case PROPERTY:
|
||||
invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step));
|
||||
break;
|
||||
default:
|
||||
throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported.");
|
||||
}
|
||||
return invoker.invoke();
|
||||
}
|
||||
|
||||
public ArrayList<Step> getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
|
||||
ArrayList<Step> steps = new ArrayList<>();
|
||||
for(int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonStep = jsonArray.getJSONObject(i);
|
||||
try {
|
||||
steps.add(Step.parse(jsonStep, relativeObject));
|
||||
} catch(Exception e) {
|
||||
log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e);
|
||||
}
|
||||
}
|
||||
return steps;
|
||||
}
|
||||
|
||||
public static boolean shouldBeExecutable(Path path) {
|
||||
return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN;
|
||||
}
|
||||
}
|
||||
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
@@ -0,0 +1,49 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.PropertyHelper;
|
||||
import qz.utils.ArgValue;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path
|
||||
*/
|
||||
public class CaInvoker extends InvokableResource {
|
||||
Step step;
|
||||
PropertyHelper properties;
|
||||
|
||||
public CaInvoker(Step step, PropertyHelper properties) {
|
||||
this.step = step;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws IOException {
|
||||
// First, write our cert file
|
||||
File caCert = dataToFile(step);
|
||||
if(caCert == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Next, handle our property step
|
||||
Step propsStep = step.clone();
|
||||
|
||||
// If the property already exists, snag it
|
||||
String key = ArgValue.AUTHCERT_OVERRIDE.getMatch();
|
||||
String value = caCert.getPath();
|
||||
if (properties.containsKey(key)) {
|
||||
value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value;
|
||||
}
|
||||
|
||||
propsStep.setData(String.format("%s=%s", key, value));
|
||||
|
||||
if (new PropertyInvoker(propsStep, properties).invoke()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
@@ -0,0 +1,26 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static qz.utils.ArgParser.ExitStatus.*;
|
||||
|
||||
public class CertInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public CertInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
File cert = dataToFile(step);
|
||||
if(cert == null) {
|
||||
return false;
|
||||
}
|
||||
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
|
||||
}
|
||||
}
|
||||
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
@@ -0,0 +1,46 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.PropertyHelper;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
|
||||
public class ConfInvoker extends PropertyInvoker {
|
||||
public ConfInvoker(Step step) {
|
||||
super(step, new PropertyHelper(calculateConfPath(step)));
|
||||
}
|
||||
|
||||
public static String calculateConfPath(Step step) {
|
||||
String relativePath = step.getArgs().get(0);
|
||||
if(SystemUtilities.isMac()) {
|
||||
return SystemUtilities.getJarParentPath().
|
||||
resolve("../PlugIns/Java.runtime/Contents/Home/conf").
|
||||
resolve(relativePath).
|
||||
normalize()
|
||||
.toString();
|
||||
} else {
|
||||
return SystemUtilities.getJarParentPath()
|
||||
.resolve("runtime/conf")
|
||||
.resolve(relativePath)
|
||||
.normalize()
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() {
|
||||
Step step = getStep();
|
||||
// Java uses the same "|" delimiter as we do, only parse one property at a time
|
||||
AbstractMap.SimpleEntry<String, String> pair = parsePropertyPair(step, step.getData());
|
||||
if (!pair.getValue().isEmpty()) {
|
||||
properties.setProperty(pair);
|
||||
if (properties.save()) {
|
||||
log.info("Successfully provisioned '1' '{}'", step.getType());
|
||||
return true;
|
||||
}
|
||||
log.error("An error occurred saving properties '{}' to file", step.getData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
@@ -0,0 +1,10 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
public interface Invokable {
|
||||
Logger log = LogManager.getLogger(Invokable.class);
|
||||
|
||||
boolean invoke() throws Exception;
|
||||
}
|
||||
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
@@ -0,0 +1,63 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Type;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public abstract class InvokableResource implements Invokable {
|
||||
static final Logger log = LogManager.getLogger(InvokableResource.class);
|
||||
|
||||
public static File dataToFile(Step step) throws IOException {
|
||||
Path resourcePath = Paths.get(step.getData());
|
||||
if(resourcePath.isAbsolute() || step.usingPath()) {
|
||||
return pathResourceToFile(step);
|
||||
}
|
||||
if(step.usingClass()) {
|
||||
return classResourceToFile(step);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the resource directly from file
|
||||
*/
|
||||
private static File pathResourceToFile(Step step) {
|
||||
String resourcePath = step.getData();
|
||||
Path dataPath = Paths.get(resourcePath);
|
||||
return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies resource from JAR to a temp file for use in installation
|
||||
*/
|
||||
private static File classResourceToFile(Step step) throws IOException {
|
||||
// Resource may be inside the jar
|
||||
InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData());
|
||||
if(in == null) {
|
||||
log.warn("Resource '{}' is missing, skipping step", step.getData());
|
||||
return null;
|
||||
}
|
||||
String suffix = "_" + Paths.get(step.getData()).getFileName().toString();
|
||||
File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix);
|
||||
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
IOUtils.closeQuietly(in);
|
||||
|
||||
// Set scripts executable
|
||||
if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) {
|
||||
destination.setExecutable(true, false);
|
||||
}
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
@@ -0,0 +1,99 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.common.Constants;
|
||||
import qz.common.PropertyHelper;
|
||||
import qz.utils.FileUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PropertyInvoker implements Invokable {
|
||||
private Step step;
|
||||
PropertyHelper properties;
|
||||
|
||||
public PropertyInvoker(Step step, PropertyHelper properties) {
|
||||
this.step = step;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
public boolean invoke() {
|
||||
HashMap<String, String> pairs = parsePropertyPairs(step);
|
||||
if (!pairs.isEmpty()) {
|
||||
for(Map.Entry<String, String> pair : pairs.entrySet()) {
|
||||
properties.setProperty(pair);
|
||||
}
|
||||
if (properties.save()) {
|
||||
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
|
||||
return true;
|
||||
}
|
||||
log.error("An error occurred saving properties '{}' to file", step.getData());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static PropertyHelper getProperties(Step step) {
|
||||
File propertiesFile;
|
||||
if(step.getRelativePath() != null) {
|
||||
// Assume qz-tray.properties is one directory up from provision folder
|
||||
// required to prevent installing to payload
|
||||
propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile();
|
||||
} else {
|
||||
// If relative path isn't set, fallback to the jar's parent path
|
||||
propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile();
|
||||
}
|
||||
log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile);
|
||||
return new PropertyHelper(propertiesFile);
|
||||
}
|
||||
|
||||
public static PropertyHelper getPreferences(Step step) {
|
||||
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
|
||||
}
|
||||
|
||||
public static HashMap<String, String> parsePropertyPairs(Step step) {
|
||||
HashMap<String, String> pairs = new HashMap<>();
|
||||
if(step.getData() != null && !step.getData().trim().isEmpty()) {
|
||||
String[] props = step.getData().split("\\|");
|
||||
for(String prop : props) {
|
||||
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
|
||||
if (pair != null) {
|
||||
if(pairs.get(pair.getKey()) != null) {
|
||||
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
|
||||
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
|
||||
}
|
||||
pairs.put(pair.getKey(), pair.getValue());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.error("Skipping Step '{}', Data is null or empty", step.getType());
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
|
||||
|
||||
public static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
|
||||
if(prop.contains("=")) {
|
||||
String[] pair = prop.split("=", 2);
|
||||
if (!pair[0].trim().isEmpty()) {
|
||||
if (!pair[1].trim().isEmpty()) {
|
||||
return new AbstractMap.SimpleEntry<>(pair[0], pair[1]);
|
||||
} else {
|
||||
log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop);
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop);
|
||||
}
|
||||
} else {
|
||||
log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Step getStep() {
|
||||
return step;
|
||||
}
|
||||
}
|
||||
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
@@ -0,0 +1,100 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.types.Remover;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class RemoverInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
private String aboutTitle; // e.g. "QZ Tray"
|
||||
private String propsFile; // e.g. "qz-tray"
|
||||
private String dataDir; // e.g. "qz"
|
||||
|
||||
|
||||
public RemoverInvoker(Step step) {
|
||||
this.step = step;
|
||||
Remover remover = Remover.parse(step.getData());
|
||||
if(remover == Remover.CUSTOM) {
|
||||
// Fields are comma delimited in the data field
|
||||
parseCustomFromData(step.getData());
|
||||
} else {
|
||||
aboutTitle = remover.getAboutTitle();
|
||||
propsFile = remover.getPropsFile();
|
||||
dataDir = remover.getDataDir();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
ArrayList<String> command = getRemoveCommand();
|
||||
if(command.size() == 0) {
|
||||
log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle);
|
||||
return true;
|
||||
}
|
||||
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
|
||||
if(!success) {
|
||||
log.error("An error occurred invoking [{}]", step.getData());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public void parseCustomFromData(String data) {
|
||||
String[] parts = data.split(",");
|
||||
aboutTitle = parts[0].trim();
|
||||
propsFile = parts[1].trim();
|
||||
dataDir = parts[2].trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installer command (including the installer itself and if needed, arguments) to
|
||||
* invoke the installer file
|
||||
*/
|
||||
public ArrayList<String> getRemoveCommand() {
|
||||
ArrayList<String> removeCmd = new ArrayList<>();
|
||||
Os os = SystemUtilities.getOs();
|
||||
switch(os) {
|
||||
case WINDOWS:
|
||||
Path win = Paths.get(System.getenv("PROGRAMFILES"))
|
||||
.resolve(aboutTitle)
|
||||
.resolve("uninstall.exe");
|
||||
|
||||
if(win.toFile().exists()) {
|
||||
removeCmd.add(win.toString());
|
||||
removeCmd.add("/S");
|
||||
break;
|
||||
}
|
||||
case MAC:
|
||||
Path legacy = Paths.get("/Applications")
|
||||
.resolve(aboutTitle + ".app")
|
||||
.resolve("Contents")
|
||||
.resolve("uninstall");
|
||||
|
||||
Path mac = Paths.get("/Applications")
|
||||
.resolve(aboutTitle + ".app")
|
||||
.resolve("Contents")
|
||||
.resolve("Resources")
|
||||
.resolve("uninstall");
|
||||
|
||||
if(legacy.toFile().exists()) {
|
||||
removeCmd.add(legacy.toString());
|
||||
} else if(mac.toFile().exists()) {
|
||||
removeCmd.add(mac.toString());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Path linux = Paths.get("/opt")
|
||||
.resolve(propsFile)
|
||||
.resolve("uninstall");
|
||||
if(linux.toFile().exists()) {
|
||||
removeCmd.add(linux.toString());
|
||||
}
|
||||
}
|
||||
return removeCmd;
|
||||
}
|
||||
}
|
||||
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
@@ -0,0 +1,19 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
|
||||
/**
|
||||
* Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks
|
||||
*/
|
||||
public class ResourceInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public ResourceInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
return dataToFile(step) != null;
|
||||
}
|
||||
}
|
||||
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
@@ -0,0 +1,77 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.types.Script;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ScriptInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public ScriptInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
File script = dataToFile(step);
|
||||
if(script == null) {
|
||||
return false;
|
||||
}
|
||||
Script engine = Script.parse(step.getData());
|
||||
ArrayList<String> command = getInterpreter(engine);
|
||||
if(command.isEmpty() && SystemUtilities.isWindows()) {
|
||||
log.warn("No interpreter found for {}, skipping", step.getData());
|
||||
return false;
|
||||
}
|
||||
command.add(script.toString());
|
||||
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
|
||||
if(!success) {
|
||||
log.error("An error occurred invoking [{}]", step.getData());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns the interpreter command (and if needed, arguments) to invoke the script file
|
||||
*
|
||||
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
|
||||
* which will allow the OS to select the correct interpreter for the given file
|
||||
*
|
||||
* No special attention is given to "shebang", behavior may differ between OSs
|
||||
*/
|
||||
private static ArrayList<String> getInterpreter(Script engine) {
|
||||
ArrayList<String> interpreter = new ArrayList<>();
|
||||
Os osType = SystemUtilities.getOs();
|
||||
switch(engine) {
|
||||
case PS1:
|
||||
if(osType == Os.WINDOWS) {
|
||||
interpreter.add("powershell.exe");
|
||||
} else if(osType == Os.MAC) {
|
||||
interpreter.add("/usr/local/bin/pwsh");
|
||||
} else {
|
||||
interpreter.add("pwsh");
|
||||
}
|
||||
interpreter.add("-File");
|
||||
break;
|
||||
case PY:
|
||||
interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3");
|
||||
break;
|
||||
case BAT:
|
||||
interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole");
|
||||
break;
|
||||
case RB:
|
||||
interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby");
|
||||
break;
|
||||
case SH:
|
||||
default:
|
||||
// Allow the environment to parse it from the shebang at invocation time
|
||||
}
|
||||
return interpreter;
|
||||
}
|
||||
}
|
||||
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
@@ -0,0 +1,87 @@
|
||||
package qz.installer.provision.invoker;
|
||||
|
||||
import qz.build.provision.Step;
|
||||
import qz.build.provision.params.Os;
|
||||
import qz.build.provision.params.types.Software;
|
||||
import qz.utils.ShellUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SoftwareInvoker extends InvokableResource {
|
||||
private Step step;
|
||||
|
||||
public SoftwareInvoker(Step step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean invoke() throws Exception {
|
||||
File payload = dataToFile(step);
|
||||
if(payload == null) {
|
||||
return false;
|
||||
}
|
||||
Software installer = Software.parse(step.getData());
|
||||
ArrayList<String> command = getInstallCommand(installer, step.getArgs(), payload);
|
||||
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile());
|
||||
if(!success) {
|
||||
log.error("An error occurred invoking [{}]", step.getData());
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the installer command (including the installer itself and if needed, arguments) to
|
||||
* invoke the installer file
|
||||
*/
|
||||
public ArrayList<String> getInstallCommand(Software installer, List<String> args, File payload) {
|
||||
ArrayList<String> interpreter = new ArrayList<>();
|
||||
Os os = SystemUtilities.getOs();
|
||||
switch(installer) {
|
||||
case EXE:
|
||||
if(!SystemUtilities.isWindows()) {
|
||||
interpreter.add("wine");
|
||||
}
|
||||
// Executable on its own
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.addAll(args); // Assume exe args come after payload
|
||||
break;
|
||||
case MSI:
|
||||
interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec");
|
||||
interpreter.add("/i"); // Assume standard install
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.addAll(args); // Assume msiexec args come after payload
|
||||
break;
|
||||
case PKG:
|
||||
if(os == Os.MAC) {
|
||||
interpreter.add("installer");
|
||||
interpreter.addAll(args); // Assume installer args come before payload
|
||||
interpreter.add("-package");
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.add("-target");
|
||||
interpreter.add("/"); // Assume we don't want this on a removable volume
|
||||
} else {
|
||||
throw new UnsupportedOperationException("PKG is not yet supported on this platform");
|
||||
}
|
||||
break;
|
||||
case DMG:
|
||||
// DMG requires "hdiutil attach", but the mount point is unknown
|
||||
throw new UnsupportedOperationException("DMG is not yet supported");
|
||||
case RUN:
|
||||
if(SystemUtilities.isWindows()) {
|
||||
interpreter.add("bash");
|
||||
interpreter.add("-c");
|
||||
}
|
||||
interpreter.add(payload.toString());
|
||||
interpreter.addAll(args); // Assume run args come after payload
|
||||
// Executable on its own
|
||||
break;
|
||||
default:
|
||||
// We'll try to parse it from the shebang just before invocation time
|
||||
}
|
||||
return interpreter;
|
||||
}
|
||||
|
||||
}
|
||||
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.installer.LinuxInstaller;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
class LinuxShortcutCreator extends ShortcutCreator {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(LinuxShortcutCreator.class);
|
||||
private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
|
||||
|
||||
public boolean canAutoStart() {
|
||||
return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME));
|
||||
}
|
||||
public void createDesktopShortcut() {
|
||||
copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP);
|
||||
}
|
||||
|
||||
private static void copyShortcut(String source, String target) {
|
||||
try {
|
||||
Files.copy(Paths.get(source), Paths.get(target));
|
||||
} catch(IOException e) {
|
||||
log.warn("Error creating shortcut {}", target, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.SAXException;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.MacUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
class MacShortcutCreator extends ShortcutCreator {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(MacShortcutCreator.class);
|
||||
private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE;
|
||||
|
||||
/**
|
||||
* Verify LaunchAgents plist file exists and parse it to verify it's enabled
|
||||
*/
|
||||
@Override
|
||||
public boolean canAutoStart() {
|
||||
// plist is stored as io.qz.plist
|
||||
Path plistPath = Paths.get("/Library/LaunchAgents", MacUtilities.getBundleId() + ".plist");
|
||||
|
||||
if (Files.exists(plistPath)) {
|
||||
try {
|
||||
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||
Document doc = dBuilder.parse(plistPath.toFile());
|
||||
doc.getDocumentElement().normalize();
|
||||
|
||||
NodeList dictList = doc.getElementsByTagName("dict");
|
||||
|
||||
// Loop to find "RunAtLoad" key, then the adjacent key
|
||||
boolean foundItem = false;
|
||||
if (dictList.getLength() > 0) {
|
||||
NodeList children = dictList.item(0).getChildNodes();
|
||||
for(int n = 0; n < children.getLength(); n++) {
|
||||
Node item = children.item(n);
|
||||
// Apple stores booleans as adjacent tags to their owner
|
||||
if (foundItem) {
|
||||
String nodeName = children.item(n).getNodeName();
|
||||
log.debug("Found RunAtLoad value {}", nodeName);
|
||||
return "true".equals(nodeName);
|
||||
}
|
||||
if (item.getNodeName().equals("key") && item.getTextContent().equals("RunAtLoad")) {
|
||||
log.debug("Found RunAtLoad key in {}", plistPath);
|
||||
foundItem = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("RunAtLoad was not in plist {}, autostart will not work.", plistPath);
|
||||
}
|
||||
catch(SAXException | IOException | ParserConfigurationException e) {
|
||||
log.warn("Error reading plist {}, autostart will not work.", plistPath, e);
|
||||
}
|
||||
} else {
|
||||
log.warn("No plist {} found, autostart will not work", plistPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void createDesktopShortcut() {
|
||||
try {
|
||||
new File(SHORTCUT_PATH).delete();
|
||||
if(SystemUtilities.getJarParentPath().endsWith("Contents")) {
|
||||
// We're probably running from an .app bundle
|
||||
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath());
|
||||
} else {
|
||||
// We're running from a mystery location, use the jar instead
|
||||
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath());
|
||||
}
|
||||
|
||||
} catch(IOException e) {
|
||||
log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
/**
|
||||
* Utility class for creating, querying and removing startup shortcuts and
|
||||
* desktop shortcuts.
|
||||
*
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public abstract class ShortcutCreator {
|
||||
private static ShortcutCreator instance;
|
||||
protected static final Logger log = LogManager.getLogger(ShortcutCreator.class);
|
||||
public abstract boolean canAutoStart();
|
||||
public abstract void createDesktopShortcut();
|
||||
|
||||
public static ShortcutCreator getInstance() {
|
||||
if (instance == null) {
|
||||
if (SystemUtilities.isWindows()) {
|
||||
instance = new WindowsShortcutCreator();
|
||||
} else if (SystemUtilities.isMac()) {
|
||||
instance = new MacShortcutCreator();
|
||||
} else {
|
||||
instance = new LinuxShortcutCreator();
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*
|
||||
*/
|
||||
|
||||
package qz.installer.shortcut;
|
||||
|
||||
import com.sun.jna.platform.win32.Win32Exception;
|
||||
import mslinks.ShellLinkException;
|
||||
import mslinks.ShellLinkHelper;
|
||||
import qz.common.Constants;
|
||||
import qz.installer.WindowsSpecialFolders;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*/
|
||||
public class WindowsShortcutCreator extends ShortcutCreator {
|
||||
private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk";
|
||||
|
||||
public void createDesktopShortcut() {
|
||||
createShortcut(WindowsSpecialFolders.DESKTOP.toString());
|
||||
}
|
||||
|
||||
public boolean canAutoStart() {
|
||||
try {
|
||||
return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME));
|
||||
} catch(Win32Exception e) {
|
||||
log.warn("An exception occurred locating the startup folder; autostart cannot be determined.", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void createShortcut(String folderPath) {
|
||||
try {
|
||||
ShellLinkHelper.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME);
|
||||
}
|
||||
catch(ShellLinkException | IOException ex) {
|
||||
log.warn("Error creating desktop shortcut", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates .exe path from .jar
|
||||
* fixme: overlaps SystemUtilities.getAppPath
|
||||
*/
|
||||
private static String getAppPath() {
|
||||
return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe");
|
||||
}
|
||||
}
|
||||
752
old code/tray/src/qz/printer/PrintOptions.java
Executable file
752
old code/tray/src/qz/printer/PrintOptions.java
Executable file
@@ -0,0 +1,752 @@
|
||||
package qz.printer;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.utils.LoggerUtilities;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.ResolutionSyntax;
|
||||
import javax.print.attribute.Size2DSyntax;
|
||||
import javax.print.attribute.standard.Chromaticity;
|
||||
import javax.print.attribute.standard.OrientationRequested;
|
||||
import javax.print.attribute.standard.PrinterResolution;
|
||||
import javax.print.attribute.standard.Sides;
|
||||
import java.awt.*;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PrintOptions {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintOptions.class);
|
||||
|
||||
private Pixel psOptions = new Pixel();
|
||||
private Raw rawOptions = new Raw();
|
||||
private Default defOptions = new Default();
|
||||
|
||||
|
||||
/**
|
||||
* Parses the provided JSON Object into relevant Pixel and Raw options
|
||||
*/
|
||||
public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities.Format format) {
|
||||
if (configOpts == null) { return; }
|
||||
|
||||
//check for raw options
|
||||
if (!configOpts.isNull("forceRaw")) {
|
||||
rawOptions.forceRaw = configOpts.optBoolean("forceRaw", false);
|
||||
} else if (!configOpts.isNull("altPrinting")) {
|
||||
log.warn("Raw option \"altPrinting\" is deprecated. Please use \"forceRaw\" instead.");
|
||||
rawOptions.forceRaw = configOpts.optBoolean("altPrinting", false);
|
||||
}
|
||||
if (rawOptions.forceRaw && SystemUtilities.isWindows()) {
|
||||
log.warn("Forced raw printing is not supported on Windows");
|
||||
rawOptions.forceRaw = false;
|
||||
}
|
||||
|
||||
if (!configOpts.isNull("encoding")) {
|
||||
JSONObject encodings = configOpts.optJSONObject("encoding");
|
||||
if (encodings != null) {
|
||||
rawOptions.srcEncoding = encodings.optString("from", null);
|
||||
rawOptions.destEncoding = encodings.optString("to", null);
|
||||
} else {
|
||||
rawOptions.destEncoding = configOpts.optString("encoding", null);
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("spool")) {
|
||||
JSONObject spool = configOpts.optJSONObject("spool");
|
||||
if (spool != null) {
|
||||
if (!spool.isNull("size")) {
|
||||
try { rawOptions.spoolSize = spool.getInt("size"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
|
||||
}
|
||||
// TODO: Implement spool.start
|
||||
if (!spool.isNull("end")) {
|
||||
rawOptions.spoolEnd = spool.optString("end");
|
||||
}
|
||||
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
|
||||
}
|
||||
} else {
|
||||
// Deprecated
|
||||
if (!configOpts.isNull("perSpool")) {
|
||||
try { rawOptions.spoolSize = configOpts.getInt("perSpool"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "perSpool", configOpts.opt("perSpool")); }
|
||||
}
|
||||
if (!configOpts.isNull("endOfDoc")) {
|
||||
rawOptions.spoolEnd = configOpts.optString("endOfDoc", null);
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("copies")) {
|
||||
try { rawOptions.copies = configOpts.getInt("copies"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
|
||||
}
|
||||
if (!configOpts.isNull("jobName")) {
|
||||
rawOptions.jobName = configOpts.optString("jobName", null);
|
||||
}
|
||||
if (!configOpts.isNull("retainTemp")) {
|
||||
rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false);
|
||||
}
|
||||
|
||||
|
||||
//check for pixel options
|
||||
if (!configOpts.isNull("units")) {
|
||||
switch(configOpts.optString("units")) {
|
||||
case "mm":
|
||||
psOptions.units = Unit.MM; break;
|
||||
case "cm":
|
||||
psOptions.units = Unit.CM; break;
|
||||
case "in":
|
||||
psOptions.units = Unit.INCH; break;
|
||||
default:
|
||||
LoggerUtilities.optionWarn(log, "valid value", "units", configOpts.opt("units")); break;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("bounds")) {
|
||||
try {
|
||||
JSONObject bounds = configOpts.getJSONObject("bounds");
|
||||
psOptions.bounds = new Bounds(bounds.optDouble("x", 0), bounds.optDouble("y", 0), bounds.optDouble("width", 0), bounds.optDouble("height", 0));
|
||||
}
|
||||
catch(JSONException e) {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "bounds", configOpts.opt("bounds"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("colorType")) {
|
||||
try {
|
||||
psOptions.colorType = ColorType.valueOf(configOpts.optString("colorType").toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
LoggerUtilities.optionWarn(log, "valid value", "colorType", configOpts.opt("colorType"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("copies")) {
|
||||
try { psOptions.copies = configOpts.getInt("copies"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
|
||||
if (psOptions.copies < 1) {
|
||||
log.warn("Cannot have less than one copy");
|
||||
psOptions.copies = 1;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("density")) {
|
||||
JSONObject asymmDPI = configOpts.optJSONObject("density");
|
||||
if (asymmDPI != null) {
|
||||
psOptions.density = asymmDPI.optInt("feed");
|
||||
psOptions.crossDensity = asymmDPI.optInt("cross");
|
||||
} else {
|
||||
List<PrinterResolution> rSupport = output.isSetService()?
|
||||
output.getNativePrinter().getResolutions():new ArrayList<>();
|
||||
|
||||
JSONArray possibleDPIs = configOpts.optJSONArray("density");
|
||||
if (possibleDPIs != null && possibleDPIs.length() > 0) {
|
||||
PrinterResolution usableRes = null;
|
||||
|
||||
if (!rSupport.isEmpty()) {
|
||||
for(int i = 0; i < possibleDPIs.length(); i++) {
|
||||
PrinterResolution compareRes;
|
||||
asymmDPI = possibleDPIs.optJSONObject(i);
|
||||
if (asymmDPI != null) {
|
||||
compareRes = new PrinterResolution(asymmDPI.optInt("cross"), asymmDPI.optInt("feed"), psOptions.units.resSyntax);
|
||||
} else {
|
||||
compareRes = new PrinterResolution(possibleDPIs.optInt(i), possibleDPIs.optInt(i), psOptions.units.resSyntax);
|
||||
}
|
||||
|
||||
if (rSupport.contains(compareRes)) {
|
||||
usableRes = compareRes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usableRes == null) {
|
||||
log.warn("Supported printer densities not found, using first value provided");
|
||||
asymmDPI = possibleDPIs.optJSONObject(0);
|
||||
if (asymmDPI != null) {
|
||||
psOptions.density = asymmDPI.optInt("feed");
|
||||
psOptions.crossDensity = asymmDPI.optInt("cross");
|
||||
} else {
|
||||
psOptions.density = possibleDPIs.optInt(0);
|
||||
}
|
||||
} else {
|
||||
psOptions.density = usableRes.getFeedResolution(psOptions.units.resSyntax);
|
||||
psOptions.crossDensity = usableRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||
}
|
||||
} else {
|
||||
String relDPI = configOpts.optString("density", "").toLowerCase(Locale.ENGLISH);
|
||||
if ("best".equals(relDPI)) {
|
||||
PrinterResolution bestRes = null;
|
||||
for(PrinterResolution pr : rSupport) {
|
||||
if (bestRes == null || !pr.lessThanOrEquals(bestRes)) {
|
||||
bestRes = pr;
|
||||
}
|
||||
}
|
||||
if (bestRes != null) {
|
||||
psOptions.density = bestRes.getFeedResolution(psOptions.units.resSyntax);
|
||||
psOptions.crossDensity = bestRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||
} else {
|
||||
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
|
||||
}
|
||||
} else if ("draft".equals(relDPI)) {
|
||||
PrinterResolution lowestRes = null;
|
||||
for(PrinterResolution pr : rSupport) {
|
||||
if (lowestRes == null || pr.lessThanOrEquals(lowestRes)) {
|
||||
lowestRes = pr;
|
||||
}
|
||||
}
|
||||
if (lowestRes != null) {
|
||||
psOptions.density = lowestRes.getFeedResolution(psOptions.units.resSyntax);
|
||||
psOptions.crossDensity = lowestRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||
} else {
|
||||
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
|
||||
}
|
||||
} else {
|
||||
try { psOptions.density = configOpts.getDouble("density"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "density", configOpts.opt("density")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("dithering")) {
|
||||
try {
|
||||
if (configOpts.getBoolean("dithering")) {
|
||||
psOptions.dithering = RenderingHints.VALUE_DITHER_ENABLE;
|
||||
} else {
|
||||
psOptions.dithering = RenderingHints.VALUE_DITHER_DISABLE;
|
||||
}
|
||||
}
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "dithering", configOpts.opt("dithering")); }
|
||||
}
|
||||
if (!configOpts.isNull("duplex")) {
|
||||
try {
|
||||
if (configOpts.getBoolean("duplex")) {
|
||||
psOptions.duplex = Sides.DUPLEX;
|
||||
}
|
||||
}
|
||||
catch(JSONException e) {
|
||||
//not a boolean, try as a string
|
||||
try {
|
||||
String duplex = configOpts.getString("duplex").toLowerCase(Locale.ENGLISH);
|
||||
if (duplex.matches("^(duplex|(two.sided.)?long(.edge)?)$")) {
|
||||
psOptions.duplex = Sides.DUPLEX;
|
||||
} else if (duplex.matches("^(tumble|(two.sided.)?short(.edge)?)$")) {
|
||||
psOptions.duplex = Sides.TUMBLE;
|
||||
}
|
||||
//else - one sided (default)
|
||||
}
|
||||
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "valid value", "duplex", configOpts.opt("duplex")); }
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("interpolation")) {
|
||||
switch(configOpts.optString("interpolation")) {
|
||||
case "bicubic":
|
||||
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; break;
|
||||
case "bilinear":
|
||||
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR; break;
|
||||
case "nearest-neighbor":
|
||||
case "nearest":
|
||||
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; break;
|
||||
default:
|
||||
LoggerUtilities.optionWarn(log, "valid value", "interpolation", configOpts.opt("interpolation")); break;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("jobName")) {
|
||||
psOptions.jobName = configOpts.optString("jobName", null);
|
||||
}
|
||||
if (!configOpts.isNull("legacy")) {
|
||||
psOptions.legacy = configOpts.optBoolean("legacy", false);
|
||||
}
|
||||
if (!configOpts.isNull("margins")) {
|
||||
Margins m = new Margins();
|
||||
JSONObject subMargins = configOpts.optJSONObject("margins");
|
||||
if (subMargins != null) {
|
||||
//each individually
|
||||
if (!subMargins.isNull("top")) {
|
||||
try { m.top = subMargins.getDouble("top"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.top", subMargins.opt("top")); }
|
||||
}
|
||||
if (!subMargins.isNull("right")) {
|
||||
try { m.right = subMargins.getDouble("right"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.right", subMargins.opt("right")); }
|
||||
}
|
||||
if (!subMargins.isNull("bottom")) {
|
||||
try { m.bottom = subMargins.getDouble("bottom"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.bottom", subMargins.opt("bottom")); }
|
||||
}
|
||||
if (!subMargins.isNull("left")) {
|
||||
try { m.left = subMargins.getDouble("left"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.left", subMargins.opt("left")); }
|
||||
}
|
||||
} else {
|
||||
try { m.setAll(configOpts.getDouble("margins")); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins", configOpts.opt("margins")); }
|
||||
}
|
||||
|
||||
psOptions.margins = m;
|
||||
}
|
||||
if (!configOpts.isNull("orientation")) {
|
||||
try {
|
||||
psOptions.orientation = Orientation.valueOf(configOpts.optString("orientation").replaceAll("-", "_").toUpperCase(Locale.ENGLISH));
|
||||
}
|
||||
catch(IllegalArgumentException e) {
|
||||
LoggerUtilities.optionWarn(log, "valid value", "orientation", configOpts.opt("orientation"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("paperThickness")) {
|
||||
try { psOptions.paperThickness = configOpts.getDouble("paperThickness"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "paperThickness", configOpts.opt("paperThickness")); }
|
||||
}
|
||||
if (!configOpts.isNull("spool")) {
|
||||
JSONObject spool = configOpts.optJSONObject("spool");
|
||||
if (spool != null) {
|
||||
if (!spool.isNull("size")) {
|
||||
try { psOptions.spoolSize = spool.getInt("size"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
|
||||
}
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("printerTray")) {
|
||||
psOptions.printerTray = configOpts.optString("printerTray", null);
|
||||
// Guard empty string value; will break pattern matching
|
||||
if(psOptions.printerTray != null && psOptions.printerTray.trim().equals("")) {
|
||||
psOptions.printerTray = null;
|
||||
}
|
||||
}
|
||||
if (!configOpts.isNull("rasterize")) {
|
||||
try { psOptions.rasterize = configOpts.getBoolean("rasterize"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "rasterize", configOpts.opt("rasterize")); }
|
||||
}
|
||||
if (!configOpts.isNull("rotation")) {
|
||||
try { psOptions.rotation = configOpts.getDouble("rotation"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "rotation", configOpts.opt("rotation")); }
|
||||
}
|
||||
if (!configOpts.isNull("scaleContent")) {
|
||||
try { psOptions.scaleContent = configOpts.getBoolean("scaleContent"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "scaleContent", configOpts.opt("scaleContent")); }
|
||||
}
|
||||
if (!configOpts.isNull("size")) {
|
||||
Size s = new Size();
|
||||
JSONObject subSize = configOpts.optJSONObject("size");
|
||||
if (subSize != null) {
|
||||
if (!subSize.isNull("width")) {
|
||||
try { s.width = subSize.getDouble("width"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.width", subSize.opt("width")); }
|
||||
}
|
||||
if (!subSize.isNull("height")) {
|
||||
try { s.height = subSize.getDouble("height"); }
|
||||
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.height", subSize.opt("height")); }
|
||||
}
|
||||
|
||||
if (s.height <= 0 && s.width <= 0) {
|
||||
log.warn("Page size has been set without dimensions, using default");
|
||||
} else {
|
||||
psOptions.size = s;
|
||||
}
|
||||
} else {
|
||||
LoggerUtilities.optionWarn(log, "JSONObject", "size", configOpts.opt("size"));
|
||||
}
|
||||
}
|
||||
|
||||
//grab any useful service defaults
|
||||
PrinterResolution defaultRes = null;
|
||||
if (output.isSetService()) {
|
||||
defaultRes = output.getNativePrinter().getResolution().value();
|
||||
|
||||
if (defaultRes == null) {
|
||||
//printer has no default resolution set, see if it is possible to pull anything
|
||||
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
|
||||
if (rSupport.size() > 0) {
|
||||
defaultRes = rSupport.get(0);
|
||||
log.warn("Default resolution for {} is missing, using fallback: {}", output.getNativePrinter().getName(), defaultRes);
|
||||
} else {
|
||||
log.warn("Default resolution for {} is missing, no fallback available.", output.getNativePrinter().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (defaultRes != null) {
|
||||
//convert dphi to unit-dependant density ourselves (to keep as double type)
|
||||
defOptions.density = (double)defaultRes.getFeedResolution(1) / psOptions.getUnits().getDPIUnits();
|
||||
} else {
|
||||
try { defOptions.density = configOpts.getDouble("fallbackDensity"); }
|
||||
catch(JSONException e) {
|
||||
LoggerUtilities.optionWarn(log, "double", "fallbackDensity", configOpts.opt("fallbackDensity"));
|
||||
//manually convert default dphi to a density value based on units
|
||||
defOptions.density = 60000d / psOptions.getUnits().getDPIUnits();
|
||||
}
|
||||
}
|
||||
if ((psOptions.isRasterize() || format == PrintingUtilities.Format.IMAGE) && psOptions.getDensity() <= 1) {
|
||||
psOptions.density = defOptions.density;
|
||||
psOptions.crossDensity = defOptions.density;
|
||||
}
|
||||
|
||||
if (output.isSetService()) {
|
||||
try {
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
defOptions.pageSize = new Size(page.getWidth(), page.getHeight());
|
||||
}
|
||||
catch(PrinterException e) {
|
||||
log.warn("Unable to find the default paper size");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Raw getRawOptions() {
|
||||
return rawOptions;
|
||||
}
|
||||
|
||||
public Pixel getPixelOptions() {
|
||||
return psOptions;
|
||||
}
|
||||
|
||||
public Default getDefaultOptions() { return defOptions; }
|
||||
|
||||
|
||||
// Option groups //
|
||||
|
||||
/** Raw printing options */
|
||||
public class Raw {
|
||||
private boolean forceRaw = false; //Alternate printing for linux systems
|
||||
private String destEncoding = null; //Text encoding / charset
|
||||
private String srcEncoding = null; //Conversion text encoding
|
||||
private String spoolEnd = null; //End of document character(s)
|
||||
private int spoolSize = 1; //Pages per spool
|
||||
private int copies = 1; //Job copies
|
||||
private String jobName = null; //Job name
|
||||
private boolean retainTemp = false; //Retain any temporary files
|
||||
|
||||
|
||||
public boolean isForceRaw() {
|
||||
return forceRaw;
|
||||
}
|
||||
|
||||
public String getDestEncoding() {
|
||||
return destEncoding;
|
||||
}
|
||||
|
||||
public String getSrcEncoding() {
|
||||
return srcEncoding;
|
||||
}
|
||||
|
||||
public String getSpoolEnd() {
|
||||
return spoolEnd;
|
||||
}
|
||||
|
||||
public int getSpoolSize() {
|
||||
return spoolSize;
|
||||
}
|
||||
|
||||
public int getCopies() {
|
||||
return copies;
|
||||
}
|
||||
|
||||
public boolean isRetainTemp() { return retainTemp; }
|
||||
|
||||
public String getJobName(String defaultVal) {
|
||||
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel printing options */
|
||||
public class Pixel {
|
||||
private Bounds bounds = null; //Bounding box rectangle
|
||||
private ColorType colorType = ColorType.COLOR; //Color / black&white
|
||||
private int copies = 1; //Job copies
|
||||
private double crossDensity = 0; //Cross feed density
|
||||
private double density = 0; //Pixel density (DPI or DPMM), feed density if crossDensity is defined
|
||||
private Object dithering = RenderingHints.VALUE_DITHER_DEFAULT; //Image dithering
|
||||
private Sides duplex = Sides.ONE_SIDED; //Multi-siding
|
||||
private Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; //Image interpolation
|
||||
private String jobName = null; //Job name
|
||||
private boolean legacy = false; //Legacy printing
|
||||
private Margins margins = new Margins(); //Page margins
|
||||
private Orientation orientation = null; //Page orientation
|
||||
private double paperThickness = -1; //Paper thickness
|
||||
private int spoolSize = 0; //Pages before sending to printer
|
||||
private String printerTray = null; //Printer tray to use
|
||||
private boolean rasterize = true; //Whether documents are rasterized before printing
|
||||
private double rotation = 0; //Image rotation
|
||||
private boolean scaleContent = true; //Adjust paper size for best image fit
|
||||
private Size size = null; //Paper size
|
||||
private Unit units = Unit.INCH; //Units for density, margins, size
|
||||
|
||||
|
||||
public Bounds getBounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
public ColorType getColorType() {
|
||||
return colorType;
|
||||
}
|
||||
|
||||
public int getCopies() {
|
||||
return copies;
|
||||
}
|
||||
|
||||
public double getCrossDensity() {
|
||||
return crossDensity;
|
||||
}
|
||||
|
||||
public double getDensity() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public Object getDithering() {
|
||||
return dithering;
|
||||
}
|
||||
|
||||
public Sides getDuplex() {
|
||||
return duplex;
|
||||
}
|
||||
|
||||
public Object getInterpolation() {
|
||||
return interpolation;
|
||||
}
|
||||
|
||||
public String getJobName(String defaultVal) {
|
||||
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
|
||||
}
|
||||
|
||||
public boolean isLegacy() {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
public Margins getMargins() {
|
||||
return margins;
|
||||
}
|
||||
|
||||
public Orientation getOrientation() {
|
||||
return orientation;
|
||||
}
|
||||
|
||||
public double getPaperThickness() {
|
||||
return paperThickness;
|
||||
}
|
||||
|
||||
public int getSpoolSize() {
|
||||
return spoolSize;
|
||||
}
|
||||
|
||||
public String getPrinterTray() {
|
||||
return printerTray;
|
||||
}
|
||||
|
||||
public boolean isRasterize() {
|
||||
return rasterize;
|
||||
}
|
||||
|
||||
public double getRotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public boolean isScaleContent() {
|
||||
return scaleContent;
|
||||
}
|
||||
|
||||
public Size getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public Unit getUnits() {
|
||||
return units;
|
||||
}
|
||||
}
|
||||
|
||||
/** PrintService Defaults **/
|
||||
public class Default {
|
||||
private double density;
|
||||
private Size pageSize;
|
||||
|
||||
|
||||
public double getDensity() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public Size getPageSize() {
|
||||
return pageSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Sub options //
|
||||
|
||||
/** Pixel page size options */
|
||||
public class Size {
|
||||
private double width = -1; //Page width
|
||||
private double height = -1; //Page height
|
||||
|
||||
|
||||
public Size() {}
|
||||
|
||||
public Size(double width, double height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel page margins options */
|
||||
public class Margins {
|
||||
private double top = 0; //Top page margin
|
||||
private double right = 0; //Right page margin
|
||||
private double bottom = 0; //Bottom page margin
|
||||
private double left = 0; //Left page margin
|
||||
|
||||
private void setAll(double margin) {
|
||||
top = margin;
|
||||
right = margin;
|
||||
bottom = margin;
|
||||
left = margin;
|
||||
}
|
||||
|
||||
|
||||
public double top() {
|
||||
return top;
|
||||
}
|
||||
|
||||
public double right() {
|
||||
return right;
|
||||
}
|
||||
|
||||
public double bottom() {
|
||||
return bottom;
|
||||
}
|
||||
|
||||
public double left() {
|
||||
return left;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bounding box generic rectangle */
|
||||
public class Bounds {
|
||||
private double x;
|
||||
private double y;
|
||||
private double width;
|
||||
private double height;
|
||||
|
||||
public Bounds(double x, double y, double width, double height) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public double getX() {
|
||||
return x;
|
||||
}
|
||||
|
||||
public double getY() {
|
||||
return y;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel dimension values */
|
||||
public enum Unit {
|
||||
INCH(ResolutionSyntax.DPI, 1.0f, 1.0f, Size2DSyntax.INCH), //1in = 1in
|
||||
CM(ResolutionSyntax.DPCM, .3937f, 2.54f, 10000), //1cm = .3937in ; 1in = 2.54cm
|
||||
MM(ResolutionSyntax.DPCM * 10, .03937f, 25.4f, Size2DSyntax.MM); //1mm = .03937in ; 1in = 25.4mm
|
||||
|
||||
private final float fromInch;
|
||||
private final float toInch; //multiplicand to convert to inches
|
||||
private final int resSyntax;
|
||||
private final int µm;
|
||||
|
||||
Unit(int resSyntax, float toIN, float fromIN, int µm) {
|
||||
toInch = toIN;
|
||||
fromInch = fromIN;
|
||||
this.resSyntax = resSyntax;
|
||||
this.µm = µm;
|
||||
}
|
||||
|
||||
public float toInches() {
|
||||
return toInch;
|
||||
}
|
||||
|
||||
public float as1Inch() {
|
||||
return fromInch;
|
||||
}
|
||||
|
||||
public int getDPIUnits() {
|
||||
return resSyntax;
|
||||
}
|
||||
|
||||
public int getMediaSizeUnits() {
|
||||
return µm;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel page orientation option */
|
||||
public enum Orientation {
|
||||
PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 0),
|
||||
REVERSE_PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 180),
|
||||
LANDSCAPE(OrientationRequested.LANDSCAPE, PageFormat.LANDSCAPE, 270),
|
||||
REVERSE_LANDSCAPE(OrientationRequested.REVERSE_LANDSCAPE, PageFormat.REVERSE_LANDSCAPE, 90);
|
||||
|
||||
private final OrientationRequested orientationRequested;
|
||||
private final int orientationFormat;
|
||||
private final int degreesRot;
|
||||
|
||||
Orientation(OrientationRequested orientationRequested, int orientationFormat, int degreesRot) {
|
||||
this.orientationRequested = orientationRequested;
|
||||
this.orientationFormat = orientationFormat;
|
||||
this.degreesRot = degreesRot;
|
||||
}
|
||||
|
||||
|
||||
public OrientationRequested getAsOrientRequested() {
|
||||
return orientationRequested;
|
||||
}
|
||||
|
||||
public int getAsOrientFormat() {
|
||||
return orientationFormat;
|
||||
}
|
||||
|
||||
public int getDegreesRot() {
|
||||
return degreesRot;
|
||||
}
|
||||
}
|
||||
|
||||
/** Pixel page color option */
|
||||
public enum ColorType {
|
||||
COLOR(Chromaticity.COLOR),
|
||||
GREYSCALE(Chromaticity.MONOCHROME),
|
||||
GRAYSCALE(Chromaticity.MONOCHROME),
|
||||
BLACKWHITE(Chromaticity.MONOCHROME),
|
||||
DEFAULT(null);
|
||||
|
||||
private final Chromaticity chromatic;
|
||||
|
||||
ColorType(Chromaticity chromatic) {
|
||||
this.chromatic = chromatic;
|
||||
}
|
||||
|
||||
|
||||
public Chromaticity getAsChromaticity() {
|
||||
return chromatic;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
92
old code/tray/src/qz/printer/PrintOutput.java
Executable file
92
old code/tray/src/qz/printer/PrintOutput.java
Executable file
@@ -0,0 +1,92 @@
|
||||
package qz.printer;
|
||||
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.utils.FileUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.attribute.standard.Media;
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
public class PrintOutput {
|
||||
|
||||
private NativePrinter printer = null;
|
||||
|
||||
private File file = null;
|
||||
|
||||
private String host = null;
|
||||
private int port = -1;
|
||||
|
||||
|
||||
public PrintOutput(JSONObject configPrinter) throws JSONException, IllegalArgumentException {
|
||||
if (configPrinter == null) { return; }
|
||||
|
||||
if (configPrinter.has("name")) {
|
||||
printer = PrintServiceMatcher.matchPrinter(configPrinter.getString("name"));
|
||||
if (printer == null) {
|
||||
throw new IllegalArgumentException("Cannot find printer with name \"" + configPrinter.getString("name") + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
if (configPrinter.has("file")) {
|
||||
String filename = configPrinter.getString("file");
|
||||
if (!FileUtilities.isGoodExtension(Paths.get(filename))) {
|
||||
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited file extension)");
|
||||
} else if (FileUtilities.isBadPath(filename)) {
|
||||
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited directory name)");
|
||||
} else {
|
||||
file = new File(filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (configPrinter.has("host")) {
|
||||
host = configPrinter.getString("host");
|
||||
port = configPrinter.optInt("port", 9100); // default to port 9100 (HP/JetDirect standard) if not provided
|
||||
}
|
||||
|
||||
//at least one method must be set for printing
|
||||
if (!isSetService() && !isSetFile() && !isSetHost()) {
|
||||
throw new IllegalArgumentException("No printer output has been specified");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean isSetService() {
|
||||
return printer != null && printer.getPrintService() != null && !printer.getPrintService().isNull();
|
||||
}
|
||||
|
||||
public PrintService getPrintService() {
|
||||
return printer.getPrintService().value();
|
||||
}
|
||||
|
||||
public NativePrinter getNativePrinter() {
|
||||
return printer;
|
||||
}
|
||||
|
||||
public boolean isSetFile() {
|
||||
return file != null;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
public boolean isSetHost() {
|
||||
return host != null;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public Media[] getSupportedMedia() {
|
||||
return (Media[])getPrintService().getSupportedAttributeValues(Media.class, null, null);
|
||||
}
|
||||
|
||||
}
|
||||
242
old code/tray/src/qz/printer/PrintServiceMatcher.java
Executable file
242
old code/tray/src/qz/printer/PrintServiceMatcher.java
Executable file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.printer;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.printer.info.CachedPrintServiceLookup;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.printer.info.NativePrinterMap;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.PrintService;
|
||||
import javax.print.PrintServiceLookup;
|
||||
import javax.print.attribute.ResolutionSyntax;
|
||||
import javax.print.attribute.standard.*;
|
||||
import java.util.*;
|
||||
|
||||
public class PrintServiceMatcher {
|
||||
private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class);
|
||||
|
||||
// PrintService is slow in CUPS, use a cache instead per JDK-7001133
|
||||
// TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream
|
||||
private static final boolean useCache = SystemUtilities.isUnix();
|
||||
|
||||
public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) {
|
||||
NativePrinterMap printers = NativePrinterMap.getInstance();
|
||||
printers.putAll(true, lookupPrintServices());
|
||||
if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); }
|
||||
if (!silent) { log.debug("Found {} printers", printers.size()); }
|
||||
return printers;
|
||||
}
|
||||
|
||||
private static PrintService[] lookupPrintServices() {
|
||||
return useCache ? CachedPrintServiceLookup.lookupPrintServices() :
|
||||
PrintServiceLookup.lookupPrintServices(null, null);
|
||||
}
|
||||
|
||||
private static PrintService lookupDefaultPrintService() {
|
||||
return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() :
|
||||
PrintServiceLookup.lookupDefaultPrintService();
|
||||
}
|
||||
|
||||
public static NativePrinterMap getNativePrinterList(boolean silent) {
|
||||
return getNativePrinterList(silent, false);
|
||||
}
|
||||
|
||||
public static NativePrinterMap getNativePrinterList() {
|
||||
return getNativePrinterList(false);
|
||||
}
|
||||
|
||||
public static NativePrinter getDefaultPrinter() {
|
||||
PrintService defaultService = lookupDefaultPrintService();
|
||||
|
||||
if(defaultService == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
NativePrinterMap printers = NativePrinterMap.getInstance();
|
||||
if (!printers.contains(defaultService)) {
|
||||
printers.putAll(false, defaultService);
|
||||
}
|
||||
|
||||
return printers.get(defaultService);
|
||||
}
|
||||
|
||||
public static String findPrinterName(String query) throws JSONException {
|
||||
NativePrinter printer = PrintServiceMatcher.matchPrinter(query);
|
||||
|
||||
if (printer != null) {
|
||||
return printer.getPrintService().value().getName();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds {@code PrintService} by looking at any matches to {@code printerSearch}.
|
||||
*
|
||||
* @param printerSearch Search query to compare against service names.
|
||||
*/
|
||||
public static NativePrinter matchPrinter(String printerSearch, boolean silent) {
|
||||
NativePrinter exact = null;
|
||||
NativePrinter begins = null;
|
||||
NativePrinter partial = null;
|
||||
|
||||
if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); }
|
||||
|
||||
// Fix for https://github.com/qzind/tray/issues/931
|
||||
// This is more than an optimization, removal will lead to a regression
|
||||
NativePrinter defaultPrinter = getDefaultPrinter();
|
||||
if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) {
|
||||
if (!silent) { log.debug("Matched default printer, skipping further search"); }
|
||||
return defaultPrinter;
|
||||
}
|
||||
|
||||
printerSearch = printerSearch.toLowerCase(Locale.ENGLISH);
|
||||
|
||||
// Search services for matches
|
||||
for(NativePrinter printer : getNativePrinterList(silent).values()) {
|
||||
if (printer.getName() == null) {
|
||||
continue;
|
||||
}
|
||||
String printerName = printer.getName().toLowerCase(Locale.ENGLISH);
|
||||
if (printerName.equals(printerSearch)) {
|
||||
exact = printer;
|
||||
break;
|
||||
}
|
||||
if (printerName.startsWith(printerSearch)) {
|
||||
begins = printer;
|
||||
continue;
|
||||
}
|
||||
if (printerName.contains(printerSearch)) {
|
||||
partial = printer;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (SystemUtilities.isMac()) {
|
||||
// 1.9 compat: fallback for old style names
|
||||
PrinterName name = printer.getLegacyName();
|
||||
if (name == null || name.getValue() == null) { continue; }
|
||||
printerName = name.getValue().toLowerCase(Locale.ENGLISH);
|
||||
if (printerName.equals(printerSearch)) {
|
||||
exact = printer;
|
||||
continue;
|
||||
}
|
||||
if (printerName.startsWith(printerSearch)) {
|
||||
begins = printer;
|
||||
continue;
|
||||
}
|
||||
if (printerName.contains(printerSearch)) {
|
||||
partial = printer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return closest match
|
||||
NativePrinter use = null;
|
||||
if (exact != null) {
|
||||
use = exact;
|
||||
} else if (begins != null) {
|
||||
use = begins;
|
||||
} else if (partial != null) {
|
||||
use = partial;
|
||||
}
|
||||
|
||||
if (use != null) {
|
||||
if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName());
|
||||
} else {
|
||||
log.warn("Printer not found: {}", printerSearch);
|
||||
}
|
||||
|
||||
return use;
|
||||
}
|
||||
|
||||
public static NativePrinter matchPrinter(String printerSearch) {
|
||||
return matchPrinter(printerSearch, false);
|
||||
}
|
||||
|
||||
public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException {
|
||||
JSONArray list = new JSONArray();
|
||||
|
||||
PrintService defaultService = lookupDefaultPrintService();
|
||||
|
||||
boolean mediaTrayCrawled = false;
|
||||
|
||||
for(NativePrinter printer : getNativePrinterList().values()) {
|
||||
PrintService ps = printer.getPrintService().value();
|
||||
JSONObject jsonService = new JSONObject();
|
||||
jsonService.put("name", ps.getName());
|
||||
|
||||
if (includeDetails) {
|
||||
jsonService.put("driver", printer.getDriver().value());
|
||||
jsonService.put("connection", printer.getConnection());
|
||||
jsonService.put("default", ps == defaultService);
|
||||
|
||||
if (!mediaTrayCrawled) {
|
||||
log.info("Gathering printer MediaTray information...");
|
||||
mediaTrayCrawled = true;
|
||||
}
|
||||
|
||||
HashSet<String> uniqueSizes = new HashSet<>(); // prevents duplicates
|
||||
JSONArray trays = new JSONArray();
|
||||
JSONArray sizes = new JSONArray();
|
||||
|
||||
for(Media m : (Media[])ps.getSupportedAttributeValues(Media.class, null, null)) {
|
||||
if (m instanceof MediaTray) { trays.put(m.toString()); }
|
||||
if (m instanceof MediaSizeName) {
|
||||
if(uniqueSizes.add(m.toString())) {
|
||||
MediaSize mediaSize = MediaSize.getMediaSizeForName((MediaSizeName)m);
|
||||
if(mediaSize == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
JSONObject size = new JSONObject();
|
||||
size.put("name", m.toString());
|
||||
|
||||
JSONObject in = new JSONObject();
|
||||
in.put("width", mediaSize.getX(MediaPrintableArea.INCH));
|
||||
in.put("height", mediaSize.getY(MediaPrintableArea.INCH));
|
||||
size.put("in", in);
|
||||
|
||||
JSONObject mm = new JSONObject();
|
||||
mm.put("width", mediaSize.getX(MediaPrintableArea.MM));
|
||||
mm.put("height", mediaSize.getY(MediaPrintableArea.MM));
|
||||
size.put("mm", mm);
|
||||
|
||||
sizes.put(size);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if(trays.length() > 0) {
|
||||
jsonService.put("trays", trays);
|
||||
}
|
||||
if(sizes.length() > 0) {
|
||||
jsonService.put("sizes", sizes);
|
||||
}
|
||||
|
||||
PrinterResolution res = printer.getResolution().value();
|
||||
int density = -1; if (res != null) { density = res.getFeedResolution(ResolutionSyntax.DPI); }
|
||||
jsonService.put("density", density);
|
||||
}
|
||||
|
||||
list.put(jsonService);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
}
|
||||
97
old code/tray/src/qz/printer/action/PrintDirect.java
Executable file
97
old code/tray/src/qz/printer/action/PrintDirect.java
Executable file
@@ -0,0 +1,97 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import org.apache.commons.codec.binary.Base64InputStream;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
import javax.print.DocFlavor;
|
||||
import javax.print.DocPrintJob;
|
||||
import javax.print.PrintException;
|
||||
import javax.print.SimpleDoc;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
public class PrintDirect extends PrintRaw {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintDirect.class);
|
||||
|
||||
private ArrayList<String> prints = new ArrayList<>();
|
||||
private ArrayList<PrintingUtilities.Flavor> flavors = new ArrayList<>();
|
||||
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.DIRECT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.optJSONObject(i);
|
||||
if (data == null) { continue; }
|
||||
|
||||
prints.add(data.getString("data"));
|
||||
flavors.add(PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrintException {
|
||||
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||
attributes.add(new JobName(options.getRawOptions().getJobName(Constants.RAW_PRINT), Locale.getDefault()));
|
||||
|
||||
for(int i = 0; i < prints.size(); i++) {
|
||||
DocPrintJob printJob = output.getPrintService().createPrintJob();
|
||||
InputStream stream = null;
|
||||
|
||||
try {
|
||||
switch(flavors.get(i)) {
|
||||
case BASE64:
|
||||
stream = new Base64InputStream(new ByteArrayInputStream(prints.get(i).getBytes("UTF-8")));
|
||||
break;
|
||||
case FILE:
|
||||
stream = new DataInputStream(new URL(prints.get(i)).openStream());
|
||||
break;
|
||||
case PLAIN:
|
||||
default:
|
||||
stream = new ByteArrayInputStream(prints.get(i).getBytes("UTF-8"));
|
||||
break;
|
||||
}
|
||||
|
||||
SimpleDoc doc = new SimpleDoc(stream, DocFlavor.INPUT_STREAM.AUTOSENSE, null);
|
||||
|
||||
waitForPrint(printJob, doc, attributes);
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new PrintException(e);
|
||||
}
|
||||
finally {
|
||||
if (stream != null) {
|
||||
try { stream.close(); } catch(Exception ignore) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
prints.clear();
|
||||
flavors.clear();
|
||||
}
|
||||
|
||||
}
|
||||
414
old code/tray/src/qz/printer/action/PrintHTML.java
Executable file
414
old code/tray/src/qz/printer/action/PrintHTML.java
Executable file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
|
||||
package qz.printer.action;
|
||||
|
||||
import com.sun.javafx.print.PrintHelper;
|
||||
import com.sun.javafx.print.Units;
|
||||
import javafx.print.*;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.printer.action.html.WebApp;
|
||||
import qz.printer.action.html.WebAppModel;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.Copies;
|
||||
import javax.print.attribute.standard.CopiesSupported;
|
||||
import javax.print.attribute.standard.Sides;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class PrintHTML extends PrintImage implements PrintProcessor {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintHTML.class);
|
||||
|
||||
private List<WebAppModel> models;
|
||||
|
||||
private JLabel legacyLabel = null;
|
||||
|
||||
|
||||
public PrintHTML() {
|
||||
super();
|
||||
models = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.HTML;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
try {
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
if (!pxlOpts.isLegacy()) {
|
||||
WebApp.initialize();
|
||||
}
|
||||
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.getJSONObject(i);
|
||||
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||
|
||||
String source;
|
||||
switch(flavor) {
|
||||
case FILE:
|
||||
case PLAIN:
|
||||
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
|
||||
source = data.getString("data");
|
||||
break;
|
||||
default:
|
||||
source = new String(flavor.read(data.getString("data")), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
double pageZoom = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()) / 72.0;
|
||||
if (pageZoom <= 1) { pageZoom = 1; }
|
||||
|
||||
double pageWidth = 0;
|
||||
double pageHeight = 0;
|
||||
double convertFactor = (72.0 / pxlOpts.getUnits().as1Inch());
|
||||
|
||||
boolean renderFromHeight = Arrays.asList(PrintOptions.Orientation.LANDSCAPE,
|
||||
PrintOptions.Orientation.REVERSE_LANDSCAPE).contains(pxlOpts.getOrientation());
|
||||
|
||||
if (pxlOpts.getSize() != null) {
|
||||
if (!renderFromHeight) {
|
||||
pageWidth = pxlOpts.getSize().getWidth() * convertFactor;
|
||||
} else {
|
||||
pageWidth = pxlOpts.getSize().getHeight() * convertFactor;
|
||||
}
|
||||
} else if (options.getDefaultOptions().getPageSize() != null) {
|
||||
if (!renderFromHeight) {
|
||||
pageWidth = options.getDefaultOptions().getPageSize().getWidth();
|
||||
} else {
|
||||
pageWidth = options.getDefaultOptions().getPageSize().getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
if (pxlOpts.getMargins() != null) {
|
||||
PrintOptions.Margins margins = pxlOpts.getMargins();
|
||||
if (!renderFromHeight || pxlOpts.isRasterize()) {
|
||||
pageWidth -= (margins.left() + margins.right()) * convertFactor;
|
||||
} else {
|
||||
pageWidth -= (margins.top() + margins.bottom()) * convertFactor; //due to vector margin matching
|
||||
}
|
||||
}
|
||||
|
||||
if (!data.isNull("options")) {
|
||||
JSONObject dataOpt = data.getJSONObject("options");
|
||||
|
||||
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
|
||||
pageWidth = dataOpt.optDouble("pageWidth") * convertFactor;
|
||||
}
|
||||
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
|
||||
pageHeight = dataOpt.optDouble("pageHeight") * convertFactor;
|
||||
}
|
||||
}
|
||||
|
||||
models.add(new WebAppModel(source, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, pxlOpts.isScaleContent(), pageZoom));
|
||||
}
|
||||
|
||||
log.debug("Parsed {} html records", models.size());
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new UnsupportedOperationException("Unable to start JavaFX service", e);
|
||||
}
|
||||
catch(NoClassDefFoundError e) {
|
||||
throw new UnsupportedOperationException("JavaFX libraries not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
if (options.getPixelOptions().isLegacy()) {
|
||||
printLegacy(output, options);
|
||||
} else if (options.getPixelOptions().isRasterize()) {
|
||||
//grab a snapshot of the pages for PrintImage instead of printing directly
|
||||
for(WebAppModel model : models) {
|
||||
try { images.add(WebApp.raster(model)); }
|
||||
catch(Throwable t) {
|
||||
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
|
||||
//probably a unrecognized image loader error, try at default zoom
|
||||
try {
|
||||
log.warn("Capture failed with increased zoom, attempting with default value");
|
||||
model.setZoom(1);
|
||||
images.add(WebApp.raster(model));
|
||||
}
|
||||
catch(Throwable tt) {
|
||||
throw new PrinterException(tt.getMessage());
|
||||
}
|
||||
} else {
|
||||
throw new PrinterException(t.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
super.print(output, options);
|
||||
} else {
|
||||
Printer fxPrinter = null;
|
||||
for(Printer p : Printer.getAllPrinters()) {
|
||||
if (p.getName().equals(output.getPrintService().getName())) {
|
||||
fxPrinter = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (fxPrinter == null) {
|
||||
throw new PrinterException("Cannot find printer under the JavaFX libraries");
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.createPrinterJob(fxPrinter);
|
||||
|
||||
|
||||
// apply option settings
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
JobSettings settings = job.getJobSettings();
|
||||
settings.setJobName(pxlOpts.getJobName(Constants.HTML_PRINT));
|
||||
settings.setPrintQuality(PrintQuality.HIGH);
|
||||
|
||||
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
|
||||
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
|
||||
settings.setPrintColor(getColor(pxlOpts));
|
||||
}
|
||||
if (pxlOpts.getDuplex() == Sides.DUPLEX || pxlOpts.getDuplex() == Sides.TWO_SIDED_LONG_EDGE) {
|
||||
settings.setPrintSides(PrintSides.DUPLEX);
|
||||
}
|
||||
if (pxlOpts.getDuplex() == Sides.TUMBLE || pxlOpts.getDuplex() == Sides.TWO_SIDED_SHORT_EDGE) {
|
||||
settings.setPrintSides(PrintSides.TUMBLE);
|
||||
}
|
||||
if (pxlOpts.getPrinterTray() != null) {
|
||||
PaperSource tray = findFXTray(fxPrinter.getPrinterAttributes().getSupportedPaperSources(), pxlOpts.getPrinterTray());
|
||||
if (tray != null) {
|
||||
settings.setPaperSource(tray);
|
||||
}
|
||||
}
|
||||
|
||||
if (pxlOpts.getDensity() > 0) {
|
||||
settings.setPrintResolution(PrintHelper.createPrintResolution((int)pxlOpts.getDensity(), (int)pxlOpts.getDensity()));
|
||||
}
|
||||
|
||||
Paper paper;
|
||||
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
|
||||
double convert = 1;
|
||||
Units units = getUnits(pxlOpts);
|
||||
if (units == null) {
|
||||
convert = 10; //need to adjust from cm to mm only for DPCM sizes
|
||||
units = Units.MM;
|
||||
}
|
||||
paper = PrintHelper.createPaper("Custom", pxlOpts.getSize().getWidth() * convert, pxlOpts.getSize().getHeight() * convert, units);
|
||||
} else {
|
||||
PrintOptions.Size paperSize = options.getDefaultOptions().getPageSize();
|
||||
paper = PrintHelper.createPaper("Default", paperSize.getWidth(), paperSize.getHeight(), Units.POINT);
|
||||
}
|
||||
|
||||
PageOrientation orient = fxPrinter.getPrinterAttributes().getDefaultPageOrientation();
|
||||
if (pxlOpts.getOrientation() != null) {
|
||||
orient = getOrientation(pxlOpts);
|
||||
}
|
||||
|
||||
try {
|
||||
PageLayout layout;
|
||||
PrintOptions.Margins m = pxlOpts.getMargins();
|
||||
if (m != null) {
|
||||
//force access to the page layout constructor as the adjusted margins on small sizes are wildly inaccurate
|
||||
Constructor<PageLayout> plCon = PageLayout.class.getDeclaredConstructor(Paper.class, PageOrientation.class, double.class, double.class, double.class, double.class);
|
||||
plCon.setAccessible(true);
|
||||
|
||||
//margins defined as pnt (1/72nds)
|
||||
double asPnt = pxlOpts.getUnits().toInches() * 72;
|
||||
if (orient == PageOrientation.PORTRAIT || orient == PageOrientation.REVERSE_PORTRAIT) {
|
||||
layout = plCon.newInstance(paper, orient, m.left() * asPnt, m.right() * asPnt, m.top() * asPnt, m.bottom() * asPnt);
|
||||
} else {
|
||||
//rotate margins to match raster prints
|
||||
layout = plCon.newInstance(paper, orient, m.top() * asPnt, m.bottom() * asPnt, m.right() * asPnt, m.left() * asPnt);
|
||||
}
|
||||
} else {
|
||||
//if margins are not provided, use default paper margins
|
||||
PageLayout valid = fxPrinter.getDefaultPageLayout();
|
||||
layout = fxPrinter.createPageLayout(paper, orient, valid.getLeftMargin(), valid.getRightMargin(), valid.getTopMargin(), valid.getBottomMargin());
|
||||
}
|
||||
|
||||
//force our layout as the default to avoid default-margin exceptions on small paper sizes
|
||||
Field field = fxPrinter.getClass().getDeclaredField("defPageLayout");
|
||||
field.setAccessible(true);
|
||||
field.set(fxPrinter, layout);
|
||||
|
||||
settings.setPageLayout(layout);
|
||||
}
|
||||
catch(Exception e) {
|
||||
log.error("Failed to set custom layout", e);
|
||||
}
|
||||
|
||||
settings.setCopies(pxlOpts.getCopies());
|
||||
log.trace("{}", settings.toString());
|
||||
|
||||
//javaFX lies about this value, so pull from original print service
|
||||
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
|
||||
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], null);
|
||||
|
||||
try {
|
||||
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
|
||||
for(WebAppModel model : models) {
|
||||
WebApp.print(job, model);
|
||||
}
|
||||
} else {
|
||||
settings.setCopies(1); //manually handle copies if they are not supported
|
||||
for(int i = 0; i < pxlOpts.getCopies(); i++) {
|
||||
for(WebAppModel model : models) {
|
||||
WebApp.print(job, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Throwable t) {
|
||||
job.cancelJob();
|
||||
throw new PrinterException(t.getMessage());
|
||||
}
|
||||
|
||||
//send pending prints
|
||||
job.endJob();
|
||||
}
|
||||
}
|
||||
|
||||
private void printLegacy(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
|
||||
java.awt.print.PrinterJob job = java.awt.print.PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
|
||||
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||
|
||||
//setup swing ui
|
||||
JFrame legacyFrame = new JFrame(pxlOpts.getJobName(Constants.HTML_PRINT));
|
||||
legacyFrame.setUndecorated(true);
|
||||
legacyFrame.setLayout(new FlowLayout());
|
||||
legacyFrame.setExtendedState(Frame.ICONIFIED);
|
||||
|
||||
legacyLabel = new JLabel();
|
||||
legacyLabel.setOpaque(true);
|
||||
legacyLabel.setBackground(Color.WHITE);
|
||||
legacyLabel.setBorder(null);
|
||||
legacyLabel.setDoubleBuffered(false);
|
||||
|
||||
legacyFrame.add(legacyLabel);
|
||||
|
||||
try {
|
||||
for(WebAppModel model : models) {
|
||||
if (model.isPlainText()) {
|
||||
legacyLabel.setText(cleanHtmlContent(model.getSource()));
|
||||
} else {
|
||||
try(InputStream fis = new URL(model.getSource()).openStream()) {
|
||||
String webPage = cleanHtmlContent(IOUtils.toString(fis, "UTF-8"));
|
||||
legacyLabel.setText(webPage);
|
||||
}
|
||||
}
|
||||
|
||||
legacyFrame.pack();
|
||||
legacyFrame.setVisible(true);
|
||||
|
||||
job.setPrintable(this);
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new PrinterException(e.getMessage());
|
||||
}
|
||||
finally {
|
||||
legacyFrame.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private String cleanHtmlContent(String html) {
|
||||
return html.replaceAll("^[\\s\\S]*<(HTML|html)\\b.*?>", "<html>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||
if (legacyLabel == null) {
|
||||
return super.print(graphics, pageFormat, pageIndex);
|
||||
} else {
|
||||
if (graphics == null) { throw new PrinterException("No graphics specified"); }
|
||||
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
|
||||
|
||||
if (pageIndex + 1 > models.size()) {
|
||||
return NO_SUCH_PAGE;
|
||||
}
|
||||
log.trace("Requested page {} for printing", pageIndex);
|
||||
|
||||
Graphics2D graphics2D = (Graphics2D)graphics;
|
||||
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
graphics2D.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
|
||||
graphics2D.scale(pageFormat.getImageableWidth() / pageFormat.getWidth(), pageFormat.getImageableHeight() / pageFormat.getHeight());
|
||||
legacyLabel.paint(graphics2D);
|
||||
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
super.cleanup();
|
||||
|
||||
models.clear();
|
||||
legacyLabel = null;
|
||||
}
|
||||
|
||||
public static Units getUnits(PrintOptions.Pixel opts) {
|
||||
switch(opts.getUnits()) {
|
||||
case INCH:
|
||||
return Units.INCH;
|
||||
case MM:
|
||||
return Units.MM;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static PageOrientation getOrientation(PrintOptions.Pixel opts) {
|
||||
switch(opts.getOrientation()) {
|
||||
case LANDSCAPE:
|
||||
return PageOrientation.LANDSCAPE;
|
||||
case REVERSE_LANDSCAPE:
|
||||
return PageOrientation.REVERSE_LANDSCAPE;
|
||||
case REVERSE_PORTRAIT:
|
||||
return PageOrientation.REVERSE_PORTRAIT;
|
||||
default:
|
||||
return PageOrientation.PORTRAIT;
|
||||
}
|
||||
}
|
||||
|
||||
public static PrintColor getColor(PrintOptions.Pixel opts) {
|
||||
switch(opts.getColorType()) {
|
||||
case COLOR:
|
||||
return PrintColor.COLOR;
|
||||
default:
|
||||
return PrintColor.MONOCHROME;
|
||||
}
|
||||
}
|
||||
}
|
||||
341
old code/tray/src/qz/printer/action/PrintImage.java
Executable file
341
old code/tray/src/qz/printer/action/PrintImage.java
Executable file
@@ -0,0 +1,341 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*
|
||||
*/
|
||||
package qz.printer.action;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.ConnectionUtilities;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.imageio.IIOException;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.ResolutionSyntax;
|
||||
import javax.print.attribute.standard.OrientationRequested;
|
||||
import javax.print.attribute.standard.PrinterResolution;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.Rectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* @author Tres Finocchiaro, Anton Mezerny
|
||||
*/
|
||||
public class PrintImage extends PrintPixel implements PrintProcessor, Printable {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintImage.class);
|
||||
|
||||
protected List<BufferedImage> images;
|
||||
|
||||
protected double dpiScale = 1;
|
||||
protected boolean scaleImage = false;
|
||||
protected Object dithering = RenderingHints.VALUE_DITHER_DEFAULT;
|
||||
protected Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
|
||||
protected double imageRotation = 0;
|
||||
protected boolean manualReverse = false;
|
||||
|
||||
public PrintImage() {
|
||||
images = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.IMAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
dpiScale = (options.getPixelOptions().getDensity() * options.getPixelOptions().getUnits().as1Inch()) / 72.0;
|
||||
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.getJSONObject(i);
|
||||
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||
|
||||
try {
|
||||
BufferedImage bi;
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' image, assume it's a URL
|
||||
case FILE:
|
||||
bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data"), true));
|
||||
break;
|
||||
default:
|
||||
bi = ImageIO.read(new ByteArrayInputStream(flavor.read(data.getString("data"))));
|
||||
}
|
||||
|
||||
images.add(bi);
|
||||
}
|
||||
catch(IIOException e) {
|
||||
if (e.getCause() != null && e.getCause() instanceof FileNotFoundException) {
|
||||
throw new UnsupportedOperationException("Image file specified could not be found.", e);
|
||||
} else {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image", flavor, data.getString("data")), e);
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Parsed {} images for printing", images.size());
|
||||
}
|
||||
|
||||
private List<BufferedImage> breakupOverPages(BufferedImage img, PageFormat page, PrintRequestAttributeSet attributes) {
|
||||
List<BufferedImage> splits = new ArrayList<>();
|
||||
|
||||
Rectangle printBounds = new Rectangle(0, 0, (int)page.getImageableWidth(), (int)page.getImageableHeight());
|
||||
PrinterResolution res = (PrinterResolution)attributes.get(PrinterResolution.class);
|
||||
float dpi = res.getFeedResolution(1) / (float)ResolutionSyntax.DPI;
|
||||
float cdpi = res.getCrossFeedResolution(1) / (float)ResolutionSyntax.DPI;
|
||||
|
||||
//printing uses 72dpi, convert so we can check split size correctly
|
||||
int useWidth = (int)((img.getWidth() / cdpi) * 72);
|
||||
int useHeight = (int)((img.getHeight() / dpi) * 72);
|
||||
|
||||
int columnsNeed = (int)Math.ceil(useWidth / page.getImageableWidth());
|
||||
int rowsNeed = (int)Math.ceil(useHeight / page.getImageableHeight());
|
||||
|
||||
if (columnsNeed == 1 && rowsNeed == 1) {
|
||||
log.trace("Unscaled image does not need spit");
|
||||
splits.add(img);
|
||||
} else {
|
||||
log.trace("Image to be printed across {} pages", columnsNeed * rowsNeed);
|
||||
//allows us to split the image at the actual dpi instead of 72
|
||||
float upscale = dpi / 72f;
|
||||
float c_upscale = cdpi / 72f;
|
||||
|
||||
for(int row = 0; row < rowsNeed; row++) {
|
||||
for(int col = 0; col < columnsNeed; col++) {
|
||||
Rectangle clip = new Rectangle((col * (int)(printBounds.width * c_upscale)), (row * (int)(printBounds.height * upscale)),
|
||||
(int)(printBounds.width * c_upscale), (int)(printBounds.height * upscale));
|
||||
|
||||
if (clip.x + clip.width > img.getWidth()) { clip.width = img.getWidth() - clip.x; }
|
||||
if (clip.y + clip.height > img.getHeight()) { clip.height = img.getHeight() - clip.y; }
|
||||
|
||||
splits.add(img.getSubimage(clip.x, clip.y, clip.width, clip.height));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return splits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
if (images.isEmpty()) {
|
||||
log.warn("Nothing to print");
|
||||
return;
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||
|
||||
scaleImage = pxlOpts.isScaleContent();
|
||||
dithering = pxlOpts.getDithering();
|
||||
interpolation = pxlOpts.getInterpolation();
|
||||
imageRotation = pxlOpts.getRotation();
|
||||
|
||||
//reverse fix for OSX
|
||||
if (SystemUtilities.isMac() && pxlOpts.getOrientation() != null
|
||||
&& pxlOpts.getOrientation().getAsOrientRequested() == OrientationRequested.REVERSE_LANDSCAPE) {
|
||||
imageRotation += 180;
|
||||
manualReverse = true;
|
||||
}
|
||||
|
||||
if (!scaleImage) {
|
||||
//breakup large images to print across pages as needed
|
||||
List<BufferedImage> split = new ArrayList<>();
|
||||
for(BufferedImage bi : images) {
|
||||
split.addAll(breakupOverPages(bi, page, attributes));
|
||||
}
|
||||
images = split;
|
||||
}
|
||||
|
||||
job.setJobName(pxlOpts.getJobName(Constants.IMAGE_PRINT));
|
||||
job.setPrintable(this, job.validatePage(page));
|
||||
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||
if (graphics == null) { throw new PrinterException("No graphics specified"); }
|
||||
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
|
||||
|
||||
if (pageIndex + 1 > images.size()) {
|
||||
return NO_SUCH_PAGE;
|
||||
}
|
||||
log.trace("Requested page {} for printing", pageIndex);
|
||||
|
||||
if ("sun.print.PeekGraphics".equals(graphics.getClass().getCanonicalName())) {
|
||||
//java uses class only to query if a page needs printed - save memory/time by short circuiting
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
|
||||
|
||||
//allows pages view to rotate in different orientations
|
||||
graphics.drawString(" ", 0, 0);
|
||||
|
||||
BufferedImage imgToPrint = fixColorModel(images.get(pageIndex));
|
||||
if (imageRotation % 360 != 0) {
|
||||
imgToPrint = rotate(imgToPrint, imageRotation);
|
||||
}
|
||||
|
||||
// apply image scaling
|
||||
double boundW = pageFormat.getImageableWidth();
|
||||
double boundH = pageFormat.getImageableHeight();
|
||||
|
||||
double imgW = imgToPrint.getWidth() / dpiScale;
|
||||
double imgH = imgToPrint.getHeight() / dpiScale;
|
||||
|
||||
if (scaleImage) {
|
||||
imgToPrint = scale(imgToPrint, pageFormat);
|
||||
|
||||
// adjust dimensions to smallest edge, keeping size ratio
|
||||
if (((float)imgToPrint.getWidth() / (float)imgToPrint.getHeight()) >= (boundW / boundH)) {
|
||||
imgW = boundW;
|
||||
imgH = (imgToPrint.getHeight() / (imgToPrint.getWidth() / boundW));
|
||||
} else {
|
||||
imgW = (imgToPrint.getWidth() / (imgToPrint.getHeight() / boundH));
|
||||
imgH = boundH;
|
||||
}
|
||||
}
|
||||
|
||||
double boundX = pageFormat.getImageableX();
|
||||
double boundY = pageFormat.getImageableY();
|
||||
|
||||
log.debug("Paper area: {},{}:{},{}", (int)boundX, (int)boundY, (int)boundW, (int)boundH);
|
||||
log.trace("Image size: {},{}", imgW, imgH);
|
||||
|
||||
// Now we perform our rendering
|
||||
Graphics2D graphics2D = (Graphics2D)graphics;
|
||||
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
log.trace("{}", graphics2D.getRenderingHints());
|
||||
|
||||
log.debug("Memory: {}m/{}m", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576, Runtime.getRuntime().maxMemory() / 1048576);
|
||||
|
||||
if (!manualReverse) {
|
||||
graphics2D.drawImage(imgToPrint, (int)boundX, (int)boundY, (int)(boundX + imgW), (int)(boundY + imgH),
|
||||
0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null);
|
||||
} else {
|
||||
graphics2D.drawImage(imgToPrint, (int)(boundW + boundX - imgW), (int)(boundH + boundY - imgH), (int)(boundW + boundX), (int)(boundH + boundY),
|
||||
0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null);
|
||||
}
|
||||
|
||||
// Valid page
|
||||
return PAGE_EXISTS;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param image
|
||||
* @param pageFormat
|
||||
* @return
|
||||
*/
|
||||
private BufferedImage scale(BufferedImage image, PageFormat pageFormat) {
|
||||
//scale up to print density (using less of a stretch if image is already larger than page)
|
||||
double upScale = dpiScale * Math.min((pageFormat.getImageableWidth() / image.getWidth()), (pageFormat.getImageableHeight() / image.getHeight()));
|
||||
if (upScale > dpiScale) { upScale = dpiScale; } else if (upScale < 1) { upScale = 1; }
|
||||
|
||||
if (upScale > 1) {
|
||||
log.debug("Scaling image up by x{}", upScale);
|
||||
|
||||
BufferedImage scaled = new BufferedImage((int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = scaled.createGraphics();
|
||||
g2d.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
g2d.drawImage(image, 0, 0, (int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), null);
|
||||
g2d.dispose();
|
||||
|
||||
return scaled;
|
||||
} else {
|
||||
log.debug("No need to upscale image");
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotates {@code image} by the specified {@code angle}.
|
||||
*
|
||||
* @param image BufferedImage to rotate
|
||||
* @param angle Rotation angle in degrees
|
||||
* @return Rotated image data
|
||||
*/
|
||||
public static BufferedImage rotate(BufferedImage image, double angle, Object dithering, Object interpolation) {
|
||||
double rads = Math.toRadians(angle);
|
||||
double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
|
||||
|
||||
int sWidth = image.getWidth(), sHeight = image.getHeight();
|
||||
int eWidth = (int)Math.floor((sWidth * cos) + (sHeight * sin)), eHeight = (int)Math.floor((sHeight * cos) + (sWidth * sin));
|
||||
|
||||
BufferedImage result;
|
||||
if(!GraphicsEnvironment.isHeadless()) {
|
||||
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0].getDefaultConfiguration();
|
||||
result = gc.createCompatibleImage(eWidth, eHeight, Transparency.TRANSLUCENT);
|
||||
} else {
|
||||
result = new BufferedImage(eWidth, eHeight, Transparency.TRANSLUCENT);
|
||||
}
|
||||
|
||||
Graphics2D g2d = result.createGraphics();
|
||||
g2d.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||
g2d.translate((eWidth - sWidth) / 2, (eHeight - sHeight) / 2);
|
||||
g2d.rotate(rads, sWidth / 2, sHeight / 2);
|
||||
|
||||
if (angle % 90 == 0 || interpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
|
||||
g2d.drawRenderedImage(image, null);
|
||||
} else {
|
||||
g2d.setPaint(new TexturePaint(image, new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight())));
|
||||
g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||
}
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private BufferedImage rotate(BufferedImage image, double angle) {
|
||||
return rotate(image, angle, dithering, interpolation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
images.clear();
|
||||
|
||||
dpiScale = 1.0;
|
||||
scaleImage = false;
|
||||
imageRotation = 0;
|
||||
dithering = RenderingHints.VALUE_DITHER_DEFAULT;
|
||||
interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
|
||||
manualReverse = false;
|
||||
}
|
||||
|
||||
}
|
||||
326
old code/tray/src/qz/printer/action/PrintPDF.java
Executable file
326
old code/tray/src/qz/printer/action/PrintPDF.java
Executable file
@@ -0,0 +1,326 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.pdfbox.io.IOUtils;
|
||||
import org.apache.pdfbox.multipdf.Splitter;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||
import org.apache.pdfbox.printing.Scaling;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import qz.common.Constants;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.printer.action.pdf.BookBundle;
|
||||
import qz.printer.action.pdf.PDFWrapper;
|
||||
import qz.utils.ConnectionUtilities;
|
||||
import qz.utils.PrintingUtilities;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.Media;
|
||||
import javax.print.attribute.standard.MediaPrintableArea;
|
||||
import java.awt.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Paper;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class PrintPDF extends PrintPixel implements PrintProcessor {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintPDF.class);
|
||||
|
||||
private List<PDDocument> originals;
|
||||
private List<PDDocument> printables;
|
||||
private Splitter splitter = new Splitter();
|
||||
|
||||
private double docWidth = 0;
|
||||
private double docHeight = 0;
|
||||
private boolean ignoreTransparency = false;
|
||||
private boolean altFontRendering = false;
|
||||
|
||||
|
||||
public PrintPDF() {
|
||||
originals = new ArrayList<>();
|
||||
printables = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.PDF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
double convert = 72.0 / pxlOpts.getUnits().as1Inch();
|
||||
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.getJSONObject(i);
|
||||
HashSet<Integer> pagesToPrint = new HashSet<>();
|
||||
|
||||
if (!data.isNull("options")) {
|
||||
JSONObject dataOpt = data.getJSONObject("options");
|
||||
|
||||
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
|
||||
docWidth = dataOpt.optDouble("pageWidth") * convert;
|
||||
}
|
||||
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
|
||||
docHeight = dataOpt.optDouble("pageHeight") * convert;
|
||||
}
|
||||
|
||||
ignoreTransparency = dataOpt.optBoolean("ignoreTransparency", false);
|
||||
altFontRendering = dataOpt.optBoolean("altFontRendering", false);
|
||||
|
||||
if (!dataOpt.isNull("pageRanges")) {
|
||||
String[] ranges = dataOpt.optString("pageRanges", "").split(",");
|
||||
for(String range : ranges) {
|
||||
range = range.trim();
|
||||
if(range.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] period = range.split("-");
|
||||
|
||||
try {
|
||||
int start = Integer.parseInt(period[0]);
|
||||
pagesToPrint.add(start);
|
||||
|
||||
if (period.length > 1) {
|
||||
int end = Integer.parseInt(period[period.length - 1]);
|
||||
pagesToPrint.addAll(IntStream.rangeClosed(start, end).boxed().collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
catch(NumberFormatException nfe) {
|
||||
log.warn("Unable to parse page range {}.", range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||
|
||||
try {
|
||||
PDDocument doc;
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
|
||||
case FILE:
|
||||
doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data"), true));
|
||||
break;
|
||||
default:
|
||||
doc = PDDocument.load(new ByteArrayInputStream(flavor.read(data.getString("data"))));
|
||||
}
|
||||
|
||||
if (pxlOpts.getBounds() != null) {
|
||||
PrintOptions.Bounds bnd = pxlOpts.getBounds();
|
||||
|
||||
for(PDPage page : doc.getPages()) {
|
||||
PDRectangle box = new PDRectangle(
|
||||
(float)(bnd.getX() * convert),
|
||||
page.getMediaBox().getUpperRightY() - (float)((bnd.getHeight() + bnd.getY()) * convert),
|
||||
(float)(bnd.getWidth() * convert),
|
||||
(float)(bnd.getHeight() * convert));
|
||||
page.setMediaBox(box);
|
||||
}
|
||||
}
|
||||
|
||||
if (pagesToPrint.isEmpty()) {
|
||||
pagesToPrint.addAll(IntStream.rangeClosed(1, doc.getNumberOfPages()).boxed().collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
originals.add(doc);
|
||||
|
||||
List<PDDocument> splitPages = splitter.split(doc);
|
||||
originals.addAll(splitPages); //ensures non-ranged page will still get closed
|
||||
|
||||
for(int pg = 0; pg < splitPages.size(); pg++) {
|
||||
if (pagesToPrint.contains(pg + 1)) { //ranges are 1-indexed
|
||||
printables.add(splitPages.get(pg));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(FileNotFoundException e) {
|
||||
throw new UnsupportedOperationException("PDF file specified could not be found.", e);
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as a PDF file: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("Parsed {} files for printing", printables.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) {
|
||||
if (pxlOpts.getOrientation() != null) {
|
||||
//page orient does not set properly on pdfs with orientation requested attribute
|
||||
page.setOrientation(pxlOpts.getOrientation().getAsOrientFormat());
|
||||
}
|
||||
|
||||
return super.applyDefaultSettings(pxlOpts, page, supported);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||
if (printables.isEmpty()) {
|
||||
log.warn("Nothing to print");
|
||||
return;
|
||||
}
|
||||
|
||||
PrinterJob job = PrinterJob.getPrinterJob();
|
||||
job.setPrintService(output.getPrintService());
|
||||
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
Scaling scale = (pxlOpts.isScaleContent()? Scaling.SCALE_TO_FIT:Scaling.ACTUAL_SIZE);
|
||||
|
||||
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, job.getPageFormat(null), (Media[])output.getPrintService().getSupportedAttributeValues(Media.class, null, null));
|
||||
|
||||
// Disable attributes per https://github.com/qzind/tray/issues/174
|
||||
if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+202")) < 0) {
|
||||
log.warn("MacOS and Java < 1.8.0u202 cannot use attributes with PDF prints, disabling");
|
||||
attributes.clear();
|
||||
}
|
||||
|
||||
RenderingHints hints = new RenderingHints(buildRenderingHints(pxlOpts.getDithering(), pxlOpts.getInterpolation()));
|
||||
double useDensity = pxlOpts.getDensity();
|
||||
|
||||
if (!pxlOpts.isRasterize()) {
|
||||
if (pxlOpts.getDensity() > 0) {
|
||||
// clear density for vector prints (applied via print attributes instead)
|
||||
useDensity = 0;
|
||||
} else if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+121")) < 0) {
|
||||
log.warn("OSX systems cannot print vector PDF's, forcing raster to prevent crash.");
|
||||
useDensity = options.getDefaultOptions().getDensity();
|
||||
}
|
||||
}
|
||||
|
||||
BookBundle bundle = new BookBundle();
|
||||
|
||||
for(PDDocument doc : printables) {
|
||||
PageFormat page = job.getPageFormat(null);
|
||||
applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||
|
||||
//trick pdfbox into an alternate doc size if specified
|
||||
if (docWidth > 0 || docHeight > 0) {
|
||||
Paper paper = page.getPaper();
|
||||
|
||||
if (docWidth <= 0) { docWidth = page.getImageableWidth(); }
|
||||
if (docHeight <= 0) { docHeight = page.getImageableHeight(); }
|
||||
|
||||
paper.setImageableArea(paper.getImageableX(), paper.getImageableY(), docWidth, docHeight);
|
||||
page.setPaper(paper);
|
||||
|
||||
scale = Scaling.SCALE_TO_FIT; //to get custom size we need to force scaling
|
||||
|
||||
//pdf uses imageable area from Paper, so this can be safely removed
|
||||
attributes.remove(MediaPrintableArea.class);
|
||||
}
|
||||
|
||||
for(PDPage pd : doc.getPages()) {
|
||||
if (pxlOpts.getRotation() % 360 != 0) {
|
||||
rotatePage(doc, pd, pxlOpts.getRotation());
|
||||
}
|
||||
|
||||
if (pxlOpts.getOrientation() == null) {
|
||||
PDRectangle bounds = pd.getBBox();
|
||||
if ((page.getImageableHeight() > page.getImageableWidth() && bounds.getWidth() > bounds.getHeight()) ^ (pd.getRotation() / 90) % 2 == 1) {
|
||||
log.info("Adjusting orientation to print landscape PDF source");
|
||||
page.setOrientation(PrintOptions.Orientation.LANDSCAPE.getAsOrientFormat());
|
||||
}
|
||||
} else if (pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) {
|
||||
//flip imageable area dimensions when in landscape
|
||||
Paper repap = page.getPaper();
|
||||
repap.setImageableArea(repap.getImageableX(), repap.getImageableY(), repap.getImageableHeight(), repap.getImageableWidth());
|
||||
page.setPaper(repap);
|
||||
|
||||
//reverse fix for OSX
|
||||
if (SystemUtilities.isMac() && pxlOpts.getOrientation() == PrintOptions.Orientation.REVERSE_LANDSCAPE) {
|
||||
pd.setRotation(pd.getRotation() + 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PDFWrapper wrapper = new PDFWrapper(doc, scale, false, ignoreTransparency, altFontRendering,
|
||||
(float)(useDensity * pxlOpts.getUnits().as1Inch()),
|
||||
false, pxlOpts.getOrientation(), hints);
|
||||
|
||||
bundle.append(wrapper, page, doc.getNumberOfPages());
|
||||
}
|
||||
|
||||
if (pxlOpts.getSpoolSize() > 0 && bundle.getNumberOfPages() > pxlOpts.getSpoolSize()) {
|
||||
int jobNum = 1;
|
||||
int offset = 0;
|
||||
while(offset < bundle.getNumberOfPages()) {
|
||||
job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT) + "-" + jobNum++);
|
||||
job.setPageable(bundle.wrapAndPresent(offset, pxlOpts.getSpoolSize()));
|
||||
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
|
||||
offset += pxlOpts.getSpoolSize();
|
||||
}
|
||||
} else {
|
||||
job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT));
|
||||
job.setPageable(bundle.wrapAndPresent());
|
||||
|
||||
printCopies(output, pxlOpts, job, attributes);
|
||||
}
|
||||
}
|
||||
|
||||
private void rotatePage(PDDocument doc, PDPage page, double rotation) {
|
||||
try {
|
||||
//copy page to object for manipulation
|
||||
PDFormXObject xobject = new PDFormXObject(doc);
|
||||
InputStream src = page.getContents();
|
||||
OutputStream dest = xobject.getStream().createOutputStream();
|
||||
|
||||
try { IOUtils.copy(src, dest); }
|
||||
finally {
|
||||
IOUtils.closeQuietly(src);
|
||||
IOUtils.closeQuietly(dest);
|
||||
}
|
||||
|
||||
xobject.setResources(page.getResources());
|
||||
xobject.setBBox(page.getBBox());
|
||||
|
||||
//draw our object at a rotated angle
|
||||
AffineTransform transform = new AffineTransform();
|
||||
transform.rotate(Math.toRadians(360 - rotation), xobject.getBBox().getWidth() / 2.0, xobject.getBBox().getHeight() / 2.0);
|
||||
xobject.setMatrix(transform);
|
||||
|
||||
PDPageContentStream stream = new PDPageContentStream(doc, page);
|
||||
stream.drawForm(xobject);
|
||||
stream.close();
|
||||
}
|
||||
catch(IOException e) {
|
||||
log.warn("Failed to rotate PDF page for printing");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
for(PDDocument doc : originals) {
|
||||
try { doc.close(); } catch(IOException ignore) {}
|
||||
}
|
||||
|
||||
originals.clear();
|
||||
printables.clear();
|
||||
|
||||
docWidth = 0;
|
||||
docHeight = 0;
|
||||
ignoreTransparency = false;
|
||||
altFontRendering = false;
|
||||
}
|
||||
}
|
||||
231
old code/tray/src/qz/printer/action/PrintPixel.java
Executable file
231
old code/tray/src/qz/printer/action/PrintPixel.java
Executable file
@@ -0,0 +1,231 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import javafx.print.PaperSource;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.SystemUtilities;
|
||||
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ColorModel;
|
||||
import java.awt.image.IndexColorModel;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Paper;
|
||||
import java.awt.print.PrinterException;
|
||||
import java.awt.print.PrinterJob;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class PrintPixel {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintPixel.class);
|
||||
|
||||
private static final List<Integer> MAC_BAD_IMAGE_TYPES = Arrays.asList(BufferedImage.TYPE_BYTE_BINARY, BufferedImage.TYPE_CUSTOM);
|
||||
|
||||
|
||||
protected PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) {
|
||||
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||
|
||||
//apply general attributes
|
||||
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
|
||||
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
|
||||
attributes.add(pxlOpts.getColorType().getAsChromaticity());
|
||||
}
|
||||
attributes.add(pxlOpts.getDuplex());
|
||||
if (pxlOpts.getOrientation() != null) {
|
||||
attributes.add(pxlOpts.getOrientation().getAsOrientRequested());
|
||||
}
|
||||
if (pxlOpts.getPrinterTray() != null && !pxlOpts.getPrinterTray().isEmpty()) {
|
||||
Media tray = findMediaTray(supported, pxlOpts.getPrinterTray());
|
||||
if (tray != null) {
|
||||
attributes.add(tray);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO - set paper thickness
|
||||
|
||||
|
||||
// Java prints using inches at 72dpi
|
||||
final float CONVERT = pxlOpts.getUnits().toInches() * 72f;
|
||||
|
||||
log.trace("DPI: [{}x{}]\tCNV: {}", pxlOpts.getDensity(), pxlOpts.getCrossDensity(), CONVERT);
|
||||
if (pxlOpts.getDensity() > 0) {
|
||||
double cross = pxlOpts.getCrossDensity();
|
||||
if (cross == 0) { cross = pxlOpts.getDensity(); }
|
||||
|
||||
attributes.add(new PrinterResolution((int)cross, (int)pxlOpts.getDensity(), pxlOpts.getUnits().getDPIUnits()));
|
||||
}
|
||||
|
||||
//apply sizing and margins
|
||||
Paper paper = page.getPaper();
|
||||
|
||||
float pageX = 0f;
|
||||
float pageY = 0f;
|
||||
float pageW = (float)page.getWidth() / CONVERT;
|
||||
float pageH = (float)page.getHeight() / CONVERT;
|
||||
|
||||
//page size
|
||||
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
|
||||
pageW = (float)pxlOpts.getSize().getWidth();
|
||||
pageH = (float)pxlOpts.getSize().getHeight();
|
||||
|
||||
paper.setSize(pageW * CONVERT, pageH * CONVERT);
|
||||
}
|
||||
|
||||
//margins
|
||||
if (pxlOpts.getMargins() != null) {
|
||||
pageX += pxlOpts.getMargins().left();
|
||||
pageY += pxlOpts.getMargins().top();
|
||||
pageW -= (pxlOpts.getMargins().right() + pxlOpts.getMargins().left());
|
||||
pageH -= (pxlOpts.getMargins().bottom() + pxlOpts.getMargins().top());
|
||||
}
|
||||
|
||||
log.trace("Drawable area: {},{}:{},{}", pageX, pageY, pageW, pageH);
|
||||
if (pageW > 0 && pageH > 0) {
|
||||
attributes.add(new MediaPrintableArea(pageX, pageY, pageW, pageH, pxlOpts.getUnits().getMediaSizeUnits()));
|
||||
paper.setImageableArea(pageX * CONVERT, pageY * CONVERT, pageW * CONVERT, pageH * CONVERT);
|
||||
page.setPaper(paper);
|
||||
} else {
|
||||
log.warn("Could not apply custom size, using printer default");
|
||||
attributes.add(new MediaPrintableArea(0, 0, (float)page.getWidth() / 72f, (float)page.getHeight() / 72f, PrintOptions.Unit.INCH.getMediaSizeUnits()));
|
||||
}
|
||||
|
||||
log.trace("{}", Arrays.toString(attributes.toArray()));
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
|
||||
protected void printCopies(PrintOutput output, PrintOptions.Pixel pxlOpts, PrinterJob job, PrintRequestAttributeSet attributes) throws PrinterException {
|
||||
log.info("Starting printing ({} copies)", pxlOpts.getCopies());
|
||||
|
||||
PrinterResolution rUsing = (PrinterResolution)attributes.get(PrinterResolution.class);
|
||||
if (rUsing != null) {
|
||||
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
|
||||
if (!rSupport.isEmpty()) {
|
||||
if (!rSupport.contains(rUsing)) {
|
||||
log.warn("Not using a supported DPI for printing");
|
||||
log.debug("Available DPI: {}", ArrayUtils.toString(rSupport));
|
||||
}
|
||||
} else {
|
||||
log.warn("Supported printer densities not found");
|
||||
}
|
||||
}
|
||||
|
||||
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
|
||||
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], attributes);
|
||||
|
||||
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
|
||||
attributes.add(new Copies(pxlOpts.getCopies()));
|
||||
job.print(attributes);
|
||||
} else {
|
||||
for(int i = 0; i < pxlOpts.getCopies(); i++) {
|
||||
job.print(attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: Temporary fix for OS X 10.10 hard crash.
|
||||
* See https://github.com/qzind/qz-print/issues/75
|
||||
*/
|
||||
protected BufferedImage fixColorModel(BufferedImage imgToPrint) {
|
||||
if (SystemUtilities.isMac()) {
|
||||
if (MAC_BAD_IMAGE_TYPES.contains(imgToPrint.getType())) {
|
||||
BufferedImage sanitizedImage;
|
||||
ColorModel cm = imgToPrint.getColorModel();
|
||||
|
||||
if (cm instanceof IndexColorModel) {
|
||||
log.info("Image converted to 256 colors for OSX 10.10 Workaround");
|
||||
sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel)cm);
|
||||
} else {
|
||||
log.info("Image converted to ARGB for OSX 10.10 Workaround");
|
||||
sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
}
|
||||
|
||||
sanitizedImage.createGraphics().drawImage(imgToPrint, 0, 0, null);
|
||||
imgToPrint = sanitizedImage;
|
||||
}
|
||||
}
|
||||
|
||||
return imgToPrint;
|
||||
}
|
||||
|
||||
protected static Map<RenderingHints.Key,Object> buildRenderingHints(Object dithering, Object interpolation) {
|
||||
Map<RenderingHints.Key,Object> rhMap = new HashMap<>();
|
||||
rhMap.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||
rhMap.put(RenderingHints.KEY_DITHERING, dithering);
|
||||
rhMap.put(RenderingHints.KEY_INTERPOLATION, interpolation);
|
||||
rhMap.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||
rhMap.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
rhMap.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||
if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(interpolation)) {
|
||||
rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
||||
rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
||||
} else {
|
||||
rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
}
|
||||
|
||||
|
||||
return rhMap;
|
||||
}
|
||||
|
||||
protected Media findMediaTray(Media[] supportedMedia, String traySelection) {
|
||||
HashMap<String,Media> mediaTrays = new HashMap<>();
|
||||
for(Media m : supportedMedia) {
|
||||
if (m instanceof MediaTray) {
|
||||
mediaTrays.put(m.toString(), m);
|
||||
}
|
||||
}
|
||||
|
||||
String tray = findTray(mediaTrays.keySet(), traySelection);
|
||||
return mediaTrays.get(tray);
|
||||
}
|
||||
|
||||
protected PaperSource findFXTray(Set<PaperSource> paperSources, String traySelection) {
|
||||
Map<String,PaperSource> fxTrays = paperSources.stream().collect(Collectors.toMap(PaperSource::getName, Function.identity()));
|
||||
|
||||
String tray = findTray(fxTrays.keySet(), traySelection);
|
||||
return fxTrays.get(tray);
|
||||
}
|
||||
|
||||
private String findTray(Set<String> trayOptions, String traySelection) {
|
||||
Pattern exactPattern = Pattern.compile("\\b" + Pattern.quote(traySelection) + "\\b", Pattern.CASE_INSENSITIVE);
|
||||
Pattern fuzzyPattern = Pattern.compile("\\b.*?[" + Pattern.quote(traySelection) + "]+.*?\\b", Pattern.CASE_INSENSITIVE);
|
||||
String bestFit = null;
|
||||
Integer fuzzyFitDelta = null;
|
||||
|
||||
for(String option : trayOptions) {
|
||||
Matcher exactly = exactPattern.matcher(option.trim());
|
||||
Matcher fuzzily = fuzzyPattern.matcher(option.trim());
|
||||
|
||||
if (exactly.find()) {
|
||||
bestFit = option;
|
||||
break;
|
||||
}
|
||||
|
||||
while(fuzzily.find()) {
|
||||
//look for as close to exact match as possible
|
||||
int delta = Math.abs(fuzzily.group().length() - traySelection.length());
|
||||
if (fuzzyFitDelta == null || delta < fuzzyFitDelta) {
|
||||
fuzzyFitDelta = delta;
|
||||
bestFit = option;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestFit;
|
||||
}
|
||||
|
||||
}
|
||||
39
old code/tray/src/qz/printer/action/PrintProcessor.java
Executable file
39
old code/tray/src/qz/printer/action/PrintProcessor.java
Executable file
@@ -0,0 +1,39 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
import javax.print.PrintException;
|
||||
import java.awt.print.PrinterException;
|
||||
|
||||
public interface PrintProcessor {
|
||||
|
||||
|
||||
PrintingUtilities.Format getFormat();
|
||||
|
||||
/**
|
||||
* Used to parse information passed from the web API for printing.
|
||||
*
|
||||
* @param printData JSON Array of printer data
|
||||
* @param options Printing options to use for the print job
|
||||
*/
|
||||
void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException;
|
||||
|
||||
|
||||
/**
|
||||
* Used to setup and send documents to the specified printing {@code service}.
|
||||
*
|
||||
* @param output Destination used for printing
|
||||
* @param options Printing options to use for the print job
|
||||
*/
|
||||
void print(PrintOutput output, PrintOptions options) throws PrintException, PrinterException;
|
||||
|
||||
/**
|
||||
* Reset a processor back to it's initial state.
|
||||
*/
|
||||
void cleanup();
|
||||
|
||||
}
|
||||
530
old code/tray/src/qz/printer/action/PrintRaw.java
Executable file
530
old code/tray/src/qz/printer/action/PrintRaw.java
Executable file
@@ -0,0 +1,530 @@
|
||||
/**
|
||||
* @author Tres Finocchiaro
|
||||
*
|
||||
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
|
||||
*
|
||||
* LGPL 2.1 This is free software. This software and source code are released under
|
||||
* the "LGPL 2.1 License". A copy of this license should be distributed with
|
||||
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
|
||||
*/
|
||||
package qz.printer.action;
|
||||
|
||||
import com.ibm.icu.text.ArabicShapingException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.codehaus.jettison.json.JSONArray;
|
||||
import org.codehaus.jettison.json.JSONException;
|
||||
import org.codehaus.jettison.json.JSONObject;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import qz.common.ByteArrayBuilder;
|
||||
import qz.common.Constants;
|
||||
import qz.exception.NullCommandException;
|
||||
import qz.exception.NullPrintServiceException;
|
||||
import qz.printer.action.raw.ImageWrapper;
|
||||
import qz.printer.action.raw.LanguageType;
|
||||
import qz.printer.PrintOptions;
|
||||
import qz.printer.PrintOutput;
|
||||
import qz.printer.action.html.WebApp;
|
||||
import qz.printer.action.html.WebAppModel;
|
||||
import qz.printer.info.NativePrinter;
|
||||
import qz.printer.status.CupsUtils;
|
||||
import qz.utils.*;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.print.*;
|
||||
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||
import javax.print.attribute.PrintRequestAttributeSet;
|
||||
import javax.print.attribute.standard.JobName;
|
||||
import javax.print.event.PrintJobEvent;
|
||||
import javax.print.event.PrintJobListener;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Sends raw data to the printer, overriding your operating system's print
|
||||
* driver. Most useful for printers such as zebra card or barcode printers.
|
||||
*
|
||||
* @author A. Tres Finocchiaro
|
||||
*/
|
||||
public class PrintRaw implements PrintProcessor {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(PrintRaw.class);
|
||||
|
||||
private ByteArrayBuilder commands;
|
||||
|
||||
private String destEncoding = null;
|
||||
|
||||
private enum Backend {
|
||||
CUPS_RSS,
|
||||
CUPS_LPR,
|
||||
WIN32_WMI
|
||||
}
|
||||
|
||||
public PrintRaw() {
|
||||
commands = new ByteArrayBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintingUtilities.Format getFormat() {
|
||||
return PrintingUtilities.Format.COMMAND;
|
||||
}
|
||||
|
||||
private byte[] getBytes(String str, String destEncoding) throws ArabicShapingException, IOException {
|
||||
switch(destEncoding.toLowerCase(Locale.ENGLISH)) {
|
||||
case "ibm864":
|
||||
case "cp864":
|
||||
case "csibm864":
|
||||
case "864":
|
||||
case "ibm-864":
|
||||
return ArabicConversionUtilities.convertToIBM864(str);
|
||||
default:
|
||||
return str.getBytes(destEncoding);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||
for(int i = 0; i < printData.length(); i++) {
|
||||
JSONObject data = printData.optJSONObject(i);
|
||||
if (data == null) {
|
||||
data = new JSONObject();
|
||||
data.put("data", printData.getString(i));
|
||||
}
|
||||
|
||||
String cmd = data.getString("data");
|
||||
JSONObject opt = data.optJSONObject("options");
|
||||
if (opt == null) { opt = new JSONObject(); }
|
||||
|
||||
PrintingUtilities.Format format = PrintingUtilities.Format.valueOf(data.optString("format", "COMMAND").toUpperCase(Locale.ENGLISH));
|
||||
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN);
|
||||
PrintOptions.Raw rawOpts = options.getRawOptions();
|
||||
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||
|
||||
destEncoding = rawOpts.getDestEncoding();
|
||||
if (destEncoding == null || destEncoding.isEmpty()) { destEncoding = Charset.defaultCharset().name(); }
|
||||
|
||||
try {
|
||||
switch(format) {
|
||||
case HTML:
|
||||
commands.append(getHtmlWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||
break;
|
||||
case IMAGE:
|
||||
commands.append(getImageWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||
break;
|
||||
case PDF:
|
||||
commands.append(getPdfWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||
break;
|
||||
case COMMAND:
|
||||
default:
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
commands.append(getBytes(cmd, destEncoding));
|
||||
break;
|
||||
default:
|
||||
commands.append(seekConversion(flavor.read(cmd, opt.optString("xmlTag", null)), rawOpts));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch(Exception e) {
|
||||
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s into a raw %s command: %s", flavor, data.getString("data"), format, e.getLocalizedMessage()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] seekConversion(byte[] rawBytes, PrintOptions.Raw rawOpts) {
|
||||
if (rawOpts.getSrcEncoding() != null) {
|
||||
if(rawOpts.getSrcEncoding().equals(rawOpts.getDestEncoding()) || rawOpts.getDestEncoding() == null) {
|
||||
log.warn("Provided srcEncoding and destEncoding are the same, skipping");
|
||||
} else {
|
||||
try {
|
||||
String rawConvert = new String(rawBytes, rawOpts.getSrcEncoding());
|
||||
return rawConvert.getBytes(rawOpts.getDestEncoding());
|
||||
}
|
||||
catch(UnsupportedEncodingException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return rawBytes;
|
||||
}
|
||||
|
||||
private ImageWrapper getImageWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
BufferedImage bi;
|
||||
// 2.0 compat
|
||||
if (data.startsWith("data:image/") && data.contains(";base64,")) {
|
||||
String[] parts = data.split(";base64,");
|
||||
data = parts[parts.length - 1];
|
||||
flavor = PrintingUtilities.Flavor.BASE64;
|
||||
}
|
||||
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' image, assume it's a URL
|
||||
case FILE:
|
||||
bi = ImageIO.read(ConnectionUtilities.getInputStream(data, true));
|
||||
break;
|
||||
default:
|
||||
bi = ImageIO.read(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
|
||||
}
|
||||
|
||||
return getWrapper(bi, opt, pxlOpts);
|
||||
}
|
||||
|
||||
private ImageWrapper getPdfWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
PDDocument doc;
|
||||
|
||||
switch(flavor) {
|
||||
case PLAIN:
|
||||
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
|
||||
case FILE:
|
||||
doc = PDDocument.load(ConnectionUtilities.getInputStream(data, true));
|
||||
break;
|
||||
default:
|
||||
doc = PDDocument.load(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
|
||||
}
|
||||
|
||||
double scale;
|
||||
PDRectangle rect = doc.getPage(0).getBBox();
|
||||
double pw = opt.optDouble("pageWidth", 0), ph = opt.optDouble("pageHeight", 0);
|
||||
if (ph <= 0 || (pw > 0 && (rect.getWidth() / rect.getHeight()) >= (pw / ph))) {
|
||||
scale = pw / rect.getWidth();
|
||||
} else {
|
||||
scale = ph / rect.getHeight();
|
||||
}
|
||||
if (scale <= 0) { scale = 1.0; }
|
||||
|
||||
BufferedImage bi = new PDFRenderer(doc).renderImage(0, (float)scale);
|
||||
return getWrapper(bi, opt, pxlOpts);
|
||||
}
|
||||
|
||||
private ImageWrapper getHtmlWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
switch(flavor) {
|
||||
case FILE:
|
||||
case PLAIN:
|
||||
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
|
||||
break;
|
||||
default:
|
||||
data = new String(seekConversion(flavor.read(data), rawOpts), destEncoding);
|
||||
}
|
||||
|
||||
double density = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch());
|
||||
if (density <= 1) {
|
||||
density = LanguageType.getType(opt.optString("language")).getDefaultDensity();
|
||||
}
|
||||
double pageZoom = density / 72.0;
|
||||
|
||||
double pageWidth = opt.optInt("pageWidth") / density * 72;
|
||||
double pageHeight = opt.optInt("pageHeight") / density * 72;
|
||||
|
||||
BufferedImage bi;
|
||||
WebAppModel model = new WebAppModel(data, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, false, pageZoom);
|
||||
|
||||
try {
|
||||
WebApp.initialize(); //starts if not already started
|
||||
bi = WebApp.raster(model);
|
||||
|
||||
// down scale back from web density
|
||||
double scaleFactor = opt.optDouble("pageWidth", 0) / bi.getWidth();
|
||||
BufferedImage scaled = new BufferedImage((int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = scaled.createGraphics();
|
||||
g2d.drawImage(bi, 0, 0, (int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), null);
|
||||
g2d.dispose();
|
||||
bi = scaled;
|
||||
}
|
||||
catch(Throwable t) {
|
||||
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
|
||||
//probably a unrecognized image loader error, try at default zoom
|
||||
try {
|
||||
log.warn("Capture failed with increased zoom, attempting with default value");
|
||||
model.setZoom(1);
|
||||
bi = WebApp.raster(model);
|
||||
}
|
||||
catch(Throwable tt) {
|
||||
log.error("Failed to capture html raster");
|
||||
throw new IOException(tt);
|
||||
}
|
||||
} else {
|
||||
log.error("Failed to capture html raster");
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
|
||||
return getWrapper(bi, opt, pxlOpts);
|
||||
}
|
||||
|
||||
private ImageWrapper getWrapper(BufferedImage img, JSONObject opt, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||
if(img == null) {
|
||||
throw new IOException("Image provided is empty or null and cannot be converted.");
|
||||
}
|
||||
// Rotate image using orientation or rotation before sending to ImageWrapper
|
||||
if (pxlOpts.getOrientation() != null && pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) {
|
||||
img = PrintImage.rotate(img, pxlOpts.getOrientation().getDegreesRot(), pxlOpts.getDithering(), pxlOpts.getInterpolation());
|
||||
} else if (pxlOpts.getRotation() % 360 != 0) {
|
||||
img = PrintImage.rotate(img, pxlOpts.getRotation(), pxlOpts.getDithering(), pxlOpts.getInterpolation());
|
||||
}
|
||||
|
||||
ImageWrapper iw = new ImageWrapper(img, LanguageType.getType(opt.optString("language")));
|
||||
iw.setCharset(Charset.forName(destEncoding));
|
||||
|
||||
//ESC/POS only
|
||||
int density = opt.optInt("dotDensity", -1);
|
||||
if (density == -1) {
|
||||
String dStr = opt.optString("dotDensity", null);
|
||||
if (dStr != null && !dStr.isEmpty()) {
|
||||
switch(dStr.toLowerCase(Locale.ENGLISH)) {
|
||||
case "single": density = 32; break;
|
||||
case "double": density = 33; break;
|
||||
case "triple": density = 39; break;
|
||||
// negative: legacy mode
|
||||
case "single-legacy": density = -32; break;
|
||||
case "double-legacy": density = -33; break;
|
||||
}
|
||||
} else {
|
||||
density = 32; //default
|
||||
}
|
||||
}
|
||||
iw.setDotDensity(density);
|
||||
|
||||
//EPL only
|
||||
iw.setxPos(opt.optInt("x", 0));
|
||||
iw.setyPos(opt.optInt("y", 0));
|
||||
|
||||
// PGL only
|
||||
iw.setLogoId(opt.optString("logoId", ""));
|
||||
iw.setIgpDots(opt.optBoolean("igpDots", false));
|
||||
|
||||
return iw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(PrintOutput output, PrintOptions options) throws PrintException {
|
||||
PrintOptions.Raw rawOpts = options.getRawOptions();
|
||||
|
||||
List<ByteArrayBuilder> pages;
|
||||
if (rawOpts.getSpoolSize() > 0 && rawOpts.getSpoolEnd() != null && !rawOpts.getSpoolEnd().isEmpty()) {
|
||||
try {
|
||||
pages = ByteUtilities.splitByteArray(commands.getByteArray(), rawOpts.getSpoolEnd().getBytes(destEncoding), rawOpts.getSpoolSize());
|
||||
}
|
||||
catch(UnsupportedEncodingException e) {
|
||||
throw new PrintException(e);
|
||||
}
|
||||
} else {
|
||||
pages = new ArrayList<>();
|
||||
pages.add(commands);
|
||||
}
|
||||
|
||||
List<File> tempFiles = null;
|
||||
for(int i = 0; i < rawOpts.getCopies(); i++) {
|
||||
for(int j = 0; j < pages.size(); j++) {
|
||||
ByteArrayBuilder bab = pages.get(j);
|
||||
try {
|
||||
if (output.isSetHost()) {
|
||||
printToHost(output.getHost(), output.getPort(), bab.getByteArray());
|
||||
} else if (output.isSetFile()) {
|
||||
printToFile(output.getFile(), bab.getByteArray(), true);
|
||||
} else {
|
||||
if (rawOpts.isForceRaw()) {
|
||||
if(tempFiles == null) {
|
||||
tempFiles = new ArrayList<>(pages.size());
|
||||
}
|
||||
File tempFile;
|
||||
if(tempFiles.size() <= j) {
|
||||
tempFile = File.createTempFile("qz_raw_", null);
|
||||
tempFiles.add(j, tempFile);
|
||||
printToFile(tempFile, bab.getByteArray(), false);
|
||||
} else {
|
||||
tempFile = tempFiles.get(j);
|
||||
}
|
||||
if(SystemUtilities.isWindows()) {
|
||||
// Placeholder only; not yet supported
|
||||
printToBackend(output.getNativePrinter(), tempFile, Backend.WIN32_WMI);
|
||||
} else {
|
||||
// Try CUPS backend first, fallback to LPR
|
||||
printToBackend(output.getNativePrinter(), tempFile, Backend.CUPS_RSS, Backend.CUPS_LPR);
|
||||
}
|
||||
} else {
|
||||
printToPrinter(output.getPrintService(), bab.getByteArray(), rawOpts);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(IOException e) {
|
||||
cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles);
|
||||
throw new PrintException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles);
|
||||
}
|
||||
|
||||
private void cleanupTempFiles(boolean retainTemp, List<File> tempFiles) {
|
||||
if(tempFiles != null) {
|
||||
if (!retainTemp) {
|
||||
for(File tempFile : tempFiles) {
|
||||
if(tempFile != null) {
|
||||
if(!tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("Temp file(s) retained: {}", Arrays.toString(tempFiles.toArray()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A brute-force, however surprisingly elegant way to send a file to a networked printer.
|
||||
* <p/>
|
||||
* Please note that this will completely bypass the Print Spooler,
|
||||
* so the Operating System will have absolutely no printer information.
|
||||
* This is printing "blind".
|
||||
*/
|
||||
private void printToHost(String host, int port, byte[] cmds) throws IOException {
|
||||
log.debug("Printing to host {}:{}", host, port);
|
||||
|
||||
//throws any exception and auto-closes socket and stream
|
||||
try(Socket socket = new Socket(host, port); DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
|
||||
out.write(cmds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the raw commands directly to a file.
|
||||
*
|
||||
* @param file File to be written
|
||||
*/
|
||||
private void printToFile(File file, byte[] cmds, boolean locationRestricted) throws IOException {
|
||||
if(file == null) throw new IOException("No file specified");
|
||||
|
||||
if(locationRestricted && !PrefsSearch.getBoolean(ArgValue.SECURITY_PRINT_TOFILE)) {
|
||||
log.error("Printing to file '{}' is not permitted. Configure property '{}' to modify this behavior.",
|
||||
file, ArgValue.SECURITY_PRINT_TOFILE.getMatch());
|
||||
throw new IOException(String.format("Printing to file '%s' is not permitted", file));
|
||||
}
|
||||
|
||||
log.debug("Printing to file: {}", file.getName());
|
||||
|
||||
//throws any exception and auto-closes stream
|
||||
try(OutputStream out = new FileOutputStream(file)) {
|
||||
out.write(cmds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a {@code SimpleDoc} with the {@code commands} byte array.
|
||||
*/
|
||||
private void printToPrinter(PrintService service, byte[] cmds, PrintOptions.Raw rawOpts) throws PrintException {
|
||||
if (service == null) { throw new NullPrintServiceException("Service cannot be null"); }
|
||||
if (cmds == null || cmds.length == 0) { throw new NullCommandException("No commands found to send to the printer"); }
|
||||
|
||||
SimpleDoc doc = new SimpleDoc(cmds, DocFlavor.BYTE_ARRAY.AUTOSENSE, null);
|
||||
|
||||
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||
attributes.add(new JobName(rawOpts.getJobName(Constants.RAW_PRINT), Locale.getDefault()));
|
||||
|
||||
DocPrintJob printJob = service.createPrintJob();
|
||||
|
||||
waitForPrint(printJob, doc, attributes);
|
||||
}
|
||||
|
||||
protected void waitForPrint(DocPrintJob printJob, Doc doc, PrintRequestAttributeSet attributes) throws PrintException {
|
||||
final AtomicBoolean finished = new AtomicBoolean(false);
|
||||
printJob.addPrintJobListener(new PrintJobListener() {
|
||||
@Override
|
||||
public void printDataTransferCompleted(PrintJobEvent printJobEvent) {
|
||||
log.debug("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobCompleted(PrintJobEvent printJobEvent) {
|
||||
log.debug("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobFailed(PrintJobEvent printJobEvent) {
|
||||
log.error("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobCanceled(PrintJobEvent printJobEvent) {
|
||||
log.warn("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobNoMoreEvents(PrintJobEvent printJobEvent) {
|
||||
log.debug("{}", printJobEvent);
|
||||
finished.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void printJobRequiresAttention(PrintJobEvent printJobEvent) {
|
||||
log.info("{}", printJobEvent);
|
||||
}
|
||||
});
|
||||
|
||||
log.trace("Sending print job to printer");
|
||||
printJob.print(doc, attributes);
|
||||
|
||||
while(!finished.get()) {
|
||||
try { Thread.sleep(100); } catch(Exception ignore) {}
|
||||
}
|
||||
|
||||
log.trace("Print job received by printer");
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct/backend printing modes for forced raw printing
|
||||
*/
|
||||
public void printToBackend(NativePrinter printer, File tempFile, Backend... backends) throws IOException, PrintException {
|
||||
boolean success = false;
|
||||
|
||||
for(Backend backend : backends) {
|
||||
switch(backend) {
|
||||
case CUPS_LPR:
|
||||
// Use command line "lp" on Linux, BSD, Solaris, OSX, etc.
|
||||
String[] lpCmd = new String[] {"lp", "-d", printer.getPrinterId(), "-o", "raw", tempFile.getAbsolutePath()};
|
||||
if (!(success = ShellUtilities.execute(lpCmd))) {
|
||||
log.debug(StringUtils.join(lpCmd, ' '));
|
||||
}
|
||||
break;
|
||||
case CUPS_RSS:
|
||||
// Submit job via cupsDoRequest(...) via JNA against localhost:631\
|
||||
success = CupsUtils.sendRawFile(printer, tempFile);
|
||||
break;
|
||||
case WIN32_WMI:
|
||||
default:
|
||||
throw new UnsupportedOperationException("Raw backend \"" + backend + "\" is not yet supported.");
|
||||
}
|
||||
if(success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
throw new PrintException("Forced raw printing failed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
commands.clear();
|
||||
destEncoding = null;
|
||||
}
|
||||
|
||||
}
|
||||
44
old code/tray/src/qz/printer/action/ProcessorFactory.java
Executable file
44
old code/tray/src/qz/printer/action/ProcessorFactory.java
Executable file
@@ -0,0 +1,44 @@
|
||||
package qz.printer.action;
|
||||
|
||||
import org.apache.commons.pool2.KeyedPooledObjectFactory;
|
||||
import org.apache.commons.pool2.PooledObject;
|
||||
import org.apache.commons.pool2.impl.DefaultPooledObject;
|
||||
import qz.utils.PrintingUtilities;
|
||||
|
||||
public class ProcessorFactory implements KeyedPooledObjectFactory<PrintingUtilities.Format,PrintProcessor> {
|
||||
|
||||
@Override
|
||||
public PooledObject<PrintProcessor> makeObject(PrintingUtilities.Format key) throws Exception {
|
||||
PrintProcessor processor;
|
||||
switch(key) {
|
||||
case HTML: processor = new PrintHTML(); break;
|
||||
case IMAGE: processor = new PrintImage(); break;
|
||||
case PDF: processor = new PrintPDF(); break;
|
||||
case DIRECT: processor = new PrintDirect(); break;
|
||||
case COMMAND: default: processor = new PrintRaw(); break;
|
||||
}
|
||||
|
||||
return new DefaultPooledObject<>(processor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) {
|
||||
return true; //no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||
//no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void passivateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||
p.getObject().cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||
//no-op
|
||||
}
|
||||
|
||||
}
|
||||
512
old code/tray/src/qz/printer/action/html/WebApp.java
Executable file
512
old code/tray/src/qz/printer/action/html/WebApp.java
Executable file
@@ -0,0 +1,512 @@
|
||||
package qz.printer.action.html;
|
||||
|
||||
import com.github.zafarkhaja.semver.Version;
|
||||
import com.sun.javafx.tk.TKPulseListener;
|
||||
import com.sun.javafx.tk.Toolkit;
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.print.PageLayout;
|
||||
import javafx.print.PrinterJob;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.transform.Scale;
|
||||
import javafx.scene.transform.Transform;
|
||||
import javafx.scene.transform.Translate;
|
||||
import javafx.scene.web.WebView;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.w3c.dom.Attr;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import qz.common.Constants;
|
||||
import qz.utils.SystemUtilities;
|
||||
import qz.ws.PrintSocketServer;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.IntPredicate;
|
||||
|
||||
/**
|
||||
* JavaFX container for taking HTML snapshots.
|
||||
* Used by PrintHTML to generate printable images.
|
||||
* <p/>
|
||||
* Do not use constructor (used by JavaFX), instead call {@code WebApp.initialize()}
|
||||
*/
|
||||
public class WebApp extends Application {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(WebApp.class);
|
||||
|
||||
private static WebApp instance = null;
|
||||
private static Version webkitVersion = null;
|
||||
private static int CAPTURE_FRAMES = 2;
|
||||
private static int VECTOR_FRAMES = 1;
|
||||
private static Stage stage;
|
||||
private static WebView webView;
|
||||
private static double pageWidth;
|
||||
private static double pageHeight;
|
||||
private static double pageZoom;
|
||||
private static boolean raster;
|
||||
private static boolean headless;
|
||||
|
||||
private static CountDownLatch startupLatch;
|
||||
private static CountDownLatch captureLatch;
|
||||
|
||||
private static IntPredicate printAction;
|
||||
private static final AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||
|
||||
// JDK-8283686: Printing WebView may results in empty page
|
||||
private static final Version JDK_8283686_START = Version.valueOf(/* WebKit */ "609.1.0");
|
||||
private static final Version JDK_8283686_END = Version.valueOf(/* WebKit */ "612.1.0");
|
||||
private static final int JDK_8283686_VECTOR_FRAMES = 30;
|
||||
|
||||
|
||||
//listens for a Succeeded state to activate image capture
|
||||
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
|
||||
log.trace("New state: {} > {}", oldState, newState);
|
||||
|
||||
// Cancelled should probably throw exception listener, but does not
|
||||
if (newState == Worker.State.CANCELLED) {
|
||||
// This can happen for file downloads, e.g. "response-content-disposition=attachment"
|
||||
// See https://github.com/qzind/tray/issues/1183
|
||||
unlatch(new IOException("Page load was cancelled for an unknown reason"));
|
||||
}
|
||||
if (newState == Worker.State.SUCCEEDED) {
|
||||
boolean hasBody = (boolean)webView.getEngine().executeScript("document.body != null");
|
||||
if (!hasBody) {
|
||||
log.warn("Loaded page has no body - likely a redirect, skipping state");
|
||||
return;
|
||||
}
|
||||
|
||||
//ensure html tag doesn't use scrollbars, clipping page instead
|
||||
Document doc = webView.getEngine().getDocument();
|
||||
NodeList tags = doc.getElementsByTagName("html");
|
||||
if (tags != null && tags.getLength() > 0) {
|
||||
Node base = tags.item(0);
|
||||
Attr applied = (Attr)base.getAttributes().getNamedItem("style");
|
||||
if (applied == null) {
|
||||
applied = doc.createAttribute("style");
|
||||
}
|
||||
applied.setValue(applied.getValue() + "; overflow: hidden;");
|
||||
base.getAttributes().setNamedItem(applied);
|
||||
}
|
||||
|
||||
//width was resized earlier (for responsive html), then calculate the best fit height
|
||||
// FIXME: Should only be needed when height is unknown but fixes blank vector prints
|
||||
double fittedHeight = findHeight();
|
||||
boolean heightNeeded = pageHeight <= 0;
|
||||
|
||||
if (heightNeeded) {
|
||||
pageHeight = fittedHeight;
|
||||
}
|
||||
|
||||
// find and set page zoom for increased quality
|
||||
double usableZoom = calculateSupportedZoom(pageWidth, pageHeight);
|
||||
if (usableZoom < pageZoom) {
|
||||
log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom);
|
||||
pageZoom = usableZoom;
|
||||
}
|
||||
webView.setZoom(pageZoom);
|
||||
log.trace("Zooming in by x{} for increased quality", pageZoom);
|
||||
|
||||
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);
|
||||
|
||||
//need to check for height again as resizing can cause partial results
|
||||
if (heightNeeded) {
|
||||
fittedHeight = findHeight();
|
||||
if (fittedHeight != pageHeight) {
|
||||
adjustSize(pageWidth * pageZoom, fittedHeight * pageZoom);
|
||||
}
|
||||
}
|
||||
|
||||
log.trace("Set HTML page height to {}", pageHeight);
|
||||
|
||||
autosize(webView);
|
||||
|
||||
Platform.runLater(() -> new AnimationTimer() {
|
||||
int frames = 0;
|
||||
|
||||
@Override
|
||||
public void handle(long l) {
|
||||
if (printAction.test(++frames)) {
|
||||
stop();
|
||||
}
|
||||
}
|
||||
}.start());
|
||||
}
|
||||
};
|
||||
|
||||
//listens for load progress
|
||||
private static ChangeListener<Number> workDoneListener = (ov, oldWork, newWork) -> log.trace("Done: {} > {}", oldWork, newWork);
|
||||
|
||||
private static ChangeListener<String> msgListener = (ov, oldMsg, newMsg) -> log.trace("New status: {}", newMsg);
|
||||
|
||||
//listens for failures
|
||||
private static ChangeListener<Throwable> exceptListener = (obs, oldExc, newExc) -> {
|
||||
if (newExc != null) { unlatch(newExc); }
|
||||
};
|
||||
|
||||
|
||||
/** Called by JavaFX thread */
|
||||
public WebApp() {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
/** Starts JavaFX thread if not already running */
|
||||
public static synchronized void initialize() throws IOException {
|
||||
if (instance == null) {
|
||||
startupLatch = new CountDownLatch(1);
|
||||
// For JDK8 compat
|
||||
headless = false;
|
||||
|
||||
// JDK11+ depends bundled javafx
|
||||
if (Constants.JAVA_VERSION.getMajorVersion() >= 11) {
|
||||
// Monocle default for unit tests
|
||||
boolean useMonocle = true;
|
||||
if (PrintSocketServer.getTrayManager() != null) {
|
||||
// Honor user monocle override
|
||||
useMonocle = PrintSocketServer.getTrayManager().isMonoclePreferred();
|
||||
// Trust TrayManager's headless detection
|
||||
headless = PrintSocketServer.getTrayManager().isHeadless();
|
||||
} else {
|
||||
// Fallback for JDK11+
|
||||
headless = true;
|
||||
}
|
||||
if (useMonocle && SystemUtilities.hasMonocle()) {
|
||||
log.trace("Initializing monocle platform");
|
||||
System.setProperty("javafx.platform", "monocle");
|
||||
// Don't set glass.platform on Linux per https://github.com/qzind/tray/issues/702
|
||||
switch(SystemUtilities.getOs()) {
|
||||
case WINDOWS:
|
||||
case MAC:
|
||||
System.setProperty("glass.platform", "Monocle");
|
||||
break;
|
||||
default:
|
||||
// don't set "glass.platform"
|
||||
}
|
||||
|
||||
//software rendering required headless environments
|
||||
if (headless) {
|
||||
System.setProperty("prism.order", "sw");
|
||||
}
|
||||
} else {
|
||||
log.warn("Monocle platform will not be used");
|
||||
}
|
||||
}
|
||||
|
||||
new Thread(() -> Application.launch(WebApp.class)).start();
|
||||
}
|
||||
|
||||
if (startupLatch.getCount() > 0) {
|
||||
try {
|
||||
log.trace("Waiting for JavaFX..");
|
||||
if (!startupLatch.await(60, TimeUnit.SECONDS)) {
|
||||
throw new IOException("JavaFX did not start");
|
||||
} else {
|
||||
log.trace("Running a test snapshot to size the stage...");
|
||||
try {
|
||||
raster(new WebAppModel("<h1>startup</h1>", true, 0, 0, true, 2));
|
||||
log.trace("JFX initialized successfully");
|
||||
}
|
||||
catch(Throwable t) {
|
||||
throw new IOException(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(InterruptedException ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage st) throws Exception {
|
||||
startupLatch.countDown();
|
||||
log.debug("Started JavaFX");
|
||||
|
||||
webView = new WebView();
|
||||
|
||||
// JDK-8283686: Printing WebView may results in empty page
|
||||
// See also https://github.com/qzind/tray/issues/778
|
||||
if(getWebkitVersion() == null ||
|
||||
(getWebkitVersion().greaterThan(JDK_8283686_START) &&
|
||||
getWebkitVersion().lessThan(JDK_8283686_END))) {
|
||||
VECTOR_FRAMES = JDK_8283686_VECTOR_FRAMES; // Additional pulses needed for vector graphics
|
||||
}
|
||||
|
||||
st.setScene(new Scene(webView));
|
||||
stage = st;
|
||||
stage.setWidth(1);
|
||||
stage.setHeight(1);
|
||||
|
||||
Worker<Void> worker = webView.getEngine().getLoadWorker();
|
||||
worker.stateProperty().addListener(stateListener);
|
||||
worker.workDoneProperty().addListener(workDoneListener);
|
||||
worker.exceptionProperty().addListener(exceptListener);
|
||||
worker.messageProperty().addListener(msgListener);
|
||||
|
||||
//prevents JavaFX from shutting down when hiding window
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the loaded source specified in the passed {@code model}.
|
||||
*
|
||||
* @param job A setup JavaFx {@code PrinterJob}
|
||||
* @param model The model specifying the web page parameters
|
||||
* @throws Throwable JavaFx will throw a generic {@code Throwable} class for any issues
|
||||
*/
|
||||
public static synchronized void print(final PrinterJob job, final WebAppModel model) throws Throwable {
|
||||
model.setZoom(1); //vector prints do not need to use zoom
|
||||
raster = false;
|
||||
|
||||
load(model, (int frames) -> {
|
||||
if(frames == VECTOR_FRAMES) {
|
||||
try {
|
||||
double printScale = 72d / 96d;
|
||||
webView.getTransforms().add(new Scale(printScale, printScale));
|
||||
|
||||
PageLayout layout = job.getJobSettings().getPageLayout();
|
||||
if (model.isScaled()) {
|
||||
double viewWidth = webView.getWidth() * printScale;
|
||||
double viewHeight = webView.getHeight() * printScale;
|
||||
|
||||
double scale;
|
||||
if ((viewWidth / viewHeight) >= (layout.getPrintableWidth() / layout.getPrintableHeight())) {
|
||||
scale = (layout.getPrintableWidth() / viewWidth);
|
||||
} else {
|
||||
scale = (layout.getPrintableHeight() / viewHeight);
|
||||
}
|
||||
webView.getTransforms().add(new Scale(scale, scale));
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
double useScale = 1;
|
||||
for(Transform t : webView.getTransforms()) {
|
||||
if (t instanceof Scale) { useScale *= ((Scale)t).getX(); }
|
||||
}
|
||||
|
||||
PageLayout page = job.getJobSettings().getPageLayout();
|
||||
Rectangle printBounds = new Rectangle(0, 0, page.getPrintableWidth(), page.getPrintableHeight());
|
||||
log.debug("Paper area: {},{}:{},{}", (int)page.getLeftMargin(), (int)page.getTopMargin(),
|
||||
(int)page.getPrintableWidth(), (int)page.getPrintableHeight());
|
||||
|
||||
Translate activePage = new Translate();
|
||||
webView.getTransforms().add(activePage);
|
||||
|
||||
int columnsNeed = Math.max(1, (int)Math.ceil(webView.getWidth() / printBounds.getWidth() * useScale - 0.1));
|
||||
int rowsNeed = Math.max(1, (int)Math.ceil(webView.getHeight() / printBounds.getHeight() * useScale - 0.1));
|
||||
log.debug("Document will be printed across {} pages", columnsNeed * rowsNeed);
|
||||
|
||||
try {
|
||||
for(int row = 0; row < rowsNeed; row++) {
|
||||
for(int col = 0; col < columnsNeed; col++) {
|
||||
activePage.setX((-col * printBounds.getWidth()) / useScale);
|
||||
activePage.setY((-row * printBounds.getHeight()) / useScale);
|
||||
|
||||
job.printPage(webView);
|
||||
}
|
||||
}
|
||||
|
||||
unlatch(null);
|
||||
}
|
||||
catch(Exception e) {
|
||||
unlatch(e);
|
||||
}
|
||||
finally {
|
||||
//reset state
|
||||
webView.getTransforms().clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch(Exception e) { unlatch(e); }
|
||||
}
|
||||
return frames >= VECTOR_FRAMES;
|
||||
});
|
||||
|
||||
log.trace("Waiting on print..");
|
||||
captureLatch.await(); //released when unlatch is called
|
||||
|
||||
if (thrown.get() != null) { throw thrown.get(); }
|
||||
}
|
||||
|
||||
public static synchronized BufferedImage raster(final WebAppModel model) throws Throwable {
|
||||
AtomicReference<BufferedImage> capture = new AtomicReference<>();
|
||||
|
||||
//ensure JavaFX has started before we run
|
||||
if (startupLatch.getCount() > 0) {
|
||||
throw new IOException("JavaFX has not been started");
|
||||
}
|
||||
|
||||
//raster still needs to show stage for valid capture
|
||||
Platform.runLater(() -> {
|
||||
stage.show();
|
||||
stage.toBack();
|
||||
});
|
||||
|
||||
raster = true;
|
||||
|
||||
load(model, (int frames) -> {
|
||||
if (frames == CAPTURE_FRAMES) {
|
||||
log.debug("Attempting image capture");
|
||||
|
||||
Toolkit.getToolkit().addPostSceneTkPulseListener(new TKPulseListener() {
|
||||
@Override
|
||||
public void pulse() {
|
||||
try {
|
||||
// TODO: Revert to Callback once JDK-8244588/SUPQZ-5 is avail (JDK11+ only)
|
||||
capture.set(SwingFXUtils.fromFXImage(webView.snapshot(null, null), null));
|
||||
unlatch(null);
|
||||
}
|
||||
catch(Exception e) {
|
||||
unlatch(e);
|
||||
}
|
||||
finally {
|
||||
Toolkit.getToolkit().removePostSceneTkPulseListener(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
Toolkit.getToolkit().requestNextPulse();
|
||||
}
|
||||
|
||||
return frames >= CAPTURE_FRAMES;
|
||||
});
|
||||
|
||||
log.trace("Waiting on capture..");
|
||||
captureLatch.await(); //released when unlatch is called
|
||||
|
||||
if (thrown.get() != null) { throw thrown.get(); }
|
||||
|
||||
return capture.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the loaded source specified in the passed {@code model}.
|
||||
*
|
||||
* @param model The model specifying the web page parameters.
|
||||
* @param action EventHandler that will be ran when the WebView completes loading.
|
||||
*/
|
||||
private static synchronized void load(WebAppModel model, IntPredicate action) {
|
||||
captureLatch = new CountDownLatch(1);
|
||||
thrown.set(null);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
//zoom should only be factored on raster prints
|
||||
pageZoom = model.getZoom();
|
||||
pageWidth = model.getWebWidth();
|
||||
pageHeight = model.getWebHeight();
|
||||
|
||||
log.trace("Setting starting size {}:{}", pageWidth, pageHeight);
|
||||
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);
|
||||
|
||||
if (pageHeight == 0) {
|
||||
webView.setMinHeight(1);
|
||||
webView.setPrefHeight(1);
|
||||
webView.setMaxHeight(1);
|
||||
}
|
||||
|
||||
autosize(webView);
|
||||
|
||||
printAction = action;
|
||||
|
||||
if (model.isPlainText()) {
|
||||
webView.getEngine().loadContent(model.getSource(), "text/html");
|
||||
} else {
|
||||
webView.getEngine().load(model.getSource());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static double findHeight() {
|
||||
String heightText = webView.getEngine().executeScript("Math.max(document.body.offsetHeight, document.body.scrollHeight)").toString();
|
||||
return Double.parseDouble(heightText);
|
||||
}
|
||||
|
||||
private static void adjustSize(double toWidth, double toHeight) {
|
||||
webView.setMinSize(toWidth, toHeight);
|
||||
webView.setPrefSize(toWidth, toHeight);
|
||||
webView.setMaxSize(toWidth, toHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix blank page after autosize is called
|
||||
*/
|
||||
public static void autosize(WebView webView) {
|
||||
webView.autosize();
|
||||
|
||||
if (!raster) {
|
||||
// Call updatePeer; fixes a bug with webView resizing
|
||||
// Can be avoided by calling stage.show() but breaks headless environments
|
||||
// See: https://github.com/qzind/tray/issues/513
|
||||
String[] methods = {"impl_updatePeer" /*jfx8*/, "doUpdatePeer" /*jfx11*/};
|
||||
try {
|
||||
for(Method m : webView.getClass().getDeclaredMethods()) {
|
||||
for(String method : methods) {
|
||||
if (m.getName().equals(method)) {
|
||||
m.setAccessible(true);
|
||||
m.invoke(webView);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(SecurityException | ReflectiveOperationException e) {
|
||||
log.warn("Unable to update peer; Blank pages may occur.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double calculateSupportedZoom(double width, double height) {
|
||||
long memory = Runtime.getRuntime().maxMemory();
|
||||
int allowance = (memory / 1048576L) > 1024? 3:2;
|
||||
if (headless) { allowance--; }
|
||||
long availSpace = memory << allowance;
|
||||
|
||||
// Memory needed for print is roughly estimated as
|
||||
// (width * height) [pixels needed] * (pageZoom * 72d) [print density used] * 3 [rgb channels]
|
||||
return Math.sqrt(availSpace / ((width * height) * (pageZoom * 72d) * 3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Final cleanup when no longer capturing
|
||||
*/
|
||||
public static void unlatch(Throwable t) {
|
||||
if (t != null) {
|
||||
thrown.set(t);
|
||||
}
|
||||
|
||||
captureLatch.countDown();
|
||||
stage.hide();
|
||||
}
|
||||
|
||||
public static Version getWebkitVersion() {
|
||||
if(webkitVersion == null) {
|
||||
if(webView != null) {
|
||||
String userAgent = webView.getEngine().getUserAgent();
|
||||
String[] parts = userAgent.split("WebKit/");
|
||||
if (parts.length > 1) {
|
||||
String[] split = parts[1].split(" ");
|
||||
if (split.length > 0) {
|
||||
try {
|
||||
webkitVersion = Version.valueOf(split[0]);
|
||||
log.info("WebKit version {} detected", webkitVersion);
|
||||
} catch(Exception ignore) {}
|
||||
}
|
||||
}
|
||||
if(webkitVersion == null) {
|
||||
log.warn("WebKit version couldn't be parsed from UserAgent: {}", userAgent);
|
||||
}
|
||||
} else {
|
||||
log.warn("Can't get WebKit version, JavaFX hasn't started yet.");
|
||||
}
|
||||
}
|
||||
return webkitVersion;
|
||||
}
|
||||
}
|
||||
81
old code/tray/src/qz/printer/action/html/WebAppModel.java
Executable file
81
old code/tray/src/qz/printer/action/html/WebAppModel.java
Executable file
@@ -0,0 +1,81 @@
|
||||
package qz.printer.action.html;
|
||||
|
||||
public class WebAppModel {
|
||||
|
||||
private String source;
|
||||
private boolean plainText;
|
||||
|
||||
private double width, webWidth;
|
||||
private double height, webHeight;
|
||||
private boolean isScaled;
|
||||
private double zoom;
|
||||
|
||||
public WebAppModel(String source, boolean plainText, double width, double height, boolean isScaled, double zoom) {
|
||||
this.source = source;
|
||||
this.plainText = plainText;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.webWidth = width * (96d / 72d);
|
||||
this.webHeight = height * (96d / 72d);
|
||||
this.isScaled = isScaled;
|
||||
this.zoom = zoom;
|
||||
}
|
||||
|
||||
public String getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
public void setSource(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public boolean isPlainText() {
|
||||
return plainText;
|
||||
}
|
||||
|
||||
public void setPlainText(boolean plainText) {
|
||||
this.plainText = plainText;
|
||||
}
|
||||
|
||||
public double getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public void setWidth(double width) {
|
||||
this.width = width;
|
||||
this.webWidth = width * (96d / 72d);
|
||||
}
|
||||
|
||||
public double getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public void setHeight(double height) {
|
||||
this.height = height;
|
||||
this.webHeight = height * (96d / 72d);
|
||||
}
|
||||
|
||||
public double getWebWidth() {
|
||||
return webWidth;
|
||||
}
|
||||
|
||||
public double getWebHeight() {
|
||||
return webHeight;
|
||||
}
|
||||
|
||||
public boolean isScaled() {
|
||||
return isScaled;
|
||||
}
|
||||
|
||||
public void setScaled(boolean scaled) {
|
||||
isScaled = scaled;
|
||||
}
|
||||
|
||||
public double getZoom() {
|
||||
return zoom;
|
||||
}
|
||||
|
||||
public void setZoom(double zoom) {
|
||||
this.zoom = zoom;
|
||||
}
|
||||
}
|
||||
80
old code/tray/src/qz/printer/action/pdf/BookBundle.java
Executable file
80
old code/tray/src/qz/printer/action/pdf/BookBundle.java
Executable file
@@ -0,0 +1,80 @@
|
||||
package qz.printer.action.pdf;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.print.Book;
|
||||
import java.awt.print.PageFormat;
|
||||
import java.awt.print.Printable;
|
||||
import java.awt.print.PrinterException;
|
||||
|
||||
/**
|
||||
* Wrapper of the {@code Book} class as a {@code Printable} type,
|
||||
* since PrinterJob implementations do not seem to handle the {@code Pageable} interface properly.
|
||||
*/
|
||||
public class BookBundle extends Book {
|
||||
|
||||
private static final Logger log = LogManager.getLogger(BookBundle.class);
|
||||
|
||||
private Printable lastPrint;
|
||||
private int lastStarted;
|
||||
|
||||
public BookBundle() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper of the wrapper class so that PrinterJob implementations will handle it as proper pageable
|
||||
*/
|
||||
public Book wrapAndPresent() {
|
||||
Book cover = new Book();
|
||||
for(int i = 0; i < getNumberOfPages(); i++) {
|
||||
cover.append(new PrintingPress(), getPageFormat(i));
|
||||
}
|
||||
|
||||
return cover;
|
||||
}
|
||||
|
||||
public Book wrapAndPresent(int offset, int length) {
|
||||
Book coverSubset = new Book();
|
||||
for(int i = offset; i < offset + length && i < getNumberOfPages(); i++) {
|
||||
coverSubset.append(new PrintingPress(offset), getPageFormat(i));
|
||||
}
|
||||
|
||||
return coverSubset;
|
||||
}
|
||||
|
||||
|
||||
/** Printable wrapper to ensure proper reading of multiple documents across spooling */
|
||||
private class PrintingPress implements Printable {
|
||||
private int pageOffset;
|
||||
|
||||
public PrintingPress() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public PrintingPress(int offset) {
|
||||
pageOffset = offset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int print(Graphics g, PageFormat format, int pageIndex) throws PrinterException {
|
||||
pageIndex += pageOffset;
|
||||
log.trace("Requested page {} for printing", pageIndex);
|
||||
|
||||
if (pageIndex < getNumberOfPages()) {
|
||||
Printable printable = getPrintable(pageIndex);
|
||||
if (printable != lastPrint) {
|
||||
lastPrint = printable;
|
||||
lastStarted = pageIndex;
|
||||
}
|
||||
|
||||
return printable.print(g, format, pageIndex - lastStarted);
|
||||
}
|
||||
|
||||
return NO_SUCH_PAGE;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user