Initial commit: RustDesk 1.4.7 macOS desktop port

- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common)
- Patched Flutter 3.44 / Dart 3.12 compatibility:
  * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64
  * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData
  * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0
  * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64
- Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter
- Verified flutter build macos --debug and --release both produce working .app
- Verified .dmg installer (27MB arm64) created via hdiutil
- Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
xuwenwei
2026-06-08 21:42:20 +08:00
parent 3d8db09123
commit d3b6026bfd
840 changed files with 291780 additions and 1 deletions
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -e
if [ "$1" = configure ]; then
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
ln -f -s /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
if [ "systemd" == "$INITSYS" ]; then
if [ -e /etc/systemd/system/rustdesk.service ]; then
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1
fi
mkdir -p /usr/lib/systemd/system/
cp /usr/share/rustdesk/files/systemd/rustdesk.service /usr/lib/systemd/system/rustdesk.service
# try fix error in Ubuntu 18.04
# Failed to reload rustdesk.service: Unit rustdesk.service is not loaded properly: Exec format error.
# /usr/lib/systemd/system/rustdesk.service:10: Executable path is not absolute: pkill -f "rustdesk --"
if [ -e /usr/bin/pkill ]; then
sed -i "s|pkill|/usr/bin/pkill|g" /usr/lib/systemd/system/rustdesk.service
fi
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
fi
fi
+11
View File
@@ -0,0 +1,11 @@
#!/bin/bash
set -e
case $1 in
purge)
rm -rf /root/.config/rustdesk || true
;;
esac
exit 0
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e
case $1 in
install|upgrade)
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
if [ "systemd" == "${INITSYS}" ]; then
service rustdesk stop || true
sleep 1
rm -rf /usr/bin/libsciter-gtk.so
fi
;;
esac
exit 0
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -e
case $1 in
remove|upgrade)
INITSYS=$(ls -al /proc/1/exe | awk -F' ' '{print $NF}' | awk -F'/' '{print $NF}')
rm -f /usr/bin/rustdesk
if [ "systemd" == "${INITSYS}" ]; then
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service /usr/lib/systemd/system/rustdesk.service || true
# workaround temp dev build between 1.1.9 and 1.2.0
serverUser=$(ps -ef | grep -E 'rustdesk +--server' | grep -v 'sudo ' | awk '{print $1}' | head -1)
if [ "$serverUser" != "" ] && [ "$serverUser" != "root" ]
then
systemctl --machine=${serverUser}@.host --user stop rustdesk >/dev/null 2>&1 || true
fi
rm /usr/lib/systemd/user/rustdesk.service >/dev/null 2>&1 || true
fi
;;
esac
exit 0
+35
View File
@@ -0,0 +1,35 @@
pkgname=rustdesk
pkgver=1.4.7
pkgrel=0
epoch=
pkgdesc=""
arch=('x86_64')
url=""
license=('AGPL-3.0')
groups=()
depends=('gtk3' 'xdotool' 'libxcb' 'libxfixes' 'alsa-lib' 'libva' 'libappindicator-gtk3' 'pam' 'gst-plugins-base' 'gst-plugin-pipewire')
makedepends=()
checkdepends=()
optdepends=()
provides=()
conflicts=()
replaces=()
backup=()
options=()
install=pacman_install
changelog=
noextract=()
md5sums=() #generate with 'makepkg -g'
package() {
if [[ ${FLUTTER} ]]; then
mkdir -p "${pkgdir}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "${pkgdir}/usr/share/rustdesk"
fi
mkdir -p "${pkgdir}/usr/bin"
pushd ${pkgdir} && ln -s /usr/share/rustdesk/rustdesk usr/bin/rustdesk && popd
install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/rustdesk.png"
install -Dm 644 $HBB/res/scalable.svg "${pkgdir}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg"
}
+791
View File
@@ -0,0 +1,791 @@
#!/usr/bin/env python3
import requests
import argparse
import json
from datetime import datetime, timedelta
def get_personal_ab(url, token):
"""Get personal address book GUID"""
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{url}/api/ab/personal", headers=headers)
if response.status_code != 200:
return f"Error: {response.status_code} - {response.text}"
return response.json()
def view_shared_abs(url, token, name=None):
"""View all shared address books (excluding personal ones)"""
headers = {"Authorization": f"Bearer {token}"}
pageSize = 30
params = {
"name": name,
}
filtered_params = {
k: "%" + v + "%" if (v != "-" and "%" not in v and k != "name") else v
for k, v in params.items()
if v is not None
}
filtered_params["pageSize"] = pageSize
abs = []
current = 0
while True:
current += 1
filtered_params["current"] = current
response = requests.get(f"{url}/api/ab/shared/profiles", headers=headers, params=filtered_params)
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
data = response_json.get("data", [])
abs.extend(data)
total = response_json.get("total", 0)
if len(data) < pageSize or current * pageSize >= total:
break
return abs
def get_ab_by_name(url, token, ab_name):
"""Get address book by name"""
abs = view_shared_abs(url, token, ab_name)
for ab in abs:
if ab["name"] == ab_name:
return ab
return None
def view_ab_peers(url, token, ab_guid, peer_id=None, alias=None):
"""View peers in an address book"""
headers = {"Authorization": f"Bearer {token}"}
pageSize = 30
params = {
"ab": ab_guid,
"id": peer_id,
"alias": alias,
}
filtered_params = {
k: "%" + v + "%" if (v != "-" and "%" not in v and k not in ["ab"]) else v
for k, v in params.items()
if v is not None
}
filtered_params["pageSize"] = pageSize
peers = []
current = 0
while True:
current += 1
filtered_params["current"] = current
response = requests.get(f"{url}/api/ab/peers", headers=headers, params=filtered_params)
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
data = response_json.get("data", [])
peers.extend(data)
total = response_json.get("total", 0)
if len(data) < pageSize or current * pageSize >= total:
break
return peers
def view_ab_tags(url, token, ab_guid):
"""View tags in an address book"""
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(f"{url}/api/ab/tags/{ab_guid}", headers=headers)
response_json = check_response(response)
# Format color values as hex
if response_json:
for tag in response_json:
if "color" in tag and tag["color"] is not None:
# Convert color to hex format
color_value = tag["color"]
if isinstance(color_value, int):
tag["color"] = f"0x{color_value:08X}"
return response_json if response_json else []
def check_response(response):
"""Check API response and return result"""
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
try:
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
return response_json
except ValueError:
return response.text or "Success"
def add_peer(url, token, ab_guid, peer_id, alias=None, note=None, tags=None, password=None):
"""Add a peer to address book"""
print(f"Adding peer {peer_id} to address book")
headers = {"Authorization": f"Bearer {token}"}
payload = {
"id": peer_id,
"note": note,
}
# Add peer info if provided
info = {}
if alias:
info["alias"] = alias
if tags:
info["tags"] = tags if isinstance(tags, list) else [tags]
if password:
info["password"] = password
if info:
payload.update(info)
response = requests.post(f"{url}/api/ab/peer/add/{ab_guid}", headers=headers, json=payload)
return check_response(response)
def delete_peer(url, token, ab_guid, peer_ids):
"""Delete peers from address book by IDs"""
if isinstance(peer_ids, str):
peer_ids = [peer_ids]
print(f"Deleting peers {peer_ids} from address book")
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/ab/peer/{ab_guid}", headers=headers, json=peer_ids)
return check_response(response)
def update_peer(url, token, ab_guid, peer_id, alias=None, note=None, tags=None, password=None):
"""Update a peer in address book"""
print(f"Updating peer {peer_id} in address book")
headers = {"Authorization": f"Bearer {token}"}
# Check if at least one parameter is provided for update
update_params = [alias, note, tags, password]
if all(param is None for param in update_params):
return "Error: At least one parameter must be specified for update"
payload = {
"id": peer_id,
}
# Add fields to update
info = {}
if alias is not None:
info["alias"] = alias
if tags is not None:
info["tags"] = tags if isinstance(tags, list) else [tags]
if password is not None:
info["password"] = password
if info:
payload.update(info)
if note is not None:
payload["note"] = note
response = requests.put(f"{url}/api/ab/peer/update/{ab_guid}", headers=headers, json=payload)
return check_response(response)
def str2color(tag_name, existing_colors=None):
"""Generate color for tag name similar to str2color2 function"""
if existing_colors is None:
existing_colors = []
color_map = {
"red": 0xFFFF0000,
"green": 0xFF008000,
"blue": 0xFF0000FF,
"orange": 0xFFFF9800,
"purple": 0xFF9C27B0,
"grey": 0xFF9E9E9E,
"cyan": 0xFF00BCD4,
"lime": 0xFFCDDC39,
"teal": 0xFF009688,
"pink": 0xFFF48FB1,
"indigo": 0xFF3F51B5,
"brown": 0xFF795548,
}
lower_name = tag_name.lower()
# Check if tag name matches a predefined color
if lower_name in color_map:
return color_map[lower_name]
# Special case for yellow
if lower_name == "yellow":
return 0xFFFFFF00
# Generate hash-based color
hash_value = 0
for char in tag_name:
hash_value += ord(char)
color_list = list(color_map.values())
hash_value = hash_value % len(color_list)
result = color_list[hash_value]
# If color is already used, try to find an unused one
if result in existing_colors:
for color in color_list:
if color not in existing_colors:
result = color
break
return result
def add_tag(url, token, ab_guid, tag_name, color=None):
"""Add a tag to address book"""
print(f"Adding tag '{tag_name}' to address book")
headers = {"Authorization": f"Bearer {token}"}
# If no color specified, generate one based on tag name
if color is None:
# Get existing tags to avoid color conflicts
try:
existing_tags = view_ab_tags(url, token, ab_guid)
existing_colors = [tag.get("color", 0) for tag in existing_tags]
color = str2color(tag_name, existing_colors)
except:
# Fallback to default color if we can't get existing tags
color = str2color(tag_name)
payload = {
"name": tag_name,
"color": color,
}
response = requests.post(f"{url}/api/ab/tag/add/{ab_guid}", headers=headers, json=payload)
return check_response(response)
def update_tag(url, token, ab_guid, tag_name, color):
"""Update a tag in address book"""
print(f"Updating tag '{tag_name}' in address book")
headers = {"Authorization": f"Bearer {token}"}
payload = {
"name": tag_name,
"color": color,
}
response = requests.put(f"{url}/api/ab/tag/update/{ab_guid}", headers=headers, json=payload)
return check_response(response)
def delete_tags(url, token, ab_guid, tag_names):
"""Delete tags from address book"""
if isinstance(tag_names, str):
tag_names = [tag_names]
print(f"Deleting tags {tag_names} from address book")
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/ab/tag/{ab_guid}", headers=headers, json=tag_names)
return check_response(response)
def add_shared_ab(url, token, name, note=None, password=None):
"""Add a new shared address book"""
print(f"Adding shared address book '{name}'")
headers = {"Authorization": f"Bearer {token}"}
payload = {
"name": name,
"note": note,
}
# Add info if password is provided
if password:
payload["info"] = {
"password": password
}
response = requests.post(f"{url}/api/ab/shared/add", headers=headers, json=payload)
return check_response(response)
def update_shared_ab(url, token, ab_guid, name=None, note=None, owner=None, password=None):
"""Update a shared address book"""
print(f"Updating shared address book {ab_guid}")
headers = {"Authorization": f"Bearer {token}"}
# Check if at least one parameter is provided for update
update_params = [name, note, owner, password]
if all(param is None for param in update_params):
return "Error: At least one parameter must be specified for update"
payload = {
"guid": ab_guid,
}
if name is not None:
payload["name"] = name
if note is not None:
payload["note"] = note
if owner is not None:
payload["owner"] = owner
if password is not None:
payload["info"] = {
"password": password
}
response = requests.put(f"{url}/api/ab/shared/update/profile", headers=headers, json=payload)
return check_response(response)
def delete_shared_abs(url, token, ab_guids):
"""Delete shared address books"""
if isinstance(ab_guids, str):
ab_guids = [ab_guids]
print(f"Deleting shared address books {ab_guids}")
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/ab/shared", headers=headers, json=ab_guids)
return check_response(response)
def permission_to_string(permission):
"""Convert numeric permission to string representation"""
permission_map = {
1: "ro", # Read
2: "rw", # ReadWrite
3: "full" # FullControl
}
return permission_map.get(permission, str(permission))
def string_to_permission(permission_str):
"""Convert string permission to numeric representation"""
permission_map = {
"ro": 1, # Read
"rw": 2, # ReadWrite
"full": 3 # FullControl
}
return permission_map.get(permission_str.lower(), None)
def view_ab_rules(url, token, ab_guid):
"""View rules in an address book"""
headers = {"Authorization": f"Bearer {token}"}
pageSize = 30
params = {
"ab": ab_guid,
"pageSize": pageSize,
}
rules = []
current = 0
while True:
current += 1
params["current"] = current
response = requests.get(f"{url}/api/ab/rules", headers=headers, params=params)
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
data = response_json.get("data", [])
rules.extend(data)
total = response_json.get("total", 0)
if len(data) < pageSize or current * pageSize >= total:
break
# Convert numeric permissions to string format
for rule in rules:
if "rule" in rule:
rule["rule"] = permission_to_string(rule["rule"])
return rules
def add_ab_rule(url, token, ab_guid, rule_type, user=None, group=None, rule=1):
"""Add a rule to address book"""
print(f"Adding {rule_type} rule to address book")
headers = {"Authorization": f"Bearer {token}"}
payload = {
"guid": ab_guid,
"rule": rule,
}
if rule_type == "user" and user:
payload["user"] = user
elif rule_type == "group" and group:
payload["group"] = group
elif rule_type == "everyone":
# For everyone, both user and group are None (not included in payload)
pass
response = requests.post(f"{url}/api/ab/rule", headers=headers, json=payload)
return check_response(response)
def update_ab_rule(url, token, rule_guid, rule):
"""Update an address book rule"""
print(f"Updating rule {rule_guid}")
headers = {"Authorization": f"Bearer {token}"}
payload = {
"guid": rule_guid,
"rule": rule,
}
response = requests.patch(f"{url}/api/ab/rule", headers=headers, json=payload)
return check_response(response)
def delete_ab_rules(url, token, rule_guids):
"""Delete address book rules"""
if isinstance(rule_guids, str):
rule_guids = [rule_guids]
print(f"Deleting rules {rule_guids}")
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/ab/rules", headers=headers, json=rule_guids)
return check_response(response)
def main():
def parse_color(value):
"""Parse color value - supports both hex (0xFF00FF00) and decimal"""
if value.startswith('0x') or value.startswith('0X'):
return int(value, 16)
else:
return int(value)
def parse_permission(value):
"""Parse permission value - supports both string (ro/rw/full) and numeric (1/2/3)"""
# Try to parse as string first
permission_num = string_to_permission(value)
if permission_num is not None:
return permission_num
# Try to parse as integer for backward compatibility
try:
num_value = int(value)
if num_value in [1, 2, 3]:
return num_value
else:
raise argparse.ArgumentTypeError(f"Invalid permission value: {value}. Must be one of: ro, rw, full, 1, 2, 3")
except ValueError:
raise argparse.ArgumentTypeError(f"Invalid permission value: {value}. Must be one of: ro, rw, full, 1, 2, 3")
parser = argparse.ArgumentParser(description="Address Book manager")
# Required arguments
parser.add_argument(
"command",
choices=["view-ab", "add-ab", "update-ab", "delete-ab", "get-personal-ab",
"view-peer", "add-peer", "update-peer", "delete-peer",
"view-tag", "add-tag", "update-tag", "delete-tag",
"view-rule", "add-rule", "update-rule", "delete-rule"],
help="Command to execute",
)
# Global arguments (used by all commands)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument("--token", required=True, help="Bearer token for authentication")
# Address book identification (used by most commands except get-personal-ab)
parser.add_argument("--ab-name", help="Address book name (for identification)")
parser.add_argument("--ab-guid", help="Address book GUID (alternative to ab-name)")
# Address book management arguments
parser.add_argument("--ab-update-name", help="New address book name (for update)")
parser.add_argument("--note", help="Note field")
parser.add_argument("--password", help="Password field")
parser.add_argument("--owner", help="Address book owner (username)")
# Peer management arguments
parser.add_argument("--peer-id", help="Peer ID")
parser.add_argument("--alias", help="Peer alias")
parser.add_argument("--tags", help="Peer tags (supports both 'tag1,tag2' and '[tag1,tag2]' formats, use '[]' to clear tags)")
# Tag management arguments
parser.add_argument("--tag-name", help="Tag name")
parser.add_argument("--tag-color", type=parse_color, help="Tag color (hex number like 0xFF00FF00 or decimal, auto-generated if not specified)")
# Rule management arguments
parser.add_argument("--rule-type", choices=["user", "group", "everyone"], help="Rule type (auto-detected if not specified)")
parser.add_argument("--rule-user", help="Rule target user name (auto-sets rule-type=user)")
parser.add_argument("--rule-group", help="Rule target group name (auto-sets rule-type=group)")
parser.add_argument("--rule-permission", type=parse_permission, help="Rule permission (ro=Read, rw=ReadWrite, full=FullControl, or numeric 1/2/3)")
parser.add_argument("--rule-guid", help="Rule GUID (for update/delete)")
args = parser.parse_args()
# Remove trailing slashes from URL
while args.url.endswith("/"):
args.url = args.url[:-1]
if args.command == "view-ab":
# View all shared address books
abs = view_shared_abs(args.url, args.token, args.ab_name)
print(json.dumps(abs, indent=2))
elif args.command == "get-personal-ab":
# Get personal address book GUID
personal_ab = get_personal_ab(args.url, args.token)
print(json.dumps(personal_ab, indent=2))
elif args.command in ["add-ab", "update-ab", "delete-ab"]:
# Address book management commands
if args.command == "add-ab":
if not args.ab_name:
print("Error: --ab-name is required for add-ab command")
return
result = add_shared_ab(args.url, args.token, args.ab_name, args.note, args.password)
print(f"Result: {result}")
elif args.command in ["update-ab", "delete-ab"]:
# Commands that need ab-name or ab-guid
if not args.ab_name and not args.ab_guid:
print("Error: --ab-name or --ab-guid is required for this command")
return
if args.ab_name and args.ab_guid:
print("Error: Cannot specify both --ab-name and --ab-guid")
return
if args.ab_guid:
ab_guid = args.ab_guid
print(f"Working with address book GUID: {ab_guid}")
else:
# Get address book by name
ab = get_ab_by_name(args.url, args.token, args.ab_name)
if not ab:
print(f"Error: Address book '{args.ab_name}' not found")
return
ab_guid = ab["guid"]
print(f"Working with address book: {args.ab_name} (GUID: {ab_guid})")
if args.command == "update-ab":
result = update_shared_ab(args.url, args.token, ab_guid, args.ab_update_name, args.note, args.owner, args.password)
print(f"Result: {result}")
elif args.command == "delete-ab":
result = delete_shared_abs(args.url, args.token, ab_guid)
print(f"Result: {result}")
elif args.command in ["view-peer", "add-peer", "update-peer", "delete-peer", "view-tag", "add-tag", "update-tag", "delete-tag", "view-rule", "add-rule", "update-rule", "delete-rule"]:
if not args.ab_name and not args.ab_guid:
print("Error: --ab-name or --ab-guid is required for this command")
return
if args.ab_name and args.ab_guid:
print("Error: Cannot specify both --ab-name and --ab-guid")
return
if args.ab_guid:
ab_guid = args.ab_guid
print(f"Working with address book GUID: {ab_guid}")
else:
# Get address book by name
ab = get_ab_by_name(args.url, args.token, args.ab_name)
if not ab:
print(f"Error: Address book '{args.ab_name}' not found")
return
ab_guid = ab["guid"]
print(f"Working with address book: {args.ab_name} (GUID: {ab_guid})")
if args.command == "view-peer":
peers = view_ab_peers(args.url, args.token, ab_guid, args.peer_id, args.alias)
print(json.dumps(peers, indent=2))
elif args.command == "add-peer":
if not args.peer_id:
print("Error: --peer-id is required for add-peer command")
return
# Handle tags parsing - support both [tag1,tag2] and tag1,tag2 formats
tags = None
if args.tags is not None:
if args.tags == "[]":
tags = [] # Empty list to clear tags
else:
# Remove brackets if present and split by comma
tags_str = args.tags.strip()
if tags_str.startswith('[') and tags_str.endswith(']'):
tags_str = tags_str[1:-1] # Remove brackets
tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
result = add_peer(
args.url,
args.token,
ab_guid,
args.peer_id,
args.alias,
args.note,
tags,
args.password
)
print(f"Result: {result}")
elif args.command == "update-peer":
if not args.peer_id:
print("Error: --peer-id is required for update-peer command")
return
# Handle tags parsing - support both [tag1,tag2] and tag1,tag2 formats
tags = None
if args.tags is not None:
if args.tags == "[]":
tags = [] # Empty list to clear tags
else:
# Remove brackets if present and split by comma
tags_str = args.tags.strip()
if tags_str.startswith('[') and tags_str.endswith(']'):
tags_str = tags_str[1:-1] # Remove brackets
tags = [tag.strip() for tag in tags_str.split(",") if tag.strip()]
result = update_peer(
args.url,
args.token,
ab_guid,
args.peer_id,
args.alias,
args.note,
tags,
args.password
)
print(f"Result: {result}")
elif args.command == "delete-peer":
if not args.peer_id:
print("Error: --peer-id is required for delete-peer command")
return
result = delete_peer(args.url, args.token, ab_guid, args.peer_id)
print(f"Result: {result}")
elif args.command == "view-tag":
tags = view_ab_tags(args.url, args.token, ab_guid)
print(json.dumps(tags, indent=2))
elif args.command == "add-tag":
if not args.tag_name:
print("Error: --tag-name is required for add-tag command")
return
result = add_tag(args.url, args.token, ab_guid, args.tag_name, args.tag_color)
print(f"Result: {result}")
elif args.command == "update-tag":
if not args.tag_name:
print("Error: --tag-name is required for update-tag command")
return
result = update_tag(args.url, args.token, ab_guid, args.tag_name, args.tag_color)
print(f"Result: {result}")
elif args.command == "delete-tag":
if not args.tag_name:
print("Error: --tag-name is required for delete-tag command")
return
result = delete_tags(args.url, args.token, ab_guid, args.tag_name)
print(f"Result: {result}")
elif args.command == "view-rule":
rules = view_ab_rules(args.url, args.token, ab_guid)
print(json.dumps(rules, indent=2))
elif args.command == "add-rule":
if not args.rule_permission:
print("Error: --rule-permission is required for add-rule command")
return
# Auto-detect rule type if not explicitly specified
if not args.rule_type:
if args.rule_user and args.rule_group:
print("Error: Cannot specify both --rule-user and --rule-group")
return
elif args.rule_user:
rule_type = "user"
elif args.rule_group:
rule_type = "group"
else:
print("Error: Must specify --rule-type=everyone, --rule-user, or --rule-group")
return
else:
rule_type = args.rule_type
# Validate explicit rule type with parameters
if rule_type == "user" and not args.rule_user:
print("Error: --rule-user is required when rule-type=user")
return
elif rule_type == "group" and not args.rule_group:
print("Error: --rule-group is required when rule-type=group")
return
elif rule_type == "user" and args.rule_group:
print("Error: Cannot specify --rule-group when rule-type=user")
return
elif rule_type == "group" and args.rule_user:
print("Error: Cannot specify --rule-user when rule-type=group")
return
elif rule_type == "everyone" and (args.rule_user or args.rule_group):
print("Error: Cannot specify --rule-user or --rule-group when rule-type=everyone")
return
result = add_ab_rule(args.url, args.token, ab_guid, rule_type, args.rule_user, args.rule_group, args.rule_permission)
print(f"Result: {result}")
elif args.command == "update-rule":
if not args.rule_guid:
print("Error: --rule-guid is required for update-rule command")
return
if not args.rule_permission:
print("Error: --rule-permission is required for update-rule command")
return
result = update_ab_rule(args.url, args.token, args.rule_guid, args.rule_permission)
print(f"Result: {result}")
elif args.command == "delete-rule":
if not args.rule_guid:
print("Error: --rule-guid is required for delete-rule command")
return
result = delete_ab_rules(args.url, args.token, args.rule_guid)
print(f"Result: {result}")
if __name__ == "__main__":
main()
Executable
+374
View File
@@ -0,0 +1,374 @@
#!/usr/bin/env python3
import requests
import argparse
import json
from datetime import datetime, timedelta, timezone
def format_timestamp(timestamp):
"""Convert Unix timestamp to readable local datetime"""
if timestamp is None:
return None
try:
# Convert to local time
local_dt = datetime.fromtimestamp(timestamp)
return local_dt.strftime("%Y-%m-%d %H:%M:%S")
except (ValueError, TypeError):
return timestamp
def parse_local_time_to_utc_string(time_str):
"""Parse local time string to UTC time string for API filtering"""
try:
# Parse the local time string
local_dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S.%f")
# Make the datetime object timezone-aware using system's local timezone
local_dt = local_dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
utc_dt = local_dt.astimezone(timezone.utc)
return utc_dt.strftime("%Y-%m-%d %H:%M:%S.000")
except ValueError:
try:
# Try without microseconds
local_dt = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
# Make the datetime object timezone-aware using system's local timezone
local_dt = local_dt.replace(tzinfo=datetime.now().astimezone().tzinfo)
utc_dt = local_dt.astimezone(timezone.utc)
return utc_dt.strftime("%Y-%m-%d %H:%M:%S.000")
except ValueError:
return None
def get_connection_type_name(conn_type):
"""Convert connection type number to readable name"""
type_map = {
0: "Remote Desktop",
1: "File Transfer",
2: "Port Transfer",
3: "View Camera",
4: "Terminal"
}
return type_map.get(conn_type, f"Unknown ({conn_type})")
def get_console_type_name(console_type):
"""Convert console audit type number to readable name"""
type_map = {
0: "Group Management",
1: "User Management",
2: "Device Management",
3: "Address Book Management"
}
return type_map.get(console_type, f"Unknown ({console_type})")
def get_console_operation_name(operation_code):
"""Convert console operation code to readable name"""
operation_map = {
0: "User Login",
1: "Add Group",
2: "Add User",
3: "Add Device",
4: "Delete Groups",
5: "Disconnect Device",
6: "Enable Users",
7: "Disable Users",
8: "Enable Devices",
9: "Disable Devices",
10: "Update Group",
11: "Update User",
12: "Update Device",
13: "Delete User",
14: "Delete Device",
15: "Add Address Book",
16: "Delete Address Book",
17: "Change Address Book Name",
18: "Delete Devices in the Address Book Recycle Bin",
19: "Empty Address Book Recycle Bin",
20: "Add Address Book Permission",
21: "Delete Address Book Permission",
22: "Update Address Book Permission"
}
return operation_map.get(operation_code, f"Unknown ({operation_code})")
def get_alarm_type_name(alarm_type):
"""Convert alarm type number to readable name"""
type_map = {
0: "Access attempt outside the IP whitelist",
1: "Over 30 consecutive access attempts",
2: "Multiple access attempts within one minute",
3: "Over 30 consecutive login attempts",
4: "Multiple login attempts within one minute",
5: "Multiple login attempts within one hour"
}
return type_map.get(alarm_type, f"Unknown ({alarm_type})")
def enhance_audit_data(data, audit_type):
"""Enhance audit data with readable formats"""
if not data:
return data
enhanced_data = []
for item in data:
enhanced_item = item.copy()
# Convert timestamps - replace original values
if 'created_at' in enhanced_item:
enhanced_item['created_at'] = format_timestamp(enhanced_item['created_at'])
if 'end_time' in enhanced_item:
enhanced_item['end_time'] = format_timestamp(enhanced_item['end_time'])
# Add type-specific enhancements - replace original values
if audit_type == 'conn':
if 'conn_type' in enhanced_item:
enhanced_item['conn_type'] = get_connection_type_name(enhanced_item['conn_type'])
else:
enhanced_item['conn_type'] = "Not Logged In"
elif audit_type == 'console':
if 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
enhanced_item['type'] = get_console_type_name(enhanced_item['typ'])
del enhanced_item['typ']
if 'iop' in enhanced_item:
# Replace iop field with operation and convert to readable name
enhanced_item['operation'] = get_console_operation_name(enhanced_item['iop'])
del enhanced_item['iop']
elif audit_type == 'alarm' and 'typ' in enhanced_item:
# Replace typ field with type and convert to readable name
enhanced_item['type'] = get_alarm_type_name(enhanced_item['typ'])
del enhanced_item['typ']
enhanced_data.append(enhanced_item)
return enhanced_data
def check_response(response):
"""Check API response and return result"""
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
try:
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
return response_json
except ValueError:
return response.text or "Success"
def view_audits_common(url, token, endpoint, filters=None, page_size=None, current=None,
created_at=None, days_ago=None, non_wildcard_fields=None):
"""Common function for viewing audits"""
headers = {"Authorization": f"Bearer {token}"}
# Set default page size and current page
if page_size is None:
page_size = 10
if current is None:
current = 1
params = {
"pageSize": page_size,
"current": current
}
# Add filter parameters if provided
if filters:
for key, value in filters.items():
if value is not None:
params[key] = value
# Handle time filters
if days_ago is not None:
# Calculate datetime from days ago
target_time = datetime.now() - timedelta(days=days_ago)
# Convert to UTC time string using system timezone
utc_timestamp = target_time.timestamp()
utc_dt = datetime.fromtimestamp(utc_timestamp, timezone.utc)
params["created_at"] = utc_dt.strftime("%Y-%m-%d %H:%M:%S.000")
elif created_at:
# Parse local time string and convert to UTC time string
utc_time_str = parse_local_time_to_utc_string(created_at)
if utc_time_str is not None:
params["created_at"] = utc_time_str
else:
# If parsing fails, pass the original value
params["created_at"] = created_at
# Apply wildcard patterns for string fields (excluding specific fields)
if non_wildcard_fields is None:
non_wildcard_fields = set()
# Always exclude these fields from wildcard treatment
non_wildcard_fields.update(["created_at", "pageSize", "current"])
string_params = {}
for k, v in params.items():
if isinstance(v, str) and k not in non_wildcard_fields:
if v != "-" and "%" not in v:
string_params[k] = "%" + v + "%"
else:
string_params[k] = v
else:
string_params[k] = v
response = requests.get(f"{url}/api/audits/{endpoint}", headers=headers, params=string_params)
response_json = check_response(response)
# Enhance the data with readable formats
data = enhance_audit_data(response_json.get("data", []), endpoint)
return {
"data": data,
"total": response_json.get("total", 0),
"current": current,
"pageSize": page_size
}
def view_conn_audits(url, token, remote=None, conn_type=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View connection audits"""
filters = {
"remote": remote,
"conn_type": conn_type
}
non_wildcard_fields = {"conn_type"}
return view_audits_common(
url, token, "conn", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
def view_file_audits(url, token, remote=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View file audits"""
filters = {
"remote": remote
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "file", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
def view_alarm_audits(url, token, device=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View alarm audits"""
filters = {
"device": device
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "alarm", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
def view_console_audits(url, token, operator=None,
page_size=None, current=None, created_at=None, days_ago=None):
"""View console audits"""
filters = {
"operator": operator
}
non_wildcard_fields = set()
return view_audits_common(
url, token, "console", filters, page_size, current, created_at, days_ago, non_wildcard_fields
)
def main():
parser = argparse.ArgumentParser(description="Audits manager")
parser.add_argument(
"command",
choices=["view-conn", "view-file", "view-alarm", "view-console"],
help="Command to execute",
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument("--token", required=True, help="Bearer token for authentication")
# Pagination parameters
parser.add_argument("--page-size", type=int, default=10, help="Number of records per page (default: 10)")
parser.add_argument("--current", type=int, default=1, help="Current page number (default: 1)")
# Time filtering parameters
parser.add_argument("--created-at", help="Filter by creation time in local time (format: 2025-09-16 14:15:57 or 2025-09-16 14:15:57.000)")
parser.add_argument("--days-ago", type=int, help="Filter by days ago (e.g., 7 for last 7 days)")
# Audit filters (simplified)
parser.add_argument("--remote", help="Remote peer ID filter (for conn/file audits)")
parser.add_argument("--device", help="Device ID filter (for alarm audits)")
parser.add_argument("--conn-type", type=int, help="Connection type filter (for conn audits only): 0=Remote Desktop, 1=File Transfer, 2=Port Transfer, 3=View Camera, 4=Terminal")
parser.add_argument("--operator", help="Operator filter (for console audits only)")
args = parser.parse_args()
# Remove trailing slashes from URL
while args.url.endswith("/"):
args.url = args.url[:-1]
if args.command == "view-conn":
# View connection audits
result = view_conn_audits(
args.url,
args.token,
args.remote,
args.conn_type,
args.page_size,
args.current,
args.created_at,
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-file":
# View file audits
result = view_file_audits(
args.url,
args.token,
args.remote,
args.page_size,
args.current,
args.created_at,
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-alarm":
# View alarm audits
result = view_alarm_audits(
args.url,
args.token,
args.device,
args.page_size,
args.current,
args.created_at,
args.days_ago
)
print(json.dumps(result, indent=2))
elif args.command == "view-console":
# View console audits
result = view_console_audits(
args.url,
args.token,
args.operator,
args.page_size,
args.current,
args.created_at,
args.days_ago
)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
#! /usr/bin/env bash
sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml libs/portable/Cargo.toml
cargo run # to bump version in cargo lock
+274
View File
@@ -0,0 +1,274 @@
#!/usr/bin/env python3
import requests
import argparse
import json
def check_response(response):
"""
Check API response and handle errors.
Two error cases:
1. Status code is not 200 -> exit with error
2. Response contains {"error": "xxx"} -> exit with error
"""
if response.status_code != 200:
print(f"Error: HTTP {response.status_code}: {response.text}")
exit(1)
# Check for {"error": "xxx"} in response
if response.text and response.text.strip():
try:
json_data = response.json()
if isinstance(json_data, dict) and "error" in json_data:
print(f"Error: {json_data['error']}")
exit(1)
return json_data
except ValueError:
return response.text
return None
def headers_with(token):
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# ---------- Device Group APIs ----------
def list_groups(url, token, name=None, page_size=50):
headers = headers_with(token)
params = {"pageSize": page_size}
if name:
params["name"] = name
data, current = [], 0
while True:
current += 1
params["current"] = current
r = requests.get(f"{url}/api/device-groups", headers=headers, params=params)
if r.status_code != 200:
print(f"Error: HTTP {r.status_code} - {r.text}")
exit(1)
res = r.json()
if "error" in res:
print(f"Error: {res['error']}")
exit(1)
rows = res.get("data", [])
data.extend(rows)
total = res.get("total", 0)
if len(rows) < page_size or current * page_size >= total:
break
return data
def get_group_by_name(url, token, name):
groups = list_groups(url, token, name)
for g in groups:
if str(g.get("name")) == name:
return g
return None
def create_group(url, token, name, note=None, accessed_from=None):
headers = headers_with(token)
payload = {"name": name}
if note:
payload["note"] = note
if accessed_from:
payload["allowed_incomings"] = accessed_from
r = requests.post(f"{url}/api/device-groups", headers=headers, json=payload)
return check_response(r)
def update_group(url, token, name, new_name=None, note=None, accessed_from=None):
headers = headers_with(token)
g = get_group_by_name(url, token, name)
if not g:
print(f"Error: Group '{name}' not found")
exit(1)
guid = g.get("guid")
payload = {}
if new_name is not None:
payload["name"] = new_name
if note is not None:
payload["note"] = note
if accessed_from is not None:
payload["allowed_incomings"] = accessed_from
r = requests.patch(f"{url}/api/device-groups/{guid}", headers=headers, json=payload)
check_response(r)
return "Success"
def delete_groups(url, token, names):
headers = headers_with(token)
if isinstance(names, str):
names = [names]
for n in names:
g = get_group_by_name(url, token, n)
if not g:
print(f"Error: Group '{n}' not found")
exit(1)
guid = g.get("guid")
r = requests.delete(f"{url}/api/device-groups/{guid}", headers=headers)
check_response(r)
return "Success"
# ---------- Device group assign APIs (name -> guid) ----------
def view_devices(url, token, group_name=None, id=None, device_name=None,
user_name=None, device_username=None, page_size=50):
"""View devices in a device group with filters"""
headers = headers_with(token)
# Separate exact match and fuzzy match params
params = {}
fuzzy_params = {
"id": id,
"device_name": device_name,
"user_name": user_name,
"device_username": device_username,
}
# Add device_group_name without wildcard (exact match)
if group_name:
params["device_group_name"] = group_name
# Add wildcard for fuzzy search to other params
for k, v in fuzzy_params.items():
if v is not None:
params[k] = "%" + v + "%" if (v != "-" and "%" not in v) else v
params["pageSize"] = page_size
data, current = [], 0
while True:
current += 1
params["current"] = current
r = requests.get(f"{url}/api/devices", headers=headers, params=params)
if r.status_code != 200:
return check_response(r)
res = r.json()
rows = res.get("data", [])
data.extend(rows)
total = res.get("total", 0)
if len(rows) < page_size or current * page_size >= total:
break
return data
def add_devices(url, token, group_name, device_ids):
headers = headers_with(token)
g = get_group_by_name(url, token, group_name)
if not g:
return f"Group '{group_name}' not found"
guid = g.get("guid")
payload = device_ids if isinstance(device_ids, list) else [device_ids]
r = requests.post(f"{url}/api/device-groups/{guid}", headers=headers, json=payload)
return check_response(r)
def remove_devices(url, token, group_name, device_ids):
headers = headers_with(token)
g = get_group_by_name(url, token, group_name)
if not g:
return f"Group '{group_name}' not found"
guid = g.get("guid")
payload = device_ids if isinstance(device_ids, list) else [device_ids]
r = requests.delete(f"{url}/api/device-groups/{guid}/devices", headers=headers, json=payload)
return check_response(r)
def parse_rules(s):
if not s:
return None
try:
v = json.loads(s)
if isinstance(v, list):
# expect list of {"type": number, "name": string}
return v
except Exception:
pass
return None
def main():
parser = argparse.ArgumentParser(description="Device Group manager")
parser.add_argument("command", choices=[
"view", "add", "update", "delete",
"view-devices", "add-devices", "remove-devices"
], help=(
"Command to execute. "
"[view/add/update/delete/add-devices/remove-devices: require Device Group Permission] "
"[view-devices: require Device Permission]"
))
parser.add_argument("--url", required=True)
parser.add_argument("--token", required=True)
parser.add_argument("--name", help="Device group name (exact match)")
parser.add_argument("--new-name", help="New device group name (for update)")
parser.add_argument("--note", help="Note")
parser.add_argument("--accessed-from", help="JSON array: '[{\"type\":0|2,\"name\":\"...\"}]' (0=User Group, 2=User)")
parser.add_argument("--ids", help="Comma separated device IDs for add-devices/remove-devices")
# Filters for view-devices command
parser.add_argument("--id", help="Device ID filter (for view-devices)")
parser.add_argument("--device-name", help="Device name filter (for view-devices)")
parser.add_argument("--user-name", help="User name filter (owner of device, for view-devices)")
parser.add_argument("--device-username", help="Device username filter (logged in user on device, for view-devices)")
args = parser.parse_args()
while args.url.endswith("/"): args.url = args.url[:-1]
if args.command == "view":
res = list_groups(args.url, args.token, args.name)
print(json.dumps(res, indent=2))
elif args.command == "add":
if not args.name:
print("Error: --name is required")
exit(1)
print(create_group(
args.url, args.token, args.name, args.note,
parse_rules(args.accessed_from)
))
elif args.command == "update":
if not args.name:
print("Error: --name is required")
exit(1)
print(update_group(
args.url, args.token, args.name, args.new_name, args.note,
parse_rules(args.accessed_from)
))
elif args.command == "delete":
if not args.name:
print("Error: --name is required (supports comma separated)")
exit(1)
names = [x.strip() for x in args.name.split(",") if x.strip()]
print(delete_groups(args.url, args.token, names))
elif args.command == "view-devices":
res = view_devices(
args.url,
args.token,
group_name=args.name,
id=args.id,
device_name=args.device_name,
user_name=args.user_name,
device_username=args.device_username
)
print(json.dumps(res, indent=2))
elif args.command in ("add-devices", "remove-devices"):
if not args.name or not args.ids:
print("Error: --name and --ids are required for add/remove devices")
exit(1)
ids = [x.strip() for x in args.ids.split(",") if x.strip()]
if args.command == "add-devices":
print(add_devices(args.url, args.token, args.name, ids))
else:
print(remove_devices(args.url, args.token, args.name, ids))
if __name__ == "__main__":
main()
Executable
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
import requests
import argparse
from datetime import datetime, timedelta
def view(
url,
token,
id=None,
device_name=None,
user_name=None,
group_name=None,
device_group_name=None,
offline_days=None,
):
headers = {"Authorization": f"Bearer {token}"}
pageSize = 30
params = {
"id": id,
"device_name": device_name,
"user_name": user_name,
"group_name": group_name,
"device_group_name": device_group_name,
}
params = {
k: "%" + v + "%" if (v != "-" and "%" not in v) else v
for k, v in params.items()
if v is not None
}
params["pageSize"] = pageSize
devices = []
current = 0
while True:
current += 1
params["current"] = current
response = requests.get(f"{url}/api/devices", headers=headers, params=params)
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
data = response_json.get("data", [])
for device in data:
if offline_days is None:
devices.append(device)
continue
last_online = datetime.strptime(
device["last_online"].split(".")[0], "%Y-%m-%dT%H:%M:%S"
) # assuming date is in this format
if (datetime.utcnow() - last_online).days >= offline_days:
devices.append(device)
total = response_json.get("total", 0)
if len(data) < pageSize or current * pageSize >= total:
break
return devices
def check(response):
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
try:
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
return response_json
except ValueError:
return response.text or "Success"
def disable(url, token, guid, id):
print("Disable", id)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{url}/api/devices/{guid}/disable", headers=headers)
return check(response)
def enable(url, token, guid, id):
print("Enable", id)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{url}/api/devices/{guid}/enable", headers=headers)
return check(response)
def delete(url, token, guid, id):
print("Delete", id)
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/devices/{guid}", headers=headers)
return check(response)
def assign(url, token, guid, id, type, value):
print("assign", id, type, value)
valid_types = [
"ab",
"strategy_name",
"user_name",
"device_group_name",
"note",
"device_username",
"device_name",
]
if type not in valid_types:
print(f"Invalid type, it must be one of: {', '.join(valid_types)}")
return
data = {"type": type, "value": value}
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(
f"{url}/api/devices/{guid}/assign", headers=headers, json=data
)
return check(response)
def main():
parser = argparse.ArgumentParser(description="Device manager")
parser.add_argument(
"command",
choices=["view", "disable", "enable", "delete", "assign"],
help="Command to execute",
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument(
"--token", required=True, help="Bearer token for authentication"
)
parser.add_argument("--id", help="Device ID")
parser.add_argument("--device_name", help="Device name")
parser.add_argument("--user_name", help="User name")
parser.add_argument("--group_name", help="User group name")
parser.add_argument("--device_group_name", help="Device group name")
parser.add_argument(
"--assign_to",
help="<type>=<value>, e.g. user_name=mike, strategy_name=test, device_group_name=group1, note=note1, device_username=username1, device_name=name1, ab=ab1, ab=ab1,tag1,alias1,password1,note1"
)
parser.add_argument(
"--offline_days", type=int, help="Offline duration in days, e.g., 7"
)
args = parser.parse_args()
while args.url.endswith("/"): args.url = args.url[:-1]
devices = view(
args.url,
args.token,
args.id,
args.device_name,
args.user_name,
args.group_name,
args.device_group_name,
args.offline_days,
)
if args.command == "view":
for device in devices:
print(device)
elif args.command in ["disable", "enable", "delete", "assign"]:
# Check if we need user confirmation for multiple devices
if len(devices) > 1:
print(f"Found {len(devices)} devices. Do you want to proceed with {args.command} operation on the devices? (Y/N)")
confirmation = input("Type 'Y' to confirm: ").strip()
if confirmation.upper() != 'Y':
print("Operation cancelled.")
return
if args.command == "disable":
for device in devices:
response = disable(args.url, args.token, device["guid"], device["id"])
print(response)
elif args.command == "enable":
for device in devices:
response = enable(args.url, args.token, device["guid"], device["id"])
print(response)
elif args.command == "delete":
for device in devices:
response = delete(args.url, args.token, device["guid"], device["id"])
print(response)
elif args.command == "assign":
if "=" not in args.assign_to:
print("Invalid assign_to format, it must be <type>=<value>")
return
type, value = args.assign_to.split("=", 1)
for device in devices:
response = assign(
args.url, args.token, device["guid"], device["id"], type, value
)
print(response)
if __name__ == "__main__":
main()
@@ -0,0 +1,16 @@
diff --git a/flutter-sdk/.gclient b/flutter-sdk/.gclient
new file mode 100644
index 0000000..fd12886
--- /dev/null
+++ b/flutter-sdk/.gclient
@@ -0,0 +1,10 @@
+solutions = [
+ {
+ "managed": False,
+ "name": "src/flutter",
+ "url": "https://github.com/flutter/engine.git@FLUTTER_VERSION_PLACEHOLDER",
+ "custom_deps": {},
+ "deps_file": "DEPS",
+ "safesync_url": "",
+ },
+]
@@ -0,0 +1,24 @@
diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle
index f4dc69e..6b835fd 100644
--- a/flutter/android/app/build.gradle
+++ b/flutter/android/app/build.gradle
@@ -67,6 +67,19 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules'
}
}
+
+ applicationVariants.all { variant ->
+ variant.outputs.each { output ->
+ output.processManifest.doLast { task ->
+ def outputDir = multiApkManifestOutputDirectory.asFile.get()
+ File manifestOutFile = new File(outputDir, "AndroidManifest.xml")
+ if (manifestOutFile.exists()) {
+ def newFileContents = manifestOutFile.getText('UTF-8').replace("android:debuggable=\"true\"", "")
+ manifestOutFile.write(newFileContents, 'UTF-8')
+ }
+ }
+ }
+ }
}
flutter {
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
for size in 16 32 64 128 256 512 1024; do
#inkscape -z -o $size.png -w $size -h $size icon.svg >/dev/null 2>/dev/null
convert icon.png -resize ${size}x${size} app_icon_$size.png
done
# from ImageMagick
convert 16.png 32.png 48.png 128.png 256.png -colors 256 icon.ico
#/bin/rm 16.png 32.png 48.png 128.png 256.png
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
import re
def strip(s): return re.sub(r'\s+\n', '\n', re.sub(r'\n\s+', '\n', s))
common_css = open('src/ui/common.css').read()
common_tis = open('src/ui/common.tis', encoding='UTF8').read()
index = open('src/ui/index.html').read() \
.replace('@import url(index.css);', open('src/ui/index.css').read()) \
.replace('include "index.tis";', open('src/ui/index.tis').read()) \
.replace('include "msgbox.tis";', open('src/ui/msgbox.tis').read()) \
.replace('include "ab.tis";', open('src/ui/ab.tis').read())
remote = open('src/ui/remote.html').read() \
.replace('@import url(remote.css);', open('src/ui/remote.css').read()) \
.replace('@import url(header.css);', open('src/ui/header.css').read()) \
.replace('@import url(file_transfer.css);', open('src/ui/file_transfer.css').read()) \
.replace('include "remote.tis";', open('src/ui/remote.tis').read()) \
.replace('include "msgbox.tis";', open('src/ui/msgbox.tis').read()) \
.replace('include "grid.tis";', open('src/ui/grid.tis').read()) \
.replace('include "header.tis";', open('src/ui/header.tis').read()) \
.replace('include "file_transfer.tis";', open('src/ui/file_transfer.tis').read()) \
.replace('include "port_forward.tis";', open('src/ui/port_forward.tis').read()) \
.replace('include "printer.tis";', open('src/ui/printer.tis').read())
chatbox = open('src/ui/chatbox.html').read()
install = open('src/ui/install.html').read().replace('include "install.tis";', open('src/ui/install.tis').read())
cm = open('src/ui/cm.html').read() \
.replace('@import url(cm.css);', open('src/ui/cm.css').read()) \
.replace('include "cm.tis";', open('src/ui/cm.tis').read())
def compress(s):
s = s.replace("\r\n", "\n")
x = bytes(s, encoding='utf-8')
return '&[u8; ' + str(len(x)) + '] = b"' + str(x)[2:-1].replace(r"\'", "'").replace(r'"',
r'\"') + '"'
with open('src/ui/inline.rs', 'wt') as fh:
fh.write('const _COMMON_CSS: ' + compress(strip(common_css)) + ';\n')
fh.write('const _COMMON_TIS: ' + compress(strip(common_tis)) + ';\n')
fh.write('const _INDEX: ' + compress(strip(index)) + ';\n')
fh.write('const _REMOTE: ' + compress(strip(remote)) + ';\n')
fh.write('const _CHATBOX: ' + compress(strip(chatbox)) + ';\n')
fh.write('const _INSTALL: ' + compress(strip(install)) + ';\n')
fh.write('const _CONNECTION_MANAGER: ' + compress(strip(cm)) + ';\n')
fh.write('''
fn get(data: &[u8]) -> String {
String::from_utf8_lossy(data).to_string()
}
fn replace(data: &[u8]) -> String {
let css = get(&_COMMON_CSS[..]);
let res = get(data).replace("@import url(common.css);", &css);
let tis = get(&_COMMON_TIS[..]);
res.replace("include \\\"common.tis\\\";", &tis)
}
#[inline]
pub fn get_index() -> String {
replace(&_INDEX[..])
}
#[inline]
pub fn get_remote() -> String {
replace(&_REMOTE[..])
}
#[inline]
pub fn get_install() -> String {
replace(&_INSTALL[..])
}
#[inline]
pub fn get_chatbox() -> String {
replace(&_CHATBOX[..])
}
#[inline]
pub fn get_cm() -> String {
replace(&_CONNECTION_MANAGER[..])
}
''')
Executable
+321
View File
@@ -0,0 +1,321 @@
#!/usr/bin/env python3
import requests
import os
import time
import argparse
import logging
import shutil
import zipfile
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s [%(filename)s:%(lineno)d]",
handlers=[logging.StreamHandler()],
)
# The URL of your Flask server
BASE_URL = os.getenv("BASE_URL") or "http://localhost:5000"
# The secret key for API authentication
SECRET_KEY = os.getenv("SECRET_KEY") or "worldpeace2024"
# The headers for API requests
HEADERS = {"Authorization": f"Bearer {SECRET_KEY}"}
SIGN_TIMEOUT = int(os.getenv("SIGN_TIMEOUT") or "30")
TIMEOUT = float(os.getenv("TIMEOUT") or "900")
def create(task_name, file_path=None):
if file_path is None:
response = requests.post(
f"{BASE_URL}/tasks/{task_name}", timeout=TIMEOUT, headers=HEADERS
)
else:
with open(file_path, "rb") as f:
files = {"file": f}
response = requests.post(
f"{BASE_URL}/tasks/{task_name}",
timeout=TIMEOUT,
headers=HEADERS,
files=files,
)
return get_json(response)
def upload_file(task_id, file_path):
with open(file_path, "rb") as f:
files = {"file": f}
response = requests.post(
f"{BASE_URL}/tasks/{task_id}/files",
timeout=TIMEOUT,
headers=HEADERS,
files=files,
)
return get_json(response)
def get_status(task_id):
response = requests.get(
f"{BASE_URL}/tasks/{task_id}/status", timeout=TIMEOUT, headers=HEADERS
)
return get_json(response)
def download_files(task_id, output_dir, fn=None):
response = requests.get(
f"{BASE_URL}/tasks/{task_id}/files",
timeout=TIMEOUT,
headers=HEADERS,
stream=True,
)
# Check if the request was successful
if fn is None:
fn = f"task_{task_id}_files.zip"
if response.status_code == 200:
# Save the file to the output directory
with open(os.path.join(output_dir, fn), "wb") as f:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
return response.ok
def download_one_file(task_id, file_id, output_dir):
response = requests.get(
f"{BASE_URL}/tasks/{task_id}/files/{file_id}",
timeout=TIMEOUT,
headers=HEADERS,
stream=True,
)
# Check if the request was successful
if response.status_code == 200:
# Save the file to the output directory
with open(os.path.join(output_dir, file_id), "wb") as f:
for chunk in response.iter_content(chunk_size=1024):
if chunk:
f.write(chunk)
return response.ok
def fetch(tag=None):
response = requests.get(
f"{BASE_URL}/tasks/fetch_task" + ("?tag=%s" % tag if tag else ""),
timeout=TIMEOUT,
headers=HEADERS,
)
return get_json(response)
def update_status(task_id, status):
response = requests.patch(
f"{BASE_URL}/tasks/{task_id}/status",
timeout=TIMEOUT,
headers=HEADERS,
json=status,
)
return get_json(response)
def delete_task(task_id):
response = requests.delete(
f"{BASE_URL}/tasks/{task_id}",
timeout=TIMEOUT,
headers=HEADERS,
)
return get_json(response)
def sign(file_path):
res = create("sign", file_path)
if res.ok:
task_id = res.task_id
# Poll the status every second
while True:
status = get_status(task_id)
if status["status"] == "done":
# Download the files
download_files(task_id, "output")
# Delete the task
delete_task(task_id)
break
time.sleep(1)
def sign_one_file(file_path):
logging.info(f"Signing {file_path}")
res = create("sign", file_path)
logging.info(f"Uploaded {file_path}")
task_id = res["id"]
n = 0
while True:
if n >= SIGN_TIMEOUT:
delete_task(task_id)
logging.error(f"Failed to sign {file_path}")
break
time.sleep(6)
n += 1
status = get_status(task_id)
if status and status.get("state") == "done":
download_one_file(
task_id, os.path.basename(file_path), os.path.dirname(file_path)
)
delete_task(task_id)
logging.info(f"Signed {file_path}")
return True
return False
def get_json(response):
try:
return response.json()
except Exception as e:
raise Exception(response.text)
SIGN_EXTENSIONS = [
".dll",
".exe",
".sys",
".vxd",
".msix",
".msixbundle",
".appx",
".appxbundle",
".msi",
".msp",
".msm",
".cab",
".ps1",
".psm1",
]
def sign_files(dir_path, only_ext=None):
if only_ext:
only_ext = only_ext.split(",")
for i in range(len(only_ext)):
if not only_ext[i].startswith("."):
only_ext[i] = "." + only_ext[i]
for root, dirs, files in os.walk(dir_path):
is_signed_dir = "RustDeskPrinterDriver" in root or "usbmmidd_v2" in root
for file in files:
file_path = os.path.join(root, file)
_, ext = os.path.splitext(file_path)
# only sign the exe files in signed dirs
if is_signed_dir and ext not in [".exe"]:
continue
if only_ext and ext not in only_ext:
continue
if ext in SIGN_EXTENSIONS:
if not sign_one_file(file_path):
logging.error(f"Failed to sign {file_path}")
break
def main():
parser = argparse.ArgumentParser(
description="Command line interface for task operations."
)
subparsers = parser.add_subparsers(dest="command")
# Create a parser for the "sign_one_file" command
sign_one_file_parser = subparsers.add_parser(
"sign_one_file", help="Sign a single file."
)
sign_one_file_parser.add_argument("file_path", help="The path of the file to sign.")
# Create a parser for the "sign_files" command
sign_files_parser = subparsers.add_parser(
"sign_files", help="Sign all files in a directory."
)
sign_files_parser.add_argument(
"dir_path", help="The path of the directory containing the files to sign."
)
sign_files_parser.add_argument(
"only_ext", help="The file extension to sign.", default=None, nargs="?"
)
# Create a parser for the "fetch" command
fetch_parser = subparsers.add_parser("fetch", help="Fetch a task.")
# Create a parser for the "update_status" command
update_status_parser = subparsers.add_parser(
"update_status", help="Update the status of a task."
)
update_status_parser.add_argument("task_id", help="The ID of the task to update.")
update_status_parser.add_argument("status", help="The new status of the task.")
# Create a parser for the "delete_task" command
delete_task_parser = subparsers.add_parser("delete_task", help="Delete a task.")
delete_task_parser.add_argument("task_id", help="The ID of the task to delete.")
# Create a parser for the "create" command
create_parser = subparsers.add_parser("create", help="Create a task.")
create_parser.add_argument("task_name", help="The name of the task to create.")
create_parser.add_argument(
"file_path",
help="The path of the file for the task.",
default=None,
nargs="?",
)
# Create a parser for the "upload_file" command
upload_file_parser = subparsers.add_parser(
"upload_file", help="Upload a file to a task."
)
upload_file_parser.add_argument(
"task_id", help="The ID of the task to upload the file to."
)
upload_file_parser.add_argument("file_path", help="The path of the file to upload.")
# Create a parser for the "get_status" command
get_status_parser = subparsers.add_parser(
"get_status", help="Get the status of a task."
)
get_status_parser.add_argument(
"task_id", help="The ID of the task to get the status of."
)
# Create a parser for the "download_files" command
download_files_parser = subparsers.add_parser(
"download_files", help="Download files from a task."
)
download_files_parser.add_argument(
"task_id", help="The ID of the task to download files from."
)
download_files_parser.add_argument(
"output_dir", help="The directory to save the downloaded files to."
)
args = parser.parse_args()
if args.command == "sign_one_file":
sign_one_file(args.file_path)
elif args.command == "sign_files":
sign_files(args.dir_path, args.only_ext)
elif args.command == "fetch":
print(fetch())
elif args.command == "update_status":
print(update_status(args.task_id, args.status))
elif args.command == "delete_task":
print(delete_task(args.task_id))
elif args.command == "create":
print(create(args.task_name, args.file_path))
elif args.command == "upload_file":
print(upload_file(args.task_id, args.file_path))
elif args.command == "get_status":
print(get_status(args.task_id))
elif args.command == "download_files":
print(download_files(args.task_id, args.output_dir))
if __name__ == "__main__":
main()
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
import os
import glob
import sys
import csv
def get_lang(lang):
out = {}
for ln in open('./src/lang/%s.rs' % lang, encoding='utf8'):
ln = ln.strip()
if ln.startswith('("'):
k, v = line_split(ln)
out[k] = v
return out
def line_split(line):
toks = line.split('", "')
if len(toks) != 2:
print(line)
assert 0
# Replace fixed position.
# Because toks[1] may be v") or v"),
k = toks[0][toks[0].find('"') + 1:]
v = toks[1][:toks[1].rfind('"')]
return k, v
def main():
if len(sys.argv) == 1:
expand()
elif sys.argv[1] == '1':
to_csv()
else:
to_rs(sys.argv[1])
def expand():
for fn in glob.glob('./src/lang/*.rs'):
lang = os.path.basename(fn)[:-3]
if lang in ['en', 'template']: continue
print(lang)
dict = get_lang(lang)
fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8')
for line in open('./src/lang/template.rs', encoding='utf8'):
line_strip = line.strip()
if line_strip.startswith('("'):
k, v = line_split(line_strip)
if k in dict:
# embraced with " to avoid empty v
line = line.replace('"%s"' % v, '"%s"' % dict[k])
else:
line = line.replace(v, "")
fw.write(line)
else:
fw.write(line)
fw.close()
def to_csv():
for fn in glob.glob('./src/lang/*.rs'):
lang = os.path.basename(fn)[:-3]
csvfile = open('./src/lang/%s.csv' % lang, "wt", encoding='utf8')
csvwriter = csv.writer(csvfile)
for line in open(fn, encoding='utf8'):
line_strip = line.strip()
if line_strip.startswith('("'):
k, v = line_split(line_strip)
csvwriter.writerow([k, v])
csvfile.close()
def to_rs(lang):
csvfile = open('%s.csv' % lang, "rt", encoding='utf8')
fw = open("./src/lang/%s.rs" % lang, "wt", encoding='utf8')
fw.write('''lazy_static::lazy_static! {
pub static ref T: std::collections::HashMap<&'static str, &'static str> =
[
''')
for row in csv.reader(csvfile):
fw.write(' ("%s", "%s"),\n' % (row[0].replace('"', '\"'), row[1].replace('"', '\"')))
fw.write(''' ].iter().cloned().collect();
}
''')
fw.close()
main()
+36
View File
@@ -0,0 +1,36 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
<!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
<!-- Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
</application>
</compatibility>
<dependency>
<dependentAssembly>
<assemblyIdentity
type="win32"
name="Microsoft.Windows.Common-Controls"
version="6.0.0.0"
processorArchitecture="*"
publicKeyToken="6595b64144ccf1df"
language="*"
/>
</dependentAssembly>
</dependency>
</assembly>
+13
View File
@@ -0,0 +1,13 @@
.vs
**/bin
**/obj
x64
packages
CustomActions/x64
CustomActions/*.user
CustomActions/*.filters
Package/Resources
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <Windows.h>
#include <string>
bool AddFirewallRule(bool add, LPWSTR exeName, LPWSTR exeFile);
bool QueryServiceStatusExW(LPCWSTR serviceName, SERVICE_STATUS_PROCESS* status);
bool IsServiceRunningW(LPCWSTR serviceName);
bool MyCreateServiceW(LPCWSTR serviceName, LPCWSTR displayName, LPCWSTR binaryPath);
bool MyDeleteServiceW(LPCWSTR serviceName);
bool MyStartServiceW(LPCWSTR serviceName);
bool MyStopServiceW(LPCWSTR serviceName);
std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key);
void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired);
namespace RemotePrinter
{
VOID installUpdatePrinter(const std::wstring& installFolder);
VOID uninstallPrinter();
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
LIBRARY "CustomActions"
EXPORTS
CustomActionHello
RemoveRuntimeGeneratedFiles
TerminateProcesses
AddFirewallRules
SetPropertyIsServiceRunning
TryStopDeleteService
CreateStartService
TryDeleteStartupShortcut
SetPropertyFromConfig
AddRegSoftwareSASGeneration
RemoveAmyuniIdd
InstallPrinter
UninstallPrinter
@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\WixToolset.WcaUtil.4.0.5\build\WixToolset.WcaUtil.props" Condition="Exists('..\packages\WixToolset.WcaUtil.4.0.5\build\WixToolset.WcaUtil.props')" />
<Import Project="..\packages\WixToolset.DUtil.4.0.5\build\WixToolset.DUtil.props" Condition="Exists('..\packages\WixToolset.DUtil.4.0.5\build\WixToolset.DUtil.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{6b3647e0-b4a3-46ae-8757-a22ee51c1dac}</ProjectGuid>
<RootNamespace>CustomActions</RootNamespace>
<PlatformToolset>v143</PlatformToolset>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;EXAMPLECADLL_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<RuntimeLibrary>MultiThreaded</RuntimeLibrary>
</ClCompile>
<Link>
<AdditionalDependencies>msi.lib;version.lib;%(AdditionalDependencies)</AdditionalDependencies>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<ModuleDefinitionFile>CustomActions.def</ModuleDefinitionFile>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Common.h" />
<ClInclude Include="framework.h" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="CustomActions.cpp" />
<ClCompile Include="DeviceUtils.cpp" />
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="FirewallRules.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="ReadConfig.cpp" />
<ClCompile Include="RemotePrinter.cpp" />
<ClCompile Include="ServiceUtils.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="CustomActions.def" />
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\WixToolset.DUtil.4.0.5\build\WixToolset.DUtil.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\WixToolset.DUtil.4.0.5\build\WixToolset.DUtil.props'))" />
<Error Condition="!Exists('..\packages\WixToolset.WcaUtil.4.0.5\build\WixToolset.WcaUtil.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\WixToolset.WcaUtil.4.0.5\build\WixToolset.WcaUtil.props'))" />
</Target>
</Project>
+84
View File
@@ -0,0 +1,84 @@
#include "pch.h"
#include <Windows.h>
#include <setupapi.h>
#include <devguid.h>
#include <cfgmgr32.h>
#pragma comment(lib, "SetupAPI.lib")
void UninstallDriver(LPCWSTR hardwareId, BOOL &rebootRequired)
{
HDEVINFO deviceInfoSet = SetupDiGetClassDevsW(&GUID_DEVCLASS_DISPLAY, NULL, NULL, DIGCF_PRESENT);
if (deviceInfoSet == INVALID_HANDLE_VALUE)
{
WcaLog(LOGMSG_STANDARD, "Failed to get device information set, last error: %d", GetLastError());
return;
}
SP_DEVINFO_LIST_DETAIL_DATA devInfoListDetail;
devInfoListDetail.cbSize = sizeof(SP_DEVINFO_LIST_DETAIL_DATA);
if (!SetupDiGetDeviceInfoListDetailW(deviceInfoSet, &devInfoListDetail))
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
WcaLog(LOGMSG_STANDARD, "Failed to call SetupDiGetDeviceInfoListDetail, last error: %d", GetLastError());
return;
}
SP_DEVINFO_DATA deviceInfoData;
deviceInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
DWORD dataType;
WCHAR deviceId[MAX_DEVICE_ID_LEN] = { 0, };
DWORD deviceIndex = 0;
while (SetupDiEnumDeviceInfo(deviceInfoSet, deviceIndex, &deviceInfoData))
{
if (!SetupDiGetDeviceRegistryPropertyW(deviceInfoSet, &deviceInfoData, SPDRP_HARDWAREID, &dataType, (PBYTE)deviceId, MAX_DEVICE_ID_LEN, NULL))
{
WcaLog(LOGMSG_STANDARD, "Failed to get hardware id, last error: %d", GetLastError());
deviceIndex++;
continue;
}
if (wcscmp(deviceId, hardwareId) != 0)
{
deviceIndex++;
continue;
}
SP_REMOVEDEVICE_PARAMS remove_device_params;
remove_device_params.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
remove_device_params.ClassInstallHeader.InstallFunction = DIF_REMOVE;
remove_device_params.Scope = DI_REMOVEDEVICE_GLOBAL;
remove_device_params.HwProfile = 0;
if (!SetupDiSetClassInstallParamsW(deviceInfoSet, &deviceInfoData, &remove_device_params.ClassInstallHeader, sizeof(SP_REMOVEDEVICE_PARAMS)))
{
WcaLog(LOGMSG_STANDARD, "Failed to set class install params, last error: %d", GetLastError());
deviceIndex++;
continue;
}
if (!SetupDiCallClassInstaller(DIF_REMOVE, deviceInfoSet, &deviceInfoData))
{
WcaLog(LOGMSG_STANDARD, "ailed to uninstall driver, last error: %d", GetLastError());
deviceIndex++;
continue;
}
SP_DEVINSTALL_PARAMS deviceParams;
if (SetupDiGetDeviceInstallParamsW(deviceInfoSet, &deviceInfoData, &deviceParams))
{
if (deviceParams.Flags & (DI_NEEDRESTART | DI_NEEDREBOOT))
{
rebootRequired = true;
}
}
WcaLog(LOGMSG_STANDARD, "Driver uninstalled successfully");
deviceIndex++;
}
SetupDiDestroyDeviceInfoList(deviceInfoSet);
}
+413
View File
@@ -0,0 +1,413 @@
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-adding-an-application-rule-edge-traversal
/********************************************************************
Copyright (C) Microsoft. All Rights Reserved.
Abstract:
This C++ file includes sample code that adds a firewall rule with
EdgeTraversalOptions (one of the EdgeTraversalOptions values).
********************************************************************/
#include "pch.h"
#include <windows.h>
#include <stdio.h>
#include <netfw.h>
#include <strsafe.h>
#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "oleaut32.lib")
#define STRING_BUFFER_SIZE 500
// Forward declarations
HRESULT WFCOMInitialize(INetFwPolicy2** ppNetFwPolicy2);
void WFCOMCleanup(INetFwPolicy2* pNetFwPolicy2);
HRESULT RemoveFirewallRule(
__in INetFwPolicy2* pNetFwPolicy2,
__in LPWSTR exeName);
HRESULT AddFirewallRuleWithEdgeTraversal(__in INetFwPolicy2* pNetFwPolicy2,
__in bool in,
__in LPWSTR exeName,
__in LPWSTR exeFile);
bool AddFirewallRule(bool add, LPWSTR exeName, LPWSTR exeFile)
{
bool result = false;
HRESULT hrComInit = S_OK;
HRESULT hr = S_OK;
INetFwPolicy2* pNetFwPolicy2 = NULL;
// Initialize COM.
hrComInit = CoInitializeEx(
0,
COINIT_APARTMENTTHREADED
);
// Ignore RPC_E_CHANGED_MODE; this just means that COM has already been
// initialized with a different mode. Since we don't care what the mode is,
// we'll just use the existing mode.
if (hrComInit != RPC_E_CHANGED_MODE)
{
if (FAILED(hrComInit))
{
WcaLog(LOGMSG_STANDARD, "CoInitializeEx failed: 0x%08lx\n", hrComInit);
goto Cleanup;
}
}
// Retrieve INetFwPolicy2
hr = WFCOMInitialize(&pNetFwPolicy2);
if (FAILED(hr))
{
goto Cleanup;
}
if (add) {
// Add firewall rule with EdgeTraversalOption=DeferApp (Windows7+) if available
// else add with Edge=True (Vista and Server 2008).
hr = AddFirewallRuleWithEdgeTraversal(pNetFwPolicy2, true, exeName, exeFile);
hr = AddFirewallRuleWithEdgeTraversal(pNetFwPolicy2, false, exeName, exeFile);
}
else {
hr = RemoveFirewallRule(pNetFwPolicy2, exeName);
}
result = SUCCEEDED(hr);
Cleanup:
// Release INetFwPolicy2
WFCOMCleanup(pNetFwPolicy2);
// Uninitialize COM.
if (SUCCEEDED(hrComInit))
{
CoUninitialize();
}
return result;
}
BSTR MakeRuleName(__in LPWSTR exeName)
{
WCHAR pwszTemp[STRING_BUFFER_SIZE] = L"";
HRESULT hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"%ls Service", exeName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr);
return NULL;
}
return SysAllocString(pwszTemp);
}
HRESULT RemoveFirewallRule(
__in INetFwPolicy2* pNetFwPolicy2,
__in LPWSTR exeName)
{
HRESULT hr = S_OK;
INetFwRules* pNetFwRules = NULL;
WCHAR pwszTemp[STRING_BUFFER_SIZE] = L"";
BSTR RuleName = NULL;
RuleName = MakeRuleName(exeName);
if (NULL == RuleName)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
hr = pNetFwPolicy2->get_Rules(&pNetFwRules);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to retrieve firewall rules collection : 0x%08lx\n", hr);
goto Cleanup;
}
// We need to "Remove()" twice, because both "in" and "out" rules are added?
// There's no remarks for this case https://learn.microsoft.com/en-us/windows/win32/api/netfw/nf-netfw-inetfwrules-remove
hr = pNetFwRules->Remove(RuleName);
hr = pNetFwRules->Remove(RuleName);
if (FAILED(hr)) {
WcaLog(LOGMSG_STANDARD, "Failed to remove firewall rule \"%ls\" : 0x%08lx\n", exeName, hr);
}
else {
WcaLog(LOGMSG_STANDARD, "Firewall rule \"%ls\" is removed\n", exeName);
}
Cleanup:
SysFreeString(RuleName);
if (pNetFwRules != NULL)
{
pNetFwRules->Release();
}
return hr;
}
// Add firewall rule with EdgeTraversalOption=DeferApp (Windows7+) if available
// else add with Edge=True (Vista and Server 2008).
HRESULT AddFirewallRuleWithEdgeTraversal(
__in INetFwPolicy2* pNetFwPolicy2,
__in bool in,
__in LPWSTR exeName,
__in LPWSTR exeFile)
{
HRESULT hr = S_OK;
INetFwRules* pNetFwRules = NULL;
INetFwRule* pNetFwRule = NULL;
INetFwRule2* pNetFwRule2 = NULL;
WCHAR pwszTemp[STRING_BUFFER_SIZE] = L"";
BSTR RuleName = NULL;
BSTR RuleGroupName = NULL;
BSTR RuleDescription = NULL;
BSTR RuleAppPath = NULL;
long CurrentProfilesBitMask = 0;
// For localization purposes, the rule name, description, and group can be
// provided as indirect strings. These indirect strings can be defined in an rc file.
// Examples of the indirect string definitions in the rc file -
// 127 "EdgeTraversalOptions Sample Application"
// 128 "Allow inbound TCP traffic to application EdgeTraversalOptions.exe"
// 129 "Allow EdgeTraversalOptions.exe to receive inbound traffic for TCP protocol
// from remote machines located within your network as well as from
// the Internet (i.e from outside of your Edge device like Firewall or NAT"
// Examples of using indirect strings -
// hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-128");
RuleName = MakeRuleName(exeName);
if (NULL == RuleName)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
// Examples of using indirect strings -
// hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-127");
hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, exeName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr);
goto Cleanup;
}
RuleGroupName = SysAllocString(pwszTemp); // Used for grouping together multiple rules
if (NULL == RuleGroupName)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
// Examples of using indirect strings -
// hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"@EdgeTraversalOptions.exe,-129");
hr = StringCchPrintfW(pwszTemp, STRING_BUFFER_SIZE, L"Allow %ls to receive \
inbound traffic from remote machines located within your network as well as \
from the Internet", exeName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to compose a resource identifier string: 0x%08lx\n", hr);
goto Cleanup;
}
RuleDescription = SysAllocString(pwszTemp);
if (NULL == RuleDescription)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
RuleAppPath = SysAllocString(exeFile);
if (NULL == RuleAppPath)
{
WcaLog(LOGMSG_STANDARD, "\nERROR: Insufficient memory\n");
goto Cleanup;
}
hr = pNetFwPolicy2->get_Rules(&pNetFwRules);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to retrieve firewall rules collection : 0x%08lx\n", hr);
goto Cleanup;
}
hr = CoCreateInstance(
__uuidof(NetFwRule), //CLSID of the class whose object is to be created
NULL,
CLSCTX_INPROC_SERVER,
__uuidof(INetFwRule), // Identifier of the Interface used for communicating with the object
(void**)&pNetFwRule);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "CoCreateInstance for INetFwRule failed: 0x%08lx\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Name(RuleName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Name failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Grouping(RuleGroupName);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Grouping failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Description(RuleDescription);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Description failed with error: 0x %x.\n", hr);
goto Cleanup;
}
// If you want the rule to avoid public, you can refer to
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ics/c-adding-an-outbound-rule
CurrentProfilesBitMask = NET_FW_PROFILE2_ALL;
hr = pNetFwRule->put_Direction(in ? NET_FW_RULE_DIR_IN : NET_FW_RULE_DIR_OUT);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Direction failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Action(NET_FW_ACTION_ALLOW);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Action failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_ApplicationName(RuleAppPath);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_ApplicationName failed with error: 0x %x.\n", hr);
goto Cleanup;
}
//hr = pNetFwRule->put_Protocol(6); // TCP
//if (FAILED(hr))
//{
// WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Protocol failed with error: 0x %x.\n", hr);
// goto Cleanup;
//}
hr = pNetFwRule->put_Profiles(CurrentProfilesBitMask);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Profiles failed with error: 0x %x.\n", hr);
goto Cleanup;
}
hr = pNetFwRule->put_Enabled(VARIANT_TRUE);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_Enabled failed with error: 0x %x.\n", hr);
goto Cleanup;
}
if (in) {
// Check if INetFwRule2 interface is available (i.e Windows7+)
// If supported, then use EdgeTraversalOptions
// Else use the EdgeTraversal boolean flag.
if (SUCCEEDED(pNetFwRule->QueryInterface(__uuidof(INetFwRule2), (void**)&pNetFwRule2)))
{
hr = pNetFwRule2->put_EdgeTraversalOptions(NET_FW_EDGE_TRAVERSAL_TYPE_DEFER_TO_APP);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_EdgeTraversalOptions failed with error: 0x %x.\n", hr);
goto Cleanup;
}
}
else
{
hr = pNetFwRule->put_EdgeTraversal(VARIANT_TRUE);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed INetFwRule::put_EdgeTraversal failed with error: 0x %x.\n", hr);
goto Cleanup;
}
}
}
hr = pNetFwRules->Add(pNetFwRule);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "Failed to add firewall rule to the firewall rules collection : 0x%08lx\n", hr);
goto Cleanup;
}
WcaLog(LOGMSG_STANDARD, "Successfully added firewall rule !\n");
Cleanup:
SysFreeString(RuleName);
SysFreeString(RuleGroupName);
SysFreeString(RuleDescription);
SysFreeString(RuleAppPath);
if (pNetFwRule2 != NULL)
{
pNetFwRule2->Release();
}
if (pNetFwRule != NULL)
{
pNetFwRule->Release();
}
if (pNetFwRules != NULL)
{
pNetFwRules->Release();
}
return hr;
}
// Instantiate INetFwPolicy2
HRESULT WFCOMInitialize(INetFwPolicy2** ppNetFwPolicy2)
{
HRESULT hr = S_OK;
hr = CoCreateInstance(
__uuidof(NetFwPolicy2),
NULL,
CLSCTX_INPROC_SERVER,
__uuidof(INetFwPolicy2),
(void**)ppNetFwPolicy2);
if (FAILED(hr))
{
WcaLog(LOGMSG_STANDARD, "CoCreateInstance for INetFwPolicy2 failed: 0x%08lx\n", hr);
goto Cleanup;
}
Cleanup:
return hr;
}
// Release INetFwPolicy2
void WFCOMCleanup(INetFwPolicy2* pNetFwPolicy2)
{
// Release the INetFwPolicy2 object (Vista+)
if (pNetFwPolicy2 != NULL)
{
pNetFwPolicy2->Release();
}
}
+36
View File
@@ -0,0 +1,36 @@
#include "pch.h"
#include <iostream>
#include <fstream>
#include <string>
#include <cwctype>
void trim(std::wstring& str) {
str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](wchar_t ch) {
return !std::iswspace(ch);
}));
str.erase(std::find_if(str.rbegin(), str.rend(), [](wchar_t ch) {
return !std::iswspace(ch);
}).base(), str.end());
}
std::wstring ReadConfig(const std::wstring& filename, const std::wstring& key)
{
std::wstring configValue;
std::wstring line;
std::wifstream file(filename);
while (std::getline(file, line)) {
trim(line);
if (line.find(key) == 0) {
std::size_t position = line.find(L"=", key.size());
if (position != std::string::npos) {
configValue = line.substr(position + 1);
trim(configValue);
break;
}
}
}
file.close();
return configValue;
}
+517
View File
@@ -0,0 +1,517 @@
#include "pch.h"
#include <Windows.h>
#include <winspool.h>
#include <setupapi.h>
#include <memory>
#include <string>
#include <functional>
#include <vector>
#include <iostream>
#include "Common.h"
#pragma comment(lib, "setupapi.lib")
#pragma comment(lib, "winspool.lib")
namespace RemotePrinter
{
#define HRESULT_ERR_ELEMENT_NOT_FOUND 0x80070490
LPCWCH RD_DRIVER_INF_PATH = L"drivers\\RustDeskPrinterDriver\\RustDeskPrinterDriver.inf";
LPCWCH RD_PRINTER_PORT = L"RustDesk Printer";
LPCWCH RD_PRINTER_NAME = L"RustDesk Printer";
LPCWCH RD_PRINTER_DRIVER_NAME = L"RustDesk v4 Printer Driver";
LPCWCH XCV_MONITOR_LOCAL_PORT = L",XcvMonitor Local Port";
using FuncEnum = std::function<BOOL(DWORD level, LPBYTE pDriverInfo, DWORD cbBuf, LPDWORD pcbNeeded, LPDWORD pcReturned)>;
template <typename T, typename R>
using FuncOnData = std::function<std::shared_ptr<R>(const T &)>;
template <typename R>
using FuncOnNoData = std::function<std::shared_ptr<R>()>;
template <class T, class R>
std::shared_ptr<R> commonEnum(std::wstring funcName, FuncEnum func, DWORD level, FuncOnData<T, R> onData, FuncOnNoData<R> onNoData)
{
DWORD needed = 0;
DWORD returned = 0;
func(level, NULL, 0, &needed, &returned);
if (needed == 0)
{
return onNoData();
}
std::vector<BYTE> buffer(needed);
if (!func(level, buffer.data(), needed, &needed, &returned))
{
return nullptr;
}
T *pPortInfo = reinterpret_cast<T *>(buffer.data());
for (DWORD i = 0; i < returned; i++)
{
auto r = onData(pPortInfo[i]);
if (r)
{
return r;
}
}
return onNoData();
}
BOOL isNameEqual(LPCWSTR lhs, LPCWSTR rhs)
{
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lstrcmpiw
// For some locales, the lstrcmpi function may be insufficient.
// If this occurs, use `CompareStringEx` to ensure proper comparison.
// For example, in Japan call with the NORM_IGNORECASE, NORM_IGNOREKANATYPE, and NORM_IGNOREWIDTH values to achieve the most appropriate non-exact string comparison.
// Note that specifying these values slows performance, so use them only when necessary.
//
// No need to consider `CompareStringEx` for now.
return lstrcmpiW(lhs, rhs) == 0 ? TRUE : FALSE;
}
BOOL enumPrinterPort(
DWORD level,
LPBYTE pPortInfo,
DWORD cbBuf,
LPDWORD pcbNeeded,
LPDWORD pcReturned)
{
// https://learn.microsoft.com/en-us/windows/win32/printdocs/enumports
// This is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors
// such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
return EnumPortsW(NULL, level, pPortInfo, cbBuf, pcbNeeded, pcReturned);
}
BOOL isPortExists(LPCWSTR port)
{
auto onData = [port](const PORT_INFO_2 &info)
{
if (isNameEqual(info.pPortName, port) == TRUE) {
return std::shared_ptr<BOOL>(new BOOL(TRUE));
}
else {
return std::shared_ptr<BOOL>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<PORT_INFO_2, BOOL>(L"EnumPortsW", enumPrinterPort, 2, onData, onNoData);
if (res == nullptr)
{
return false;
}
else
{
return *res;
}
}
BOOL executeOnLocalPort(LPCWSTR port, LPCWSTR command)
{
PRINTER_DEFAULTSW dft = {0};
dft.DesiredAccess = SERVER_WRITE;
HANDLE hMonitor = NULL;
if (OpenPrinterW(const_cast<LPWSTR>(XCV_MONITOR_LOCAL_PORT), &hMonitor, &dft) == FALSE)
{
return FALSE;
}
DWORD outputNeeded = 0;
DWORD status = 0;
if (XcvDataW(hMonitor, command, (LPBYTE)port, (lstrlenW(port) + 1) * 2, NULL, 0, &outputNeeded, &status) == FALSE)
{
ClosePrinter(hMonitor);
return FALSE;
}
ClosePrinter(hMonitor);
return TRUE;
}
BOOL addLocalPort(LPCWSTR port)
{
return executeOnLocalPort(port, L"AddPort");
}
BOOL deleteLocalPort(LPCWSTR port)
{
return executeOnLocalPort(port, L"DeletePort");
}
BOOL checkAddLocalPort(LPCWSTR port)
{
if (!isPortExists(port))
{
return addLocalPort(port);
}
return TRUE;
}
std::wstring getPrinterInstalledOnPort(LPCWSTR port);
BOOL checkDeleteLocalPort(LPCWSTR port)
{
if (isPortExists(port))
{
if (getPrinterInstalledOnPort(port) != L"")
{
WcaLog(LOGMSG_STANDARD, "The printer is installed on the port. Please remove the printer first.\n");
return FALSE;
}
return deleteLocalPort(port);
}
return TRUE;
}
BOOL enumPrinterDriver(
DWORD level,
LPBYTE pDriverInfo,
DWORD cbBuf,
LPDWORD pcbNeeded,
LPDWORD pcReturned)
{
// https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinterdrivers
// This is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors
// such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
return EnumPrinterDriversW(
NULL,
NULL,
level,
pDriverInfo,
cbBuf,
pcbNeeded,
pcReturned);
}
DWORDLONG getInstalledDriverVersion(LPCWSTR name)
{
auto onData = [name](const DRIVER_INFO_6W &info)
{
if (isNameEqual(name, info.pName) == TRUE)
{
return std::shared_ptr<DWORDLONG>(new DWORDLONG(info.dwlDriverVersion));
}
else
{
return std::shared_ptr<DWORDLONG>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<DRIVER_INFO_6W, DWORDLONG>(L"EnumPrinterDriversW", enumPrinterDriver, 6, onData, onNoData);
if (res == nullptr)
{
return 0;
}
else
{
return *res;
}
}
std::wstring findInf(LPCWSTR name)
{
auto onData = [name](const DRIVER_INFO_8W &info)
{
if (isNameEqual(name, info.pName) == TRUE)
{
return std::shared_ptr<std::wstring>(new std::wstring(info.pszInfPath));
}
else
{
return std::shared_ptr<std::wstring>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<DRIVER_INFO_8W, std::wstring>(L"EnumPrinterDriversW", enumPrinterDriver, 8, onData, onNoData);
if (res == nullptr)
{
return L"";
}
else
{
return *res;
}
}
BOOL deletePrinterDriver(LPCWSTR name)
{
// If the printer is used after the spooler service is started. E.g., printing a document through RustDesk Printer.
// `DeletePrinterDriverExW()` may fail with `ERROR_PRINTER_DRIVER_IN_USE`(3001, 0xBB9).
// We can only ignore this error for now.
// Though restarting the spooler service is a solution, it's not a good idea to restart the service.
//
// Deleting the printer driver after deleting the printer is a common practice.
// No idea why `DeletePrinterDriverExW()` fails with `ERROR_UNKNOWN_PRINTER_DRIVER` after using the printer once.
// https://github.com/ChromiumWebApps/chromium/blob/c7361d39be8abd1574e6ce8957c8dbddd4c6ccf7/cloud_print/virtual_driver/win/install/setup.cc#L422
// AnyDesk printer driver and the simplest printer driver also have the same issue.
BOOL res = DeletePrinterDriverExW(NULL, NULL, const_cast<LPWSTR>(name), DPD_DELETE_ALL_FILES, 0);
if (res == FALSE)
{
DWORD error = GetLastError();
if (error == ERROR_UNKNOWN_PRINTER_DRIVER)
{
return TRUE;
}
else
{
WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver. Error (%d)\n", error);
}
}
return res;
}
BOOL deletePrinterDriverPackage(const std::wstring &inf)
{
// https://learn.microsoft.com/en-us/windows/win32/printdocs/deleteprinterdriverpackage
// This function is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
int tries = 3;
HRESULT result = S_FALSE;
while ((result = DeletePrinterDriverPackage(NULL, inf.c_str(), NULL)) != S_OK)
{
if (result == HRESULT_ERR_ELEMENT_NOT_FOUND)
{
return TRUE;
}
WcaLog(LOGMSG_STANDARD, "Failed to delete printer driver package. HRESULT (%d)\n", result);
tries--;
if (tries <= 0)
{
return FALSE;
}
Sleep(2000);
}
return S_OK;
}
BOOL uninstallDriver(LPCWSTR name)
{
auto infFile = findInf(name);
if (!deletePrinterDriver(name))
{
return FALSE;
}
if (infFile != L"" && !deletePrinterDriverPackage(infFile))
{
return FALSE;
}
return TRUE;
}
BOOL installDriver(LPCWSTR name, LPCWSTR inf)
{
DWORD size = MAX_PATH * 10;
wchar_t package_path[MAX_PATH * 10] = {0};
HRESULT result = UploadPrinterDriverPackage(
NULL, inf, NULL,
UPDP_SILENT_UPLOAD | UPDP_UPLOAD_ALWAYS, NULL, package_path, &size);
if (result != S_OK)
{
WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache silently, failed. Will retry with user UI. HRESULT (%d)\n", result);
result = UploadPrinterDriverPackage(
NULL, inf, NULL, UPDP_UPLOAD_ALWAYS,
GetForegroundWindow(), package_path, &size);
if (result != S_OK)
{
WcaLog(LOGMSG_STANDARD, "Uploading the printer driver package to the driver cache failed with user UI. Aborting...\n");
return FALSE;
}
}
result = InstallPrinterDriverFromPackage(
NULL, package_path, name, NULL, IPDFP_COPY_ALL_FILES);
if (result != S_OK)
{
WcaLog(LOGMSG_STANDARD, "Installing the printer driver failed. HRESULT (%d)\n", result);
}
return result == S_OK;
}
BOOL enumLocalPrinter(
DWORD level,
LPBYTE pPrinterInfo,
DWORD cbBuf,
LPDWORD pcbNeeded,
LPDWORD pcReturned)
{
// https://learn.microsoft.com/en-us/windows/win32/printdocs/enumprinters
// This is a blocking or synchronous function and might not return immediately.
// How quickly this function returns depends on run-time factors
// such as network status, print server configuration, and printer driver implementation factors that are difficult to predict when writing an application.
// Calling this function from a thread that manages interaction with the user interface could make the application appear to be unresponsive.
return EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, level, pPrinterInfo, cbBuf, pcbNeeded, pcReturned);
}
BOOL isPrinterAdded(LPCWSTR name)
{
auto onData = [name](const PRINTER_INFO_1W &info)
{
if (isNameEqual(name, info.pName) == TRUE)
{
return std::shared_ptr<BOOL>(new BOOL(TRUE));
}
else
{
return std::shared_ptr<BOOL>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<PRINTER_INFO_1W, BOOL>(L"EnumPrintersW", enumLocalPrinter, 1, onData, onNoData);
if (res == nullptr)
{
return FALSE;
}
else
{
return *res;
}
}
std::wstring getPrinterInstalledOnPort(LPCWSTR port)
{
auto onData = [port](const PRINTER_INFO_2W &info)
{
if (isNameEqual(port, info.pPortName) == TRUE)
{
return std::shared_ptr<std::wstring>(new std::wstring(info.pPrinterName));
}
else
{
return std::shared_ptr<std::wstring>(nullptr);
} };
auto onNoData = []()
{ return nullptr; };
auto res = commonEnum<PRINTER_INFO_2W, std::wstring>(L"EnumPrintersW", enumLocalPrinter, 2, onData, onNoData);
if (res == nullptr)
{
return L"";
}
else
{
return *res;
}
}
BOOL addPrinter(LPCWSTR name, LPCWSTR driver, LPCWSTR port)
{
PRINTER_INFO_2W printerInfo = {0};
printerInfo.pPrinterName = const_cast<LPWSTR>(name);
printerInfo.pPortName = const_cast<LPWSTR>(port);
printerInfo.pDriverName = const_cast<LPWSTR>(driver);
printerInfo.pPrintProcessor = const_cast<LPWSTR>(L"WinPrint");
printerInfo.pDatatype = const_cast<LPWSTR>(L"RAW");
printerInfo.Attributes = PRINTER_ATTRIBUTE_LOCAL;
HANDLE hPrinter = AddPrinterW(NULL, 2, (LPBYTE)&printerInfo);
return hPrinter == NULL ? FALSE : TRUE;
}
VOID deletePrinter(LPCWSTR name)
{
PRINTER_DEFAULTSW dft = {0};
dft.DesiredAccess = PRINTER_ALL_ACCESS;
HANDLE hPrinter = NULL;
if (OpenPrinterW(const_cast<LPWSTR>(name), &hPrinter, &dft) == FALSE)
{
DWORD error = GetLastError();
if (error == ERROR_INVALID_PRINTER_NAME)
{
return;
}
WcaLog(LOGMSG_STANDARD, "Failed to open printer. error (%d)\n", error);
return;
}
if (SetPrinterW(hPrinter, 0, NULL, PRINTER_CONTROL_PURGE) == FALSE)
{
ClosePrinter(hPrinter);
WcaLog(LOGMSG_STANDARD, "Failed to purge printer queue. error (%d)\n", GetLastError());
return;
}
if (DeletePrinter(hPrinter) == FALSE)
{
ClosePrinter(hPrinter);
WcaLog(LOGMSG_STANDARD, "Failed to delete printer. error (%d)\n", GetLastError());
return;
}
ClosePrinter(hPrinter);
}
bool FileExists(const std::wstring &filePath)
{
DWORD fileAttributes = GetFileAttributes(filePath.c_str());
return (fileAttributes != INVALID_FILE_ATTRIBUTES && !(fileAttributes & FILE_ATTRIBUTE_DIRECTORY));
}
// Steps:
// 1. Add the local port.
// 2. Check if the driver is installed.
// Uninstall the existing driver if it is installed.
// We should not check the driver version because the driver is deployed with the application.
// It's better to uninstall the existing driver and install the driver from the application.
// 3. Add the printer.
VOID installUpdatePrinter(const std::wstring &installFolder)
{
const std::wstring infFile = installFolder + L"\\" + RemotePrinter::RD_DRIVER_INF_PATH;
if (!FileExists(infFile))
{
WcaLog(LOGMSG_STANDARD, "Printer driver INF file not found, aborting...\n");
return;
}
if (!checkAddLocalPort(RD_PRINTER_PORT))
{
WcaLog(LOGMSG_STANDARD, "Failed to check add local port, error (%d)\n", GetLastError());
return;
}
else
{
WcaLog(LOGMSG_STANDARD, "Local port added successfully\n");
}
if (getInstalledDriverVersion(RD_PRINTER_DRIVER_NAME) > 0)
{
deletePrinter(RD_PRINTER_NAME);
if (FALSE == uninstallDriver(RD_PRINTER_DRIVER_NAME))
{
WcaLog(LOGMSG_STANDARD, "Failed to uninstall previous printer driver, error (%d)\n", GetLastError());
}
}
if (FALSE == installDriver(RD_PRINTER_DRIVER_NAME, infFile.c_str()))
{
WcaLog(LOGMSG_STANDARD, "Driver installation failed, still try to add the printer\n");
}
else
{
WcaLog(LOGMSG_STANDARD, "Driver installed successfully\n");
}
if (FALSE == addPrinter(RD_PRINTER_NAME, RD_PRINTER_DRIVER_NAME, RD_PRINTER_PORT))
{
WcaLog(LOGMSG_STANDARD, "Failed to add printer, error (%d)\n", GetLastError());
}
else
{
WcaLog(LOGMSG_STANDARD, "Printer installed successfully\n");
}
}
VOID uninstallPrinter()
{
deletePrinter(RD_PRINTER_NAME);
WcaLog(LOGMSG_STANDARD, "Deleted the printer\n");
uninstallDriver(RD_PRINTER_DRIVER_NAME);
WcaLog(LOGMSG_STANDARD, "Uninstalled the printer driver\n");
checkDeleteLocalPort(RD_PRINTER_PORT);
WcaLog(LOGMSG_STANDARD, "Deleted the local port\n");
}
}
+175
View File
@@ -0,0 +1,175 @@
// https://learn.microsoft.com/en-us/windows/win32/services/installing-a-service
#include "pch.h"
#include <iostream>
#include <Windows.h>
#include <strsafe.h>
bool MyCreateServiceW(LPCWSTR serviceName, LPCWSTR displayName, LPCWSTR binaryPath)
{
SC_HANDLE schSCManager;
SC_HANDLE schService;
// Get a handle to the SCM database.
schSCManager = OpenSCManagerW(
NULL, // local computer
NULL, // ServicesActive database
SC_MANAGER_ALL_ACCESS); // full access rights
if (NULL == schSCManager)
{
WcaLog(LOGMSG_STANDARD, "OpenSCManager failed (%d)\n", GetLastError());
return false;
}
// Create the service
schService = CreateServiceW(
schSCManager, // SCM database
serviceName, // name of service
displayName, // service name to display
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_AUTO_START, // start type
SERVICE_ERROR_NORMAL, // error control type
binaryPath, // path to service's binary
NULL, // no load ordering group
NULL, // no tag identifier
NULL, // no dependencies
NULL, // LocalSystem account
NULL); // no password
if (schService == NULL)
{
WcaLog(LOGMSG_STANDARD, "CreateService failed (%d)\n", GetLastError());
CloseServiceHandle(schSCManager);
return false;
}
else
{
WcaLog(LOGMSG_STANDARD, "Service installed successfully\n");
}
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);
return true;
}
bool MyDeleteServiceW(LPCWSTR serviceName)
{
SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
if (hSCManager == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager, error: 0x%02X", GetLastError());
return false;
}
SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_STOP | DELETE);
if (hService == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls, error: 0x%02X", serviceName, GetLastError());
CloseServiceHandle(hSCManager);
return false;
}
SERVICE_STATUS serviceStatus;
if (ControlService(hService, SERVICE_CONTROL_STOP, &serviceStatus)) {
WcaLog(LOGMSG_STANDARD, "Stopping service: %ls", serviceName);
}
bool success = DeleteService(hService);
if (!success) {
WcaLog(LOGMSG_STANDARD, "Failed to delete service: %ls, error: 0x%02X", serviceName, GetLastError());
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
return success;
}
bool MyStartServiceW(LPCWSTR serviceName)
{
SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
if (hSCManager == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager, error: 0x%02X", GetLastError());
return false;
}
SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_START);
if (hService == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls, error: 0x%02X", serviceName, GetLastError());
CloseServiceHandle(hSCManager);
return false;
}
bool success = StartServiceW(hService, 0, NULL);
if (!success) {
WcaLog(LOGMSG_STANDARD, "Failed to start service: %ls, error: 0x%02X", serviceName, GetLastError());
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
return success;
}
bool MyStopServiceW(LPCWSTR serviceName)
{
SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
if (hSCManager == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager");
return false;
}
SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_STOP);
if (hService == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls", serviceName);
CloseServiceHandle(hSCManager);
return false;
}
SERVICE_STATUS serviceStatus;
if (!ControlService(hService, SERVICE_CONTROL_STOP, &serviceStatus)) {
WcaLog(LOGMSG_STANDARD, "Failed to stop service: %ls", serviceName);
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
return false;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
return true;
}
bool QueryServiceStatusExW(LPCWSTR serviceName, SERVICE_STATUS_PROCESS* status)
{
SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_CONNECT);
if (hSCManager == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open Service Control Manager");
return false;
}
SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_QUERY_STATUS);
if (hService == NULL) {
WcaLog(LOGMSG_STANDARD, "Failed to open service: %ls", serviceName);
CloseServiceHandle(hSCManager);
return false;
}
DWORD bytesNeeded;
BOOL success = QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, reinterpret_cast<LPBYTE>(status), sizeof(*status), &bytesNeeded);
if (!success) {
WcaLog(LOGMSG_STANDARD, "Failed to query service: %ls", serviceName);
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCManager);
return success;
}
bool IsServiceRunningW(LPCWSTR serviceName)
{
SERVICE_STATUS_PROCESS serviceStatus;
QueryServiceStatusExW(serviceName, &serviceStatus);
return (serviceStatus.dwCurrentState == SERVICE_RUNNING);
}
+26
View File
@@ -0,0 +1,26 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
BOOL APIENTRY DllMain(
__in HMODULE hModule,
__in DWORD ulReasonForCall,
__in LPVOID
)
{
switch (ulReasonForCall)
{
case DLL_PROCESS_ATTACH:
WcaGlobalInitialize(hModule);
break;
case DLL_PROCESS_DETACH:
WcaGlobalFinalize();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
// Windows Header Files
#include <windows.h>
#include <strsafe.h>
#include <msiquery.h>
// WiX Header Files:
#include <wcautil.h>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="WixToolset.DUtil" version="4.0.5" targetFramework="native" />
<package id="WixToolset.WcaUtil" version="4.0.5" targetFramework="native" />
</packages>
+5
View File
@@ -0,0 +1,5 @@
// pch.cpp: source file corresponding to the pre-compiled header
#include "pch.h"
// When you are using pre-compiled headers, this source file is necessary for compilation to succeed.
+13
View File
@@ -0,0 +1,13 @@
// pch.h: This is a precompiled header file.
// Files listed below are compiled only once, improving build performance for future builds.
// This also affects IntelliSense performance, including code completion and many code browsing features.
// However, files listed here are ALL re-compiled if any one of them is updated between builds.
// Do not add files here that you will be updating frequently as this negates the performance advantage.
#ifndef PCH_H
#define PCH_H
// add headers that you want to pre-compile here
#include "framework.h"
#endif //PCH_H
+45
View File
@@ -0,0 +1,45 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<?include ../Includes.wxi?>
<Fragment>
<!-- For compatibility with command line values from previous versions -->
<Property Id="INSTALLFOLDER" Secure="yes">
<RegistrySearch Id="InstallFolderSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="INSTALLFOLDER" Type="raw" />
</Property>
<!-- If a property value has been passed via the command line (which includes when set from the bundle), the registry search will
overwrite the command line value, these actions temporarily store the command line value before the registry search
is performed so they can be restored after the registry search is complete -->
<SetProperty Id="SavedInstallFolderCmdLineValue" Value="[INSTALLFOLDER]" Before="AppSearch" Sequence="first" Condition="INSTALLFOLDER" />
<!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedInstallFolderValue" Id="INSTALLFOLDER" Value="[SavedInstallFolderCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedInstallFolderCmdLineValue" />
<!-- Normalize INSTALLFOLDER from the command line or registry before assigning INSTALLFOLDER_INNER. -->
<!-- Case 1: already ends with \$(var.Product)\, keep it unchanged. -->
<SetProperty Action="SetInstallFolderInnerFromProductDir" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot;" />
<!-- Case 2: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<SetProperty Action="SetInstallFolderInnerFromProductDirNoSlash" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- Case 3: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- Case 4: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<SetProperty Action="SetInstallFolderInnerAppendSlashProduct" Id="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER]\$(var.Product)\" After="RestoreSavedInstallFolderValue" Sequence="first" Condition="INSTALLFOLDER AND NOT INSTALLFOLDER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- INSTALLFOLDER_INNER is defined for compatibility with previous versions of the installer. -->
<!-- Because we need to use INSTALLFOLDER as the command line argument. -->
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER_INNER" Name="$(var.Product)" />
</StandardDirectory>
<StandardDirectory Id="CommonAppDataFolder">
<Directory Id="App.Data.Folder" Name="$(var.Product)" />
</StandardDirectory>
<StandardDirectory Id="ProgramMenuFolder">
<Directory Id="App.StartMenu" Name="$(var.Product)" />
</StandardDirectory>
<StandardDirectory Id="DesktopFolder" />
</Fragment>
</Wix>
+56
View File
@@ -0,0 +1,56 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
<?include ../Includes.wxi?>
<Fragment>
<!-- Regs for shortcuts are defined in "Fragments/ShortcutProperties.wxs" -->
<!-- Component that persists the property values to the registry so they are available during an upgrade/modify -->
<DirectoryRef Id="INSTALLFOLDER_INNER">
<Component Id="Product.Registry.InstallFolder" Guid="3196EDA7-9AEF-4705-A0C8-E3F3ECCCB153">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="INSTALLFOLDER" Value="[INSTALLFOLDER_INNER]" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.DefaultIcon" Guid="6DBF2690-0955-4C6A-940F-634DDA503F49">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)\DefaultIcon">
<RegistryValue Type="string" Value='"[INSTALLFOLDER_INNER]$(var.Product).exe",0' />
</RegistryKey>
</Component>
<Component Id="Product.Registry.CommandPlay" Guid="613C9E4F-2F1F-45A3-96E2-26EBBEBA6B0E">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)\shell" />
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)\shell\open" />
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)\shell\open\command">
<RegistryValue Type="string" Value='"[INSTALLFOLDER_INNER]$(var.Product).exe" --play "%1"' />
</RegistryKey>
</Component>
<Component Id="Product.Registry.URLProtocol" Guid="565BE3F8-23A7-4B9D-B0DE-6D51CC86FC0B">
<RegistryKey Root="HKCR" Key="$(var.ProductLower)">
<RegistryValue Type="string" Name="URL Protocol" Value="" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.Command" Guid="BC8D581C-5960-4843-93DC-E347CD43BD49">
<RegistryKey Root="HKCR" Key="$(var.ProductLower)\shell" />
<RegistryKey Root="HKCR" Key="$(var.ProductLower)\shell\open" />
<RegistryKey Root="HKCR" Key="$(var.ProductLower)\shell\open\command">
<RegistryValue Type="string" Value='"[INSTALLFOLDER_INNER]$(var.Product).exe" "%1"' />
</RegistryKey>
</Component>
<!--For compatibility with registry values from previous versions-->
<Component Id="Product.Registry.UninstallApp" Guid="FC1A3D2E-5642-FBD8-CFA6-5ECAC6DE69A8">
<RegistryKey Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" >
<RegistryValue Type="string" Name="BuildDate" Value="$(var.BuildDate)" />
<RegistryValue Type="string" Name="share_rdp" Value="" />
<!--$ArpStart$-->
<!--$ArpEnd$-->
</RegistryKey>
</Component>
</DirectoryRef>
</Fragment>
</Wix>
+154
View File
@@ -0,0 +1,154 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:fire="http://wixtoolset.org/schemas/v4/wxs/firewall">
<Fragment>
<?include ../Includes.wxi?>
<DirectoryRef Id="INSTALLFOLDER_INNER" FileSource="$(var.BuildDir)">
<Component Id="App.exe" Guid="620F0F69-4C17-4320-A619-495E329712A4">
<File Id="App.exe" Name="$(var.Product).exe" KeyPath="yes" Checksum="yes">
<!--<fire:FirewallException Id="AppEx" Name="$(var.Product) Service" Scope="any" IgnoreFailure="yes" />-->
</File>
</Component>
</DirectoryRef>
<CustomAction Id="RemoveRuntimeGeneratedFiles.SetParam" Return="check" Property="RemoveRuntimeGeneratedFiles" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="AddFirewallRules.SetParam" Return="check" Property="AddFirewallRules" Value="1[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="RemoveFirewallRules.SetParam" Return="check" Property="RemoveFirewallRules" Value="0[INSTALLFOLDER_INNER]$(var.Product).exe" />
<CustomAction Id="CreateStartService.SetParam" Return="check" Property="CreateStartService" Value="$(var.Product);&quot;[INSTALLFOLDER_INNER]$(var.Product).exe&quot; --service" />
<CustomAction Id="TryStopDeleteService.SetParam" Return="check" Property="TryStopDeleteService" Value="$(var.Product)" />
<CustomAction Id="LaunchApp" ExeCommand="" Return="asyncNoWait" FileRef="App.exe" />
<CustomAction Id="LaunchAppTray" ExeCommand=" --tray" Return="asyncNoWait" FileRef="App.exe" />
<Property Id="TerminateProcesses" Value="AppTest.exe" />
<CustomAction Id="TerminateProcesses.SetParam" Return="check" Property="TerminateProcesses" Value="$(var.Product).exe" />
<CustomAction Id="TerminateBrokers.SetParam" Return="check" Property="TerminateProcesses" Value="RuntimeBroker_rustdesk.exe" />
<CustomAction Id="SetPropertyIsServiceRunning.SetParam.AppName" Return="check" Property="AppName" Value="$(var.Product)" />
<CustomAction Id="SetPropertyIsServiceRunning.SetParam.PropertyName" Return="check" Property="PropertyName" Value="STOP_SERVICE" />
<CustomAction Id="SetPropertyServiceStop.SetParam.ConfigFile" Return="check" Property="ConfigFile" Value="[AppDataFolder]$(var.Product)\config\$(var.Product)2.toml" />
<CustomAction Id="SetPropertyServiceStop.SetParam.ConfigKey" Return="check" Property="ConfigKey" Value="stop-service" />
<CustomAction Id="SetPropertyServiceStop.SetParam.PropertyName" Return="check" Property="PropertyName" Value="STOP_SERVICE" />
<CustomAction Id="TryDeleteStartupShortcut.SetParam" Return="check" Property="ShortcutName" Value="$(var.Product) Tray" />
<CustomAction Id="RemoveAmyuniIdd.SetParam" Return="check" Property="RemoveAmyuniIdd" Value="[INSTALLFOLDER_INNER]" />
<CustomAction Id="InstallPrinter.SetParam" Return="check" Property="InstallPrinter" Value="[INSTALLFOLDER_INNER]" />
<InstallExecuteSequence>
<Custom Action="SetPropertyIsServiceRunning" After="InstallInitialize" Condition="Installed" />
<Custom Action="SetPropertyIsServiceRunning.SetParam.AppName" Before="SetPropertyIsServiceRunning" Condition="Installed" />
<Custom Action="SetPropertyIsServiceRunning.SetParam.PropertyName" Before="SetPropertyIsServiceRunning" Condition="Installed" />
<Custom Action="SetPropertyServiceStop" After="InstallInitialize" Condition="NOT Installed" />
<Custom Action="SetPropertyServiceStop.SetParam.ConfigFile" Before="SetPropertyServiceStop" Condition="NOT Installed" />
<Custom Action="SetPropertyServiceStop.SetParam.ConfigKey" Before="SetPropertyServiceStop" Condition="NOT Installed" />
<Custom Action="SetPropertyServiceStop.SetParam.PropertyName" Before="SetPropertyServiceStop" Condition="NOT Installed" />
<!-- Do not call CreateStartService if is uninstalling. -->
<!-- (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) means uninstalling. -->
<Custom Action="CreateStartService" Before="InstallFinalize" Condition="(NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)" />
<Custom Action="CreateStartService.SetParam" Before="CreateStartService" Condition="(NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)" />
<Custom Action="CustomActionHello" Before="InstallFinalize" />
<!--Shortcut is in InstallValidate section. So we just let it be created, then try delete if stopping service.-->
<Custom Action="TryDeleteStartupShortcut" After="InstallFinalize" Condition="STOP_SERVICE=&quot;&apos;Y&apos;&quot;" />
<Custom Action="TryDeleteStartupShortcut.SetParam" Before="SetPropertyIsServiceRunning" Condition="STOP_SERVICE=&quot;&apos;Y&apos;&quot;" />
<!-- Launch ClientLauncher if installing or already installed and not uninstalling -->
<!-- https://learn.microsoft.com/en-us/windows/win32/msi/uilevel -->
<Custom Action="LaunchApp" After="InstallFinalize" Condition="(NOT UILevel=2) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) "/>
<Custom Action="LaunchAppTray" After="InstallFinalize" Condition="(LAUNCH_TRAY_APP=&quot;Y&quot; OR LAUNCH_TRAY_APP=&quot;1&quot;) AND (NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)) AND (NOT STOP_SERVICE=&quot;&apos;Y&apos;&quot;) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<!-- https://learn.microsoft.com/en-us/windows/win32/msi/operating-system-property-values -->
<!-- We have to use `VersionNT` to instead of `IsWindows10OrGreater()` in the custom action.
Because `IsWindows10OrGreater()` requires the manifest file to be embedded in the executable/dll file.
Even I have embedded the manifest file, it still does not work correctly in my case.
https://learn.microsoft.com/en-us/windows/win32/sysinfo/version-helper-apis -->
<!-- VersionNT >= 603 means can't differentiate between Windows 8.1 and Windows 10.
Some msi packages reset the `VersionNT` value to 1000 on Windows 10.
https://www.advancedinstaller.com/user-guide/qa-OS-dependent-install.html -->
<!-- Remote printer also works on Win8.1 in my test. -->
<Custom Action="InstallPrinter" Before="InstallFinalize" Condition="VersionNT &gt;= 603 AND (PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;)" />
<Custom Action="InstallPrinter.SetParam" Before="InstallPrinter" Condition="VersionNT &gt;= 603" />
<!--Workaround of "fire:FirewallException". If Outbound="Yes" or Outbound="true", the following error occurs.-->
<!--ExecFirewallExceptions: Error 0x80070057: failed to add app to the authorized apps list-->
<Custom Action="AddFirewallRules" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)"/>
<Custom Action="AddFirewallRules.SetParam" Before="AddFirewallRules" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE)"/>
<Custom Action="AddRegSoftwareSASGeneration" Before="InstallFinalize" Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE) AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)"/>
<Custom Action="RemoveRuntimeGeneratedFiles" Before="RemoveFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="RemoveRuntimeGeneratedFiles.SetParam" Before="RemoveRuntimeGeneratedFiles" Condition="Installed AND (REMOVE=&quot;ALL&quot; OR UPGRADINGPRODUCTCODE)"/>
<Custom Action="TryStopDeleteService" Before="RemoveRuntimeGeneratedFiles.SetParam" />
<Custom Action="TryStopDeleteService.SetParam" Before="TryStopDeleteService" />
<Custom Action="RemoveFirewallRules" Before="RemoveFiles"/>
<Custom Action="RemoveFirewallRules.SetParam" Before="RemoveFirewallRules"/>
<Custom Action="UninstallPrinter" Before="RemoveRuntimeGeneratedFiles" Condition="VersionNT &gt;= 603" />
<Custom Action="TerminateProcesses" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateProcesses.SetParam" Before="TerminateProcesses"/>
<Custom Action="TerminateBrokers" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="TerminateBrokers.SetParam" Before="TerminateBrokers"/>
<Custom Action="RemoveAmyuniIdd" Before="RemoveRuntimeGeneratedFiles"/>
<Custom Action="RemoveAmyuniIdd.SetParam" Before="RemoveAmyuniIdd"/>
</InstallExecuteSequence>
<!-- Shortcuts -->
<DirectoryRef Id="App.StartMenu">
<Component Id="App.StartMenu" Guid="30F6D57A-B805-4DA4-A071-05A3B22400CA">
<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu" Type="string" Value="1" KeyPath="yes" />
<RemoveFolder Id="Remove.App.StartMenu" On="uninstall" />
</Component>
</DirectoryRef>
<DirectoryRef Id="App.StartMenu">
<Component Id="App.StartMenu.Shortcut" Guid="43ABCAC7-E47D-42D8-A408-25EC70DBB993" Condition="STARTMENUSHORTCUTS = 1 OR STARTMENUSHORTCUTS = &quot;Y&quot; OR STARTMENUSHORTCUTS = &quot;y&quot;">
<Shortcut Id="App.StartMenu.Shortcut" Name="!(loc.SC_Client)" Description="!(loc.SC_Client_Desc)" Target="[!App.exe]" Icon="AppIcon" WorkingDirectory="INSTALLFOLDER_INNER" />
<!--
Fix ICE 38 by adding a dummy registry key that is the key for this shortcut.
https://learn.microsoft.com/en-us/windows/win32/msi/ice38
-->
<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu.Shortcut" Type="string" Value="1" KeyPath="yes" />
</Component>
<Component Id="App.StartMenu.ShortcutUninstall" Guid="E100D7F8-D607-4513-28DA-2C95E5EA698E" Condition="STARTMENUSHORTCUTS = 1 OR STARTMENUSHORTCUTS = &quot;Y&quot; OR STARTMENUSHORTCUTS = &quot;y&quot;">
<Shortcut Id="App.StartMenu.ShortcutUninstall" Name="!(loc.SC_Uninstall)" Description="!(loc.SC_Uninstall_Desc)" Target="[System6432Folder]msiexec.exe" Arguments="/x [ProductCode]" Icon="AppIcon" />
<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartMenu.ShortcutUninstall" Type="string" Value="1" KeyPath="yes" />
</Component>
</DirectoryRef>
<StandardDirectory Id="DesktopFolder">
<Component Id="App.Desktop.Shortcut" Guid="CA8FB7AA-17F7-4E36-A58A-5A016A303709" Condition="DESKTOPSHORTCUTS = 1 OR DESKTOPSHORTCUTS = &quot;Y&quot; OR DESKTOPSHORTCUTS = &quot;y&quot;">
<Shortcut Id="App.Desktop.Shortcut" Name="!(loc.SC_Client)" Description="!(loc.SC_Client_Desc)" Target="[!App.exe]" Icon="AppIcon" WorkingDirectory="INSTALLFOLDER_INNER" />
<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.Desktop.Shortcut" Type="string" Value="1" KeyPath="yes" />
</Component>
</StandardDirectory>
<StandardDirectory Id="StartupFolder">
<Component Id="App.StartupFolder.ShortcutTray" Guid="B1D1E2BB-E53E-E159-DB7C-744D5C726A8C" Condition="STARTUPSHORTCUTS = 1 AND (NOT CC_CONNECTION_TYPE=&quot;outgoing&quot;)">
<Shortcut Id="App.StartupFolder.ShortcutTray" Name="!(loc.SC_Client_Tray)" Description="!(loc.SC_Client_Tray_Desc)" Target="[!App.exe]" Arguments="--tray" Icon="AppIcon" WorkingDirectory="INSTALLFOLDER_INNER" />
<RegistryValue Root="HKCU" Key="Software\$(var.Product)" Name="App.StartupFolder.ShortcutTray" Type="string" Value="1" KeyPath="yes" />
</Component>
</StandardDirectory>
<!--<DirectoryRef Id="INSTALLFOLDER_INNER">
<Component Id="App.UninstallShortcut" Guid="FB0F2AC7-2AE5-4C54-B860-5E472620B6B1">
<Shortcut Id="App.UninstallShortcut" Name="!(loc.SC_Uninstall)" Description="!(loc.SC_Uninstall_Desc)" Target="[System6432Folder]msiexec.exe" Arguments="/x [ProductCode]" Icon="AppIcon" />
</Component>
</DirectoryRef>-->
<ComponentGroup Id="Components" Directory="INSTALLFOLDER_INNER">
<ComponentRef Id="App.exe" />
<ComponentRef Id="App.Desktop.Shortcut" />
<!--<ComponentRef Id="App.UninstallShortcut" />-->
<ComponentRef Id="App.StartMenu.Shortcut" />
<ComponentRef Id="App.StartMenu.ShortcutUninstall" />
<ComponentRef Id="App.StartupFolder.ShortcutTray" />
<!--$AutoComonentStart$-->
<!--$AutoComponentEnd$-->
</ComponentGroup>
</Fragment>
</Wix>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<?include ../Includes.wxi?>
<Fragment>
<Property Id="AddRemovePropertiesFile" Value="1" />
<!--STOP_SERVICE is set to 'Y'. Because the config value may be empty or 'Y'-->
<Property Id="STOP_SERVICE" Value="&apos;Y&apos;" />
<Property Id="LAUNCH_TRAY_APP" Value="Y" />
<!--
Support entries shown when clicking "Click here for support information"
in Control Panel's Add/Remove Programs https://learn.microsoft.com/en-us/windows/win32/msi/property-reference
-->
<!--<Property Id="ARPCOMMENTS" Value="!(loc.AR_Comment)" />
<Property Id="ARPCONTACT" Value="https://github.com/rustdesk/rustdesk" />
<Property Id="ARPHELPLINK" Value="https://github.com/rustdesk/rustdesk" />
<Property Id="ARPREADME" Value="https://github.com/rustdesk/rustdesk" />
<Property Id="ARPURLINFOABOUT" Value="https://github.com/rustdesk/rustdesk" />
<Property Id="ARPURLUPDATEINFO" Value="https://github.com/rustdesk/rustdesk" />-->
<Property Id="ARPPRODUCTICON" Value="AppIcon" />
<!--$ArpStart$-->
<!--$ArpEnd$-->
<!--$CustomClientPropsStart$-->
<!--$CustomClientPropsEnd$-->
<Property Id="APP_WINDOWS_INSTALLER">
<RegistrySearch Id="AppWindowsInstallerFolderSearch" Root="HKLM" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\$(var.Product)" Name="WindowsInstaller" Type="raw" />
</Property>
</Fragment>
</Wix>
@@ -0,0 +1,23 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<?include ../Includes.wxi?>
<Binary Id="Custom_Actions_Dll" SourceFile="$(var.CustomActions.TargetDir)$(var.CustomActions.TargetName).dll" />
<CustomAction Id="CustomActionHello" DllEntry="CustomActionHello" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveRuntimeGeneratedFiles" DllEntry="RemoveRuntimeGeneratedFiles" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateProcesses" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TerminateBrokers" DllEntry="TerminateProcesses" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveFirewallRules" DllEntry="AddFirewallRules" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="SetPropertyIsServiceRunning" DllEntry="SetPropertyIsServiceRunning" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="CreateStartService" DllEntry="CreateStartService" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TryStopDeleteService" DllEntry="TryStopDeleteService" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="TryDeleteStartupShortcut" DllEntry="TryDeleteStartupShortcut" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="SetPropertyServiceStop" DllEntry="SetPropertyFromConfig" Impersonate="yes" Execute="immediate" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="AddRegSoftwareSASGeneration" DllEntry="AddRegSoftwareSASGeneration" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="RemoveAmyuniIdd" DllEntry="RemoveAmyuniIdd" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="InstallPrinter" DllEntry="InstallPrinter" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
<CustomAction Id="UninstallPrinter" DllEntry="UninstallPrinter" Impersonate="no" Execute="deferred" Return="ignore" BinaryRef="Custom_Actions_Dll"/>
</Fragment>
</Wix>
@@ -0,0 +1,87 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<?include ..\Includes.wxi?>
<!--
Properties and related actions for specifying whether to install shortcuts and the printer.
-->
<!-- These are the actual properties that get used in conditions to determine whether to
install start menu shortcuts or the printer. Shortcut properties default to install;
PRINTER defaults to not install. The CREATE* properties below update shortcut
properties from command line, bundle, or registry values. -->
<Property Id="STARTMENUSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="DESKTOPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="STARTUPSHORTCUTS" Value="1" Secure="yes"></Property>
<Property Id="PRINTER" Secure="yes"></Property>
<!-- These properties get set from either the command line, bundle or registry value,
if set they update the properties above with their value. -->
<Property Id="CREATESTARTMENUSHORTCUTS" Secure="yes">
<RegistrySearch Id="CreateStartMenuShortcutsSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="STARTMENUSHORTCUTS" Type="raw" />
</Property>
<Property Id="CREATEDESKTOPSHORTCUTS" Secure="yes">
<RegistrySearch Id="CreateDesktopShortcutsSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="DESKTOPSHORTCUTS" Type="raw" />
</Property>
<Property Id="INSTALLPRINTER" Secure="yes">
<RegistrySearch Id="InstallPrinterSearch" Root="HKCR" Key="$(var.RegKeyRoot)" Name="PRINTER" Type="raw" />
</Property>
<!-- Component that persists the property values to the registry so they are available during an upgrade/modify -->
<DirectoryRef Id="INSTALLFOLDER_INNER">
<Component Id="Product.Registry.PersistedStartMenuShortcutProperties1" Guid="62F79BCF-3367-4ACF-950F-F8BCABACDDC0" Condition="STARTMENUSHORTCUTS = 1 OR STARTMENUSHORTCUTS = &quot;Y&quot; OR STARTMENUSHORTCUTS = &quot;y&quot;">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="STARTMENUSHORTCUTS" Value="1" KeyPath="yes" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.PersistedStartMenuShortcutProperties0" Guid="8EA2D5A8-6E5D-4BDD-9019-2099297FF519" Condition="NOT (STARTMENUSHORTCUTS = 1 OR STARTMENUSHORTCUTS = &quot;Y&quot; OR STARTMENUSHORTCUTS = &quot;y&quot;)">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="STARTMENUSHORTCUTS" Value="0" KeyPath="yes" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.PersistedDesktopShortcutProperties1" Guid="1BBAD054-6EC2-4362-BF1B-E8BDE988B597" Condition="DESKTOPSHORTCUTS = 1 OR DESKTOPSHORTCUTS = &quot;Y&quot; OR DESKTOPSHORTCUTS = &quot;y&quot;">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="DESKTOPSHORTCUTS" Value="1" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.PersistedDesktopShortcutProperties0" Guid="FA992614-D2E1-4795-9696-D45A5EF1B9C8" Condition="NOT (DESKTOPSHORTCUTS = 1 OR DESKTOPSHORTCUTS = &quot;Y&quot; OR DESKTOPSHORTCUTS = &quot;y&quot;)">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="DESKTOPSHORTCUTS" Value="0" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.PersistedPrinterProperties1" Guid="AF617116-2502-EB3D-5B52-B47AA89EB4B0" Condition="PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="PRINTER" Value="1" />
</RegistryKey>
</Component>
<Component Id="Product.Registry.PersistedPrinterProperties0" Guid="51F944D3-AAEB-F167-03A1-081A38E9468A" Condition="NOT (PRINTER = 1 OR PRINTER = &quot;Y&quot; OR PRINTER = &quot;y&quot;)">
<RegistryKey Root="HKCR" Key="$(var.RegKeyRoot)">
<RegistryValue Type="string" Name="PRINTER" Value="0" />
</RegistryKey>
</Component>
</DirectoryRef>
<!-- If a property value has been passed via the command line (which includes when set from the bundle), the registry search will
overwrite the command line value, these actions temporarily store the command line value before the registry search
is performed so they can be restored after the registry search is complete -->
<SetProperty Id="SavedStartMenuShortcutsCmdLineValue" Value="[CREATESTARTMENUSHORTCUTS]" Before="AppSearch" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS" />
<SetProperty Id="SavedDesktopShortcutsCmdLineValue" Value="[CREATEDESKTOPSHORTCUTS]" Before="AppSearch" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS" />
<SetProperty Id="SavedPrinterCmdLineValue" Value="[INSTALLPRINTER]" Before="AppSearch" Sequence="first" Condition="INSTALLPRINTER" />
<!-- If a command line value was stored, restore it after the registry search has been performed -->
<SetProperty Action="RestoreSavedStartMenuShortcutsValue" Id="CREATESTARTMENUSHORTCUTS" Value="[SavedStartMenuShortcutsCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedStartMenuShortcutsCmdLineValue" />
<SetProperty Action="RestoreSavedDesktopShortcutsValue" Id="CREATEDESKTOPSHORTCUTS" Value="[SavedDesktopShortcutsCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedDesktopShortcutsCmdLineValue" />
<SetProperty Action="RestoreSavedPrinterValue" Id="INSTALLPRINTER" Value="[SavedPrinterCmdLineValue]" After="AppSearch" Sequence="first" Condition="SavedPrinterCmdLineValue" />
<!-- If a command line value or registry value was set, update the main properties with the value -->
<SetProperty Id="STARTMENUSHORTCUTS" Value="" After="RestoreSavedStartMenuShortcutsValue" Sequence="first" Condition="CREATESTARTMENUSHORTCUTS AND NOT (CREATESTARTMENUSHORTCUTS = 1 OR CREATESTARTMENUSHORTCUTS = &quot;Y&quot; OR CREATESTARTMENUSHORTCUTS = &quot;y&quot;)" />
<SetProperty Id="DESKTOPSHORTCUTS" Value="" After="RestoreSavedDesktopShortcutsValue" Sequence="first" Condition="CREATEDESKTOPSHORTCUTS AND NOT (CREATEDESKTOPSHORTCUTS = 1 OR CREATEDESKTOPSHORTCUTS = &quot;Y&quot; OR CREATEDESKTOPSHORTCUTS = &quot;y&quot;)" />
<!-- PRINTER defaults to empty now, so a saved or command-line INSTALLPRINTER=1
must explicitly enable the main PRINTER property. Non-truthy INSTALLPRINTER
values still clear PRINTER so upgrades preserve an explicit disabled choice. -->
<SetProperty Action="SetPrinterValueEnabled" Id="PRINTER" Value="1" After="RestoreSavedPrinterValue" Sequence="first" Condition="INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;" />
<SetProperty Action="SetPrinterValueDisabled" Id="PRINTER" Value="" After="SetPrinterValueEnabled" Sequence="first" Condition="INSTALLPRINTER AND NOT (INSTALLPRINTER = 1 OR INSTALLPRINTER = &quot;Y&quot; OR INSTALLPRINTER = &quot;y&quot;)" />
</Fragment>
</Wix>
+10
View File
@@ -0,0 +1,10 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<Property Id="UpgradesFile" Value="1" />
<!--$UpgradeStart$-->
<!--$UpgradeEnd$-->
</Fragment>
</Wix>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Include xmlns="http://wixtoolset.org/schemas/v4/wxs">
<!--$PreVarsStart$-->
<!--$PreVarsEnd$-->
</Include>
@@ -0,0 +1,56 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization Culture="en-us" Codepage="1252"
xmlns="http://wixtoolset.org/schemas/v4/wxl">
<String Id="SummaryCodepage" Value="1252" />
<String Id="ProductLanguage" Value="1033" />
<!-- General strings -->
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
<String Id="AR_Comment" Value="RustDesk" />
<String Id="F_App" Value="RustDesk" />
<String Id="F_App_Desc" Value="RustDesk - Main programs installations." />
<String Id="SC_Uninstall" Value="Uninstall RustDesk" />
<String Id="SC_Uninstall_Desc" Value="Removes RustDesk or parts of it from the computer" />
<!-- Client related strings -->
<String Id="F_Client" Value="Client" />
<String Id="F_Client_Desc" Value="The user interface. Plays media files." />
<String Id="F_Client_Plugins" Value="Plugins" />
<String Id="F_Client_Plugins_Desc" Value="Plugins for the client." />
<String Id="F_LAVFilters" Value="LAV Filters" />
<String Id="F_LAVFilters_Desc" Value="Recommended directshow filters for best audio and video playback experience." />
<String Id="SC_Client" Value="RustDesk" />
<String Id="SC_Client_Desc" Value="Start RustDesk." />
<String Id="SC_Client_Tray" Value="RustDesk Tray" />
<String Id="SC_Client_Tray_Desc" Value="Start RustDesk tray." />
<!-- Server related strings -->
<String Id="F_Server" Value="Server" />
<String Id="F_Server_Desc" Value="The server part of RustDesk. Provides the MediaLibrary and other services." />
<String Id="F_Server_Plugins" Value="Plugins" />
<String Id="F_Server_Plugins_Desc" Value="Plugins for the server." />
<String Id="Service_DisplayName" Value="RustDesk Service" />
<String Id="Service_Description" Value="This service runs the RustDesk Server." />
<!-- Launch Conditions -->
<String Id="LC_OS" Value="[ProductName] requires minimum Windows 7 / 2008 R2 as operating system." />
<String Id="LC_ADMIN" Value="You need to be an administrator to install [ProductName]." />
<!-- User Interfaces -->
<String Id="AnotherAppDialogTitle" Value="Cancel installation."/>
<String Id="AnotherAppDialogDescription" Value="The application is installed by self-installation method, please uninstall it first."/>
<String Id="MyInstallDirDlgDesktopShortcuts" Value="Create desktop icon" />
<String Id="MyInstallDirDlgStartMenuShortcuts" Value="Create start menu shortcuts" />
<String Id="MyInstallDirDlgPrinter" Value="Install RustDesk Printer" />
</WixLocalization>
+32
View File
@@ -0,0 +1,32 @@
<!--
Copyright (c) Microsoft Corporation. All rights reserved.
The use and distribution terms for this software are covered by the
Common Public License 1.0 (http://opensource.org/licenses/cpl1.0.php)
which can be found in the file CPL.TXT at the root of this distribution.
By using this software in any fashion, you are agreeing to be bound by
the terms of this license.
You must not remove this notice, or any other, from this software.
-->
<!--
Adapted by RustDesk for Custom Dialogs insertions
-->
<WixLocalization Culture="en-us"
xmlns="http://wixtoolset.org/schemas/v4/wxl">
<!-- Firewall Extension needs translation here -->
<String Id="msierrFirewallCannotConnect" Overridable="yes" Value="Cannot connect to Windows Firewall. ([2] [3] [4] [5])" />
<String Id="WixSchedFirewallExceptionsInstall" Overridable="yes" Value="Configuring Windows Firewall" />
<String Id="WixSchedFirewallExceptionsUninstall" Overridable="yes" Value="Configuring Windows Firewall" />
<String Id="WixRollbackFirewallExceptionsInstall" Overridable="yes" Value="Rolling back Windows Firewall configuration" />
<String Id="WixExecFirewallExceptionsInstall" Overridable="yes" Value="Installing Windows Firewall configuration" />
<String Id="WixRollbackFirewallExceptionsUninstall" Overridable="yes" Value="Rolling back Windows Firewall configuration" />
<String Id="WixExecFirewallExceptionsUninstall" Overridable="yes" Value="Uninstalling Windows Firewall configuration" />
<!-- Util Extension does not need translation here, because it is already in official WiX Toolkit -->
<String Id="msierrSecureObjectsFailedCreateSD" Overridable="yes" Value="Could not create security descriptor [3]\[4], error: [2]" />
<String Id="msierrSecureObjectsFailedSet" Overridable="yes" Value="Could not apply object security descriptor [3], error: [2]" />
<String Id="msierrSecureObjectsUnknownType" Overridable="yes" Value="Unknown object type [3], error: [2]" />
</WixLocalization>
+303
View File
@@ -0,0 +1,303 @@
{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff1\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang1033\deflangfe2052\themelang1033\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f1\fbidi \fswiss\fcharset0\fprq2{\*\panose 020b0604020202020204}Arial;}
{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}
{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}
{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}
{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f45\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f46\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\f48\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f49\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f50\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f51\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\f52\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f53\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f55\fbidi \fswiss\fcharset238\fprq2 Arial CE;}{\f56\fbidi \fswiss\fcharset204\fprq2 Arial Cyr;}
{\f58\fbidi \fswiss\fcharset161\fprq2 Arial Greek;}{\f59\fbidi \fswiss\fcharset162\fprq2 Arial Tur;}{\f60\fbidi \fswiss\fcharset177\fprq2 Arial (Hebrew);}{\f61\fbidi \fswiss\fcharset178\fprq2 Arial (Arabic);}
{\f62\fbidi \fswiss\fcharset186\fprq2 Arial Baltic;}{\f63\fbidi \fswiss\fcharset163\fprq2 Arial (Vietnamese);}{\f385\fbidi \froman\fcharset238\fprq2 Cambria Math CE;}{\f386\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr;}
{\f388\fbidi \froman\fcharset161\fprq2 Cambria Math Greek;}{\f389\fbidi \froman\fcharset162\fprq2 Cambria Math Tur;}{\f392\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic;}{\f393\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese);}
{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}
{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}
{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);}{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}
{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}
{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}
{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}
{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}
{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}
{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}
{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}
{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}
{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}
{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}
{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}
{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;
\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0;}{\*\defchp \fs22\kerning2\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap
\ql \li0\ri0\sa160\sl259\slmult1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\upr{\stylesheet{\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0
\fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \snext0 \sqformat \spriority0 Normal;}{\s1\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel0\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0
\fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink15 \sqformat heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025
\ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink16 \sqformat heading 2;}{\s3\ql \li0\ri0\sb240\sa60\keepn\nowidctlpar\wrapdefault\faauto\outlinelevel2\rin0\lin0\itap0 \rtlch\fcs1
\ab\af0\afs26\alang1025 \ltrch\fcs0 \b\fs26\lang1031\langfe2052\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink17 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid2828669 heading 3;}{\*\cs10 \additive
\ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\*
\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1
\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052
\snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0 \b\fs32\lang1031\langfe0\kerning32\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink1 \spriority9 ?? 1 ??;}{\*\cs16
\additive \rtlch\fcs1 \ab\ai\af0\afs28 \ltrch\fcs0 \b\i\fs28\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink2 \ssemihidden \spriority9 ?? 2 ??;}{\*\cs17 \additive \rtlch\fcs1 \ab\af0\afs26
\ltrch\fcs0 \b\fs26\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink3 \ssemihidden \spriority9 \styrsid2828669 ?? 3 ??;}}{\*\ud\uc0{\stylesheet{
\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \snext0 \sqformat \spriority0 Normal;}{
\s1\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel0\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink15 \sqformat
heading 1;}{\s2\ql \li0\ri0\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0 \fs24\lang1031\langfe2052\loch\f1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052
\sbasedon0 \snext0 \slink16 \sqformat heading 2;}{\s3\ql \li0\ri0\sb240\sa60\keepn\nowidctlpar\wrapdefault\faauto\outlinelevel2\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs26\alang1025 \ltrch\fcs0
\b\fs26\lang1031\langfe2052\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1031\langfenp2052 \sbasedon0 \snext0 \slink17 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid2828669 heading 3;}{\*\cs10 \additive
\ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\*
\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv \ql \li0\ri0\sa160\sl259\slmult1
\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs22\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052
\snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0 \b\fs32\lang1031\langfe0\kerning32\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink1 \spriority9
{\uc1\u26631 ?\u-26472 ? 1 \u23383 ?\u31526 ?};}{\*\cs16 \additive \rtlch\fcs1 \ab\ai\af0\afs28 \ltrch\fcs0 \b\i\fs28\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0 \sbasedon10 \slink2 \ssemihidden \spriority9
{\uc1\u26631 ?\u-26472 ? 2 \u23383 ?\u31526 ?};}{\*\cs17 \additive \rtlch\fcs1 \ab\af0\afs26 \ltrch\fcs0 \b\fs26\lang1031\langfe0\kerning0\loch\f31502\hich\af31502\dbch\af31501\langnp1031\langfenp0
\sbasedon10 \slink3 \ssemihidden \spriority9 \styrsid2828669 {\uc1\u26631 ?\u-26472 ? 3 \u23383 ?\u31526 ?};}}}}{\*\listtable{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0
\levelindent0{\leveltext\leveltemplateid67698713\'02\'00.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fbias0 \fi-360\li720\lin720 }{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0
{\leveltext\leveltemplateid67698713\'02\'01.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li1440\lin1440 }{\listlevel\levelnfc2\levelnfcn2\leveljc2\leveljcn2\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698715\'02\'02.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-180\li2160\lin2160 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698703\'02\'03.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li2880\lin2880 }{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698713\'02\'04.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li3600\lin3600 }{\listlevel\levelnfc2\levelnfcn2\leveljc2\leveljcn2\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698715\'02\'05.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-180\li4320\lin4320 }{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698703\'02\'06.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li5040\lin5040 }{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698713\'02\'07.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-360\li5760\lin5760 }{\listlevel\levelnfc2\levelnfcn2\leveljc2\leveljcn2\levelfollow0\levelstartat1\lvltentative\levelspace0\levelindent0{\leveltext
\leveltemplateid67698715\'02\'08.;}{\levelnumbers\'01;}\rtlch\fcs1 \af0 \ltrch\fcs0 \fi-180\li6480\lin6480 }{\listname ;}\listid825630566}}{\*\listoverridetable{\listoverride\listid825630566\listoverridecount0\ls1}}{\*\pgptbl {\pgp\ipgp3\itap0\li0\ri0\sb0
\sa0}{\pgp\ipgp1\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp2\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid83947\rsid598512\rsid1001100\rsid1384617\rsid1523795\rsid1598568\rsid1917520\rsid2380571
\rsid2828669\rsid3408922\rsid3425199\rsid3630109\rsid3677587\rsid3958023\rsid4071099\rsid4291156\rsid4471570\rsid5244407\rsid5732487\rsid6045443\rsid6178595\rsid7167559\rsid8404575\rsid8598301\rsid8797129\rsid8979511\rsid9005387\rsid9112532\rsid9119790
\rsid9381137\rsid9706100\rsid9788126\rsid9898965\rsid10905159\rsid11670652\rsid11828428\rsid12264694\rsid12287738\rsid12650743\rsid12661227\rsid12936610\rsid13186388\rsid13721198\rsid13789023\rsid14101758\rsid14699391\rsid15087333\rsid15155177
\rsid15274734\rsid15295953\rsid15335801\rsid16412676}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\operator hoosm}{\creatim\yr2024\mo3\dy30\hr15\min56}
{\revtim\yr2024\mo3\dy30\hr16\min44}{\version52}{\edmins17}{\nofpages2}{\nofwords1021}{\nofchars5820}{\nofcharsws6828}{\vern75}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}
\paperw12240\paperh15840\margl1440\margr1440\margt1440\margb1440\gutter0\ltrsect
\widowctrl\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont0\relyonvml0\donotembedlingdata1\grfdocevents0\validatexml0\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors0\horzdoc\dghspace120\dgvspace120\dghorigin1701
\dgvorigin1984\dghshow0\dgvshow3\jcompress\viewkind1\viewscale150\rsidroot598512 \fet0{\*\wgrffmtfilter 2450}\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\sectdefaultcl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2
\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6
\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang
{\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\s2\qc \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 \rtlch\fcs1 \af1\afs24\alang1025 \ltrch\fcs0
\fs24\lang1031\langfe2052\loch\af1\hich\af1\dbch\af31505\cgrid\langnp1031\langfenp2052 {\rtlch\fcs1 \ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid2380571\charrsid2380571 \hich\af1\dbch\af31505\loch\f1 Privacy policy}{\rtlch\fcs1
\ab\af1 \ltrch\fcs0 \b\ul\cf2\lang1033\langfe2052\langnp1033\insrsid1917520
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid8979511 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \hich\af1\dbch\af31505\loch\f1
\hich\f1 This Privacy Policy (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Policy}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1 ) governs the terms and conditions under which Purslane Ltd. (hereinafter \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 us}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
\hich\f1 or \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 we}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 \hich\f1
), processes personal data in connection with the activities and services concerning the operation of the website rustdesk.com and other websites or social media profiles run and managed by us (hereinafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0
\b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 Websites}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94
\loch\f1 ).
\par \hich\af1\dbch\af31505\loch\f1 We are serious about protecting your personal data and want you to feel safe and comfortable while browsing our Websites. We therefore respect \hich\af1\dbch\af31505\loch\f1
the confidentiality of your personal data and always proceed in accordance with the provisions of data protection legislation, in particular, Regulation (EU) 2016/679 of the European Parliament and of the Council (General Data Protection Regulation, herei
\hich\af1\dbch\af31505\loch\f1 \hich\f1 nafter the \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid1523795 \hich\af1\dbch\af31505\loch\f1 GDPR}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid8979511\charrsid8979511 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 ), and follow this Policy.
\par \hich\af1\dbch\af31505\loch\f1 With respect to the above, we use this Policy to inform you about how, for what purposes and to what extent we use your personal data and what information about you as a user of the Websites we may process.
\par }\pard \ltrpar\s2\ql \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 \hich\af1\dbch\af31505\loch\f1 0. Def
\hich\af1\dbch\af31505\loch\f1 initions.
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1523795 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1
\'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Personal data}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\hich\af1\dbch\af31505\loch\f1 means any information relating to a data subject;
\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Controller}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
means the natural or legal person, public authority, agency or other body which, alone or jointly with others, determines the purposes and means of the processing of personal data;
\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Data subject}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
means any identified or identifiable person who can be identified, directly or indirectly, in particular by reference to an identifier such as a name, an identification number, location data, an online identifier or to one or more factors specific to t
\hich\af1\dbch\af31505\loch\f1 he physical, physiological, genetic, mental, economic, cultural or social identity of that natural person;
\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Data processor}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1 means a natural or legal person, public authority, agency or other body which processes personal data on behalf of the controller;
\par \loch\af1\dbch\af31505\hich\f1 \'93}{\rtlch\fcs1 \ab\af1\afs21 \ltrch\fcs0 \b\fs21\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Processing}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \loch\af1\dbch\af31505\hich\f1 \'94\loch\f1
means any operation or set of operations which is performed on personal data or on sets of personal data, whether or not by automated means, such as collection, recording, organi}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid14101758 \hich\af1\dbch\af31505\loch\f1 z}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1523795\charrsid1523795 \hich\af1\dbch\af31505\loch\f1
ation, structuring, storage, adaptation or alteration, retrieval,\hich\af1\dbch\af31505\loch\f1 consultation, use, disclosure by transmission, dissemination or otherwise making available, alignment or combination, restriction, erasure or destruction.
\par }\pard \ltrpar\s2\ql \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 \hich\af1\dbch\af31505\loch\f1 1. }{\rtlch\fcs1
\ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid13186388\charrsid13186388 \hich\af1\dbch\af31505\loch\f1 Basic information about personal data processing conducted by us}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0
\b\fs18\lang1033\langfe2052\langnp1033\insrsid6045443 .}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid12650743 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743
\hich\af1\dbch\af31505\loch\f1
We always process your personal data lawfully, fairly, in a transparent manner and for specified, explicit and legitimate purposes. We process personal data only to the minimum necessary extent and we keep them in a form which permits your identification
\hich\af1\dbch\af31505\loch\f1 for no longer than is necessary \hich\af1\dbch\af31505\loch\f1 \hich\f1 vis-\'e0\loch\f1 -vis the purpose of the processing.
\par \hich\af1\dbch\af31505\loch\f1 We process your personal data in a manner that sufficiently ensures their integrity and confidentiality, i.e. by appropriate technical or organi}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid12661227 \hich\af1\dbch\af31505\loch\f1 z}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 \hich\af1\dbch\af31505\loch\f1
ational measures and appropriate protection against }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid6178595 \hich\af1\dbch\af31505\loch\f1 u}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0
\fs18\lang1033\langfe2052\langnp1033\insrsid6178595\charrsid6178595 \hich\af1\dbch\af31505\loch\f1 nauthorized }{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 \hich\af1\dbch\af31505\loch\f1
or unlawful processing and against loss, destruction or damage. We take care to ensure that personal data that are inaccurate, having regard to the purpose for which we process them, are erased or rectified without delay.
\par \hich\af1\dbch\af31505\loch\f1 We respect the principle of refraining \hich\af1\dbch\af31505\loch\f1 from personal data processing and the principle of data minimi}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12287738
\hich\af1\dbch\af31505\loch\f1 z}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid12650743\charrsid12650743 \hich\af1\dbch\af31505\loch\f1
ation. We therefore only retain your personal data if it is necessary in order to achieve the purpose of the processing or for various retention periods specified by law. The relevant data are erased in accordance with the law if the relevant purpose ceas
\hich\af1\dbch\af31505\loch\f1 es to exist as a result of the withdrawal of your consent and/or upon the expiration of the lawful retention period.
\par \hich\af1\dbch\af31505\loch\f1 For the above reasons, we use computer security such as a firewall and data e\hich\af1\dbch\af31505\loch\f1
ncryption to operate our Websites. We have implemented adequate physical, electronic and procedural safeguards and use reliable IT service providers. However, given the nature of the internet, we would like to bring to your attention the fact that certain
\hich\af1\dbch\af31505\loch\f1 security gaps may exist in the transmission of personal data via the internet (e.g. in communication via e-mail) and that full protection of personal data preventing third party access is impossible.}{\rtlch\fcs1
\af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1917520\charrsid12650743
\par }\pard \ltrpar\s2\ql \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 \hich\af1\dbch\af31505\loch\f1 2. }{\rtlch\fcs1
\ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid11670652\charrsid11670652 \hich\af1\dbch\af31505\loch\f1 Legal ground, purpose and extent of the processing of your personal data}{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0
\b\fs18\lang1033\langfe2052\langnp1033\insrsid1917520 .
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid15087333 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid4291156\charrsid4291156 \hich\af1\dbch\af31505\loch\f1
We may process your personal data for the following legal grounds and for the following purposes:}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid4291156
\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1384617\charrsid15295953
\hich\af1\dbch\af31505\loch\f1 a. }{\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid16412676\charrsid15295953 \hich\af1\dbch\af31505\loch\f1 Provision and improvement of and support for our Websites}{\rtlch\fcs1
\ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid83947\charrsid15295953
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid9706100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1
We process various information about your online activity, e.g. the time of access to our Websites, the time spent on our websites, conversions (i.e. completed activity on our Websites), etc., for the purposes of technical support and improvement of our W
\hich\af1\dbch\af31505\loch\f1 ebsites as well as monitoring of functionalities thereof (for details regarding the extent of the \hich\af1\dbch\af31505\loch\f1 data being processed see Article 4 (c) - d) of this Policy).
\par \hich\af1\dbch\af31505\loch\f1 For this purpose of personal data processing, we process your personal data under the lawful ground of legitimate interest (operation of the Websites, statistical purposes and data security).}{\rtlch\fcs1 \af1\afs18
\ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid14699391\charrsid1001100
\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid2828669
\hich\af1\dbch\af31505\loch\f1 b. Processing of the personal data of the visitors to the Websites
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1
If you publish any personal data on our Websites, we may process such personal data to the extent published for the purpose of responding to your post. Usually, we process following personal d\hich\af1\dbch\af31505\loch\f1
ata categories on our Websites: your name, surname and any personal data which you upload on the Websites or which we receive via personal messages.
\par \hich\af1\dbch\af31505\loch\f1
For these purposes of personal data processing, we process the above mentioned personal data under lawful ground of negotiation and performance of a contract (customer and technical support under the RustDesk software license agreement concluded between u
\hich\af1\dbch\af31505\loch\f1 s and yourself) and legitimate interest (general communication between us and yourself).
\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid2828669
\hich\af1\dbch\af31505\loch\f1 c. Cookies
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1
We use v\hich\af1\dbch\af31505\loch\f1
arious cookie files, which may contain your personal data (e.g. your IP address or the configuration of your browser and computer). We use cookies on the basis of your consent that you express via the cookies settings displayed to you in a banner during y
\hich\af1\dbch\af31505\loch\f1 our first visit to our Websites. This consent can be subsequently amended / withdrawn via your web browser settings (to the extended allowed by the respective browser).
\par }\pard \ltrpar\s2\qj \li0\ri0\sb120\sa120\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid9112532 {\rtlch\fcs1 \ab\af1\afs18 \ltrch\fcs0 \b\fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid2828669
\hich\af1\dbch\af31505\loch\f1 d. RustDesk
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid1001100 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1
To provide you with the RustDesk software application and to constantly impr\hich\af1\dbch\af31505\loch\f1
ove our services including customer support, we process following personal data about you and your device: start of the RustDesk software application, IP-address of the device, statistical information about your computer (e.g. CPU-type, screen resolution)
\hich\af1\dbch\af31505\loch\f1 , time and duration of RustDesk software sessions and RustDesk-IDs of the RustDesk\hich\f1 \rquote \loch\f1 s session participants.
\par }\pard \ltrpar\s2\qj \li0\ri0\sb100\sa100\nowidctlpar\wrapdefault\faauto\outlinelevel1\rin0\lin0\itap0\pararsid3630109 {\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1001100\charrsid1001100 \hich\af1\dbch\af31505\loch\f1
We process the personal data acquired via the RustDesk software under lawful grounds of performance of a contract (performance of RustDesk software license agreement concluded between us and yourself including customer support) and our legitimate interest
\hich\af1\dbch\af31505\loch\f1 (RustDesk software development).}{\rtlch\fcs1 \af1\afs18 \ltrch\fcs0 \fs18\lang1033\langfe2052\langnp1033\insrsid1917520\charrsid3630109
\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a
9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad
5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6
b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0
0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6
a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f
c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512
0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462
a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865
6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b
4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b
4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100b9540503a5070000d0200000160000007468656d652f7468656d652f
7468656d65312e786d6cec595f8b1b47127f3fc87718e65dd6bf19fd592c0769247963efdac6921df2d82bb566dadb332da65bbb16c1109ca71008049290870b
1cf7720fc791c005cee41eeebb9c0f9b5cee435c75cf68a65b6ad9bb8b0fccb1bbcba2e9f955f5afabaaab4add373f7c1a53e70ca79cb0a4e7d66fd45c072733
362749d8731f4dc7958eeb70819239a22cc13d778db9fbe1ad0f7e77131d8808c7d801f9841fa09e1b09b13ca856f90c8611bfc1963881770b96c648c0631a56
e7293a07bd31ad366ab556354624719d04c5a0f6fe624166d8f9d78bbffffac7effff9d997f0e7dedacc31a2305122b81c98d1742267c086a0c2ce4feb12c1d7
3ca0a9738668cf85e9e6ec7c8a9f0ad7a1880b78d1736beac7addeba594507b910157b6435b9b1fac9e57281f96943cd998627c5a49ee77bad7ea15f01a8d8c5
8ddaa3d6a855e85300349bc14a332ea6ce7623f072ac06ca3e5a740fdbc366ddc06bfa9b3b9cfbbefc35f00a94e9f776f0e371005634f00a94e1fd1dbc3fe80e
86a67e05caf0ad1d7cbbd61f7a6d43bf02459424a73be89adf6a069bd5169005a3875678d7f7c6ed46aebc44413414d125a758b044ec8bb5183d61e918001248
91208923d64bbc403308e60051729212e788841104de12258cc370ad511bd79af05ffe7aea93f2283ac0489396bc8009df19927c1c3e4bc952f4dc3ba0d5d520
af5ebc78f9fce797cffff6f2f3cf5f3eff319f5ba932e40e5112ea72bffde9ebfffcf099f3efbffee1b76fbecda6dec6731dfffa2f5fbcfee51f6f520f2b2e4d
f1eabb9f5efffcd3abefbffaf5cfdf58b4f75374a2c3a724c6dcb987cf9d872c86055af8e393f47212d308115da29f841c2548ce62d13f129181beb746145970
036cdaf1710aa9c606bcbd7a62109e44e94a108bc6bb516c008f19a303965aad7057cea59979ba4a42fbe4e94ac73d44e8cc36778012c3cba3d512722cb1a90c
226cd07c4051225088132c1cf98e9d626c59dd278418763d26b39471b610ce27c419206235c9949c18d1540a1d9218fcb2b611047f1bb6397eec0c18b5ad7a88
cf4c24ec0d442de4a7981a66bc8d5602c53695531453dde0474844369293753ad371232ec0d321a6cc19cd31e73699fb29ac5773fa5d483376b71fd3756c2253
414e6d3a8f10633a72c84e8308c54b1b76429248c77ec44f214491f380091bfc98993b443e831f50b2d7dd8f0936dcfdf66cf00832ac4ea90c10f966955a7c79
1b33237e276bba40d8966afa696ca4d87e4aacd131588546681f614cd1399a63ec3cfac8c260c09686cd4bd27722c82a87d81658779019abf239c11c3baab9d9
cd9347841b213bc121dbc3e778bd9578d6288951ba4ff33df0ba6ef31194bad81600f7e9ec5407de23d00a42bc588d729f830e2db8f76a7d1021a380c9676e8f
d7756af8ef227b0cf6e51383c605f625c8e04bcb4062d765de689b29a2c60465c04c117419b6740b2286fb4b11595c95d8ca2ab730376de906e88e8ca62726c9
5b3ba0addec7ffdff53ed061bcfafd0f96cdf66efa1dbb6223595db2d3d9974c0eb7fa9b7db8edae2660e99cbcff4dcd10ad920718eac86ec6baee69ae7b1af7
ffbea7d9b79faf3b997dfdc67527e3428771ddc9e4872befa693299b17e86be4814776d0a38e7de2bda73e0b42e944ac293ee2eae087c3f799f91806a59c3af8
c4c529e032828fb2ccc104062e4c91927152263e26229a446809a74375572a0979ae3ae4ce9271383452c356dd124f57f1319b67879df5ba3cd8cc2a2b47a21c
aff9c5381c54890cdd6a970778857ac5365407ad1b0252f63224b4c94c124d0b89f666501a491deb82d12c24d4cade098bae854547aadfb86a8705502bbc025f
b81df89ade737d0f444008cee3a0399f4b3f65aede785739f35d7a7a9f318d0880067b1301a5a7bb92ebdee5c9d565a176014f1b24b470334928cba8068f47f0
35388f4e397a111a97f575b774a9414f9a42cd07a155d26877dec4e2aabe06b9eddc40133d53d0c439efb9ada60f213343cb9ebb804363f8182f2176b8fcce85
680817303391661bfe2a9965997231443cca0cae924e960d622270ea5012f75cb9fcc20d3451394471ab372021bcb7e4ba9056de3772e074d3c978b1c033a1bb
5d1b9196ce1e21c367b9c2fa56895f1d2c25d90adc3d89e6e7ce095da50f118498dfae4b03ce0987bb837a66cd3981cbb0229195f1b75598f2b4abdf46a918ca
c6115d4628af287a32cfe02a951774d4536103ed295f33185433495e084f42596075a31ad5b4a81a1987bd55f7ed42d2725ad22c6ba6915564d5b4673163864d
19d8b2e5d58abcc66a6362c8697a85cf52f776caed6e72dd569f5054093078613f4bd5bd4041d0a8959319d424e3dd342c73763e6ad68ecd02df42ed224542cb
faad8dda2dbb1535c23a1d0c5ea9f283dc76d4c2d062d3572a4babcb73fd629b9d3c81e431842e77450557ae846beb14414334513d499636608b3c15f9d6804f
ce2a253df7d39adff782861f546a1d7f54f19a5eadd2f1fbcd4adff79bf5915faf0d078d6750584414d7fdece27e0c1718749d5fdfabf19d2bfc7873477363c6
e22a5357f455455c5de1d71bb62bfca9bc9c771d0249e7d35663dc6d7607ad4ab7d91f57bce1a053e906ad4165d80adac3f130f03bddf133d7395360afdf0cbc
d6a85369d583a0e2b56a927ea75b697b8d46df6bf73b23afff2c6f6360e559fac86d01e655bc6efd170000ffff0300504b0304140006000800000021000dd190
9fb60000001b010000270000007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f7827708
6f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64
b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd500199650
9affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff00
00001c0200001300000000000000000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0
000000360100000b00000000000000000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a000000
1c00000000000000000000000000190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d0014000600080000002100b954
0503a5070000d02000001600000000000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d001400060008000000
21000dd1909fb60000001b0100002700000000000000000000000000af0a00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000aa0b00000000}
{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d
617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169
6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363
656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e}
{\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdlocked0 heading 1;\lsdqformat1 \lsdlocked0 heading 2;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;
\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;
\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong;\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;
\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;
\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;
\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;
\lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;
\lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;
\lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;
\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;
\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;
\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5;
\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6;
\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6;
\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6;
\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis;
\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography;
\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4;
\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4;
\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1;
\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1;
\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2;
\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2;
\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3;
\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4;
\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4;
\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5;
\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5;
\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6;
\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6;
\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark;
\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1;
\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3;
\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3;
\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4;
\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4;
\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5;
\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5;
\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6;
\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention;
\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}}
+22
View File
@@ -0,0 +1,22 @@
<Project Sdk="WixToolset.Sdk/4.0.5">
<PropertyGroup>
<IncludeSearchPaths>
</IncludeSearchPaths>
<Configurations>Release</Configurations>
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<Content Include="Includes.wxi" />
<Content Include="Resources\icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="WixToolset.Firewall.wixext" Version="4.0.5" />
<PackageReference Include="WixToolset.Heat" Version="4.0.5" />
<PackageReference Include="WixToolset.Netfx.wixext" Version="4.0.5" />
<PackageReference Include="WixToolset.UI.wixext" Version="4.0.5" />
<PackageReference Include="WixToolset.Util.wixext" Version="4.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\CustomActions\CustomActions.vcxproj" />
</ItemGroup>
</Project>
+62
View File
@@ -0,0 +1,62 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<?include Includes.wxi?>
<Package Name="$(var.Product)" Version="$(var.Version)" Manufacturer="$(var.Manufacturer)" Language="!(loc.ProductLanguage)" UpgradeCode="$(var.UpgradeCode)" Scope="perMachine">
<SummaryInformation Keywords="Installer" Description="$(var.Description)" Codepage="!(loc.SummaryCodepage)" />
<!--<PropertyRef Id="UpgradesFile" />-->
<PropertyRef Id="AddRemovePropertiesFile" />
<Media Id="1" Cabinet="cab1.cab" EmbedCab="yes" CompressionLevel="high" />
<Icon Id="AppIcon" SourceFile="Resources\icon.ico" />
<!-- User Interface -->
<WixVariable Id="WixUILicenseRtf" Value="License.rtf" />
<ui:WixUI Id="UI_MyInstallDialog" InstallDirectory="INSTALLFOLDER_INNER" />
<UIRef Id="WixUI_ErrorProgressText" />
<InstallUISequence>
<Show Dialog="UI_AnotherAppDialog" Before="WelcomeDlg" Condition="Not installed AND APP_WINDOWS_INSTALLER=&quot;#0&quot;"/>
</InstallUISequence>
<InstallExecuteSequence>
<InstallExecute After="RemoveExistingProducts" />
<!--Only do InstallValidate if is not Uninstall-->
<!--<InstallValidate Condition="NOT (Installed AND REMOVE AND NOT UPGRADINGPRODUCTCODE )" />-->
<!--Only do InstallValidate if is Install-->
<InstallValidate Condition="NOT Installed" />
</InstallExecuteSequence>
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" Schedule="afterInstallInitialize" AllowSameVersionUpgrades="yes" />
<Feature Id="App" Level="1" AllowAdvertise="no" Display="expand" Title="!(loc.F_App)" Description="!(loc.F_App_Desc)" AllowAbsent="no">
<ComponentGroupRef Id="Components" />
<ComponentRef Id="Product.Registry.InstallFolder" />
<ComponentRef Id="Product.Registry.DefaultIcon" />
<ComponentRef Id="Product.Registry.CommandPlay" />
<ComponentRef Id="Product.Registry.URLProtocol" />
<ComponentRef Id="Product.Registry.Command" />
<ComponentRef Id="Product.Registry.UninstallApp" />
<ComponentRef Id="App.StartMenu" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties1" />
<ComponentRef Id="Product.Registry.PersistedStartMenuShortcutProperties0" />
<ComponentRef Id="Product.Registry.PersistedDesktopShortcutProperties1" />
<ComponentRef Id="Product.Registry.PersistedDesktopShortcutProperties0" />
<ComponentRef Id="Product.Registry.PersistedPrinterProperties1" />
<ComponentRef Id="Product.Registry.PersistedPrinterProperties0" />
</Feature>
<!--https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set-->
<!--$CustomBitmapsStart$-->
<!--$CustomBitmapsEnd$-->
</Package>
</Wix>
+15
View File
@@ -0,0 +1,15 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<UI>
<Dialog Id="UI_AnotherAppDialog" Width="370" Height="270" Title="!(loc.ExitDialog_Title)">
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Default="yes" Cancel="yes" Text="!(loc.WixUICancel)">
<Publish Event="EndDialog" Value="ErrorAbort" />
</Control>
<Control Id="Bitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="234" TabSkip="no" Text="!(loc.ExitDialogBitmap)" />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="Description" Type="Text" X="135" Y="70" Width="220" Height="40" Transparent="yes" NoPrefix="yes" Text="!(loc.AnotherAppDialogDescription)" />
<Control Id="Title" Type="Text" X="135" Y="20" Width="220" Height="60" Transparent="yes" NoPrefix="yes" Text="!(loc.AnotherAppDialogTitle)" />
</Dialog>
</UI>
</Fragment>
</Wix>
+32
View File
@@ -0,0 +1,32 @@
<!-- https://github.com/wixtoolset/wix/blob/ce73352b1fa1d4f9cded10a0ee410f2e786bd326/src/ext/UI/wixlib/InstallDirDlg.wxs -->
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
<Fragment>
<UI>
<Dialog Id="MyInstallDirDlg" Width="370" Height="270" Title="!(loc.InstallDirDlg_Title)">
<Control Id="Next" Type="PushButton" X="236" Y="243" Width="56" Height="17" Default="yes" Text="!(loc.WixUINext)" />
<Control Id="Back" Type="PushButton" X="180" Y="243" Width="56" Height="17" Text="!(loc.WixUIBack)" />
<Control Id="Cancel" Type="PushButton" X="304" Y="243" Width="56" Height="17" Cancel="yes" Text="!(loc.WixUICancel)">
<Publish Event="SpawnDialog" Value="CancelDlg" />
</Control>
<Control Id="Description" Type="Text" X="25" Y="23" Width="280" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgDescription)" />
<Control Id="Title" Type="Text" X="15" Y="6" Width="200" Height="15" Transparent="yes" NoPrefix="yes" Text="!(loc.InstallDirDlgTitle)" />
<Control Id="BannerBitmap" Type="Bitmap" X="0" Y="0" Width="370" Height="44" TabSkip="no" Text="!(loc.InstallDirDlgBannerBitmap)" />
<Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
<Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />
<Control Id="FolderLabel" Type="Text" X="20" Y="60" Width="290" Height="18" NoPrefix="yes" Text="!(loc.InstallDirDlgFolderLabel)" />
<Control Id="Folder" Type="PathEdit" X="20" Y="80" Width="320" Height="18" Property="WIXUI_INSTALLDIR" Indirect="yes" />
<Control Id="ChangeFolder" Type="PushButton" X="20" Y="100" Width="56" Height="17" Text="!(loc.InstallDirDlgChange)" />
<Control Id="ChkBoxStartMenuShortcuts" Type="CheckBox" X="20" Y="140" Width="290" Height="17" Property="STARTMENUSHORTCUTS" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgStartMenuShortcuts)" />
<Control Id="ChkBoxDesktopShortcuts" Type="CheckBox" X="20" Y="160" Width="290" Height="17" Property="DESKTOPSHORTCUTS" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgDesktopShortcuts)" />
<Control Id="ChkBoxInstallPrinter" Type="CheckBox" X="20" Y="180" Width="290" Height="17" Property="PRINTER" CheckBoxValue="1" Text="!(loc.MyInstallDirDlgPrinter)" />
</Dialog>
</UI>
</Fragment>
</Wix>
+95
View File
@@ -0,0 +1,95 @@
<!-- From https://github.com/wixtoolset/wix/blob/ce73352b1fa1d4f9cded10a0ee410f2e786bd326/src/ext/UI/wixlib/WixUI_InstallDir.wxs -->
<!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. -->
<!--
First-time install dialog sequence:
- WixUI_WelcomeDlg
- WixUI_LicenseAgreementDlg
- WixUI_InstallDirDlg
- WixUI_VerifyReadyDlg
- WixUI_DiskCostDlg
Maintenance dialog sequence:
- WixUI_MaintenanceWelcomeDlg
- WixUI_MaintenanceTypeDlg
- WixUI_InstallDirDlg
- WixUI_VerifyReadyDlg
Patch dialog sequence:
- WixUI_WelcomeDlg
- WixUI_VerifyReadyDlg
-->
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<?include ../Includes.wxi?>
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="UI_MyInstallDialog_$(WIXUIARCH)">
<Publish Dialog="LicenseAgreementDlg" Control="Print" Event="DoAction" Value="WixUIPrintEula_$(WIXUIARCH)" />
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="5" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
<UIRef Id="UI_MyInstallDialog" />
</Fragment>
<?endforeach?>
<Fragment>
<UI Id="file UI_MyInstallDialog">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="LicenseAgreementDlg" Condition="NOT Installed" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="Installed AND PATCH" />
<Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" />
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog" Value="MyInstallDirDlg" Condition="LicenseAccepted = &quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" />
<!-- Normalize INSTALLFOLDER_INNER before SetTargetPath and WixUIValidatePath run. -->
<!-- UI case 1: already ends with \$(var.Product) but has no trailing slash, add the slash. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\" Order="1" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;" />
<!-- UI case 2: ends with a slash but not \$(var.Product)\, append $(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]$(var.Product)\" Order="2" Condition="INSTALLFOLDER_INNER AND INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<!-- UI case 3: has no trailing slash and does not end with \$(var.Product), append \$(var.Product)\. -->
<Publish Dialog="MyInstallDirDlg" Control="Next" Property="INSTALLFOLDER_INNER" Value="[INSTALLFOLDER_INNER]\$(var.Product)\" Order="3" Condition="INSTALLFOLDER_INNER AND NOT INSTALLFOLDER_INNER ~&gt;&gt; &quot;\&quot; AND NOT (INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)\&quot; OR INSTALLFOLDER_INNER ~&gt;&gt; &quot;\$(var.Product)&quot;)" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="4" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="6" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="7" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="MyInstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MyInstallDirDlg" Order="1" Condition="NOT Installed" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2" Condition="Installed AND NOT PATCH" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2" Condition="Installed AND PATCH" />
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg" />
<Property Id="ARPNOMODIFY" Value="1" />
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
</Wix>
+44
View File
@@ -0,0 +1,44 @@
# RustDesk msi project
Use Visual Studio 2022 to compile this project.
This project is mainly derived from <https://github.com/MediaPortal/MediaPortal-2.git> .
## Steps
1. `python preprocess.py`, see `python preprocess.py -h` for help.
2. Build the .sln solution.
Run `msiexec /i package.msi /l*v install.log` to record the log.
## Usage
1. Put the custom dialog bitmaps in "Resources" directory. The supported bitmaps are `['WixUIBannerBmp', 'WixUIDialogBmp', 'WixUIExclamationIco', 'WixUIInfoIco', 'WixUINewIco', 'WixUIUpIco']`.
## Knowledge
### properties
[wix-toolset-set-custom-action-run-only-on-uninstall](https://www.advancedinstaller.com/versus/wix-toolset/wix-toolset-set-custom-action-run-only-on-uninstall.html)
| Property Name | Install | Uninstall | Change | Repair | Upgrade |
| ------ | ------ | ------ | ------ | ------ | ------ |
| Installed | False | True | True | True | True |
| REINSTALL | False | False | False | True | False |
| UPGRADINGPRODUCTCODE | False | False | False | False | True |
| REMOVE | False | True | False | False | True |
## TODOs
1. Start menu. Uninstall
1. custom options
1. Custom client.
1. firewall and tcp allow. Outgoing
1. Show license ?
1. Do create service. Outgoing.
## Refs
1. [windows-installer-portal](https://learn.microsoft.com/en-us/windows/win32/Msi/windows-installer-portal)
1. [wxs](https://wixtoolset.org/docs/schema/wxs/)
1. [wxs github](https://github.com/wixtoolset/wix)
+26
View File
@@ -0,0 +1,26 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34003.232
MinimumVisualStudioVersion = 10.0.40219.1
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Package", "Package\Package.wixproj", "{F403A403-CEFF-4399-B51C-CC646C8E98CF}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CustomActions", "CustomActions\CustomActions.vcxproj", "{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.ActiveCfg = Release|x64
{F403A403-CEFF-4399-B51C-CC646C8E98CF}.Release|x64.Build.0 = Release|x64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.ActiveCfg = Release|x64
{6B3647E0-B4A3-46AE-8757-A22EE51C1DAC}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {95277884-55F2-4A1F-BFFB-E82EFE847DC2}
EndGlobalSection
EndGlobal
+560
View File
@@ -0,0 +1,560 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import sys
import uuid
import argparse
import datetime
import subprocess
import re
import platform
from pathlib import Path
from itertools import chain
import shutil
g_indent_unit = "\t"
g_version = ""
g_build_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
# Replace the following links with your own in the custom arp properties.
# https://learn.microsoft.com/en-us/windows/win32/msi/property-reference
g_arpsystemcomponent = {
"Comments": {
"msi": "ARPCOMMENTS",
"t": "string",
"v": "!(loc.AR_Comment)",
},
"Contact": {
"msi": "ARPCONTACT",
"v": "https://github.com/rustdesk/rustdesk",
},
"HelpLink": {
"msi": "ARPHELPLINK",
"v": "https://github.com/rustdesk/rustdesk/issues/",
},
"ReadMe": {
"msi": "ARPREADME",
"v": "https://github.com/rustdesk/rustdesk",
},
}
def default_revision_version():
return int(datetime.datetime.now().timestamp() / 60)
def make_parser():
parser = argparse.ArgumentParser(description="Msi preprocess script.")
parser.add_argument(
"-d",
"--dist-dir",
type=str,
default="../../rustdesk",
help="The dist directory to install.",
)
parser.add_argument(
"--arp",
action="store_true",
help="Is ARPSYSTEMCOMPONENT",
default=False,
)
parser.add_argument(
"--custom-arp",
type=str,
default="{}",
help='Custom arp properties, e.g. \'["Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}]\'',
)
parser.add_argument(
"-c", "--custom", action="store_true", help="Is custom client", default=False
)
parser.add_argument(
"--conn-type",
type=str,
default="",
help='Connection type, e.g. "incoming", "outgoing". Default is empty, means incoming-outgoing',
)
parser.add_argument(
"--app-name", type=str, default="RustDesk", help="The app name."
)
parser.add_argument(
"-v", "--version", type=str, default="", help="The app version."
)
parser.add_argument(
"--revision-version", type=int, default=default_revision_version(), help="The revision version."
)
parser.add_argument(
"-m",
"--manufacturer",
type=str,
default="PURSLANE",
help="The app manufacturer.",
)
return parser
def read_lines_and_start_index(file_path, tag_start, tag_end):
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
index_start = -1
index_end = -1
for i, line in enumerate(lines):
if tag_start in line:
index_start = i
if tag_end in line:
index_end = i
if index_start == -1:
print(f'Error: start tag "{tag_start}" not found')
return None, None
if index_end == -1:
print(f'Error: end tag "{tag_end}" not found')
return None, None
return lines, index_start
def insert_components_between_tags(lines, index_start, app_name, dist_dir):
indent = g_indent_unit * 3
path = Path(dist_dir)
idx = 1
for file_path in path.glob("**/*"):
if file_path.is_file():
if file_path.name.lower() == f"{app_name}.exe".lower():
continue
subdir = str(file_path.parent.relative_to(path))
dir_attr = ""
if subdir != ".":
dir_attr = f'Subdirectory="{subdir}"'
# Don't generate Component Id and File Id like 'Component_{idx}' and 'File_{idx}'
# because it will cause error
# "Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'"
to_insert_lines = f"""
{indent}<Component Guid="{uuid.uuid4()}" {dir_attr}>
{indent}{g_indent_unit}<File Source="{file_path.as_posix()}" KeyPath="yes" Checksum="yes" />
{indent}</Component>
"""
lines.insert(index_start + 1, to_insert_lines[1:])
index_start += 1
idx += 1
return True
def gen_auto_component(app_name, dist_dir):
return gen_content_between_tags(
"Package/Components/RustDesk.wxs",
"<!--$AutoComonentStart$-->",
"<!--$AutoComponentEnd$-->",
lambda lines, index_start: insert_components_between_tags(
lines, index_start, app_name, dist_dir
),
)
def gen_pre_vars(args, dist_dir):
def func(lines, index_start):
upgrade_code = uuid.uuid5(uuid.NAMESPACE_OID, app_name + ".exe")
indent = g_indent_unit * 1
to_insert_lines = [
f'{indent}<?define Version="{g_version}" ?>\n',
f'{indent}<?define Manufacturer="{args.manufacturer}" ?>\n',
f'{indent}<?define Product="{args.app_name}" ?>\n',
f'{indent}<?define Description="{args.app_name} Installer" ?>\n',
f'{indent}<?define ProductLower="{args.app_name.lower()}" ?>\n',
f'{indent}<?define RegKeyRoot=".$(var.ProductLower)" ?>\n',
f'{indent}<?define RegKeyInstall="$(var.RegKeyRoot)\\Install" ?>\n',
f'{indent}<?define BuildDir="{dist_dir}" ?>\n',
f'{indent}<?define BuildDate="{g_build_date}" ?>\n',
"\n",
f"{indent}<!-- The UpgradeCode must be consistent for each product. ! -->\n"
f'{indent}<?define UpgradeCode = "{upgrade_code}" ?>\n',
]
for i, line in enumerate(to_insert_lines):
lines.insert(index_start + i + 1, line)
return lines
return gen_content_between_tags(
"Package/Includes.wxi", "<!--$PreVarsStart$-->", "<!--$PreVarsEnd$-->", func
)
def replace_app_name_in_langs(app_name):
langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language")
for file_path in langs_dir.glob("*.wxl"):
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
for i, line in enumerate(lines):
lines[i] = line.replace("RustDesk", app_name)
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines)
def replace_app_name_in_custom_actions(app_name):
custion_actions_dir = Path(sys.argv[0]).parent.joinpath("CustomActions")
for file_path in chain(custion_actions_dir.glob("*.cpp"), custion_actions_dir.glob("*.h")):
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
for i, line in enumerate(lines):
line = re.sub(r"\bRustDesk\b", app_name, line)
line = line.replace(f"{app_name} v4 Printer Driver", "RustDesk v4 Printer Driver")
lines[i] = line
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines)
def gen_upgrade_info():
def func(lines, index_start):
indent = g_indent_unit * 3
vs = g_version.split(".")
major = vs[0]
upgrade_id = uuid.uuid4()
to_insert_lines = [
f'{indent}<Upgrade Id="{upgrade_id}">\n',
f'{indent}{g_indent_unit}<UpgradeVersion Property="OLD_VERSION_FOUND" Minimum="{major}.0.0" Maximum="{major}.99.99" IncludeMinimum="yes" IncludeMaximum="yes" OnlyDetect="no" IgnoreRemoveFailure="yes" MigrateFeatures="yes" />\n',
f"{indent}</Upgrade>\n",
]
for i, line in enumerate(to_insert_lines):
lines.insert(index_start + i + 1, line)
return lines
return gen_content_between_tags(
"Package/Fragments/Upgrades.wxs",
"<!--$UpgradeStart$-->",
"<!--$UpgradeEnd$-->",
func,
)
def gen_custom_dialog_bitmaps():
def func(lines, index_start):
indent = g_indent_unit * 2
# https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set
vars = [
"WixUIBannerBmp",
"WixUIDialogBmp",
"WixUIExclamationIco",
"WixUIInfoIco",
"WixUINewIco",
"WixUIUpIco",
]
to_insert_lines = []
for var in vars:
if Path(f"Package/Resources/{var}.bmp").exists():
to_insert_lines.append(
f'{indent}<WixVariable Id="{var}" Value="Resources\\{var}.bmp" />\n'
)
for i, line in enumerate(to_insert_lines):
lines.insert(index_start + i + 1, line)
return lines
return gen_content_between_tags(
"Package/Package.wxs",
"<!--$CustomBitmapsStart$-->",
"<!--$CustomBitmapsEnd$-->",
func,
)
def gen_custom_ARPSYSTEMCOMPONENT_False(args):
def func(lines, index_start):
indent = g_indent_unit * 2
lines_new = []
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/arpsystemcomponent?redirectedfrom=MSDN-->\n"
)
lines_new.append(
f'{indent}<!--<Property Id="ARPSYSTEMCOMPONENT" Value="1" />-->\n\n'
)
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
)
for _, v in g_arpsystemcomponent.items():
if "msi" in v and "v" in v:
lines_new.append(
f'{indent}<Property Id="{v["msi"]}" Value="{v["v"]}" />\n'
)
for i, line in enumerate(lines_new):
lines.insert(index_start + i + 1, line)
return lines
return gen_content_between_tags(
"Package/Fragments/AddRemoveProperties.wxs",
"<!--$ArpStart$-->",
"<!--$ArpEnd$-->",
func,
)
def get_folder_size(folder_path):
total_size = 0
folder = Path(folder_path)
for file in folder.glob("**/*"):
if file.is_file():
total_size += file.stat().st_size
return total_size
def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
def func(lines, index_start):
indent = g_indent_unit * 5
lines_new = []
lines_new.append(
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayName" Value="{args.app_name}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayIcon" Value="[INSTALLFOLDER_INNER]{args.app_name}.exe" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="DisplayVersion" Value="{g_version}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="Publisher" Value="{args.manufacturer}" />\n'
)
installDate = datetime.datetime.now().strftime("%Y%m%d")
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallDate" Value="{installDate}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="InstallSource" Value="[InstallSource]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
)
# EstimatedSize in uninstall registry must be in KB.
estimated_size_bytes = get_folder_size(dist_dir)
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="ModifyPath" Value="MsiExec.exe /X [ProductCode]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Id="NoModify" Value="1" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="UninstallString" Value="MsiExec.exe /X [ProductCode]" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="expandable" Name="QuietUninstallString" Value="MsiExec.exe /qn /X [ProductCode]" />\n'
)
vs = g_version.split(".")
major, minor, build = vs[0], vs[1], vs[2]
lines_new.append(
f'{indent}<RegistryValue Type="string" Name="Version" Value="{g_version}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionMajor" Value="{major}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionMinor" Value="{minor}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="VersionBuild" Value="{build}" />\n'
)
lines_new.append(
f'{indent}<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />\n'
)
for k, v in g_arpsystemcomponent.items():
if "v" in v:
t = v["t"] if "t" in v is None else "string"
lines_new.append(
f'{indent}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n'
)
for i, line in enumerate(lines_new):
lines.insert(index_start + i + 1, line)
return lines
return gen_content_between_tags(
"Package/Components/Regs.wxs",
"<!--$ArpStart$-->",
"<!--$ArpEnd$-->",
func,
)
def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
try:
custom_arp = json.loads(args.custom_arp)
g_arpsystemcomponent.update(custom_arp)
except json.JSONDecodeError as e:
print(f"Failed to decode custom arp: {e}")
return False
if args.arp:
return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir)
else:
return gen_custom_ARPSYSTEMCOMPONENT_False(args)
def gen_conn_type(args):
def func(lines, index_start):
indent = g_indent_unit * 3
lines_new = []
if args.conn_type != "":
lines_new.append(
f"""{indent}<Property Id="CC_CONNECTION_TYPE" Value="{args.conn_type}" />\n"""
)
for i, line in enumerate(lines_new):
lines.insert(index_start + i + 1, line)
return lines
return gen_content_between_tags(
"Package/Fragments/AddRemoveProperties.wxs",
"<!--$CustomClientPropsStart$-->",
"<!--$CustomClientPropsEnd$-->",
func,
)
def gen_content_between_tags(filename, tag_start, tag_end, func):
target_file = Path(sys.argv[0]).parent.joinpath(filename)
lines, index_start = read_lines_and_start_index(target_file, tag_start, tag_end)
if lines is None:
return False
func(lines, index_start)
with open(target_file, "w", encoding="utf-8") as f:
f.writelines(lines)
return True
def prepare_resources():
icon_src = Path(sys.argv[0]).parent.joinpath("../icon.ico")
icon_dst = Path(sys.argv[0]).parent.joinpath("Package/Resources/icon.ico")
if icon_src.exists():
icon_dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(icon_src, icon_dst)
return True
else:
# unreachable
print(f"Error: icon.ico not found in {icon_src}")
return False
def init_global_vars(dist_dir, app_name, args):
dist_app = dist_dir.joinpath(app_name + ".exe")
def read_process_output(args):
process = subprocess.Popen(
f"{dist_app} {args}",
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
)
output, _ = process.communicate()
return output.decode("utf-8").strip()
global g_version
global g_build_date
g_version = args.version.replace("-", ".")
if g_version == "":
g_version = read_process_output("--version")
version_pattern = re.compile(r"\d+\.\d+\.\d+.*")
if not version_pattern.match(g_version):
print(f"Error: version {g_version} not found in {dist_app}")
return False
if g_version.count(".") == 2:
# https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Version.cs
if args.revision_version < 0 or args.revision_version > 2147483647:
raise ValueError(f"Invalid revision version: {args.revision_version}")
g_version = f"{g_version}.{args.revision_version}"
g_build_date = read_process_output("--build-date")
build_date_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}")
if not build_date_pattern.match(g_build_date):
print(f"Error: build date {g_build_date} not found in {dist_app}")
return False
return True
def update_license_file(app_name):
if app_name == "RustDesk":
return
license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf")
with open(license_file, "r", encoding="utf-8") as f:
license_content = f.read()
license_content = license_content.replace("website rustdesk.com and other ", "")
license_content = license_content.replace("RustDesk", app_name)
license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
with open(license_file, "w", encoding="utf-8") as f:
f.write(license_content)
def replace_component_guids_in_wxs():
langs_dir = Path(sys.argv[0]).parent.joinpath("Package")
for file_path in langs_dir.glob("**/*.wxs"):
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
# <Component Id="Product.Registry.DefaultIcon" Guid="6DBF2690-0955-4C6A-940F-634DDA503F49">
for i, line in enumerate(lines):
match = re.search(r'Component.+Guid="([^"]+)"', line)
if match:
lines[i] = re.sub(r'Guid="[^"]+"', f'Guid="{uuid.uuid4()}"', line)
with open(file_path, "w", encoding="utf-8") as f:
f.writelines(lines)
if __name__ == "__main__":
parser = make_parser()
args = parser.parse_args()
app_name = args.app_name
dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve()
if not prepare_resources():
sys.exit(-1)
if not init_global_vars(dist_dir, app_name, args):
sys.exit(-1)
update_license_file(app_name)
if not gen_pre_vars(args, dist_dir):
sys.exit(-1)
if app_name != "RustDesk":
replace_component_guids_in_wxs()
if not gen_upgrade_info():
sys.exit(-1)
if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
sys.exit(-1)
if not gen_conn_type(args):
sys.exit(-1)
if not gen_auto_component(app_name, dist_dir):
sys.exit(-1)
if not gen_custom_dialog_bitmaps():
sys.exit(-1)
replace_app_name_in_langs(args.app_name)
replace_app_name_in_custom_actions(args.app_name)
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
echo $MACOS_CODESIGN_IDENTITY
cargo install flutter_rust_bridge_codegen --version 1.80.1 --features uuid --locked
cd flutter; flutter pub get; cd -
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
./build.py --flutter
rm rustdesk-$VERSION.dmg
# security find-identity -v
codesign --force --options runtime -s $MACOS_CODESIGN_IDENTITY --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-$VERSION.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
codesign --force --options runtime -s $MACOS_CODESIGN_IDENTITY --deep --strict rustdesk-$VERSION.dmg -vvv
# notarize the rustdesk-${{ env.VERSION }}.dmg
rcodesign notary-submit --api-key-path ~/.p12/api-key.json --staple rustdesk-$VERSION.dmg
+47
View File
@@ -0,0 +1,47 @@
# arg 1: the new package version
#pre_install() {
#}
# arg 1: the new package version
post_install() {
# do something here
cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
update-desktop-database
}
# arg 1: the new package version
# arg 2: the old package version
pre_upgrade() {
systemctl stop rustdesk || true
}
# arg 1: the new package version
# arg 2: the old package version
post_upgrade() {
cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
update-desktop-database
}
# arg 1: the old package version
pre_remove() {
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
}
# arg 1: the old package version
post_remove() {
rm /usr/share/applications/rustdesk.desktop || true
rm /usr/share/applications/rustdesk-link.desktop || true
update-desktop-database
}
+5
View File
@@ -0,0 +1,5 @@
#%PAM-1.0
@include common-auth
@include common-account
@include common-session
@include common-password
+5
View File
@@ -0,0 +1,5 @@
#%PAM-1.0
auth include common-auth
account include common-account
session include common-session
password include common-password
+98
View File
@@ -0,0 +1,98 @@
Name: rustdesk
Version: 1.4.7
Release: 0
Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1 xdotool
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
%description
The best open-source remote desktop client software, written in Rust.
%prep
# we have no source, so nothing here
%build
# we have no source, so nothing here
# %global __python %{__python3}
%install
mkdir -p "%{buildroot}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/rustdesk"
mkdir -p "%{buildroot}/usr/bin"
install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png"
install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg"
%files
/usr/share/rustdesk/*
/usr/share/rustdesk/files/rustdesk.service
/usr/share/icons/hicolor/256x256/apps/rustdesk.png
/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
/usr/share/rustdesk/files/rustdesk.desktop
/usr/share/rustdesk/files/rustdesk-link.desktop
%changelog
# let's skip this for now
%pre
# can do something for centos7
case "$1" in
1)
# for install
;;
2)
# for upgrade
systemctl stop rustdesk || true
;;
esac
%post
cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
update-desktop-database
%preun
case "$1" in
0)
# for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun
case "$1" in
0)
# for uninstall
rm /usr/bin/rustdesk || true
rmdir /usr/lib/rustdesk || true
rmdir /usr/local/rustdesk || true
rmdir /usr/share/rustdesk || true
rm /usr/share/applications/rustdesk.desktop || true
rm /usr/share/applications/rustdesk-link.desktop || true
update-desktop-database
;;
1)
# for upgrade
rmdir /usr/lib/rustdesk || true
rmdir /usr/local/rustdesk || true
;;
esac
+98
View File
@@ -0,0 +1,98 @@
Name: rustdesk
Version: 1.4.7
Release: 0
Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb libXfixes alsa-lib libva pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3 libxdo
Provides: libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libfile_selector_linux_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
%description
The best open-source remote desktop client software, written in Rust.
%prep
# we have no source, so nothing here
%build
# we have no source, so nothing here
# %global __python %{__python3}
%install
mkdir -p "%{buildroot}/usr/share/rustdesk" && cp -r ${HBB}/flutter/build/linux/x64/release/bundle/* -t "%{buildroot}/usr/share/rustdesk"
mkdir -p "%{buildroot}/usr/bin"
install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files"
install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png"
install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg"
%files
/usr/share/rustdesk/*
/usr/share/rustdesk/files/rustdesk.service
/usr/share/icons/hicolor/256x256/apps/rustdesk.png
/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
/usr/share/rustdesk/files/rustdesk.desktop
/usr/share/rustdesk/files/rustdesk-link.desktop
%changelog
# let's skip this for now
%pre
# can do something for centos7
case "$1" in
1)
# for install
;;
2)
# for upgrade
systemctl stop rustdesk || true
;;
esac
%post
cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
ln -sf /usr/share/rustdesk/rustdesk /usr/bin/rustdesk
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
update-desktop-database
%preun
case "$1" in
0)
# for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun
case "$1" in
0)
# for uninstall
rm /usr/bin/rustdesk || true
rmdir /usr/lib/rustdesk || true
rmdir /usr/local/rustdesk || true
rmdir /usr/share/rustdesk || true
rm /usr/share/applications/rustdesk.desktop || true
rm /usr/share/applications/rustdesk-link.desktop || true
update-desktop-database
;;
1)
# for upgrade
rmdir /usr/lib/rustdesk || true
rmdir /usr/local/rustdesk || true
;;
esac
+93
View File
@@ -0,0 +1,93 @@
Name: rustdesk
Version: 1.1.9
Release: 0
Summary: RPM package
License: GPL-3.0
Requires: gtk3 libxcb1 libXfixes3 alsa-utils libXtst6 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
Recommends: libayatana-appindicator3-1 xdotool
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
%description
The best open-source remote desktop client software, written in Rust.
%prep
# we have no source, so nothing here
%build
# we have no source, so nothing here
%global __python %{__python3}
%install
mkdir -p %{buildroot}/usr/bin/
mkdir -p %{buildroot}/usr/share/rustdesk/
mkdir -p %{buildroot}/usr/share/rustdesk/files/
mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/
mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/
install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk
install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so
install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/
install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png
install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/
install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/
%files
/usr/bin/rustdesk
/usr/share/rustdesk/libsciter-gtk.so
/usr/share/rustdesk/files/rustdesk.service
/usr/share/icons/hicolor/256x256/apps/rustdesk.png
/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
/usr/share/rustdesk/files/rustdesk.desktop
/usr/share/rustdesk/files/rustdesk-link.desktop
%changelog
# let's skip this for now
%pre
# can do something for centos7
case "$1" in
1)
# for install
;;
2)
# for upgrade
systemctl stop rustdesk || true
;;
esac
%post
cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
update-desktop-database
%preun
case "$1" in
0)
# for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun
case "$1" in
0)
# for uninstall
rm /usr/share/applications/rustdesk.desktop || true
rm /usr/share/applications/rustdesk-link.desktop || true
update-desktop-database
;;
1)
# for upgrade
;;
esac
+96
View File
@@ -0,0 +1,96 @@
Name: rustdesk
Version: 1.4.7
Release: 0
Summary: RPM package
License: GPL-3.0
URL: https://rustdesk.com
Vendor: rustdesk <info@rustdesk.com>
Requires: gtk3 libxcb libXfixes alsa-lib libva2 pam gstreamer1-plugins-base
Recommends: libayatana-appindicator-gtk3 libxdo
# https://docs.fedoraproject.org/en-US/packaging-guidelines/Scriptlets/
%description
The best open-source remote desktop client software, written in Rust.
%prep
# we have no source, so nothing here
%build
# we have no source, so nothing here
%global __python %{__python3}
%install
mkdir -p %{buildroot}/usr/bin/
mkdir -p %{buildroot}/usr/share/rustdesk/
mkdir -p %{buildroot}/usr/share/rustdesk/files/
mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/
mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/
install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk
install $HBB/libsciter-gtk.so %{buildroot}/usr/share/rustdesk/libsciter-gtk.so
install $HBB/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/
install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png
install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/
install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/
%files
/usr/bin/rustdesk
/usr/share/rustdesk/libsciter-gtk.so
/usr/share/rustdesk/files/rustdesk.service
/usr/share/icons/hicolor/256x256/apps/rustdesk.png
/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
/usr/share/rustdesk/files/rustdesk.desktop
/usr/share/rustdesk/files/rustdesk-link.desktop
/usr/share/rustdesk/files/__pycache__/*
%changelog
# let's skip this for now
%pre
# can do something for centos7
case "$1" in
1)
# for install
;;
2)
# for upgrade
systemctl stop rustdesk || true
;;
esac
%post
cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
systemctl daemon-reload
systemctl enable rustdesk
systemctl start rustdesk
update-desktop-database
%preun
case "$1" in
0)
# for uninstall
systemctl stop rustdesk || true
systemctl disable rustdesk || true
rm /etc/systemd/system/rustdesk.service || true
;;
1)
# for upgrade
;;
esac
%postun
case "$1" in
0)
# for uninstall
rm /usr/share/applications/rustdesk.desktop || true
rm /usr/share/applications/rustdesk-link.desktop || true
update-desktop-database
;;
1)
# for upgrade
;;
esac
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
Name=RustDesk
NoDisplay=true
MimeType=x-scheme-handler/rustdesk;
TryExec=rustdesk
Exec=rustdesk %u
Icon=rustdesk
Terminal=false
Type=Application
StartupNotify=false
StartupWMClass=rustdesk
+19
View File
@@ -0,0 +1,19 @@
[Desktop Entry]
Name=RustDesk
GenericName=Remote Desktop
Comment=Remote Desktop
Exec=rustdesk %u
Icon=rustdesk
Terminal=false
Type=Application
StartupNotify=true
Categories=Network;RemoteAccess;GTK;
Keywords=internet;linux;dart;rust;remote-control;p2p;teamviewer;rust-lang;rdp;remote-desktop;vnc;
Actions=new-window;
StartupWMClass=rustdesk
X-Desktop-File-Install-Version=0.23
[Desktop Action new-window]
Name=Open a New Window
Exec=rustdesk %u
+22
View File
@@ -0,0 +1,22 @@
[Unit]
Description=RustDesk
Requires=network.target
After=systemd-user-sessions.service
[Service]
Type=simple
ExecStart=/usr/bin/rustdesk --service
# kill --tray and --server both
ExecStop=pkill -f "rustdesk --"
# below two lines do not work, have to use above one line
#ExecStop=pkill -f "rustdesk --tray"
#ExecStop=pkill -f "rustdesk --server"
PIDFile=/run/rustdesk.pid
KillMode=mixed
TimeoutStopSec=30
User=root
LimitNOFILE=100000
Environment="PULSE_LATENCY_MSEC=60" "PIPEWIRE_LATENCY=1024/48000"
[Install]
WantedBy=multi-user.target
Executable
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# This script is derived from https://github.com/neutrinolabs/xrdp/sesman/startwm.sh.
#
# This script is an example. You might need to edit this script
# depending on your distro if it doesn't work for you.
#
# Uncomment the following line for debug:
# exec xterm
# Execution sequence for interactive login shell - pseudocode
#
# IF /etc/profile is readable THEN
# execute ~/.bash_profile
# END IF
# IF ~/.bash_profile is readable THEN
# execute ~/.bash_profile
# ELSE
# IF ~/.bash_login is readable THEN
# execute ~/.bash_login
# ELSE
# IF ~/.profile is readable THEN
# execute ~/.profile
# END IF
# END IF
# END IF
pre_start()
{
if [ -r /etc/profile ]; then
. /etc/profile
fi
if [ -r ~/.bash_profile ]; then
. ~/.bash_profile
else
if [ -r ~/.bash_login ]; then
. ~/.bash_login
else
if [ -r ~/.profile ]; then
. ~/.profile
fi
fi
fi
return 0
}
# When loging out from the interactive shell, the execution sequence is:
#
# IF ~/.bash_logout exists THEN
# execute ~/.bash_logout
# END IF
post_start()
{
if [ -r ~/.bash_logout ]; then
. ~/.bash_logout
fi
return 0
}
#start the window manager
wm_start()
{
if [ -r /etc/default/locale ]; then
. /etc/default/locale
export LANG LANGUAGE
fi
# debian
if [ -r /etc/X11/Xsession ]; then
pre_start
. /etc/X11/Xsession
post_start
exit 0
fi
# alpine
# Don't use /etc/X11/xinit/Xsession - it doesn't work
if [ -f /etc/alpine-release ]; then
if [ -f /etc/X11/xinit/xinitrc ]; then
pre_start
/etc/X11/xinit/xinitrc
post_start
else
echo "** xinit package isn't installed" >&2
exit 1
fi
fi
# el
if [ -r /etc/X11/xinit/Xsession ]; then
pre_start
. /etc/X11/xinit/Xsession
post_start
exit 0
fi
# suse
if [ -r /etc/X11/xdm/Xsession ]; then
# since the following script run a user login shell,
# do not execute the pseudo login shell scripts
. /etc/X11/xdm/Xsession
exit 0
elif [ -r /usr/etc/X11/xdm/Xsession ]; then
. /usr/etc/X11/xdm/Xsession
exit 0
fi
pre_start
xterm
post_start
}
#. /etc/environment
#export PATH=$PATH
#export LANG=$LANG
# change PATH to be what your environment needs usually what is in
# /etc/environment
#PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"
#export PATH=$PATH
# for PATH and LANG from /etc/environment
# pam will auto process the environment file if /etc/pam.d/xrdp-sesman
# includes
# auth required pam_env.so readenv=1
wm_start
exit 1
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
import requests
import argparse
import json
def check_response(response):
"""
Check API response and handle errors.
Two error cases:
1. Status code is not 200 -> exit with error
2. Response contains {"error": "xxx"} -> exit with error
"""
if response.status_code != 200:
print(f"Error: HTTP {response.status_code}: {response.text}")
exit(1)
# Check for {"error": "xxx"} in response
if response.text and response.text.strip():
try:
json_data = response.json()
if isinstance(json_data, dict) and "error" in json_data:
print(f"Error: {json_data['error']}")
exit(1)
return json_data
except ValueError:
return response.text
return None
def headers_with(token):
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# ---------- Strategies APIs ----------
def list_strategies(url, token):
"""List all strategies"""
headers = headers_with(token)
r = requests.get(f"{url}/api/strategies", headers=headers)
return check_response(r)
def get_strategy_by_guid(url, token, guid):
"""Get strategy by GUID"""
headers = headers_with(token)
r = requests.get(f"{url}/api/strategies/{guid}", headers=headers)
return check_response(r)
def get_strategy_by_name(url, token, name):
"""Get strategy by name"""
strategies = list_strategies(url, token)
if not strategies:
return None
for s in strategies:
if str(s.get("name")) == name:
return s
return None
def enable_strategy(url, token, name):
"""Enable a strategy"""
headers = headers_with(token)
strategy = get_strategy_by_name(url, token, name)
if not strategy:
print(f"Error: Strategy '{name}' not found")
exit(1)
guid = strategy.get("guid")
r = requests.put(f"{url}/api/strategies/{guid}/status", headers=headers, json=True)
check_response(r)
return "Success"
def disable_strategy(url, token, name):
"""Disable a strategy"""
headers = headers_with(token)
strategy = get_strategy_by_name(url, token, name)
if not strategy:
print(f"Error: Strategy '{name}' not found")
exit(1)
guid = strategy.get("guid")
r = requests.put(f"{url}/api/strategies/{guid}/status", headers=headers, json=False)
check_response(r)
return "Success"
def get_device_guid_by_id(url, token, device_id):
"""Get device GUID by device ID (exact match)"""
headers = headers_with(token)
params = {"id": device_id, "pageSize": 50}
r = requests.get(f"{url}/api/devices", headers=headers, params=params)
res = check_response(r)
if not res:
return None
devices_data = res.get("data", []) if isinstance(res, dict) else res
for d in devices_data:
if d.get("id") == device_id:
return d.get("guid")
return None
def get_user_guid_by_name(url, token, name):
"""Get user GUID by exact name match"""
headers = headers_with(token)
params = {"name": name, "pageSize": 50}
r = requests.get(f"{url}/api/users", headers=headers, params=params)
res = check_response(r)
if not res:
return None
users_data = res.get("data", []) if isinstance(res, dict) else res
for u in users_data:
if u.get("name") == name:
return u.get("guid")
return None
def get_device_group_guid_by_name(url, token, name):
"""Get device group GUID by exact name match"""
headers = headers_with(token)
params = {"pageSize": 50, "name": name}
r = requests.get(f"{url}/api/device-groups", headers=headers, params=params)
res = check_response(r)
if not res:
return None
groups_data = res.get("data", []) if isinstance(res, dict) else res
for g in groups_data:
if g.get("name") == name:
return g.get("guid")
return None
def assign_strategy(url, token, strategy_name, peers=None, users=None, device_groups=None):
"""
Assign strategy to peers, users, or device groups
Args:
strategy_name: Name of the strategy (or None to unassign)
peers: List of device IDs or GUIDs
users: List of user names or GUIDs
device_groups: List of device group names or GUIDs
"""
headers = headers_with(token)
# Get strategy GUID if strategy_name is provided
strategy_guid = None
if strategy_name:
strategy = get_strategy_by_name(url, token, strategy_name)
if not strategy:
print(f"Error: Strategy '{strategy_name}' not found")
exit(1)
strategy_guid = strategy.get("guid")
# Convert device IDs to GUIDs
peer_guids = []
if peers:
for peer in peers:
# Check if it's already a GUID format
if len(peer) == 36 and peer.count('-') == 4:
peer_guids.append(peer)
else:
# Treat as device ID, look it up
guid = get_device_guid_by_id(url, token, peer)
if not guid:
print(f"Error: Device '{peer}' not found")
exit(1)
peer_guids.append(guid)
# Convert user names to GUIDs
user_guids = []
if users:
for user in users:
# Check if it's already a GUID format
if len(user) == 36 and user.count('-') == 4:
user_guids.append(user)
else:
# Treat as username, look it up
guid = get_user_guid_by_name(url, token, user)
if not guid:
print(f"Error: User '{user}' not found")
exit(1)
user_guids.append(guid)
# Convert device group names to GUIDs
device_group_guids = []
if device_groups:
for dg in device_groups:
# Check if it's already a GUID format
if len(dg) == 36 and dg.count('-') == 4:
device_group_guids.append(dg)
else:
# Treat as device group name, look it up
guid = get_device_group_guid_by_name(url, token, dg)
if not guid:
print(f"Error: Device group '{dg}' not found")
exit(1)
device_group_guids.append(guid)
# Build payload
payload = {}
if strategy_guid:
payload["strategy"] = strategy_guid
payload["peers"] = peer_guids
payload["users"] = user_guids
payload["groups"] = device_group_guids
r = requests.post(f"{url}/api/strategies/assign", headers=headers, json=payload)
check_response(r)
def main():
parser = argparse.ArgumentParser(description="Strategy manager")
parser.add_argument("command", choices=[
"list", "view", "enable", "disable", "assign", "unassign"
])
parser.add_argument("--url", required=True, help="Server URL")
parser.add_argument("--token", required=True, help="API token")
parser.add_argument("--name", help="Strategy name (for view/enable/disable/assign commands)")
parser.add_argument("--guid", help="Strategy GUID (for view command, alternative to --name)")
# For assign/unassign commands
parser.add_argument("--peers", help="Comma separated device IDs or GUIDs (requires Device Permission:r)")
parser.add_argument("--users", help="Comma separated user names or GUIDs (requires User Permission:r)")
parser.add_argument("--device-groups", help="Comma separated device group names or GUIDs (requires Device Group Permission:r)")
args = parser.parse_args()
while args.url.endswith("/"): args.url = args.url[:-1]
if args.command == "list":
res = list_strategies(args.url, args.token)
print(json.dumps(res, indent=2))
elif args.command == "view":
if args.guid:
res = get_strategy_by_guid(args.url, args.token, args.guid)
print(json.dumps(res, indent=2))
elif args.name:
strategy = get_strategy_by_name(args.url, args.token, args.name)
if not strategy:
print(f"Error: Strategy '{args.name}' not found")
exit(1)
# Get full details by GUID
guid = strategy.get("guid")
res = get_strategy_by_guid(args.url, args.token, guid)
print(json.dumps(res, indent=2))
else:
print("Error: --name or --guid is required for view command")
exit(1)
elif args.command == "enable":
if not args.name:
print("Error: --name is required")
exit(1)
print(enable_strategy(args.url, args.token, args.name))
elif args.command == "disable":
if not args.name:
print("Error: --name is required")
exit(1)
print(disable_strategy(args.url, args.token, args.name))
elif args.command == "assign":
if not args.name:
print("Error: --name is required")
exit(1)
if not args.peers and not args.users and not args.device_groups:
print("Error: at least one of --peers, --users, or --device-groups is required")
exit(1)
peers = [x.strip() for x in args.peers.split(",") if x.strip()] if args.peers else None
users = [x.strip() for x in args.users.split(",") if x.strip()] if args.users else None
device_groups = [x.strip() for x in args.device_groups.split(",") if x.strip()] if args.device_groups else None
assign_strategy(args.url, args.token, args.name, peers=peers, users=users, device_groups=device_groups)
count = (len(peers) if peers else 0) + (len(users) if users else 0) + (len(device_groups) if device_groups else 0)
print(f"Success: Assigned strategy '{args.name}' to {count} target(s)")
elif args.command == "unassign":
if not args.peers and not args.users and not args.device_groups:
print("Error: at least one of --peers, --users, or --device-groups is required")
exit(1)
peers = [x.strip() for x in args.peers.split(",") if x.strip()] if args.peers else None
users = [x.strip() for x in args.users.split(",") if x.strip()] if args.users else None
device_groups = [x.strip() for x in args.device_groups.split(",") if x.strip()] if args.device_groups else None
assign_strategy(args.url, args.token, None, peers=peers, users=users, device_groups=device_groups)
count = (len(peers) if peers else 0) + (len(users) if users else 0) + (len(device_groups) if device_groups else 0)
print(f"Success: Unassigned strategy from {count} target(s)")
if __name__ == "__main__":
main()
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env python3
import requests
import argparse
import json
def check_response(response):
"""
Check API response and handle errors.
Two error cases:
1. Status code is not 200 -> exit with error
2. Response contains {"error": "xxx"} -> exit with error
"""
if response.status_code != 200:
print(f"Error: HTTP {response.status_code}: {response.text}")
exit(1)
# Check for {"error": "xxx"} in response
if response.text and response.text.strip():
try:
json_data = response.json()
if isinstance(json_data, dict) and "error" in json_data:
print(f"Error: {json_data['error']}")
exit(1)
return json_data
except ValueError:
return response.text
return None
def headers_with(token):
return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
# ---------- User Group APIs ----------
def list_groups(url, token, name=None, page_size=50):
headers = headers_with(token)
params = {"pageSize": page_size}
if name:
params["name"] = name
data, current = [], 0
while True:
current += 1
params["current"] = current
r = requests.get(f"{url}/api/user-groups", headers=headers, params=params)
if r.status_code != 200:
print(f"Error: HTTP {r.status_code} - {r.text}")
exit(1)
res = r.json()
if "error" in res:
print(f"Error: {res['error']}")
exit(1)
rows = res.get("data", [])
data.extend(rows)
total = res.get("total", 0)
if len(rows) < page_size or current * page_size >= total:
break
return data
def get_group_by_name(url, token, name):
groups = list_groups(url, token, name)
for g in groups:
if str(g.get("name")) == name:
return g
return None
def create_group(url, token, name, note=None, accessed_from=None, access_to=None):
headers = headers_with(token)
payload = {"name": name}
if note:
payload["note"] = note
if accessed_from:
payload["allowed_incomings"] = accessed_from
if access_to:
payload["allowed_outgoings"] = access_to
r = requests.post(f"{url}/api/user-groups", headers=headers, json=payload)
return check_response(r)
def update_group(url, token, name, new_name=None, note=None, accessed_from=None, access_to=None):
headers = headers_with(token)
g = get_group_by_name(url, token, name)
if not g:
print(f"Error: Group '{name}' not found")
exit(1)
guid = g.get("guid")
payload = {}
if new_name is not None:
payload["name"] = new_name
if note is not None:
payload["note"] = note
if accessed_from is not None:
payload["allowed_incomings"] = accessed_from
if access_to is not None:
payload["allowed_outgoings"] = access_to
r = requests.patch(f"{url}/api/user-groups/{guid}", headers=headers, json=payload)
check_response(r)
return "Success"
def delete_groups(url, token, names):
headers = headers_with(token)
if isinstance(names, str):
names = [names]
for n in names:
g = get_group_by_name(url, token, n)
if not g:
print(f"Error: Group '{n}' not found")
exit(1)
guid = g.get("guid")
r = requests.delete(f"{url}/api/user-groups/{guid}", headers=headers)
check_response(r)
return "Success"
# ---------- User management in group ----------
def view_users(url, token, group_name=None, name=None, page_size=50):
"""View users in a user group with filters"""
headers = headers_with(token)
# Separate exact match and fuzzy match params
params = {}
fuzzy_params = {
"name": name,
}
# Add group_name without wildcard (exact match)
if group_name:
params["group_name"] = group_name
# Add wildcard for fuzzy search to other params
for k, v in fuzzy_params.items():
if v is not None:
params[k] = "%" + v + "%" if (v != "-" and "%" not in v) else v
params["pageSize"] = page_size
data, current = [], 0
while True:
current += 1
params["current"] = current
r = requests.get(f"{url}/api/users", headers=headers, params=params)
if r.status_code != 200:
return check_response(r)
res = r.json()
rows = res.get("data", [])
data.extend(rows)
total = res.get("total", 0)
if len(rows) < page_size or current * page_size >= total:
break
return data
def add_users(url, token, group_name, user_names):
"""Add users to a user group"""
headers = headers_with(token)
if isinstance(user_names, str):
user_names = [user_names]
# Get the user group guid
g = get_group_by_name(url, token, group_name)
if not g:
print(f"Error: Group '{group_name}' not found")
exit(1)
guid = g.get("guid")
# Get user GUIDs
user_guids = []
errors = []
for user_name in user_names:
# Get user by exact name match
params = {"name": user_name, "pageSize": 50}
r = requests.get(f"{url}/api/users", headers=headers, params=params)
if r.status_code != 200:
errors.append(f"{user_name}: HTTP {r.status_code}")
continue
users_data = r.json()
users_list = users_data.get("data", [])
user = None
for u in users_list:
if u.get("name") == user_name:
user = u
break
if not user:
errors.append(f"{user_name}: User not found")
continue
user_guids.append(user["guid"])
if not user_guids:
msg = "Error: No valid users found"
if errors:
msg += ". " + "; ".join(errors)
print(msg)
exit(1)
# Add users to group using POST /api/user-groups/:guid
r = requests.post(f"{url}/api/user-groups/{guid}", headers=headers, json=user_guids)
check_response(r)
success_msg = f"Success: Added {len(user_guids)} user(s) to group '{group_name}'"
if errors:
return success_msg + " (with errors: " + "; ".join(errors) + ")"
return success_msg
def parse_rules(s):
if not s:
return None
try:
v = json.loads(s)
if isinstance(v, list):
# expect list of {"type": number, "name": string}
return v
except Exception:
pass
return None
def main():
parser = argparse.ArgumentParser(description="User Group manager")
parser.add_argument("command", choices=[
"view", "add", "update", "delete",
"view-users", "add-users"
], help=(
"Command to execute. "
"[view/add/update/delete/add-users: require User Group Permission] "
"[view-users: require User Permission]"
))
parser.add_argument("--url", required=True)
parser.add_argument("--token", required=True)
parser.add_argument("--name", help="User group name (exact match)")
parser.add_argument("--new-name", help="New user group name (for update)")
parser.add_argument("--note", help="Note")
parser.add_argument("--accessed-from", help="JSON array: '[{\"type\":0|2,\"name\":\"...\"}]' (0=User Group, 2=User)")
parser.add_argument("--access-to", help="JSON array: '[{\"type\":0|1,\"name\":\"...\"}]' (0=User Group, 1=Device Group)")
parser.add_argument("--users", help="Comma separated usernames for add-users")
# Filters for view-users command
parser.add_argument("--user-name", help="User name filter (for view-users, supports fuzzy search)")
args = parser.parse_args()
while args.url.endswith("/"): args.url = args.url[:-1]
if args.command == "view":
res = list_groups(args.url, args.token, args.name)
print(json.dumps(res, indent=2))
elif args.command == "add":
if not args.name:
print("Error: --name is required")
exit(1)
print(create_group(
args.url, args.token, args.name, args.note,
parse_rules(args.accessed_from),
parse_rules(args.access_to)
))
elif args.command == "update":
if not args.name:
print("Error: --name is required")
exit(1)
print(update_group(
args.url, args.token, args.name, args.new_name, args.note,
parse_rules(args.accessed_from),
parse_rules(args.access_to)
))
elif args.command == "delete":
if not args.name:
print("Error: --name is required (supports comma separated)")
exit(1)
names = [x.strip() for x in args.name.split(",") if x.strip()]
print(delete_groups(args.url, args.token, names))
elif args.command == "view-users":
res = view_users(
args.url,
args.token,
group_name=args.name,
name=args.user_name
)
print(json.dumps(res, indent=2))
elif args.command == "add-users":
if not args.name or not args.users:
print("Error: --name and --users are required")
exit(1)
users = [x.strip() for x in args.users.split(",") if x.strip()]
print(add_users(args.url, args.token, args.name, users))
if __name__ == "__main__":
main()
Executable
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env python3
import requests
import argparse
from datetime import datetime, timedelta
def check_response(response):
"""
Check API response and handle errors properly.
Exit with code 1 if there's an error.
"""
if response.status_code != 200:
print(f"Error: HTTP {response.status_code}: {response.text}")
exit(1)
if response.text and response.text.strip():
try:
json_data = response.json()
if isinstance(json_data, dict) and "error" in json_data:
print(f"Error: {json_data['error']}")
exit(1)
return json_data
except ValueError:
return response.text
return None
def view(
url,
token,
name=None,
group_name=None,
):
headers = {"Authorization": f"Bearer {token}"}
pageSize = 30
params = {
"name": name,
"group_name": group_name,
}
params = {
k: "%" + v + "%" if (v != "-" and "%" not in v) else v
for k, v in params.items()
if v is not None
}
params["pageSize"] = pageSize
users = []
current = 0
while True:
current += 1
params["current"] = current
response = requests.get(f"{url}/api/users", headers=headers, params=params)
if response.status_code != 200:
print(f"Error: HTTP {response.status_code} - {response.text}")
exit(1)
response_json = response.json()
if "error" in response_json:
print(f"Error: {response_json['error']}")
exit(1)
data = response_json.get("data", [])
users.extend(data)
total = response_json.get("total", 0)
if len(data) < pageSize or current * pageSize >= total:
break
return users
def disable(url, token, guid, name):
print("Disable", name)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{url}/api/users/{guid}/disable", headers=headers)
check_response(response)
def enable(url, token, guid, name):
print("Enable", name)
headers = {"Authorization": f"Bearer {token}"}
response = requests.post(f"{url}/api/users/{guid}/enable", headers=headers)
check_response(response)
def delete_user(url, token, guid, name):
print("Delete", name)
headers = {"Authorization": f"Bearer {token}"}
response = requests.delete(f"{url}/api/users/{guid}", headers=headers)
check_response(response)
def new_user(url, token, name, password, group_name=None, email=None, note=None):
"""Create a new user"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"name": name,
"password": password,
}
if group_name:
payload["group_name"] = group_name
if email:
payload["email"] = email
if note:
payload["note"] = note
response = requests.post(f"{url}/api/users", headers=headers, json=payload)
check_response(response)
def invite_user(url, token, email, name, group_name=None, note=None):
"""Invite a user by email"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"email": email,
"name": name,
}
if group_name:
payload["group_name"] = group_name
if note:
payload["note"] = note
response = requests.post(f"{url}/api/users/invite", headers=headers, json=payload)
check_response(response)
def enable_2fa_enforce(url, token, user_guids, base_url):
"""Enable 2FA enforcement for users"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
"enforce": True,
"url": base_url
}
response = requests.put(f"{url}/api/users/tfa/totp/enforce", headers=headers, json=payload)
check_response(response)
def disable_2fa_enforce(url, token, user_guids, base_url=""):
"""Disable 2FA enforcement for users"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
"enforce": False,
"url": base_url
}
response = requests.put(f"{url}/api/users/tfa/totp/enforce", headers=headers, json=payload)
check_response(response)
def disable_email_verification(url, token, user_guids):
"""Disable email login verification for users"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
"type": "email"
}
response = requests.put(f"{url}/api/users/disable_login_verification", headers=headers, json=payload)
check_response(response)
def reset_2fa(url, token, user_guids):
"""Reset 2FA for users"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
"type": "2fa"
}
response = requests.put(f"{url}/api/users/disable_login_verification", headers=headers, json=payload)
check_response(response)
def force_logout(url, token, user_guids):
"""Force logout users"""
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
payload = {
"user_guids": user_guids if isinstance(user_guids, list) else [user_guids],
}
response = requests.post(f"{url}/api/users/force-logout", headers=headers, json=payload)
check_response(response)
def main():
parser = argparse.ArgumentParser(description="User manager")
parser.add_argument(
"command",
choices=["view", "disable", "enable", "delete", "new", "invite",
"enable-2fa-enforce", "disable-2fa-enforce",
"disable-email-verification", "reset-2fa", "force-logout"],
help="Command to execute",
)
parser.add_argument("--url", required=True, help="URL of the API")
parser.add_argument(
"--token", required=True, help="Bearer token for authentication"
)
parser.add_argument("--name", help="User name")
parser.add_argument("--group_name", help="Group name (for filtering in view, or for new/invite command)")
parser.add_argument("--password", help="User password (for new command)")
parser.add_argument("--email", help="User email (for invite command)")
parser.add_argument("--note", help="User note (for new/invite command)")
parser.add_argument("--web-console-url", help="Web console URL (for 2FA enforce commands)")
args = parser.parse_args()
while args.url.endswith("/"): args.url = args.url[:-1]
if args.command == "new":
if not args.name or not args.password or not args.group_name:
print("Error: --name and --password and --group_name are required for new command")
exit(1)
new_user(args.url, args.token, args.name, args.password, args.group_name, args.email, args.note)
print("Success: User created")
return
if args.command == "invite":
if not args.email or not args.name or not args.group_name:
print("Error: --email and --name and --group_name are required for invite command")
exit(1)
invite_user(args.url, args.token, args.email, args.name, args.group_name, args.note)
print("Success: Invitation sent")
return
users = view(
args.url,
args.token,
args.name,
args.group_name,
)
if args.command == "view":
if len(users) == 0:
print("Found 0 users")
else:
for user in users:
print(user)
elif args.command in ["disable", "enable", "delete", "enable-2fa-enforce",
"disable-2fa-enforce", "disable-email-verification", "reset-2fa", "force-logout"]:
if len(users) == 0:
print("Found 0 users")
return
# Check if we need user confirmation for multiple users
if len(users) > 1:
print(f"Found {len(users)} users. Do you want to proceed with {args.command} operation on the users? (Y/N)")
confirmation = input("Type 'Y' to confirm: ").strip()
if confirmation.upper() != 'Y':
print("Operation cancelled.")
return
if args.command == "disable":
for user in users:
disable(args.url, args.token, user["guid"], user["name"])
print("Success")
elif args.command == "enable":
for user in users:
enable(args.url, args.token, user["guid"], user["name"])
print("Success")
elif args.command == "delete":
for user in users:
delete_user(args.url, args.token, user["guid"], user["name"])
print("Success")
elif args.command == "enable-2fa-enforce":
if not args.web_console_url:
print("Error: --web-console-url is required for enable-2fa-enforce")
exit(1)
user_guids = [user["guid"] for user in users]
enable_2fa_enforce(args.url, args.token, user_guids, args.web_console_url)
print(f"Success: Enabled 2FA enforcement for {len(users)} user(s)")
elif args.command == "disable-2fa-enforce":
user_guids = [user["guid"] for user in users]
web_url = args.web_console_url or ""
disable_2fa_enforce(args.url, args.token, user_guids, web_url)
print(f"Success: Disabled 2FA enforcement for {len(users)} user(s)")
elif args.command == "disable-email-verification":
user_guids = [user["guid"] for user in users]
disable_email_verification(args.url, args.token, user_guids)
print(f"Success: Disabled email verification for {len(users)} user(s)")
elif args.command == "reset-2fa":
user_guids = [user["guid"] for user in users]
reset_2fa(args.url, args.token, user_guids)
print(f"Success: Reset 2FA for {len(users)} user(s)")
elif args.command == "force-logout":
user_guids = [user["guid"] for user in users]
force_logout(args.url, args.token, user_guids)
print(f"Success: Force logout for {len(users)} user(s)")
if __name__ == "__main__":
main()
+60
View File
@@ -0,0 +1,60 @@
diff --git a/build/cmake/cpu.cmake b/build/cmake/cpu.cmake
index acebe20..8c67d89 100644
--- a/build/cmake/cpu.cmake
+++ b/build/cmake/cpu.cmake
@@ -120,6 +120,19 @@ elseif("${AOM_TARGET_CPU}" MATCHES "^x86")
set(RTCD_ARCH_X86_64 "yes")
endif()
+ # AVX2 requires __m256i definition starting v3.9.0
+
+ if(ENABLE_AVX2)
+ aom_check_source_compiles("x86_64_avx2_m256i_available" "
+#include <emmintrin.h>
+#ifndef __m256i
+#error 1
+#endif" HAVE_AVX2_M256I)
+ if(HAVE_AVX2_M256I EQUAL 0)
+ set(ENABLE_AVX2 0)
+ endif()
+ endif()
+
set(X86_FLAVORS "MMX;SSE;SSE2;SSE3;SSSE3;SSE4_1;SSE4_2;AVX;AVX2")
foreach(flavor ${X86_FLAVORS})
if(ENABLE_${flavor} AND NOT disable_remaining_flavors)
diff --git a/aom_dsp/x86/synonyms.h b/aom_dsp/x86/synonyms.h
index 0d51cdf..6744ec5 100644
--- a/aom_dsp/x86/synonyms.h
+++ b/aom_dsp/x86/synonyms.h
@@ -46,13 +46,6 @@ static INLINE __m128i xx_loadu_128(const void *a) {
return _mm_loadu_si128((const __m128i *)a);
}
-// Load 64 bits from each of hi and low, and pack into an SSE register
-// Since directly loading as `int64_t`s and using _mm_set_epi64 may violate
-// the strict aliasing rule, this takes a different approach
-static INLINE __m128i xx_loadu_2x64(const void *hi, const void *lo) {
- return _mm_unpacklo_epi64(_mm_loadu_si64(lo), _mm_loadu_si64(hi));
-}
-
static INLINE void xx_storel_32(void *const a, const __m128i v) {
const int val = _mm_cvtsi128_si32(v);
memcpy(a, &val, sizeof(val));
diff --git a/aom_dsp/x86/synonyms_avx2.h b/aom_dsp/x86/synonyms_avx2.h
index d4e8f69..45be17e 100644
--- a/aom_dsp/x86/synonyms_avx2.h
+++ b/aom_dsp/x86/synonyms_avx2.h
@@ -25,6 +25,13 @@
* Intrinsics prefixed with yy_ operate on or return 256bit YMM registers.
*/
+// Load 64 bits from each of hi and low, and pack into an SSE register
+// Since directly loading as `int64_t`s and using _mm_set_epi64 may violate
+// the strict aliasing rule, this takes a different approach
+static INLINE __m128i xx_loadu_2x64(const void *hi, const void *lo) {
+ return _mm_unpacklo_epi64(_mm_loadu_si64(lo), _mm_loadu_si64(hi));
+}
+
// Loads and stores to do away with the tedium of casting the address
// to the right type.
static INLINE __m256i yy_load_256(const void *a) {
+75
View File
@@ -0,0 +1,75 @@
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 18190f647..f4b1b359d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,6 +23,9 @@ endif()
project(AOM C CXX)
+include(GNUInstallDirs)
+include(CMakePackageConfigHelpers)
+
# GENERATED source property global visibility.
if(POLICY CMP0118)
cmake_policy(SET CMP0118 NEW)
@@ -302,6 +305,52 @@ if(BUILD_SHARED_LIBS)
set(AOM_LIB_TARGETS ${AOM_LIB_TARGETS} aom_static)
endif()
+set(PUBLIC_HEADERS
+ aom/aom.h
+ aom/aom_codec.h
+ aom/aom_decoder.h
+ aom/aom_encoder.h
+ aom/aom_frame_buffer.h
+ aom/aom_image.h
+ aom/aom_integer.h
+ aom/aomcx.h
+ aom/aomdx.h
+)
+
+set_target_properties(aom PROPERTIES
+ PUBLIC_HEADER "${PUBLIC_HEADERS}")
+
+
+target_include_directories(aom
+ PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
+ $<INSTALL_INTERFACE:include>)
+
+install(TARGETS aom
+ EXPORT unofficial-aom-targets
+ ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+ PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/aom")
+
+install(EXPORT unofficial-aom-targets
+ FILE unofficial-aom-targets.cmake
+ NAMESPACE unofficial::
+ DESTINATION lib/cmake/aom)
+
+configure_package_config_file(cmake/aom-config.cmake.in
+ ${CMAKE_CURRENT_BINARY_DIR}/aom-config.cmake
+ INSTALL_DESTINATION lib/cmake/aom
+ NO_SET_AND_CHECK_MACRO
+ NO_CHECK_REQUIRED_COMPONENTS_MACRO)
+
+write_basic_package_version_file(${CMAKE_CURRENT_BINARY_DIR}/aom-config-version.cmake
+ VERSION ${SO_FILE_VERSION}
+ COMPATIBILITY SameMajorVersion)
+
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/aom-config.cmake
+ ${CMAKE_CURRENT_BINARY_DIR}/aom-config-version.cmake
+ DESTINATION lib/cmake/aom)
+
# Setup dependencies.
if(CONFIG_THREE_PASS)
setup_ivf_dec_targets()
diff --git a/cmake/aom-config.cmake.in b/cmake/aom-config.cmake.in
new file mode 100644
index 000000000..91cac3b5b
--- /dev/null
+++ b/cmake/aom-config.cmake.in
@@ -0,0 +1,2 @@
+@PACKAGE_INIT@
+include(${CMAKE_CURRENT_LIST_DIR}/unofficial-aom-targets.cmake)
@@ -0,0 +1,13 @@
diff --git a/build/cmake/aom_configure.cmake b/build/cmake/aom_configure.cmake
index aaef2c310..5500ad4a3 100644
--- a/build/cmake/aom_configure.cmake
+++ b/build/cmake/aom_configure.cmake
@@ -309,6 +309,8 @@ if(MSVC)
# Disable MSVC warnings that suggest making code non-portable.
add_compiler_flag_if_supported("/wd4996")
+ # Disable MSVC warnings for potentially uninitialized local pointer variable.
+ add_compiler_flag_if_supported("/wd4703")
if(ENABLE_WERROR)
add_compiler_flag_if_supported("/WX")
endif()
+79
View File
@@ -0,0 +1,79 @@
# NASM is required to build AOM
vcpkg_find_acquire_program(NASM)
get_filename_component(NASM_EXE_PATH ${NASM} DIRECTORY)
vcpkg_add_to_path(${NASM_EXE_PATH})
# Perl is required to build AOM
vcpkg_find_acquire_program(PERL)
get_filename_component(PERL_PATH ${PERL} DIRECTORY)
vcpkg_add_to_path(${PERL_PATH})
if(DEFINED ENV{USE_AOM_391})
vcpkg_from_git(
OUT_SOURCE_PATH SOURCE_PATH
URL "https://aomedia.googlesource.com/aom"
REF 8ad484f8a18ed1853c094e7d3a4e023b2a92df28 # 3.9.1
PATCHES
aom-uninitialized-pointer.diff
aom-avx2.diff
aom-install.diff
)
else()
vcpkg_from_git(
OUT_SOURCE_PATH SOURCE_PATH
URL "https://aomedia.googlesource.com/aom"
REF 10aece4157eb79315da205f39e19bf6ab3ee30d0 # 3.12.1
PATCHES
aom-uninitialized-pointer.diff
# aom-avx2.diff
# Can be dropped when https://bugs.chromium.org/p/aomedia/issues/detail?id=3029 is merged into the upstream
aom-install.diff
)
endif()
set(aom_target_cpu "")
if(VCPKG_TARGET_IS_UWP OR (VCPKG_TARGET_IS_WINDOWS AND VCPKG_TARGET_ARCHITECTURE MATCHES "^arm"))
# UWP + aom's assembler files result in weirdness and build failures
# Also, disable assembly on ARM and ARM64 Windows to fix compilation issues.
set(aom_target_cpu "-DAOM_TARGET_CPU=generic")
endif()
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" AND VCPKG_TARGET_IS_LINUX)
set(aom_target_cpu "-DENABLE_NEON=OFF")
endif()
vcpkg_cmake_configure(
SOURCE_PATH ${SOURCE_PATH}
OPTIONS
${aom_target_cpu}
-DENABLE_DOCS=OFF
-DENABLE_EXAMPLES=OFF
-DENABLE_TESTDATA=OFF
-DENABLE_TESTS=OFF
-DENABLE_TOOLS=OFF
)
vcpkg_cmake_install()
vcpkg_copy_pdbs()
vcpkg_fixup_pkgconfig()
if(VCPKG_TARGET_IS_WINDOWS)
vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/aom.pc" " -lm" "")
if(NOT VCPKG_BUILD_TYPE)
vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/aom.pc" " -lm" "")
endif()
endif()
# Move cmake configs
vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT})
# Remove duplicate files
file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include
${CURRENT_PACKAGES_DIR}/debug/share)
# Handle copyright
file(INSTALL ${SOURCE_PATH}/LICENSE DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright)
vcpkg_fixup_pkgconfig()
+18
View File
@@ -0,0 +1,18 @@
{
"name": "aom",
"version-semver": "3.12.1",
"port-version": 0,
"description": "AV1 codec library",
"homepage": "https://aomedia.googlesource.com/aom",
"license": "BSD-2-Clause",
"dependencies": [
{
"name": "vcpkg-cmake",
"host": true
},
{
"name": "vcpkg-cmake-config",
"host": true
}
]
}
@@ -0,0 +1,27 @@
diff --git a/configure b/configure
index 1f0b9497cb..3243e23021 100644
--- a/configure
+++ b/configure
@@ -5697,17 +5697,19 @@ case $target_os in
;;
win32|win64)
disable symver
- if enabled shared; then
+# if enabled shared; then
# Link to the import library instead of the normal static library
# for shared libs.
LD_LIB='%.lib'
# Cannot build both shared and static libs with MSVC or icl.
- disable static
- fi
+# disable static
+# fi
! enabled small && test_cmd $windres --version && enable gnu_windres
enabled x86_32 && check_ldflags -LARGEADDRESSAWARE
add_cppflags -DWIN32_LEAN_AND_MEAN
shlibdir_default="$bindir_default"
+ LIBPREF=""
+ LIBSUF=".lib"
SLIBPREF=""
SLIBSUF=".dll"
SLIBNAME_WITH_VERSION='$(SLIBPREF)$(FULLNAME)-$(LIBVERSION)$(SLIBSUF)'
+11
View File
@@ -0,0 +1,11 @@
diff --git a/configure b/configure
--- a/configure
+++ b/configure
@@ -6162,6 +6162,7 @@ EOF
test -n "$extern_prefix" && append X86ASMFLAGS "-DPREFIX"
case "$objformat" in
elf*) enabled debug && append X86ASMFLAGS $x86asm_debug ;;
+ win*) enabled debug && append X86ASMFLAGS "-g" ;;
esac
enabled avx512 && check_x86asm avx512_external "vmovdqa32 [eax]{k1}{z}, zmm0"
@@ -0,0 +1,13 @@
diff --git a/fftools/cmdutils.c b/fftools/cmdutils.c
--- a/fftools/cmdutils.c
+++ b/fftools/cmdutils.c
@@ -51,6 +51,8 @@
#include "fopen_utf8.h"
#include "opt_common.h"
#ifdef _WIN32
+#define _WIN32_WINNT 0x0502
+#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "compat/w32dlfcn.h"
#endif
+65
View File
@@ -0,0 +1,65 @@
diff --git a/configure b/configure
index a8b74e0..c99f41c 100755
--- a/configure
+++ b/configure
@@ -6633,7 +6633,7 @@ fi
enabled zlib && { check_pkg_config zlib zlib "zlib.h" zlibVersion ||
check_lib zlib zlib.h zlibVersion -lz; }
-enabled bzlib && check_lib bzlib bzlib.h BZ2_bzlibVersion -lbz2
+enabled bzlib && require_pkg_config bzlib bzip2 bzlib.h BZ2_bzlibVersion
enabled lzma && check_lib lzma lzma.h lzma_version_number -llzma
enabled zlib && test_exec $zlib_extralibs <<EOF && enable zlib_gzip
@@ -6757,7 +6757,8 @@ if enabled libmfx; then
fi
enabled libmodplug && require_pkg_config libmodplug libmodplug libmodplug/modplug.h ModPlug_Load
-enabled libmp3lame && require "libmp3lame >= 3.98.3" lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs
+enabled libmp3lame && { check_lib libmp3lame lame/lame.h lame_set_VBR_quality -lmp3lame $libm_extralibs ||
+ require libmp3lame lame/lame.h lame_set_VBR_quality -llibmp3lame-static -llibmpghip-static $libm_extralibs; }
enabled libmysofa && { check_pkg_config libmysofa libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine ||
require libmysofa mysofa.h mysofa_neighborhood_init_withstepdefine -lmysofa $zlib_extralibs; }
enabled libnpp && { check_lib libnpp npp.h nppGetLibVersion -lnppig -lnppicc -lnppc -lnppidei -lnppif ||
@@ -6772,7 +6773,7 @@ require_pkg_config libopencv opencv opencv/cxcore.h cvCreateImageHeader; }
enabled libopenh264 && require_pkg_config libopenh264 "openh264 >= 1.3.0" wels/codec_api.h WelsGetCodecVersion
enabled libopenjpeg && { check_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version ||
{ require_pkg_config libopenjpeg "libopenjp2 >= 2.1.0" openjpeg.h opj_version -DOPJ_STATIC && add_cppflags -DOPJ_STATIC; } }
-enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create -lstdc++ && append libopenmpt_extralibs "-lstdc++"
+enabled libopenmpt && require_pkg_config libopenmpt "libopenmpt >= 0.2.6557" libopenmpt/libopenmpt.h openmpt_module_create
enabled libopenvino && { { check_pkg_config libopenvino openvino openvino/c/openvino.h ov_core_create && enable openvino2; } ||
{ check_pkg_config libopenvino openvino c_api/ie_c_api.h ie_c_api_version ||
require libopenvino c_api/ie_c_api.h ie_c_api_version -linference_engine_c_api; } }
@@ -6796,8 +6797,8 @@ enabled libshaderc && require_pkg_config spirv_compiler "shaderc >= 2019.
enabled libshine && require_pkg_config libshine shine shine/layer3.h shine_encode_buffer
enabled libsmbclient && { check_pkg_config libsmbclient smbclient libsmbclient.h smbc_init ||
require libsmbclient libsmbclient.h smbc_init -lsmbclient; }
-enabled libsnappy && require libsnappy snappy-c.h snappy_compress -lsnappy -lstdc++
-enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr
+enabled libsnappy && require_pkg_config libsnappy snappy snappy-c.h snappy_compress
+enabled libsoxr && require libsoxr soxr.h soxr_create -lsoxr $libm_extralibs
enabled libssh && require_pkg_config libssh "libssh >= 0.6.0" libssh/sftp.h sftp_init
enabled libspeex && require_pkg_config libspeex speex speex/speex.h speex_decoder_init
enabled libsrt && require_pkg_config libsrt "srt >= 1.3.0" srt/srt.h srt_socket
@@ -6880,6 +6881,8 @@ enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h"
enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel ||
check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" ||
check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL ||
+ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -lAdvapi32 -lOle32 -lCfgmgr32||
+ check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL -pthread -ldl ||
die "ERROR: opencl not found"; } &&
{ test_cpp_condition "OpenCL/cl.h" "defined(CL_VERSION_1_2)" ||
test_cpp_condition "CL/cl.h" "defined(CL_VERSION_1_2)" ||
@@ -7204,10 +7207,10 @@ enabled amf &&
"(AMF_VERSION_MAJOR << 48 | AMF_VERSION_MINOR << 32 | AMF_VERSION_RELEASE << 16 | AMF_VERSION_BUILD_NUM) >= 0x0001000400210000"
# Funny iconv installations are not unusual, so check it after all flags have been set
-if enabled libc_iconv; then
+if enabled libc_iconv && disabled iconv; then
check_func_headers iconv.h iconv
elif enabled iconv; then
- check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv
+ check_func_headers iconv.h iconv || check_lib iconv iconv.h iconv -liconv || check_lib iconv iconv.h iconv -liconv -lcharset
fi
enabled debug && add_cflags -g"$debuglevel" && add_asflags -g"$debuglevel"
+78
View File
@@ -0,0 +1,78 @@
diff --git a/libavcodec/x86/mlpdsp.asm b/libavcodec/x86/mlpdsp.asm
index 3dc641e..609b834 100644
--- a/libavcodec/x86/mlpdsp.asm
+++ b/libavcodec/x86/mlpdsp.asm
@@ -23,7 +23,9 @@
SECTION .text
-%if ARCH_X86_64
+%ifn ARCH_X86_64
+mlpdsp_placeholder: times 4 db 0
+%else
%macro SHLX 2
%if cpuflag(bmi2)
diff --git a/libavcodec/x86/proresdsp.asm b/libavcodec/x86/proresdsp.asm
index 65c9fad..5ad73f3 100644
--- a/libavcodec/x86/proresdsp.asm
+++ b/libavcodec/x86/proresdsp.asm
@@ -24,7 +24,10 @@
%include "libavutil/x86/x86util.asm"
-%if ARCH_X86_64
+%ifn ARCH_X86_64
+SECTION .rdata
+proresdsp_placeholder: times 4 db 0
+%else
SECTION_RODATA
diff --git a/libavcodec/x86/vvc/vvc_mc.asm b/libavcodec/x86/vvc/vvc_mc.asm
index 30aa97c..3975f98 100644
--- a/libavcodec/x86/vvc/vvc_mc.asm
+++ b/libavcodec/x86/vvc/vvc_mc.asm
@@ -31,7 +31,9 @@
SECTION_RODATA 32
-%if ARCH_X86_64
+%ifn ARCH_X86_64
+vvc_mc_placeholder: times 4 db 0
+%else
%if HAVE_AVX2_EXTERNAL
diff --git a/libavfilter/x86/vf_atadenoise.asm b/libavfilter/x86/vf_atadenoise.asm
index 4945ad3..748b65a 100644
--- a/libavfilter/x86/vf_atadenoise.asm
+++ b/libavfilter/x86/vf_atadenoise.asm
@@ -20,7 +20,10 @@
;* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
;******************************************************************************
-%if ARCH_X86_64
+%ifn ARCH_X86_64
+SECTION .rdata
+vf_atadenoise_placeholder: times 4 db 0
+%else
%include "libavutil/x86/x86util.asm"
diff --git a/libavfilter/x86/vf_nlmeans.asm b/libavfilter/x86/vf_nlmeans.asm
index 8f57801..9aef3a4 100644
--- a/libavfilter/x86/vf_nlmeans.asm
+++ b/libavfilter/x86/vf_nlmeans.asm
@@ -21,7 +21,10 @@
%include "libavutil/x86/x86util.asm"
-%if HAVE_AVX2_EXTERNAL && ARCH_X86_64
+%ifn HAVE_AVX2_EXTERNAL && ARCH_X86_64
+SECTION .rdata
+vf_nlmeans_placeholder: times 4 db 0
+%else
SECTION_RODATA 32
@@ -0,0 +1,12 @@
diff --git a/configure b/configure
index d6c4388..75b96c3 100644
--- a/configure
+++ b/configure
@@ -4781,6 +4781,7 @@ msvc_common_flags(){
-mfp16-format=*) ;;
-lz) echo zlib.lib ;;
-lx264) echo libx264.lib ;;
+ -lmp3lame) echo libmp3lame.lib ;;
-lstdc++) ;;
-l*) echo ${flag#-l}.lib ;;
-LARGEADDRESSAWARE) echo $flag ;;
+15
View File
@@ -0,0 +1,15 @@
diff --color -Naur src_old/libavcodec/mf_utils.c src/libavcodec/mf_utils.c
--- src_old/libavcodec/mf_utils.c 2020-07-11 05:26:17.000000000 +0700
+++ src/libavcodec/mf_utils.c 2020-11-13 12:55:57.226976400 +0700
@@ -22,6 +22,11 @@
#define _WIN32_WINNT 0x0602
#endif
+#if !defined(WINVER) || WINVER < 0x0602
+#undef WINVER
+#define WINVER 0x0602
+#endif
+
#include "mf_utils.h"
#include "libavutil/pixdesc.h"
@@ -0,0 +1,28 @@
diff --git a/libswscale/aarch64/yuv2rgb_neon.S b/libswscale/aarch64/yuv2rgb_neon.S
index 89d69e7f6c..4bc1607a7a 100644
--- a/libswscale/aarch64/yuv2rgb_neon.S
+++ b/libswscale/aarch64/yuv2rgb_neon.S
@@ -169,19 +169,19 @@ function ff_\ifmt\()_to_\ofmt\()_neon, export=1
sqdmulh v26.8h, v26.8h, v0.8h // ((Y1*(1<<3) - y_offset) * y_coeff) >> 15
sqdmulh v27.8h, v27.8h, v0.8h // ((Y2*(1<<3) - y_offset) * y_coeff) >> 15
-.ifc \ofmt,argb // 1 2 3 0
+.ifc \ofmt,argb
compute_rgba v5.8b,v6.8b,v7.8b,v4.8b, v17.8b,v18.8b,v19.8b,v16.8b
.endif
-.ifc \ofmt,rgba // 0 1 2 3
+.ifc \ofmt,rgba
compute_rgba v4.8b,v5.8b,v6.8b,v7.8b, v16.8b,v17.8b,v18.8b,v19.8b
.endif
-.ifc \ofmt,abgr // 3 2 1 0
+.ifc \ofmt,abgr
compute_rgba v7.8b,v6.8b,v5.8b,v4.8b, v19.8b,v18.8b,v17.8b,v16.8b
.endif
-.ifc \ofmt,bgra // 2 1 0 3
+.ifc \ofmt,bgra
compute_rgba v6.8b,v5.8b,v4.8b,v7.8b, v18.8b,v17.8b,v16.8b,v19.8b
.endif
@@ -0,0 +1,15 @@
diff --git a/configure b/configure
index 4f5353f84b..dd9147c677 100755
--- a/configure
+++ b/configure
@@ -5607,8 +5607,8 @@ check_cppflags -D_FILE_OFFSET_BITS=64
check_cppflags -D_LARGEFILE_SOURCE
add_host_cppflags -D_ISOC11_SOURCE
check_host_cflags_cc -std=$stdc ctype.h "__STDC_VERSION__ >= 201112L" ||
- check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L" || die "Host compiler lacks C11 support"
+ check_host_cflags_cc -std=c11 ctype.h "__STDC_VERSION__ >= 201112L"
check_host_cflags -Wall
check_host_cflags $host_cflags_speed
@@ -0,0 +1,35 @@
diff --git a/libavformat/avformat.h b/libavformat/avformat.h
index cd7b0d941c..b4a6dce885 100644
--- a/libavformat/avformat.h
+++ b/libavformat/avformat.h
@@ -1169,7 +1169,11 @@ typedef struct AVStreamGroup {
} AVStreamGroup;
struct AVCodecParserContext *av_stream_get_parser(const AVStream *s);
+// Chromium: We use the internal field first_dts vvv
+int64_t av_stream_get_first_dts(const AVStream *st);
+// Chromium: We use the internal field first_dts ^^^
+
#define AV_PROGRAM_RUNNING 1
/**
diff --git a/libavformat/mux_utils.c b/libavformat/mux_utils.c
index de7580c32d..0ef0fe530e 100644
--- a/libavformat/mux_utils.c
+++ b/libavformat/mux_utils.c
@@ -29,7 +29,14 @@ #include "avformat.h"
#include "avio.h"
#include "internal.h"
#include "mux.h"
+// Chromium: We use the internal field first_dts vvv
+int64_t av_stream_get_first_dts(const AVStream *st)
+{
+ return cffstream(st)->first_dts;
+}
+// Chromium: We use the internal field first_dts ^^^
+
int avformat_query_codec(const AVOutputFormat *ofmt, enum AVCodecID codec_id,
int std_compliance)
{
@@ -0,0 +1,13 @@
diff --git a/libavdevice/opengl_enc.c b/libavdevice/opengl_enc.c
index b2ac6eb..6351614 100644
--- a/libavdevice/opengl_enc.c
+++ b/libavdevice/opengl_enc.c
@@ -116,7 +116,7 @@ typedef void (APIENTRY *FF_PFNGLATTACHSHADERPROC) (GLuint program, GLuint shad
typedef GLuint (APIENTRY *FF_PFNGLCREATESHADERPROC) (GLenum type);
typedef void (APIENTRY *FF_PFNGLDELETESHADERPROC) (GLuint shader);
typedef void (APIENTRY *FF_PFNGLCOMPILESHADERPROC) (GLuint shader);
-typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* *string, const GLint *length);
+typedef void (APIENTRY *FF_PFNGLSHADERSOURCEPROC) (GLuint shader, GLsizei count, const char* const *string, const GLint *length);
typedef void (APIENTRY *FF_PFNGLGETSHADERIVPROC) (GLuint shader, GLenum pname, GLint *params);
typedef void (APIENTRY *FF_PFNGLGETSHADERINFOLOGPROC) (GLuint shader, GLsizei bufSize, GLsizei *length, char *infoLog);
@@ -0,0 +1,9 @@
diff --git a/ffbuild/libversion.sh b/ffbuild/libversion.sh
index a94ab58..ecaa90c 100644
--- a/ffbuild/libversion.sh
+++ b/ffbuild/libversion.sh
@@ -1,3 +1,4 @@
+#!/bin/sh
toupper(){
echo "$@" | tr abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ
}
+12
View File
@@ -0,0 +1,12 @@
diff --git a/libavfilter/textutils.c b/libavfilter/textutils.c
index ef658d0..c61b0ad 100644
--- a/libavfilter/textutils.c
+++ b/libavfilter/textutils.c
@@ -31,6 +31,7 @@
#include "libavutil/file.h"
#include "libavutil/mem.h"
#include "libavutil/time.h"
+#include "libavutil/time_internal.h"
static int ff_expand_text_function_internal(FFExpandTextContext *expand_text, AVBPrint *bp,
char *name, unsigned argc, char **argv)
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env bash
set -e
export PATH="$PATH:/usr/bin"
command -v cygpath >/dev/null && have_cygpath=1
cygpath() {
if [ -n "$have_cygpath" ]; then
command cygpath "$@"
else
eval _p='$'$#
printf '%s\n' "$_p"
fi
}
move_binary() {
SOURCE=$1
TARGET=$2
BINARY=$3
# run lipo over the command to check whether it really
# is a binary that we need to merge architectures
lipo $SOURCE/$BINARY -info &> /dev/null || return 0
# get the directory name the file is in
DIRNAME=$(dirname $BINARY)
# ensure the directory to move the binary to exists
mkdir -p $TARGET/$DIRNAME
# now finally move the binary
mv $SOURCE/$BINARY $TARGET/$BINARY
}
move_binaries() {
SOURCE=$1
TARGET=$2
[ ! -d $SOURCE ] && return 0
pushd $SOURCE
for BINARY in $(find . -type f); do
move_binary $SOURCE $TARGET $BINARY
done
popd
}
merge_binaries() {
TARGET=$1
SOURCE=$2
shift
shift
pushd $SOURCE/$1
BINARIES=$(find . -type f)
popd
for BINARY in $BINARIES; do
COMMAND="lipo -create -output $TARGET/$BINARY"
for ARCH in $@; do
COMMAND="$COMMAND -arch $ARCH $SOURCE/$ARCH/$BINARY"
done
$($COMMAND)
done
}
export PKG_CONFIG_PATH="$(cygpath -p "${PKG_CONFIG_PATH}")"
# Export HTTP(S)_PROXY as http(s)_proxy:
[ -n "$HTTP_PROXY" ] && export http_proxy="$HTTP_PROXY"
[ -n "$HTTPS_PROXY" ] && export https_proxy="$HTTPS_PROXY"
PATH_TO_BUILD_DIR=$( cygpath "@BUILD_DIR@")
PATH_TO_SRC_DIR=$( cygpath "@SOURCE_PATH@")
PATH_TO_PACKAGE_DIR=$(cygpath "@INST_PREFIX@")
JOBS=@VCPKG_CONCURRENCY@
OSX_ARCHS="@OSX_ARCHS@"
OSX_ARCH_COUNT=0@OSX_ARCH_COUNT@
# Default to hardware concurrency if unset.
: ${JOBS:=$(nproc)}
# Disable asm and x86asm on all android targets because they trigger build failures:
# arm64 Android build fails with 'relocation R_AARCH64_ADR_PREL_PG_HI21 cannot be used against symbol ff_cos_32; recompile with -fPIC'
# x86 Android build fails with 'error: inline assembly requires more registers than available'.
# x64 Android build fails with 'relocation R_X86_64_PC32 cannot be used against symbol ff_h264_cabac_tables; recompile with -fPIC'
if [ "@VCPKG_CMAKE_SYSTEM_NAME@" = "Android" ]; then
OPTIONS_arm=" --disable-asm --disable-x86asm"
OPTIONS_arm64=" --disable-asm --disable-x86asm"
OPTIONS_x86=" --disable-asm --disable-x86asm"
OPTIONS_x86_64="${OPTIONS_x86}"
else
OPTIONS_arm=" --disable-asm --disable-x86asm"
OPTIONS_arm64=" --enable-asm --disable-x86asm"
OPTIONS_x86=" --enable-asm --enable-x86asm"
OPTIONS_x86_64="${OPTIONS_x86}"
fi
build_ffmpeg() {
# extract build architecture
BUILD_ARCH=$1
shift
echo "BUILD_ARCH=${BUILD_ARCH}"
# get architecture-specific options
OPTION_VARIABLE="OPTIONS_${BUILD_ARCH}"
echo "OPTION_VARIABLE=${OPTION_VARIABLE}"
echo "=== CONFIGURING ==="
sh "$PATH_TO_SRC_DIR/configure" "--prefix=$PATH_TO_PACKAGE_DIR" @CONFIGURE_OPTIONS@ --arch=${BUILD_ARCH} ${!OPTION_VARIABLE} $@
echo "=== BUILDING ==="
make -j${JOBS} V=1
echo "=== INSTALLING ==="
make install
}
cd "$PATH_TO_BUILD_DIR"
if [ $OSX_ARCH_COUNT -gt 0 ]; then
for ARCH in $OSX_ARCHS; do
echo "=== CLEANING FOR $ARCH ==="
make clean && make distclean
build_ffmpeg $ARCH --extra-cflags=-arch --extra-cflags=$ARCH --extra-ldflags=-arch --extra-ldflags=$ARCH
echo "=== COLLECTING BINARIES FOR $ARCH ==="
move_binaries $PATH_TO_PACKAGE_DIR/lib $PATH_TO_BUILD_DIR/stage/$ARCH/lib
move_binaries $PATH_TO_PACKAGE_DIR/bin $PATH_TO_BUILD_DIR/stage/$ARCH/bin
done
echo "=== MERGING ARCHITECTURES ==="
merge_binaries $PATH_TO_PACKAGE_DIR $PATH_TO_BUILD_DIR/stage $OSX_ARCHS
else
build_ffmpeg @BUILD_ARCH@
fi
@@ -0,0 +1,71 @@
From da6921d5bcb50961193526f47aa2dbe71ee5fe81 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Tue, 10 Dec 2024 13:40:46 +0800
Subject: [PATCH 1/5] avcodec/amfenc: add query_timeout option for h264/hevc
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/amfenc.h | 1 +
libavcodec/amfenc_h264.c | 4 ++++
libavcodec/amfenc_hevc.c | 4 ++++
3 files changed, 9 insertions(+)
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index d985d01bb1..320c66919e 100644
--- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h
@@ -91,6 +91,7 @@ typedef struct AmfContext {
int quality;
int b_frame_delta_qp;
int ref_b_frame_delta_qp;
+ int64_t query_timeout;
// Dynamic options, can be set after Init() call
diff --git a/libavcodec/amfenc_h264.c b/libavcodec/amfenc_h264.c
index 8edd39c633..6ad4961b2f 100644
--- a/libavcodec/amfenc_h264.c
+++ b/libavcodec/amfenc_h264.c
@@ -137,6 +137,7 @@ static const AVOption options[] = {
{ "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg) , AV_OPT_TYPE_BOOL, { .i64 = 0 }, 0, 1, VE },
+ { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE },
//Pre Analysis options
{ "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE },
@@ -228,6 +229,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_FRAMERATE, framerate);
+ if (ctx->query_timeout >= 0)
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_QUERY_TIMEOUT, ctx->query_timeout);
+
switch (avctx->profile) {
case AV_PROFILE_H264_BASELINE:
profile = AMF_VIDEO_ENCODER_PROFILE_BASELINE;
diff --git a/libavcodec/amfenc_hevc.c b/libavcodec/amfenc_hevc.c
index 4898824f3a..22cb95c7ce 100644
--- a/libavcodec/amfenc_hevc.c
+++ b/libavcodec/amfenc_hevc.c
@@ -104,6 +104,7 @@ static const AVOption options[] = {
{ "log_to_dbg", "Enable AMF logging to debug output", OFFSET(log_to_dbg), AV_OPT_TYPE_BOOL,{ .i64 = 0 }, 0, 1, VE },
+ { "query_timeout", "Timeout for QueryOutput call in ms", OFFSET(query_timeout), AV_OPT_TYPE_INT64, { .i64 = -1 }, -1, 1000, VE },
//Pre Analysis options
{ "preanalysis", "Enable preanalysis", OFFSET(preanalysis), AV_OPT_TYPE_BOOL, {.i64 = -1 }, -1, 1, VE },
@@ -194,6 +195,9 @@ FF_ENABLE_DEPRECATION_WARNINGS
AMF_ASSIGN_PROPERTY_RATE(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_FRAMERATE, framerate);
+ if (ctx->query_timeout >= 0)
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_QUERY_TIMEOUT, ctx->query_timeout);
+
switch (avctx->profile) {
case AV_PROFILE_HEVC_MAIN:
profile = AMF_VIDEO_ENCODER_HEVC_PROFILE_MAIN;
--
2.43.0.windows.1
@@ -0,0 +1,71 @@
From 8d061adb7b00fc765b8001307c025437ef1cad88 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Thu, 5 Sep 2024 16:32:16 +0800
Subject: [PATCH 2/5] libavcodec/amfenc: reconfig when bitrate change
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/amfenc.c | 20 ++++++++++++++++++++
libavcodec/amfenc.h | 1 +
2 files changed, 21 insertions(+)
diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c
index a47aea6108..f70f0109f6 100644
--- a/libavcodec/amfenc.c
+++ b/libavcodec/amfenc.c
@@ -275,6 +275,7 @@ static int amf_init_context(AVCodecContext *avctx)
ctx->hwsurfaces_in_queue = 0;
ctx->hwsurfaces_in_queue_max = 16;
+ ctx->av_bitrate = avctx->bit_rate;
// configure AMF logger
// the return of these functions indicates old state and do not affect behaviour
@@ -640,6 +641,23 @@ static void amf_release_buffer_with_frame_ref(AMFBuffer *frame_ref_storage_buffe
frame_ref_storage_buffer->pVtbl->Release(frame_ref_storage_buffer);
}
+static int reconfig_encoder(AVCodecContext *avctx)
+{
+ AmfContext *ctx = avctx->priv_data;
+ AMF_RESULT res = AMF_OK;
+
+ if (ctx->av_bitrate != avctx->bit_rate) {
+ av_log(ctx, AV_LOG_INFO, "change bitrate from %d to %d\n", ctx->av_bitrate, avctx->bit_rate);
+ ctx->av_bitrate = avctx->bit_rate;
+ if (avctx->codec->id == AV_CODEC_ID_H264) {
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_TARGET_BITRATE, avctx->bit_rate);
+ } else if (avctx->codec->id == AV_CODEC_ID_HEVC) {
+ AMF_ASSIGN_PROPERTY_INT64(res, ctx->encoder, AMF_VIDEO_ENCODER_HEVC_TARGET_BITRATE, avctx->bit_rate);
+ }
+ }
+ return 0;
+}
+
int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
{
AmfContext *ctx = avctx->priv_data;
@@ -653,6 +671,8 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
int query_output_data_flag = 0;
AMF_RESULT res_resubmit;
+ reconfig_encoder(avctx);
+
if (!ctx->encoder)
return AVERROR(EINVAL);
diff --git a/libavcodec/amfenc.h b/libavcodec/amfenc.h
index 320c66919e..481e0fb75d 100644
--- a/libavcodec/amfenc.h
+++ b/libavcodec/amfenc.h
@@ -115,6 +115,7 @@ typedef struct AmfContext {
int max_b_frames;
int qvbr_quality_level;
int hw_high_motion_quality_boost;
+ int64_t av_bitrate;
// HEVC - specific options
--
2.43.0.windows.1
@@ -0,0 +1,85 @@
From d74de94b49efcf7a0b25673ace6016938d1b9272 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Tue, 10 Dec 2024 14:12:01 +0800
Subject: [PATCH 3/5] videotoolbox changing bitrate
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/videotoolboxenc.c | 40 ++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
diff --git a/libavcodec/videotoolboxenc.c b/libavcodec/videotoolboxenc.c
index da7b291b03..3c866177f5 100644
--- a/libavcodec/videotoolboxenc.c
+++ b/libavcodec/videotoolboxenc.c
@@ -279,6 +279,8 @@ typedef struct VTEncContext {
int max_slice_bytes;
int power_efficient;
int max_ref_frames;
+
+ int last_bit_rate;
} VTEncContext;
static void vtenc_free_buf_node(BufNode *info)
@@ -1180,6 +1182,7 @@ static int vtenc_create_encoder(AVCodecContext *avctx,
int64_t one_second_value = 0;
void *nums[2];
+ vtctx->last_bit_rate = bit_rate;
int status = VTCompressionSessionCreate(kCFAllocatorDefault,
avctx->width,
avctx->height,
@@ -2638,6 +2641,42 @@ out:
return status;
}
+static void update_config(AVCodecContext *avctx)
+{
+ VTEncContext *vtctx = avctx->priv_data;
+
+ if (avctx->codec_id != AV_CODEC_ID_PRORES) {
+ if (avctx->bit_rate != vtctx->last_bit_rate) {
+ av_log(avctx, AV_LOG_INFO, "Setting bit rate to %d\n", avctx->bit_rate);
+ vtctx->last_bit_rate = avctx->bit_rate;
+ SInt32 bit_rate = avctx->bit_rate;
+ CFNumberRef bit_rate_num = CFNumberCreate(kCFAllocatorDefault,
+ kCFNumberSInt32Type,
+ &bit_rate);
+ if (!bit_rate_num) return;
+
+ if (vtctx->constant_bit_rate) {
+ int status = VTSessionSetProperty(vtctx->session,
+ compat_keys.kVTCompressionPropertyKey_ConstantBitRate,
+ bit_rate_num);
+ if (status == kVTPropertyNotSupportedErr) {
+ av_log(avctx, AV_LOG_ERROR, "Error: -constant_bit_rate true is not supported by the encoder.\n");
+ }
+ } else {
+ int status = VTSessionSetProperty(vtctx->session,
+ kVTCompressionPropertyKey_AverageBitRate,
+ bit_rate_num);
+ if (status) {
+ av_log(avctx, AV_LOG_ERROR, "Error: cannot set average bit rate: %d\n", status);
+ }
+ }
+
+ CFRelease(bit_rate_num);
+ }
+ }
+}
+
+
static av_cold int vtenc_frame(
AVCodecContext *avctx,
AVPacket *pkt,
@@ -2650,6 +2689,7 @@ static av_cold int vtenc_frame(
CMSampleBufferRef buf = NULL;
ExtraSEI sei = {0};
+ update_config(avctx);
if (frame) {
status = vtenc_send_frame(avctx, vtctx, frame);
--
2.43.0.windows.1
@@ -0,0 +1,246 @@
From 7323bd68c1b34e9298ea557ff7a3e1883b653957 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Tue, 10 Dec 2024 14:28:16 +0800
Subject: [PATCH 4/5] mediacodec changing bitrate
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/mediacodec_wrapper.c | 98 +++++++++++++++++++++++++++++++++
libavcodec/mediacodec_wrapper.h | 7 +++
libavcodec/mediacodecenc.c | 18 ++++++
3 files changed, 123 insertions(+)
diff --git a/libavcodec/mediacodec_wrapper.c b/libavcodec/mediacodec_wrapper.c
index 96c886666a..06b8504304 100644
--- a/libavcodec/mediacodec_wrapper.c
+++ b/libavcodec/mediacodec_wrapper.c
@@ -35,6 +35,8 @@
#include "ffjni.h"
#include "mediacodec_wrapper.h"
+#define PARAMETER_KEY_VIDEO_BITRATE "video-bitrate"
+
struct JNIAMediaCodecListFields {
jclass mediacodec_list_class;
@@ -195,6 +197,8 @@ struct JNIAMediaCodecFields {
jmethodID set_input_surface_id;
jmethodID signal_end_of_input_stream_id;
+ jmethodID set_parameters_id;
+
jclass mediainfo_class;
jmethodID init_id;
@@ -248,6 +252,8 @@ static const struct FFJniField jni_amediacodec_mapping[] = {
{ "android/media/MediaCodec", "setInputSurface", "(Landroid/view/Surface;)V", FF_JNI_METHOD, OFFSET(set_input_surface_id), 0 },
{ "android/media/MediaCodec", "signalEndOfInputStream", "()V", FF_JNI_METHOD, OFFSET(signal_end_of_input_stream_id), 0 },
+ { "android/media/MediaCodec", "setParameters", "(Landroid/os/Bundle;)V", FF_JNI_METHOD, OFFSET(set_parameters_id), 0 },
+
{ "android/media/MediaCodec$BufferInfo", NULL, NULL, FF_JNI_CLASS, OFFSET(mediainfo_class), 1 },
{ "android/media/MediaCodec.BufferInfo", "<init>", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 },
@@ -292,6 +298,24 @@ typedef struct FFAMediaCodecJni {
static const FFAMediaCodec media_codec_jni;
+struct JNIABundleFields
+{
+ jclass bundle_class;
+ jmethodID init_id;
+ jmethodID put_int_id;
+};
+
+#define OFFSET(x) offsetof(struct JNIABundleFields, x)
+static const struct FFJniField jni_abundle_mapping[] = {
+ { "android/os/Bundle", NULL, NULL, FF_JNI_CLASS, OFFSET(bundle_class), 1 },
+
+ { "android/os/Bundle", "<init>", "()V", FF_JNI_METHOD, OFFSET(init_id), 1 },
+ { "android/os/Bundle", "putInt", "(Ljava/lang/String;I)V", FF_JNI_METHOD, OFFSET(put_int_id), 1 },
+
+ { NULL }
+};
+#undef OFFSET
+
#define JNI_GET_ENV_OR_RETURN(env, log_ctx, ret) do { \
(env) = ff_jni_get_env(log_ctx); \
if (!(env)) { \
@@ -1762,6 +1786,70 @@ static int mediacodec_jni_signalEndOfInputStream(FFAMediaCodec *ctx)
return 0;
}
+
+static int mediacodec_jni_setParameter(FFAMediaCodec *ctx, const char* name, int value)
+{
+ JNIEnv *env = NULL;
+ struct JNIABundleFields jfields = { 0 };
+ jobject object = NULL;
+ jstring key = NULL;
+ FFAMediaCodecJni *codec = (FFAMediaCodecJni *)ctx;
+ void *log_ctx = codec;
+ int ret = -1;
+
+ JNI_GET_ENV_OR_RETURN(env, codec, AVERROR_EXTERNAL);
+
+ if (ff_jni_init_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx) < 0) {
+ av_log(log_ctx, AV_LOG_ERROR, "Failed to init jfields\n");
+ goto fail;
+ }
+
+ object = (*env)->NewObject(env, jfields.bundle_class, jfields.init_id);
+ if (!object) {
+ av_log(log_ctx, AV_LOG_ERROR, "Failed to create bundle object\n");
+ goto fail;
+ }
+
+ key = ff_jni_utf_chars_to_jstring(env, name, log_ctx);
+ if (!key) {
+ av_log(log_ctx, AV_LOG_ERROR, "Failed to convert key to jstring\n");
+ goto fail;
+ }
+
+ (*env)->CallVoidMethod(env, object, jfields.put_int_id, key, value);
+ if (ff_jni_exception_check(env, 1, log_ctx) < 0) {
+ goto fail;
+ }
+
+ if (!codec->jfields.set_parameters_id) {
+ av_log(log_ctx, AV_LOG_ERROR, "System doesn't support setParameters\n");
+ goto fail;
+ }
+
+ (*env)->CallVoidMethod(env, codec->object, codec->jfields.set_parameters_id, object);
+ if (ff_jni_exception_check(env, 1, log_ctx) < 0) {
+ goto fail;
+ }
+
+ ret = 0;
+
+fail:
+ if (key) {
+ (*env)->DeleteLocalRef(env, key);
+ }
+ if (object) {
+ (*env)->DeleteLocalRef(env, object);
+ }
+ ff_jni_reset_jfields(env, &jfields, jni_abundle_mapping, 0, log_ctx);
+
+ return ret;
+}
+
+static int mediacodec_jni_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate)
+{
+ return mediacodec_jni_setParameter(ctx, PARAMETER_KEY_VIDEO_BITRATE, bitrate);
+}
+
static const FFAMediaFormat media_format_jni = {
.class = &amediaformat_class,
@@ -1821,6 +1909,8 @@ static const FFAMediaCodec media_codec_jni = {
.getConfigureFlagEncode = mediacodec_jni_getConfigureFlagEncode,
.cleanOutputBuffers = mediacodec_jni_cleanOutputBuffers,
.signalEndOfInputStream = mediacodec_jni_signalEndOfInputStream,
+
+ .setDynamicBitrate = mediacodec_jni_setDynamicBitrate,
};
typedef struct FFAMediaFormatNdk {
@@ -2335,6 +2425,12 @@ static int mediacodec_ndk_signalEndOfInputStream(FFAMediaCodec *ctx)
return 0;
}
+static int mediacodec_ndk_setDynamicBitrate(FFAMediaCodec *ctx, int bitrate)
+{
+ av_log(ctx, AV_LOG_ERROR, "ndk setDynamicBitrate unavailable\n");
+ return -1;
+}
+
static const FFAMediaFormat media_format_ndk = {
.class = &amediaformat_ndk_class,
@@ -2396,6 +2492,8 @@ static const FFAMediaCodec media_codec_ndk = {
.getConfigureFlagEncode = mediacodec_ndk_getConfigureFlagEncode,
.cleanOutputBuffers = mediacodec_ndk_cleanOutputBuffers,
.signalEndOfInputStream = mediacodec_ndk_signalEndOfInputStream,
+
+ .setDynamicBitrate = mediacodec_ndk_setDynamicBitrate,
};
FFAMediaFormat *ff_AMediaFormat_new(int ndk)
diff --git a/libavcodec/mediacodec_wrapper.h b/libavcodec/mediacodec_wrapper.h
index 11a4260497..86c64556ad 100644
--- a/libavcodec/mediacodec_wrapper.h
+++ b/libavcodec/mediacodec_wrapper.h
@@ -219,6 +219,8 @@ struct FFAMediaCodec {
// For encoder with FFANativeWindow as input.
int (*signalEndOfInputStream)(FFAMediaCodec *);
+
+ int (*setDynamicBitrate)(FFAMediaCodec *codec, int bitrate);
};
static inline char *ff_AMediaCodec_getName(FFAMediaCodec *codec)
@@ -343,6 +345,11 @@ static inline int ff_AMediaCodec_signalEndOfInputStream(FFAMediaCodec *codec)
return codec->signalEndOfInputStream(codec);
}
+static inline int ff_AMediaCodec_setDynamicBitrate(FFAMediaCodec *codec, int bitrate)
+{
+ return codec->setDynamicBitrate(codec, bitrate);
+}
+
int ff_Build_SDK_INT(AVCodecContext *avctx);
enum FFAMediaFormatColorRange {
diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c
index 6ca3968a24..221f7360f4 100644
--- a/libavcodec/mediacodecenc.c
+++ b/libavcodec/mediacodecenc.c
@@ -76,6 +76,8 @@ typedef struct MediaCodecEncContext {
int level;
int pts_as_dts;
int extract_extradata;
+
+ int last_bit_rate;
} MediaCodecEncContext;
enum {
@@ -193,6 +195,8 @@ static av_cold int mediacodec_init(AVCodecContext *avctx)
int ret;
int gop;
+ s->last_bit_rate = avctx->bit_rate;
+
if (s->use_ndk_codec < 0)
s->use_ndk_codec = !av_jni_get_java_vm(avctx);
@@ -542,11 +546,25 @@ static int mediacodec_send(AVCodecContext *avctx,
return 0;
}
+static void update_config(AVCodecContext *avctx)
+{
+ MediaCodecEncContext *s = avctx->priv_data;
+ if (avctx->bit_rate != s->last_bit_rate) {
+ s->last_bit_rate = avctx->bit_rate;
+ if (0 != ff_AMediaCodec_setDynamicBitrate(s->codec, avctx->bit_rate)) {
+ av_log(avctx, AV_LOG_ERROR, "Failed to set bitrate to %d\n", avctx->bit_rate);
+ } else {
+ av_log(avctx, AV_LOG_INFO, "Set bitrate to %d\n", avctx->bit_rate);
+ }
+ }
+}
+
static int mediacodec_encode(AVCodecContext *avctx, AVPacket *pkt)
{
MediaCodecEncContext *s = avctx->priv_data;
int ret;
+ update_config(avctx);
// Return on three case:
// 1. Serious error
// 2. Got a packet success
--
2.43.0.windows.1
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,30 @@
From 595f0468e127f204741b6c37a479d71daaf571eb Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Tue, 10 Dec 2024 21:17:14 +0800
Subject: [PATCH] fix linux configure
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
configure | 6 ------
1 file changed, 6 deletions(-)
diff --git a/configure b/configure
index d77a55b653..48ca90ac5e 100755
--- a/configure
+++ b/configure
@@ -7071,12 +7071,6 @@ enabled mmal && { check_lib mmal interface/mmal/mmal.h mmal_port_co
check_lib mmal interface/mmal/mmal.h mmal_port_connect -lmmal_core -lmmal_util -lmmal_vc_client -lbcm_host; } ||
die "ERROR: mmal not found" &&
check_func_headers interface/mmal/mmal.h "MMAL_PARAMETER_VIDEO_MAX_NUM_CALLBACKS"; }
-enabled openal && { check_pkg_config openal "openal >= 1.1" "AL/al.h" alGetError ||
- { for al_extralibs in "${OPENAL_LIBS}" "-lopenal" "-lOpenAL32"; do
- check_lib openal 'AL/al.h' alGetError "${al_extralibs}" && break; done } ||
- die "ERROR: openal not found"; } &&
- { test_cpp_condition "AL/al.h" "defined(AL_VERSION_1_1)" ||
- die "ERROR: openal must be installed and version must be 1.1 or compatible"; }
enabled opencl && { check_pkg_config opencl OpenCL CL/cl.h clEnqueueNDRangeKernel ||
check_lib opencl OpenCL/cl.h clEnqueueNDRangeKernel "-framework OpenCL" ||
check_lib opencl CL/cl.h clEnqueueNDRangeKernel -lOpenCL ||
--
2.34.1
@@ -0,0 +1,26 @@
From 1440f556234d135ce58a2ef38916c6a63b05870e Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Sat, 14 Dec 2024 21:39:44 +0800
Subject: [PATCH] remove amf loop query
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/amfenc.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libavcodec/amfenc.c b/libavcodec/amfenc.c
index f70f0109f6..a53a05b16b 100644
--- a/libavcodec/amfenc.c
+++ b/libavcodec/amfenc.c
@@ -886,7 +886,7 @@ int ff_amf_receive_packet(AVCodecContext *avctx, AVPacket *avpkt)
av_usleep(1000);
}
}
- } while (block_and_wait);
+ } while (false); // already set query timeout
if (res_query == AMF_EOF) {
ret = AVERROR_EOF;
--
2.43.0.windows.1
@@ -0,0 +1,28 @@
From bec8d49e75b37806e1cff39c75027860fde0bfa2 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Fri, 27 Dec 2024 08:43:12 +0800
Subject: [PATCH] fix nvenc reconfigure blur
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/nvenc.c | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c
index 2cce478be0..f4c559b7ce 100644
--- a/libavcodec/nvenc.c
+++ b/libavcodec/nvenc.c
@@ -2741,8 +2741,8 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame)
}
if (reconfig_bitrate) {
- params.resetEncoder = 1;
- params.forceIDR = 1;
+ params.resetEncoder = 0;
+ params.forceIDR = 0;
needs_encode_config = 1;
needs_reconfig = 1;
--
2.43.0.windows.1
@@ -0,0 +1,31 @@
diff --git a/compat/w32dlfcn.h b/compat/w32dlfcn.h
index ac20e83..1e83aa6 100644
--- a/compat/w32dlfcn.h
+++ b/compat/w32dlfcn.h
@@ -76,6 +76,7 @@ static inline HMODULE win32_dlopen(const char *name)
if (!name_w)
goto exit;
namelen = wcslen(name_w);
+ /*
// Try local directory first
path = get_module_filename(NULL);
if (!path)
@@ -91,6 +92,7 @@ static inline HMODULE win32_dlopen(const char *name)
path = new_path;
wcscpy(path + pathlen + 1, name_w);
module = LoadLibraryExW(path, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
+ */
if (module == NULL) {
// Next try System32 directory
pathlen = GetSystemDirectoryW(path, pathsize);
@@ -131,7 +133,9 @@ exit:
return NULL;
module = LoadPackagedLibrary(name_w, 0);
#else
-#define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32)
+// #define LOAD_FLAGS (LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32)
+// Don't dynamic-link libraries from the application directory.
+ #define LOAD_FLAGS LOAD_LIBRARY_SEARCH_SYSTEM32
/* filename may be be in CP_ACP */
if (!name_w)
return LoadLibraryExA(name, NULL, LOAD_FLAGS);
@@ -0,0 +1,42 @@
From a609e1666c79ccce4faf7aa61d509bf202df9149 Mon Sep 17 00:00:00 2001
From: 21pages <sunboeasy@gmail.com>
Date: Fri, 5 Sep 2025 21:35:37 +0800
Subject: [PATCH] android mediacodec encode align 64
Signed-off-by: 21pages <sunboeasy@gmail.com>
---
libavcodec/mediacodecenc.c | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/libavcodec/mediacodecenc.c b/libavcodec/mediacodecenc.c
index 221f7360f4..768c8151df 100644
--- a/libavcodec/mediacodecenc.c
+++ b/libavcodec/mediacodecenc.c
@@ -242,18 +242,19 @@ static av_cold int mediacodec_init(AVCodecContext *avctx)
ff_AMediaFormat_setString(format, "mime", codec_mime);
// Workaround the alignment requirement of mediacodec. We can't do it
// silently for AV_PIX_FMT_MEDIACODEC.
+ const int align = 64;
if (avctx->pix_fmt != AV_PIX_FMT_MEDIACODEC &&
(avctx->codec_id == AV_CODEC_ID_H264 ||
avctx->codec_id == AV_CODEC_ID_HEVC)) {
- s->width = FFALIGN(avctx->width, 16);
- s->height = FFALIGN(avctx->height, 16);
+ s->width = FFALIGN(avctx->width, align);
+ s->height = FFALIGN(avctx->height, align);
} else {
s->width = avctx->width;
s->height = avctx->height;
- if (s->width % 16 || s->height % 16)
+ if (s->width % align || s->height % align)
av_log(avctx, AV_LOG_WARNING,
- "Video size %dx%d isn't align to 16, it may have device compatibility issue\n",
- s->width, s->height);
+ "Video size %dx%d isn't align to %d, it may have device compatibility issue\n",
+ s->width, s->height, align);
}
ff_AMediaFormat_setInt32(format, "width", s->width);
ff_AMediaFormat_setInt32(format, "height", s->height);
--
2.43.0.windows.1
@@ -0,0 +1,60 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: RustDesk <support@rustdesk.com>
Date: Fri, 1 Nov 2025 08:00:00 +0000
Subject: [PATCH] Fix CVBufferCopyAttachments crash on macOS Big Sur
Use weak linking for CVBufferCopyAttachments to avoid symbol resolution
crash on macOS < 12. The function will be NULL on older systems and the
code will fall back to the deprecated CVBufferGetAttachments.
This fixes a crash on macOS Big Sur (11.x) where CVBufferCopyAttachments
is not available. The runtime check with __builtin_available is not enough
because the symbol is still resolved at load time, causing a dyld error.
Fixes: https://github.com/rustdesk/rustdesk/issues/13377
---
libavutil/hwcontext_videotoolbox.c | 21 ++++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/libavutil/hwcontext_videotoolbox.c b/libavutil/hwcontext_videotoolbox.c
index 0000000000..1111111111 100644
--- a/libavutil/hwcontext_videotoolbox.c
+++ b/libavutil/hwcontext_videotoolbox.c
@@ -33,6 +33,25 @@
#include "pixfmt.h"
#include "pixdesc.h"
+// Weak import CVBufferCopyAttachments to support macOS < 12
+// The runtime check with __builtin_available is not enough because
+// the symbol is still resolved at load time, causing dyld errors on Big Sur.
+// With weak_import, the function pointer will be NULL on older systems.
+#if TARGET_OS_OSX && defined(__MAC_12_0) && __MAC_OS_X_VERSION_MAX_ALLOWED >= __MAC_12_0
+extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode)
+ __attribute__((weak_import));
+#endif
+#if TARGET_OS_IOS && defined(__IPHONE_15_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0
+extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode)
+ __attribute__((weak_import));
+#endif
+#if TARGET_OS_TV && defined(__TVOS_15_0) && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0
+extern CFDictionaryRef CVBufferCopyAttachments(CVBufferRef buffer, CVAttachmentMode mode)
+ __attribute__((weak_import));
+#endif
+
+// End of weak import section
+
typedef struct VTFramesContext {
/**
* The public AVVTFramesContext. See hwcontext_videotoolbox.h for it.
@@ -547,7 +566,7 @@ static CFDictionaryRef vt_cv_buffer_copy_attachments(CVBufferRef buffer,
(TARGET_OS_TV && defined(__TVOS_15_0) && __TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0)
// On recent enough versions, just use the respective API
if (__builtin_available(macOS 12.0, iOS 15.0, tvOS 15.0, *))
- return CVBufferCopyAttachments(buffer, attachment_mode);
+ if (CVBufferCopyAttachments != NULL) return CVBufferCopyAttachments(buffer, attachment_mode);
#endif
// Check that the target is lower than macOS 12 / iOS 15 / tvOS 15
--
2.43.0
+705
View File
@@ -0,0 +1,705 @@
vcpkg_from_github(
OUT_SOURCE_PATH SOURCE_PATH
REPO ffmpeg/ffmpeg
REF "n${VERSION}"
SHA512 3b273769ef1a1b63aed0691eef317a760f8c83b1d0e1c232b67bbee26db60b4864aafbc88df0e86d6bebf07185bbd057f33e2d5258fde6d97763b9994cd48b6f
HEAD_REF master
PATCHES
0001-create-lib-libraries.patch
0002-fix-msvc-link.patch
0003-fix-windowsinclude.patch
0004-dependencies.patch
0005-fix-nasm.patch
0007-fix-lib-naming.patch
0013-define-WINVER.patch
0020-fix-aarch64-libswscale.patch
0024-fix-osx-host-c11.patch
0040-ffmpeg-add-av_stream_get_first_dts-for-chromium.patch # Do not remove this patch. It is required by chromium
0041-add-const-for-opengl-definition.patch
0043-fix-miss-head.patch
patch/0001-avcodec-amfenc-add-query_timeout-option-for-h264-hev.patch
patch/0002-libavcodec-amfenc-reconfig-when-bitrate-change.patch
patch/0004-videotoolbox-changing-bitrate.patch
patch/0005-mediacodec-changing-bitrate.patch
patch/0006-dlopen-libva.patch
patch/0007-fix-linux-configure.patch
patch/0008-remove-amf-loop-query.patch
patch/0009-fix-nvenc-reconfigure-blur.patch
patch/0010.disable-loading-DLLs-from-app-dir.patch
patch/0011-android-mediacodec-encode-align-64.patch
patch/0012-fix-macos-big-sur-CVBufferCopyAttachments.patch
)
if(SOURCE_PATH MATCHES " ")
message(FATAL_ERROR "Error: ffmpeg will not build with spaces in the path. Please use a directory with no spaces")
endif()
if(NOT VCPKG_TARGET_ARCHITECTURE STREQUAL "wasm32")
vcpkg_find_acquire_program(NASM)
get_filename_component(NASM_EXE_PATH "${NASM}" DIRECTORY)
vcpkg_add_to_path("${NASM_EXE_PATH}")
endif()
set(OPTIONS "\
--disable-shared \
--enable-static \
--enable-pic \
--disable-everything \
--disable-programs \
--disable-doc \
--disable-htmlpages \
--disable-manpages \
--disable-podpages \
--disable-txtpages \
--disable-network \
--disable-appkit \
--disable-coreimage \
--disable-metal \
--disable-sdl2 \
--disable-securetransport \
--disable-vulkan \
--disable-audiotoolbox \
--disable-v4l2-m2m \
--disable-debug \
--disable-valgrind-backtrace \
--disable-large-tests \
--disable-bzlib \
--disable-avdevice \
--enable-avcodec \
--enable-avformat \
--disable-avfilter \
--disable-swresample \
--disable-swscale \
--disable-postproc \
--enable-decoder=h264 \
--enable-decoder=hevc \
--enable-parser=h264 \
--enable-parser=hevc \
--enable-bsf=h264_mp4toannexb \
--enable-bsf=hevc_mp4toannexb \
--enable-bsf=h264_metadata \
--enable-bsf=hevc_metadata \
--enable-muxer=mp4 \
--enable-protocol=file \
")
if(VCPKG_HOST_IS_WINDOWS)
vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16)
set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe")
vcpkg_add_to_path("${MSYS_ROOT}/usr/share/automake-1.16")
string(APPEND OPTIONS " --pkg-config=${CURRENT_HOST_INSTALLED_DIR}/tools/pkgconf/pkgconf${VCPKG_HOST_EXECUTABLE_SUFFIX}")
else()
find_program(SHELL bash)
endif()
if(VCPKG_TARGET_IS_LINUX)
string(APPEND OPTIONS "\
--target-os=linux \
--enable-pthreads \
--disable-vdpau \
")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm")
else()
string(APPEND OPTIONS "\
--enable-cuda \
--enable-ffnvcodec \
--enable-encoder=h264_nvenc \
--enable-encoder=hevc_nvenc \
--enable-hwaccel=h264_nvdec \
--enable-hwaccel=hevc_nvdec \
--enable-amf \
--enable-encoder=h264_amf \
--enable-encoder=hevc_amf \
--enable-hwaccel=h264_vaapi \
--enable-hwaccel=hevc_vaapi \
--enable-encoder=h264_vaapi \
--enable-encoder=hevc_vaapi \
")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
string(APPEND OPTIONS "\
--enable-cuda_llvm \
")
endif()
endif()
elseif(VCPKG_TARGET_IS_WINDOWS)
string(APPEND OPTIONS "\
--target-os=win32 \
--toolchain=msvc \
--cc=cl \
--enable-gpl \
--enable-d3d11va \
--enable-cuda \
--enable-ffnvcodec \
--enable-hwaccel=h264_nvdec \
--enable-hwaccel=hevc_nvdec \
--enable-hwaccel=h264_d3d11va \
--enable-hwaccel=hevc_d3d11va \
--enable-hwaccel=h264_d3d11va2 \
--enable-hwaccel=hevc_d3d11va2 \
--enable-amf \
--enable-encoder=h264_amf \
--enable-encoder=hevc_amf \
--enable-encoder=h264_nvenc \
--enable-encoder=hevc_nvenc \
--enable-libmfx \
--enable-encoder=h264_qsv \
--enable-encoder=hevc_qsv \
")
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
set(LIB_MACHINE_ARG /machine:x86)
string(APPEND OPTIONS " --arch=i686 --enable-cross-compile")
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(LIB_MACHINE_ARG /machine:x64)
string(APPEND OPTIONS " --arch=x86_64")
else()
message(FATAL_ERROR "Unsupported target architecture")
endif()
elseif(VCPKG_TARGET_IS_OSX)
string(APPEND OPTIONS "\
--disable-autodetect \
--enable-videotoolbox \
--enable-encoder=h264_videotoolbox,hevc_videotoolbox \
--enable-hwaccel=h264_videotoolbox,hevc_videotoolbox \
")
elseif(VCPKG_TARGET_IS_IOS)
string(APPEND OPTIONS "\
--arch=arm64 \
--disable-autodetect \
--disable-hwaccels \
--disable-encoders \
--disable-videotoolbox \
--extra-cflags=\"-arch arm64 -mios-version-min=8.0 -fembed-bitcode\" \
--extra-ldflags=\"-arch arm64 -mios-version-min=8.0 -fembed-bitcode\" \
")
elseif(VCPKG_CMAKE_SYSTEM_NAME STREQUAL "Android")
string(APPEND OPTIONS "\
--target-os=android \
--disable-asm \
--disable-iconv \
--enable-jni \
--enable-mediacodec \
--disable-hwaccels \
--enable-encoder=h264_mediacodec \
--enable-encoder=hevc_mediacodec \
--enable-decoder=h264_mediacodec \
--enable-decoder=hevc_mediacodec \
")
endif()
if(VCPKG_TARGET_IS_OSX)
list(JOIN VCPKG_OSX_ARCHITECTURES " " OSX_ARCHS)
list(LENGTH VCPKG_OSX_ARCHITECTURES OSX_ARCH_COUNT)
endif()
vcpkg_cmake_get_vars(cmake_vars_file)
include("${cmake_vars_file}")
if(VCPKG_DETECTED_MSVC)
string(APPEND OPTIONS " --disable-inline-asm") # clang-cl has inline assembly but this leads to undefined symbols.
set(OPTIONS "--toolchain=msvc ${OPTIONS}")
# This is required because ffmpeg depends upon optimizations to link correctly
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -O2")
string(REGEX REPLACE "(^| )-RTC1( |$)" " " VCPKG_COMBINED_C_FLAGS_DEBUG "${VCPKG_COMBINED_C_FLAGS_DEBUG}")
string(REGEX REPLACE "(^| )-Od( |$)" " " VCPKG_COMBINED_C_FLAGS_DEBUG "${VCPKG_COMBINED_C_FLAGS_DEBUG}")
string(REGEX REPLACE "(^| )-Ob0( |$)" " " VCPKG_COMBINED_C_FLAGS_DEBUG "${VCPKG_COMBINED_C_FLAGS_DEBUG}")
endif()
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include\"")
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include\"")
if(VCPKG_TARGET_IS_WINDOWS)
string(APPEND VCPKG_COMBINED_C_FLAGS_DEBUG " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"")
string(APPEND VCPKG_COMBINED_C_FLAGS_RELEASE " -I \"${CURRENT_INSTALLED_DIR}/include/mfx\"")
endif()
# # Setup vcpkg toolchain
set(prog_env "")
if(VCPKG_DETECTED_CMAKE_C_COMPILER)
get_filename_component(CC_path "${VCPKG_DETECTED_CMAKE_C_COMPILER}" DIRECTORY)
get_filename_component(CC_filename "${VCPKG_DETECTED_CMAKE_C_COMPILER}" NAME)
set(ENV{CC} "${CC_filename}")
string(APPEND OPTIONS " --cc=${CC_filename}")
if(VCPKG_HOST_IS_WINDOWS)
string(APPEND OPTIONS " --host_cc=${CC_filename}")
endif()
list(APPEND prog_env "${CC_path}")
endif()
if(VCPKG_DETECTED_CMAKE_CXX_COMPILER)
get_filename_component(CXX_path "${VCPKG_DETECTED_CMAKE_CXX_COMPILER}" DIRECTORY)
get_filename_component(CXX_filename "${VCPKG_DETECTED_CMAKE_CXX_COMPILER}" NAME)
set(ENV{CXX} "${CXX_filename}")
string(APPEND OPTIONS " --cxx=${CXX_filename}")
# string(APPEND OPTIONS " --host_cxx=${CC_filename}")
list(APPEND prog_env "${CXX_path}")
endif()
if(VCPKG_DETECTED_CMAKE_RC_COMPILER)
get_filename_component(RC_path "${VCPKG_DETECTED_CMAKE_RC_COMPILER}" DIRECTORY)
get_filename_component(RC_filename "${VCPKG_DETECTED_CMAKE_RC_COMPILER}" NAME)
set(ENV{WINDRES} "${RC_filename}")
string(APPEND OPTIONS " --windres=${RC_filename}")
list(APPEND prog_env "${RC_path}")
endif()
if(VCPKG_DETECTED_CMAKE_LINKER AND VCPKG_TARGET_IS_WINDOWS AND NOT VCPKG_TARGET_IS_MINGW)
get_filename_component(LD_path "${VCPKG_DETECTED_CMAKE_LINKER}" DIRECTORY)
get_filename_component(LD_filename "${VCPKG_DETECTED_CMAKE_LINKER}" NAME)
set(ENV{LD} "${LD_filename}")
string(APPEND OPTIONS " --ld=${LD_filename}")
# string(APPEND OPTIONS " --host_ld=${LD_filename}")
list(APPEND prog_env "${LD_path}")
endif()
if(VCPKG_DETECTED_CMAKE_NM)
get_filename_component(NM_path "${VCPKG_DETECTED_CMAKE_NM}" DIRECTORY)
get_filename_component(NM_filename "${VCPKG_DETECTED_CMAKE_NM}" NAME)
set(ENV{NM} "${NM_filename}")
string(APPEND OPTIONS " --nm=${NM_filename}")
list(APPEND prog_env "${NM_path}")
endif()
if(VCPKG_DETECTED_CMAKE_AR)
get_filename_component(AR_path "${VCPKG_DETECTED_CMAKE_AR}" DIRECTORY)
get_filename_component(AR_filename "${VCPKG_DETECTED_CMAKE_AR}" NAME)
if(AR_filename MATCHES [[^(llvm-)?lib\.exe$]])
set(ENV{AR} "ar-lib ${AR_filename}")
string(APPEND OPTIONS " --ar='ar-lib ${AR_filename}'")
else()
set(ENV{AR} "${AR_filename}")
string(APPEND OPTIONS " --ar='${AR_filename}'")
endif()
list(APPEND prog_env "${AR_path}")
endif()
if(VCPKG_DETECTED_CMAKE_RANLIB)
get_filename_component(RANLIB_path "${VCPKG_DETECTED_CMAKE_RANLIB}" DIRECTORY)
get_filename_component(RANLIB_filename "${VCPKG_DETECTED_CMAKE_RANLIB}" NAME)
set(ENV{RANLIB} "${RANLIB_filename}")
string(APPEND OPTIONS " --ranlib=${RANLIB_filename}")
list(APPEND prog_env "${RANLIB_path}")
endif()
if(VCPKG_DETECTED_CMAKE_STRIP)
get_filename_component(STRIP_path "${VCPKG_DETECTED_CMAKE_STRIP}" DIRECTORY)
get_filename_component(STRIP_filename "${VCPKG_DETECTED_CMAKE_STRIP}" NAME)
set(ENV{STRIP} "${STRIP_filename}")
string(APPEND OPTIONS " --strip=${STRIP_filename}")
list(APPEND prog_env "${STRIP_path}")
endif()
if(VCPKG_HOST_IS_WINDOWS)
vcpkg_acquire_msys(MSYS_ROOT PACKAGES automake1.16)
set(SHELL "${MSYS_ROOT}/usr/bin/bash.exe")
list(APPEND prog_env "${MSYS_ROOT}/usr/bin" "${MSYS_ROOT}/usr/share/automake-1.16")
else()
# find_program(SHELL bash)
endif()
list(REMOVE_DUPLICATES prog_env)
vcpkg_add_to_path(PREPEND ${prog_env})
# More? OBJCC BIN2C
file(REMOVE_RECURSE "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg" "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel")
set(FFMPEG_PKGCONFIG_MODULES libavutil)
set(OPTIONS_CROSS "--enable-cross-compile")
# ffmpeg needs --cross-prefix option to use appropriate tools for cross-compiling.
if(VCPKG_DETECTED_CMAKE_C_COMPILER MATCHES "([^\/]*-)gcc$")
string(APPEND OPTIONS_CROSS " --cross-prefix=${CMAKE_MATCH_1}")
endif()
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(BUILD_ARCH "x86_64")
else()
set(BUILD_ARCH ${VCPKG_TARGET_ARCHITECTURE})
endif()
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm" OR VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64")
if(VCPKG_TARGET_IS_WINDOWS)
vcpkg_find_acquire_program(GASPREPROCESSOR)
foreach(GAS_PATH ${GASPREPROCESSOR})
get_filename_component(GAS_ITEM_PATH ${GAS_PATH} DIRECTORY)
vcpkg_add_to_path("${GAS_ITEM_PATH}")
endforeach(GAS_PATH)
endif()
endif()
set(OPTIONS_DEBUG "--disable-optimizations")
set(OPTIONS_RELEASE "--enable-optimizations")
set(OPTIONS "${OPTIONS} ${OPTIONS_CROSS}")
if(VCPKG_TARGET_IS_MINGW)
set(OPTIONS "${OPTIONS} --extra_cflags=-D_WIN32_WINNT=0x0601")
elseif(VCPKG_TARGET_IS_WINDOWS)
set(OPTIONS "${OPTIONS} --extra-cflags=-DHAVE_UNISTD_H=0")
endif()
vcpkg_find_acquire_program(PKGCONFIG)
set(OPTIONS "${OPTIONS} --pkg-config=${PKGCONFIG}")
if(VCPKG_LIBRARY_LINKAGE STREQUAL "static")
set(OPTIONS "${OPTIONS} --pkg-config-flags=--static")
endif()
message(STATUS "Building Options: ${OPTIONS}")
# Release build
if(NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "release")
if(VCPKG_DETECTED_MSVC)
set(OPTIONS_RELEASE "${OPTIONS_RELEASE} --extra-ldflags=-libpath:\"${CURRENT_INSTALLED_DIR}/lib\"")
else()
set(OPTIONS_RELEASE "${OPTIONS_RELEASE} --extra-ldflags=-L\"${CURRENT_INSTALLED_DIR}/lib\"")
endif()
message(STATUS "Building Release Options: ${OPTIONS_RELEASE}")
set(ENV{PKG_CONFIG_PATH} "${CURRENT_INSTALLED_DIR}/lib/pkgconfig")
message(STATUS "Building ${PORT} for Release")
file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel")
# We use response files here as the only known way to handle spaces in paths
set(crsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/cflags.rsp")
string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_C_FLAGS_RELEASE_SANITIZED "${VCPKG_COMBINED_C_FLAGS_RELEASE}")
file(WRITE "${crsp}" "${VCPKG_COMBINED_C_FLAGS_RELEASE_SANITIZED}")
set(ldrsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/ldflags.rsp")
string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_SHARED_LINKER_FLAGS_RELEASE_SANITIZED "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_RELEASE}")
file(WRITE "${ldrsp}" "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_RELEASE_SANITIZED}")
set(ENV{CFLAGS} "@${crsp}")
# All tools except the msvc arm{,64} assembler accept @... as response file syntax.
# For that assembler, there is no known way to pass in flags. We must hope that not passing flags will work acceptably.
if(NOT VCPKG_DETECTED_MSVC OR NOT VCPKG_TARGET_ARCHITECTURE MATCHES "^arm")
set(ENV{ASFLAGS} "@${crsp}")
endif()
set(ENV{LDFLAGS} "@${ldrsp}")
set(ENV{ARFLAGS} "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_RELEASE}")
set(BUILD_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel")
set(CONFIGURE_OPTIONS "${OPTIONS} ${OPTIONS_RELEASE}")
set(INST_PREFIX "${CURRENT_PACKAGES_DIR}")
configure_file("${CMAKE_CURRENT_LIST_DIR}/build.sh.in" "${BUILD_DIR}/build.sh" @ONLY)
z_vcpkg_setup_pkgconfig_path(CONFIG RELEASE)
vcpkg_execute_required_process(
COMMAND "${SHELL}" ./build.sh
WORKING_DIRECTORY "${BUILD_DIR}"
LOGNAME "build-${TARGET_TRIPLET}-rel"
SAVE_LOG_FILES ffbuild/config.log
)
z_vcpkg_restore_pkgconfig_path()
endif()
# Debug build
if(NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug")
if(VCPKG_DETECTED_MSVC)
set(OPTIONS_DEBUG "${OPTIONS_DEBUG} --extra-ldflags=-libpath:\"${CURRENT_INSTALLED_DIR}/debug/lib\"")
else()
set(OPTIONS_DEBUG "${OPTIONS_DEBUG} --extra-ldflags=-L\"${CURRENT_INSTALLED_DIR}/debug/lib\"")
endif()
message(STATUS "Building Debug Options: ${OPTIONS_DEBUG}")
set(ENV{LDFLAGS} "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG}")
set(ENV{PKG_CONFIG_PATH} "${CURRENT_INSTALLED_DIR}/debug/lib/pkgconfig")
message(STATUS "Building ${PORT} for Debug")
file(MAKE_DIRECTORY "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg")
set(crsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/cflags.rsp")
string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_C_FLAGS_DEBUG_SANITIZED "${VCPKG_COMBINED_C_FLAGS_DEBUG}")
file(WRITE "${crsp}" "${VCPKG_COMBINED_C_FLAGS_DEBUG_SANITIZED}")
set(ldrsp "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg/ldflags.rsp")
string(REGEX REPLACE "-arch [A-Za-z0-9_]+" "" VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG_SANITIZED "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG}")
file(WRITE "${ldrsp}" "${VCPKG_COMBINED_SHARED_LINKER_FLAGS_DEBUG_SANITIZED}")
set(ENV{CFLAGS} "@${crsp}")
if(NOT VCPKG_DETECTED_MSVC OR NOT VCPKG_TARGET_ARCHITECTURE MATCHES "^arm")
set(ENV{ASFLAGS} "@${crsp}")
endif()
set(ENV{LDFLAGS} "@${ldrsp}")
set(ENV{ARFLAGS} "${VCPKG_COMBINED_STATIC_LINKER_FLAGS_DEBUG}")
set(BUILD_DIR "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-dbg")
set(CONFIGURE_OPTIONS "${OPTIONS} ${OPTIONS_DEBUG}")
set(INST_PREFIX "${CURRENT_PACKAGES_DIR}/debug")
configure_file("${CMAKE_CURRENT_LIST_DIR}/build.sh.in" "${BUILD_DIR}/build.sh" @ONLY)
z_vcpkg_setup_pkgconfig_path(CONFIG DEBUG)
vcpkg_execute_required_process(
COMMAND "${SHELL}" ./build.sh
WORKING_DIRECTORY "${BUILD_DIR}"
LOGNAME "build-${TARGET_TRIPLET}-dbg"
SAVE_LOG_FILES ffbuild/config.log
)
z_vcpkg_restore_pkgconfig_path()
endif()
if(VCPKG_TARGET_IS_WINDOWS)
file(GLOB DEF_FILES "${CURRENT_PACKAGES_DIR}/lib/*.def" "${CURRENT_PACKAGES_DIR}/debug/lib/*.def")
if(NOT VCPKG_TARGET_IS_MINGW)
if(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm")
set(LIB_MACHINE_ARG /machine:ARM)
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "arm64")
set(LIB_MACHINE_ARG /machine:ARM64)
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x86")
set(LIB_MACHINE_ARG /machine:x86)
elseif(VCPKG_TARGET_ARCHITECTURE STREQUAL "x64")
set(LIB_MACHINE_ARG /machine:x64)
else()
message(FATAL_ERROR "Unsupported target architecture")
endif()
foreach(DEF_FILE ${DEF_FILES})
get_filename_component(DEF_FILE_DIR "${DEF_FILE}" DIRECTORY)
get_filename_component(DEF_FILE_NAME "${DEF_FILE}" NAME)
string(REGEX REPLACE "-[0-9]*\\.def" "${VCPKG_TARGET_STATIC_LIBRARY_SUFFIX}" OUT_FILE_NAME "${DEF_FILE_NAME}")
file(TO_NATIVE_PATH "${DEF_FILE}" DEF_FILE_NATIVE)
file(TO_NATIVE_PATH "${DEF_FILE_DIR}/${OUT_FILE_NAME}" OUT_FILE_NATIVE)
message(STATUS "Generating ${OUT_FILE_NATIVE}")
vcpkg_execute_required_process(
COMMAND lib.exe "/def:${DEF_FILE_NATIVE}" "/out:${OUT_FILE_NATIVE}" ${LIB_MACHINE_ARG}
WORKING_DIRECTORY "${CURRENT_PACKAGES_DIR}"
LOGNAME "libconvert-${TARGET_TRIPLET}"
)
endforeach()
endif()
file(GLOB EXP_FILES "${CURRENT_PACKAGES_DIR}/lib/*.exp" "${CURRENT_PACKAGES_DIR}/debug/lib/*.exp")
file(GLOB LIB_FILES "${CURRENT_PACKAGES_DIR}/bin/*${VCPKG_TARGET_STATIC_LIBRARY_SUFFIX}" "${CURRENT_PACKAGES_DIR}/debug/bin/*${VCPKG_TARGET_STATIC_LIBRARY_SUFFIX}")
if(VCPKG_TARGET_IS_MINGW)
file(GLOB LIB_FILES_2 "${CURRENT_PACKAGES_DIR}/bin/*.lib" "${CURRENT_PACKAGES_DIR}/debug/bin/*.lib")
endif()
set(files_to_remove ${EXP_FILES} ${LIB_FILES} ${LIB_FILES_2} ${DEF_FILES})
if(files_to_remove)
file(REMOVE ${files_to_remove})
endif()
endif()
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include" "${CURRENT_PACKAGES_DIR}/debug/share")
if(VCPKG_LIBRARY_LINKAGE STREQUAL "static")
file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/bin" "${CURRENT_PACKAGES_DIR}/debug/bin")
endif()
vcpkg_copy_pdbs()
if(VCPKG_TARGET_IS_WINDOWS)
set(_dirs "/")
if(NOT VCPKG_BUILD_TYPE OR VCPKG_BUILD_TYPE STREQUAL "debug")
list(APPEND _dirs "/debug/")
endif()
foreach(_debug IN LISTS _dirs)
foreach(PKGCONFIG_MODULE IN LISTS FFMPEG_PKGCONFIG_MODULES)
set(PKGCONFIG_FILE "${CURRENT_PACKAGES_DIR}${_debug}lib/pkgconfig/${PKGCONFIG_MODULE}.pc")
# remove redundant cygwin style -libpath entries
execute_process(
COMMAND "${MSYS_ROOT}/usr/bin/cygpath.exe" -u "${CURRENT_INSTALLED_DIR}"
OUTPUT_VARIABLE CYG_INSTALLED_DIR
OUTPUT_STRIP_TRAILING_WHITESPACE
)
vcpkg_replace_string("${PKGCONFIG_FILE}" "-libpath:${CYG_INSTALLED_DIR}${_debug}lib/pkgconfig/../../lib " "")
# transform libdir, includedir, and prefix paths from cygwin style to windows style
file(READ "${PKGCONFIG_FILE}" PKGCONFIG_CONTENT)
foreach(PATH_NAME prefix libdir includedir)
string(REGEX MATCH "${PATH_NAME}=[^\n]*" PATH_VALUE "${PKGCONFIG_CONTENT}")
string(REPLACE "${PATH_NAME}=" "" PATH_VALUE "${PATH_VALUE}")
if(NOT PATH_VALUE)
message(FATAL_ERROR "failed to find pkgconfig variable ${PATH_NAME}")
endif()
execute_process(
COMMAND "${MSYS_ROOT}/usr/bin/cygpath.exe" -w "${PATH_VALUE}"
OUTPUT_VARIABLE FIXED_PATH
OUTPUT_STRIP_TRAILING_WHITESPACE
)
file(TO_CMAKE_PATH "${FIXED_PATH}" FIXED_PATH)
vcpkg_replace_string("${PKGCONFIG_FILE}" "${PATH_NAME}=${PATH_VALUE}" "${PATH_NAME}=${FIXED_PATH}")
endforeach()
# list libraries with -l flag (so pkgconf knows they are libraries and not just linker flags)
foreach(LIBS_ENTRY Libs Libs.private)
string(REGEX MATCH "${LIBS_ENTRY}: [^\n]*" LIBS_VALUE "${PKGCONFIG_CONTENT}")
if(NOT LIBS_VALUE)
message(FATAL_ERROR "failed to find pkgconfig entry ${LIBS_ENTRY}")
endif()
string(REPLACE "${LIBS_ENTRY}: " "" LIBS_VALUE "${LIBS_VALUE}")
if(LIBS_VALUE)
set(LIBS_VALUE_OLD "${LIBS_VALUE}")
string(REGEX REPLACE "([^ ]+)[.]lib" "-l\\1" LIBS_VALUE "${LIBS_VALUE}")
set(LIBS_VALUE_NEW "${LIBS_VALUE}")
vcpkg_replace_string("${PKGCONFIG_FILE}" "${LIBS_ENTRY}: ${LIBS_VALUE_OLD}" "${LIBS_ENTRY}: ${LIBS_VALUE_NEW}")
endif()
endforeach()
endforeach()
endforeach()
endif()
vcpkg_fixup_pkgconfig()
# Handle dependencies
x_vcpkg_pkgconfig_get_modules(PREFIX FFMPEG_PKGCONFIG MODULES ${FFMPEG_PKGCONFIG_MODULES} LIBS)
function(append_dependencies_from_libs out)
cmake_parse_arguments(PARSE_ARGV 1 "arg" "" "LIBS" "")
string(REGEX REPLACE "[ ]+" ";" contents "${arg_LIBS}")
list(FILTER contents EXCLUDE REGEX "^-F.+")
list(FILTER contents EXCLUDE REGEX "^-framework$")
list(FILTER contents EXCLUDE REGEX "^-L.+")
list(FILTER contents EXCLUDE REGEX "^-libpath:.+")
list(TRANSFORM contents REPLACE "^-Wl,-framework," "-l")
list(FILTER contents EXCLUDE REGEX "^-Wl,.+")
list(TRANSFORM contents REPLACE "^-l" "")
list(FILTER contents EXCLUDE REGEX "^avutil$")
list(FILTER contents EXCLUDE REGEX "^avcodec$")
list(FILTER contents EXCLUDE REGEX "^avdevice$")
list(FILTER contents EXCLUDE REGEX "^avfilter$")
list(FILTER contents EXCLUDE REGEX "^avformat$")
list(FILTER contents EXCLUDE REGEX "^postproc$")
list(FILTER contents EXCLUDE REGEX "^swresample$")
list(FILTER contents EXCLUDE REGEX "^swscale$")
if(VCPKG_TARGET_IS_WINDOWS)
list(TRANSFORM contents TOLOWER)
endif()
if(contents)
list(APPEND "${out}" "${contents}")
set("${out}" "${${out}}" PARENT_SCOPE)
endif()
endfunction()
append_dependencies_from_libs(FFMPEG_DEPENDENCIES_RELEASE LIBS "${FFMPEG_PKGCONFIG_LIBS_RELEASE}")
append_dependencies_from_libs(FFMPEG_DEPENDENCIES_DEBUG LIBS "${FFMPEG_PKGCONFIG_LIBS_DEBUG}")
# must remove duplicates from the front to respect link order so reverse first
list(REVERSE FFMPEG_DEPENDENCIES_RELEASE)
list(REVERSE FFMPEG_DEPENDENCIES_DEBUG)
list(REMOVE_DUPLICATES FFMPEG_DEPENDENCIES_RELEASE)
list(REMOVE_DUPLICATES FFMPEG_DEPENDENCIES_DEBUG)
list(REVERSE FFMPEG_DEPENDENCIES_RELEASE)
list(REVERSE FFMPEG_DEPENDENCIES_DEBUG)
message(STATUS "Dependencies (release): ${FFMPEG_DEPENDENCIES_RELEASE}")
message(STATUS "Dependencies (debug): ${FFMPEG_DEPENDENCIES_DEBUG}")
# Handle version strings
function(extract_regex_from_file out)
cmake_parse_arguments(PARSE_ARGV 1 "arg" "MAJOR" "FILE_WITHOUT_EXTENSION;REGEX" "")
file(READ "${arg_FILE_WITHOUT_EXTENSION}.h" contents)
if(contents MATCHES "${arg_REGEX}")
if(NOT CMAKE_MATCH_COUNT EQUAL 1)
message(FATAL_ERROR "Could not identify match group in regular expression \"${arg_REGEX}\"")
endif()
else()
if(arg_MAJOR)
file(READ "${arg_FILE_WITHOUT_EXTENSION}_major.h" contents)
if(contents MATCHES "${arg_REGEX}")
if(NOT CMAKE_MATCH_COUNT EQUAL 1)
message(FATAL_ERROR "Could not identify match group in regular expression \"${arg_REGEX}\"")
endif()
else()
message(WARNING "Could not find line matching \"${arg_REGEX}\" in file \"${arg_FILE_WITHOUT_EXTENSION}_major.h\"")
endif()
else()
message(WARNING "Could not find line matching \"${arg_REGEX}\" in file \"${arg_FILE_WITHOUT_EXTENSION}.h\"")
endif()
endif()
set("${out}" "${CMAKE_MATCH_1}" PARENT_SCOPE)
endfunction()
function(extract_version_from_component out)
cmake_parse_arguments(PARSE_ARGV 1 "arg" "" "COMPONENT" "")
string(TOLOWER "${arg_COMPONENT}" component_lower)
string(TOUPPER "${arg_COMPONENT}" component_upper)
extract_regex_from_file(major_version
FILE_WITHOUT_EXTENSION "${SOURCE_PATH}/${component_lower}/version"
MAJOR
REGEX "#define ${component_upper}_VERSION_MAJOR[ ]+([0-9]+)"
)
extract_regex_from_file(minor_version
FILE_WITHOUT_EXTENSION "${SOURCE_PATH}/${component_lower}/version"
REGEX "#define ${component_upper}_VERSION_MINOR[ ]+([0-9]+)"
)
extract_regex_from_file(micro_version
FILE_WITHOUT_EXTENSION "${SOURCE_PATH}/${component_lower}/version"
REGEX "#define ${component_upper}_VERSION_MICRO[ ]+([0-9]+)"
)
set("${out}" "${major_version}.${minor_version}.${micro_version}" PARENT_SCOPE)
endfunction()
extract_regex_from_file(FFMPEG_VERSION
FILE_WITHOUT_EXTENSION "${CURRENT_BUILDTREES_DIR}/${TARGET_TRIPLET}-rel/libavutil/ffversion"
REGEX "#define FFMPEG_VERSION[ ]+\"(.+)\""
)
extract_version_from_component(LIBAVUTIL_VERSION
COMPONENT libavutil)
extract_version_from_component(LIBAVCODEC_VERSION
COMPONENT libavcodec)
extract_version_from_component(LIBAVDEVICE_VERSION
COMPONENT libavdevice)
extract_version_from_component(LIBAVFILTER_VERSION
COMPONENT libavfilter)
extract_version_from_component(LIBAVFORMAT_VERSION
COMPONENT libavformat)
extract_version_from_component(LIBSWRESAMPLE_VERSION
COMPONENT libswresample)
extract_version_from_component(LIBSWSCALE_VERSION
COMPONENT libswscale)
# Handle copyright
file(STRINGS "${CURRENT_BUILDTREES_DIR}/build-${TARGET_TRIPLET}-rel-out.log" LICENSE_STRING REGEX "License: .*" LIMIT_COUNT 1)
if(LICENSE_STRING STREQUAL "License: LGPL version 2.1 or later")
set(LICENSE_FILE "COPYING.LGPLv2.1")
elseif(LICENSE_STRING STREQUAL "License: LGPL version 3 or later")
set(LICENSE_FILE "COPYING.LGPLv3")
elseif(LICENSE_STRING STREQUAL "License: GPL version 2 or later")
set(LICENSE_FILE "COPYING.GPLv2")
elseif(LICENSE_STRING STREQUAL "License: GPL version 3 or later")
set(LICENSE_FILE "COPYING.GPLv3")
elseif(LICENSE_STRING STREQUAL "License: nonfree and unredistributable")
set(LICENSE_FILE "COPYING.NONFREE")
file(WRITE "${SOURCE_PATH}/${LICENSE_FILE}" "${LICENSE_STRING}")
else()
message(FATAL_ERROR "Failed to identify license (${LICENSE_STRING})")
endif()
configure_file("${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake" "${CURRENT_PACKAGES_DIR}/share/${PORT}/vcpkg-cmake-wrapper.cmake" @ONLY)
vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/${LICENSE_FILE}")
@@ -0,0 +1,47 @@
set(FFMPEG_PREV_MODULE_PATH ${CMAKE_MODULE_PATH})
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR})
include(SelectLibraryConfigurations)
cmake_policy(SET CMP0012 NEW)
set(vcpkg_no_avcodec_target ON)
set(vcpkg_no_avformat_target ON)
set(vcpkg_no_avutil_target ON)
if(TARGET FFmpeg::avcodec)
set(vcpkg_no_avcodec_target OFF)
endif()
if(TARGET FFmpeg::avformat)
set(vcpkg_no_avformat_target OFF)
endif()
if(TARGET FFmpeg::avutil)
set(vcpkg_no_avutil_target OFF)
endif()
_find_package(${ARGS})
if(WIN32)
set(PKG_CONFIG_EXECUTABLE "${CMAKE_CURRENT_LIST_DIR}/../../../@_HOST_TRIPLET@/tools/pkgconf/pkgconf.exe" CACHE STRING "" FORCE)
endif()
set(PKG_CONFIG_USE_CMAKE_PREFIX_PATH ON) # Required for CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 3.1 which otherwise ignores CMAKE_PREFIX_PATH
if(@WITH_MFX@)
find_package(PkgConfig )
pkg_check_modules(libmfx IMPORTED_TARGET libmfx)
list(APPEND FFMPEG_LIBRARIES PkgConfig::libmfx)
if(vcpkg_no_avcodec_target AND TARGET FFmpeg::avcodec)
target_link_libraries(FFmpeg::avcodec INTERFACE PkgConfig::libmfx)
endif()
if(vcpkg_no_avutil_target AND TARGET FFmpeg::avutil)
target_link_libraries(FFmpeg::avutil INTERFACE PkgConfig::libmfx)
endif()
endif()
set(FFMPEG_LIBRARY ${FFMPEG_LIBRARIES})
set(CMAKE_MODULE_PATH ${FFMPEG_PREV_MODULE_PATH})
unset(vcpkg_no_avformat_target)
unset(vcpkg_no_avcodec_target)
unset(vcpkg_no_avutil_target)

Some files were not shown because too many files have changed in this diff Show More