mirror of
https://github.com/hensm/fx_cast.git
synced 2026-06-08 08:39:59 +00:00
wip: Replace unmaintained mdns module with a custom native module
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,7 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
bridge/node_modules/
|
||||
bridge/build
|
||||
bridge/build/
|
||||
extension/node_modules/
|
||||
test/ChromeProfile/
|
||||
.idea/
|
||||
|
||||
23
.vscode/c_cpp_properties.json
vendored
Normal file
23
.vscode/c_cpp_properties.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Win32",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"${env:USERPROFILE}/AppData/Local/node-gyp/Cache/**/include/node",
|
||||
"${workspaceFolder}/bridge/node_modules/node-addon-api"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"UNICODE",
|
||||
"_UNICODE"
|
||||
],
|
||||
"windowsSdkVersion": "10.0.22621.0",
|
||||
"compilerPath": "cl.exe",
|
||||
"cStandard": "c17",
|
||||
"cppStandard": "c++20",
|
||||
"intelliSenseMode": "windows-msvc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
9
.vscode/settings.json
vendored
9
.vscode/settings.json
vendored
@@ -1,3 +1,10 @@
|
||||
{
|
||||
"eslint.validate": ["javascript", "svelte"]
|
||||
"eslint.validate": ["javascript", "svelte"],
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/bower_components": true,
|
||||
"**/*.code-search": true,
|
||||
"**/dist": true,
|
||||
"**/build": true
|
||||
},
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ if (!supportedTargets[process.platform]?.includes(argv.arch)) {
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
|
||||
const ROOT_PATH = path.join(__dirname, "..");
|
||||
const BUILD_PATH = path.join(ROOT_PATH, "build");
|
||||
const BUILD_PATH = path.join(ROOT_PATH, "dist/app");
|
||||
|
||||
const spawnOptions = {
|
||||
shell: true,
|
||||
@@ -70,15 +70,14 @@ const spawnOptions = {
|
||||
* build directories, just in case.
|
||||
*/
|
||||
fs.rmSync(BUILD_PATH, { force: true, recursive: true });
|
||||
fs.rmSync(paths.DIST_PATH, { force: true, recursive: true });
|
||||
fs.mkdirSync(BUILD_PATH, { recursive: true });
|
||||
if (argv.package) {
|
||||
fs.rmSync(paths.DIST_PATH, { force: true, recursive: true });
|
||||
fs.mkdirSync(paths.DIST_PATH, { recursive: true });
|
||||
}
|
||||
|
||||
const MDNS_BINDING_PATH = path.join(
|
||||
__dirname,
|
||||
"../node_modules/mdns/build/Release/"
|
||||
);
|
||||
const MDNS_BINDING_NAME = "dns_sd_bindings.node";
|
||||
const NATIVE_BINDING_PATH = path.join(ROOT_PATH, "build/Release");
|
||||
const NATIVE_BINDING_NAME = "dns_sd.node";
|
||||
|
||||
async function build() {
|
||||
// Run tsc
|
||||
@@ -140,8 +139,8 @@ async function build() {
|
||||
]);
|
||||
|
||||
fs.copySync(
|
||||
path.join(MDNS_BINDING_PATH, MDNS_BINDING_NAME),
|
||||
path.join(BUILD_PATH, MDNS_BINDING_NAME)
|
||||
path.join(NATIVE_BINDING_PATH, NATIVE_BINDING_NAME),
|
||||
path.join(BUILD_PATH, NATIVE_BINDING_NAME)
|
||||
);
|
||||
|
||||
fs.rmSync(path.join(BUILD_PATH, "src"), {
|
||||
@@ -190,8 +189,20 @@ NODE_PATH="${modulesDir}" node $(dirname $0)/src/main.js --__name $(basename $0)
|
||||
}
|
||||
|
||||
manifest.path = path.join(paths.DIST_PATH, path.basename(launcherPath));
|
||||
|
||||
// Copy native binding into build/Release so bindings() finds it
|
||||
fs.copySync(
|
||||
path.join(NATIVE_BINDING_PATH, NATIVE_BINDING_NAME),
|
||||
path.join(BUILD_PATH, "build", "Release", NATIVE_BINDING_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
// Write a package.json so the bindings module resolves from this directory
|
||||
fs.writeFileSync(
|
||||
path.join(BUILD_PATH, "package.json"),
|
||||
"{}"
|
||||
);
|
||||
|
||||
// Write app manifest
|
||||
fs.writeFileSync(
|
||||
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
||||
@@ -318,8 +329,8 @@ function packageDarwin(
|
||||
path.join(rootExecutableDirectory, platformExecutableName)
|
||||
);
|
||||
fs.moveSync(
|
||||
path.join(BUILD_PATH, MDNS_BINDING_NAME),
|
||||
path.join(rootExecutableDirectory, MDNS_BINDING_NAME)
|
||||
path.join(BUILD_PATH, NATIVE_BINDING_NAME),
|
||||
path.join(rootExecutableDirectory, NATIVE_BINDING_NAME)
|
||||
);
|
||||
fs.moveSync(
|
||||
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
||||
@@ -416,8 +427,8 @@ function packageLinuxDeb(
|
||||
path.join(rootExecutableDirectory, platformExecutableName)
|
||||
);
|
||||
fs.moveSync(
|
||||
path.join(BUILD_PATH, MDNS_BINDING_NAME),
|
||||
path.join(rootExecutableDirectory, MDNS_BINDING_NAME)
|
||||
path.join(BUILD_PATH, NATIVE_BINDING_NAME),
|
||||
path.join(rootExecutableDirectory, NATIVE_BINDING_NAME)
|
||||
);
|
||||
fs.moveSync(
|
||||
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
||||
@@ -490,7 +501,7 @@ function packageLinuxRpm(
|
||||
manifestPath: platformManifestDirectory,
|
||||
executableName: platformExecutableName,
|
||||
manifestName: paths.MANIFEST_NAME,
|
||||
bindingName: MDNS_BINDING_NAME
|
||||
bindingName: NATIVE_BINDING_NAME
|
||||
};
|
||||
|
||||
fs.writeFileSync(
|
||||
@@ -539,7 +550,7 @@ function packageWin32(
|
||||
executableName: platformExecutableName,
|
||||
executablePath: platformExecutableDirectory,
|
||||
manifestName: paths.MANIFEST_NAME,
|
||||
bindingName: MDNS_BINDING_NAME,
|
||||
bindingName: NATIVE_BINDING_NAME,
|
||||
winRegistryKey: paths.REGISTRY_KEY,
|
||||
outputName,
|
||||
licensePath: paths.LICENSE_PATH,
|
||||
|
||||
60
bridge/binding.gyp
Normal file
60
bridge/binding.gyp
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "dns_sd",
|
||||
"cflags!": ["-fno-exceptions"],
|
||||
"cflags_cc!": ["-fno-exceptions"],
|
||||
"defines": ["NAPI_VERSION=8", "NAPI_DISABLE_CPP_EXCEPTIONS"],
|
||||
"include_dirs": ["<!@(node -p \"require('node-addon-api').include\")"],
|
||||
"conditions": [
|
||||
[
|
||||
"OS=='mac'",
|
||||
{
|
||||
"sources": [
|
||||
"src/dns_sd/native/addon.cc",
|
||||
"src/dns_sd/native/dns_sd_browser.cc",
|
||||
"src/dns_sd/native/dns_sd_platform_browser_unix.cc",
|
||||
],
|
||||
"cflags_cc": ["-std=c++20"],
|
||||
"xcode_settings": {
|
||||
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
||||
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
|
||||
"CLANG_CXX_LIBRARY": "libc++",
|
||||
"MACOSX_DEPLOYMENT_TARGET": "10.15",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS=='linux'",
|
||||
{
|
||||
"sources": [
|
||||
"src/dns_sd/native/addon.cc",
|
||||
"src/dns_sd/native/dns_sd_browser.cc",
|
||||
"src/dns_sd/native/dns_sd_platform_browser_unix.cc",
|
||||
],
|
||||
"cflags_cc": ["-std=c++20"],
|
||||
"libraries": ["-ldns_sd"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS=='win'",
|
||||
{
|
||||
"sources": [
|
||||
"src/dns_sd/native/addon.cc",
|
||||
"src/dns_sd/native/dns_sd_browser.cc",
|
||||
"src/dns_sd/native/dns_sd_platform_browser_win.cc",
|
||||
],
|
||||
"libraries": ["dnsapi.lib", "ws2_32.lib"],
|
||||
"msvs_settings": {
|
||||
"VCCLCompilerTool": {
|
||||
"ExceptionHandling": 1,
|
||||
"AdditionalOptions": ["/std:c++20"],
|
||||
}
|
||||
},
|
||||
"defines": ["UNICODE", "_UNICODE", "WIN32_LEAN_AND_MEAN"],
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
]
|
||||
} # type: ignore
|
||||
104
bridge/package-lock.json
generated
104
bridge/package-lock.json
generated
@@ -4,21 +4,22 @@
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"bplist-creator": "^0.1.0",
|
||||
"bplist-parser": "^0.3.1",
|
||||
"castv2": "^0.1.10",
|
||||
"chalk": "^4.1.2",
|
||||
"fast-srp-hap": "^2.0.4",
|
||||
"mdns": "^2.7.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^8.5.0",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdns": "^0.0.34",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/minimist": "^1.2.2",
|
||||
"@types/node": "^22.0.0",
|
||||
@@ -32,6 +33,14 @@
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
},
|
||||
"dns_sd": {
|
||||
"version": "1.0.0",
|
||||
"extraneous": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.16.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
||||
@@ -160,15 +169,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
||||
},
|
||||
"node_modules/@types/mdns": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdns/-/mdns-0.0.34.tgz",
|
||||
"integrity": "sha512-4Rrt/0wRAudtOnmhfDdoFhy5r20yHe0KiDK+/+I9RBBMW67F4S6y8tJH06AzrUDZzS/SH/U2pw1W0lrgQ+OlPg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime-types": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
||||
@@ -328,9 +328,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bindings": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
|
||||
"integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bl": {
|
||||
"version": "4.1.0",
|
||||
@@ -776,6 +780,12 @@
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -1115,16 +1125,6 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mdns": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mdns/-/mdns-2.7.2.tgz",
|
||||
"integrity": "sha512-NBOQT22DKvuNWVY7nKNbs6w9eGRyPwnc4ZjKOsCG2G/4wNt1+IyiHvc+5yhcAUZLG46cOY321YW7Ufz3lMtrhw==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bindings": "~1.2.1",
|
||||
"nan": "^2.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -1242,11 +1242,6 @@
|
||||
"mustache": "bin/mustache"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
||||
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
@@ -1271,6 +1266,15 @@
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz",
|
||||
"integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -2390,15 +2394,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
||||
},
|
||||
"@types/mdns": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdns/-/mdns-0.0.34.tgz",
|
||||
"integrity": "sha512-4Rrt/0wRAudtOnmhfDdoFhy5r20yHe0KiDK+/+I9RBBMW67F4S6y8tJH06AzrUDZzS/SH/U2pw1W0lrgQ+OlPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/mime-types": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
||||
@@ -2522,9 +2517,12 @@
|
||||
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
|
||||
},
|
||||
"bindings": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
|
||||
"integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"requires": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
},
|
||||
"bl": {
|
||||
"version": "4.1.0",
|
||||
@@ -2848,6 +2846,11 @@
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"file-uri-to-path": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
@@ -3108,15 +3111,6 @@
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mdns": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/mdns/-/mdns-2.7.2.tgz",
|
||||
"integrity": "sha512-NBOQT22DKvuNWVY7nKNbs6w9eGRyPwnc4ZjKOsCG2G/4wNt1+IyiHvc+5yhcAUZLG46cOY321YW7Ufz3lMtrhw==",
|
||||
"requires": {
|
||||
"bindings": "~1.2.1",
|
||||
"nan": "^2.14.0"
|
||||
}
|
||||
},
|
||||
"merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -3198,11 +3192,6 @@
|
||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.15.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
|
||||
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ=="
|
||||
},
|
||||
"napi-build-utils": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||
@@ -3226,6 +3215,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-addon-api": {
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz",
|
||||
"integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q=="
|
||||
},
|
||||
"node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
|
||||
@@ -2,25 +2,26 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node bin/build.js",
|
||||
"install": "node-gyp rebuild",
|
||||
"package": "node bin/build.js --package",
|
||||
"install-manifest": "node bin/install-manifest.js",
|
||||
"remove-manifest": "node bin/install-manifest.js --remove"
|
||||
},
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"bplist-creator": "^0.1.0",
|
||||
"bplist-parser": "^0.3.1",
|
||||
"castv2": "^0.1.10",
|
||||
"chalk": "^4.1.2",
|
||||
"fast-srp-hap": "^2.0.4",
|
||||
"mdns": "^2.7.2",
|
||||
"mime-types": "^2.1.35",
|
||||
"node-addon-api": "^8.0.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"ws": "^8.5.0",
|
||||
"yargs": "^17.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mdns": "^0.0.34",
|
||||
"@types/mime-types": "^2.1.1",
|
||||
"@types/minimist": "^1.2.2",
|
||||
"@types/node": "^22.0.0",
|
||||
|
||||
@@ -35,16 +35,12 @@ SetCompressor /SOLID LZMA
|
||||
!insertmacro MUI_LANGUAGE "German"
|
||||
|
||||
# lang:en
|
||||
LangString MSG__INSTALL_BONJOUR ${LANG_ENGLISH} \
|
||||
"Install Bonjour dependency?"
|
||||
LangString MSG__FIREFOX_OPEN ${LANG_ENGLISH} \
|
||||
"Firefox must be closed during uninstallation if the extension is \
|
||||
installed. Close Firefox and click $\"Retry$\", click $\"Ignore$\" \
|
||||
to force close or $\"Abort$\" to cancel uninstallation."
|
||||
|
||||
# lang:es
|
||||
LangString MSG__INSTALL_BONJOUR ${LANG_SPANISH} \
|
||||
"¿Instalar dependencia Bonjour?"
|
||||
LangString MSG__FIREFOX_OPEN ${LANG_SPANISH} \
|
||||
"Firefox debe estar cerrado durante la desinstalación si la extensión \
|
||||
está instalada. Cierra Firefox y aprieta $\"Reintentar$\", aprieta \
|
||||
@@ -52,8 +48,6 @@ LangString MSG__FIREFOX_OPEN ${LANG_SPANISH} \
|
||||
desinstalación."
|
||||
|
||||
# lang:de
|
||||
LangString MSG__INSTALL_BONJOUR ${LANG_GERMAN} \
|
||||
"Bonjour installieren?"
|
||||
LangString MSG__FIREFOX_OPEN ${LANG_GERMAN} \
|
||||
"Firefox muss während der Deinstallation geschlossen werden, wenn die \
|
||||
Erweiterung installiert ist. Schließen Sie Firefox und klicken Sie auf \
|
||||
@@ -86,23 +80,6 @@ Section
|
||||
File "{{bindingName}}"
|
||||
File "{{manifestName}}"
|
||||
|
||||
# Install Bonjour
|
||||
IfFileExists "$SYSDIR\dnssd.dll" skipInstallBonjour
|
||||
MessageBox MB_YESNO \
|
||||
$(MSG__INSTALL_BONJOUR) \
|
||||
IDNO skipInstallBonjour
|
||||
|
||||
${If} ${ARCH} == "x86"
|
||||
File /oname=Bonjour.msi "C:\Program Files\Bonjour SDK\Installer\Bonjour.msi"
|
||||
${ElseIf} ${ARCH} == "x64"
|
||||
File /oname=Bonjour.msi "C:\Program Files\Bonjour SDK\Installer\Bonjour64.msi"
|
||||
${EndIf}
|
||||
|
||||
ExecWait "msiexec /i $\"$INSTDIR\Bonjour.msi$\""
|
||||
|
||||
skipInstallBonjour:
|
||||
Delete "$INSTDIR\Bonjour.msi"
|
||||
|
||||
# Native manifest key
|
||||
WriteRegStr HKLM "${KEY_MANIFEST}" "" "$INSTDIR\{{manifestName}}"
|
||||
|
||||
|
||||
@@ -180,7 +180,8 @@ export default class Session extends CastClient {
|
||||
type: "LAUNCH",
|
||||
appId: this.appId
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
// Handle client connection closed
|
||||
this.client.on("close", () => {
|
||||
|
||||
@@ -66,8 +66,19 @@ export default class CastClient {
|
||||
*/
|
||||
connect(host: string, options?: CastClientConnectOptions) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let connected = false;
|
||||
|
||||
// Handle errors
|
||||
this.client.on("error", reject);
|
||||
this.client.on("error", err => {
|
||||
if (!connected) {
|
||||
reject(err);
|
||||
} else {
|
||||
try {
|
||||
this.client.close();
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
this.client.on("close", () => {
|
||||
if (this.heartbeatChannel && this.heartbeatIntervalId) {
|
||||
clearInterval(this.heartbeatIntervalId);
|
||||
@@ -84,6 +95,7 @@ export default class CastClient {
|
||||
},
|
||||
// On connection callback
|
||||
() => {
|
||||
connected = true;
|
||||
this.connectionChannel = this.createChannel(NS_CONNECTION);
|
||||
this.heartbeatChannel = this.createChannel(NS_HEARTBEAT);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import mdns from "mdns";
|
||||
|
||||
import { EventEmitter } from "events";
|
||||
import { DnsSdBrowser } from "../../../dns_sd";
|
||||
import type { ReceiverDevice } from "../../messagingTypes";
|
||||
|
||||
/**
|
||||
* Chromecast TXT record
|
||||
* Chromecast TXT record fields.
|
||||
*/
|
||||
interface CastRecord {
|
||||
// Device ID
|
||||
@@ -27,54 +27,47 @@ interface CastRecord {
|
||||
rs: string;
|
||||
}
|
||||
|
||||
interface DiscoveryOptions {
|
||||
onDeviceFound(device: ReceiverDevice): void;
|
||||
onDeviceDown(deviceId: string): void;
|
||||
}
|
||||
export default class CastDeviceBrowser extends EventEmitter<{
|
||||
deviceUp: [device: ReceiverDevice];
|
||||
deviceDown: [deviceId: string];
|
||||
}> {
|
||||
browser = new DnsSdBrowser("_googlecast._tcp");
|
||||
|
||||
export default class Discovery {
|
||||
browser = mdns.createBrowser(mdns.tcp("googlecast"), {
|
||||
resolverSequence: [
|
||||
mdns.rst.DNSServiceResolve(),
|
||||
"DNSServiceGetAddrInfo" in mdns.dns_sd
|
||||
? mdns.rst.DNSServiceGetAddrInfo()
|
||||
: // Some issues on Linux with IPv6, so restrict to IPv4
|
||||
mdns.rst.getaddrinfo({ families: [4] }),
|
||||
mdns.rst.makeAddressesUnique()
|
||||
]
|
||||
});
|
||||
|
||||
constructor(opts: DiscoveryOptions) {
|
||||
constructor() {
|
||||
super();
|
||||
/**
|
||||
* When a service is found, gather device info from service
|
||||
* object and TXT record, then send a `main:deviceUp` message.
|
||||
* When a service is found, gather device info from service object and
|
||||
* TXT record, then send a `main:deviceUp` message.
|
||||
*/
|
||||
this.browser.on("serviceUp", service => {
|
||||
// Filter invalid results
|
||||
if (!service.txtRecord || !service.name) return;
|
||||
|
||||
const record = service.txtRecord as CastRecord;
|
||||
const address = service.address4 ?? service.address6;
|
||||
if (!address) return;
|
||||
|
||||
const record = service.txtRecord as unknown as CastRecord;
|
||||
const device: ReceiverDevice = {
|
||||
id: record.id,
|
||||
friendlyName: record.fn,
|
||||
modelName: record.md,
|
||||
capabilities: parseInt(record.ca),
|
||||
host: service.addresses[0],
|
||||
host: address,
|
||||
port: service.port
|
||||
};
|
||||
|
||||
opts.onDeviceFound(device);
|
||||
this.emit("deviceUp", device);
|
||||
});
|
||||
|
||||
/**
|
||||
* When a service is lost, send a `main:deviceDown` message with
|
||||
* the service name as the `deviceId`.
|
||||
* When a service is lost, send a `main:deviceDown` message with the
|
||||
* service name as the `deviceId`.
|
||||
*/
|
||||
this.browser.on("serviceDown", service => {
|
||||
this.browser.on("serviceDown", name => {
|
||||
// Filter invalid results
|
||||
if (!service.name) return;
|
||||
if (!name) return;
|
||||
|
||||
opts.onDeviceDown(service.name);
|
||||
this.emit("deviceDown", name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ export default class Remote extends CastClient {
|
||||
})
|
||||
.then(() => {
|
||||
this.sendReceiverMessage({ type: "GET_STATUS" });
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
@@ -85,7 +86,8 @@ export default class Remote extends CastClient {
|
||||
type: "GET_STATUS",
|
||||
requestId: 0
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
this.options?.onApplicationFound?.();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Messenger, Message } from "./messaging";
|
||||
|
||||
import { handleCastMessage } from "./components/cast";
|
||||
import Discovery from "./components/cast/discovery";
|
||||
import CastDeviceBrowser from "./components/cast/deviceBrowser";
|
||||
import Remote from "./components/cast/remote";
|
||||
|
||||
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
||||
@@ -9,7 +9,7 @@ import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
||||
import { applicationVersion } from "../../config.json";
|
||||
|
||||
process.on("SIGTERM", async () => {
|
||||
discovery?.stop();
|
||||
deviceBrowser?.stop();
|
||||
try {
|
||||
await stopMediaServer();
|
||||
} catch (err) {
|
||||
@@ -19,15 +19,15 @@ process.on("SIGTERM", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
let discovery: Discovery | null = null;
|
||||
let deviceBrowser: CastDeviceBrowser | null = null;
|
||||
const remotes = new Map<string, Remote>();
|
||||
|
||||
/**
|
||||
* Handle incoming messages from the extension and forward
|
||||
* them to the appropriate handlers.
|
||||
* Handle incoming messages from the extension and forward them to the
|
||||
* appropriate handlers.
|
||||
*
|
||||
* Initializes the counterpart objects and is responsible
|
||||
* for managing existing ones.
|
||||
* Initializes the counterpart objects and is responsible for managing existing
|
||||
* ones.
|
||||
*/
|
||||
export function run(messaging: Messenger) {
|
||||
messaging.on("message", (message: Message) => {
|
||||
@@ -41,8 +41,9 @@ export function run(messaging: Messenger) {
|
||||
case "bridge:startDiscovery": {
|
||||
const { shouldWatchStatus } = message.data;
|
||||
|
||||
discovery = new Discovery({
|
||||
onDeviceFound(device) {
|
||||
deviceBrowser = new CastDeviceBrowser();
|
||||
|
||||
deviceBrowser.on("deviceUp", device => {
|
||||
messaging.sendMessage({
|
||||
subject: "main:deviceUp",
|
||||
data: {
|
||||
@@ -83,8 +84,9 @@ export function run(messaging: Messenger) {
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
onDeviceDown(deviceId) {
|
||||
});
|
||||
|
||||
deviceBrowser.on("deviceDown", deviceId => {
|
||||
messaging.sendMessage({
|
||||
subject: "main:deviceDown",
|
||||
data: { deviceId }
|
||||
@@ -96,11 +98,9 @@ export function run(messaging: Messenger) {
|
||||
remotes.delete(deviceId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
discovery.start();
|
||||
|
||||
deviceBrowser.start();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
3
bridge/src/dns_sd/.clang-format
Normal file
3
bridge/src/dns_sd/.clang-format
Normal file
@@ -0,0 +1,3 @@
|
||||
BasedOnStyle: Webkit
|
||||
ColumnLimit: 100
|
||||
SortIncludes: false
|
||||
68
bridge/src/dns_sd/index.ts
Normal file
68
bridge/src/dns_sd/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
const native = require("bindings")("dns_sd");
|
||||
|
||||
export interface Service {
|
||||
/** Service instance name */
|
||||
name: string;
|
||||
/** Resolved hostname */
|
||||
host: string;
|
||||
/** Service port */
|
||||
port: number;
|
||||
/** Resolved IPv4 address */
|
||||
address4?: string;
|
||||
/** Resolved IPv6 address */
|
||||
address6?: string;
|
||||
/** DNS TXT record key-value pairs */
|
||||
txtRecord: Record<string, string>;
|
||||
}
|
||||
|
||||
interface NativeDnsSdBrowser {
|
||||
start(): void;
|
||||
stop(): void;
|
||||
}
|
||||
const NativeDnsSdBrowser = native.DnsSdBrowser as {
|
||||
new (
|
||||
serviceType: string,
|
||||
callback: (eventType: string, data: Service | string) => void
|
||||
): NativeDnsSdBrowser;
|
||||
};
|
||||
|
||||
export interface DnsSdBrowserEvents {
|
||||
serviceUp: [service: Service];
|
||||
serviceDown: [name: string];
|
||||
}
|
||||
|
||||
export class DnsSdBrowser extends EventEmitter<DnsSdBrowserEvents> {
|
||||
private nativeBrowser: NativeDnsSdBrowser | null = null;
|
||||
|
||||
constructor(private serviceType: string) {
|
||||
super();
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
if (!this.nativeBrowser) {
|
||||
this.nativeBrowser = new NativeDnsSdBrowser(
|
||||
this.serviceType,
|
||||
(eventType, data) => {
|
||||
switch (eventType) {
|
||||
case "serviceUp":
|
||||
this.emit("serviceUp", data as Service);
|
||||
break;
|
||||
case "serviceDown":
|
||||
this.emit("serviceDown", data as string);
|
||||
break;
|
||||
}
|
||||
}
|
||||
);
|
||||
this.nativeBrowser.start();
|
||||
}
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
if (this.nativeBrowser) {
|
||||
this.nativeBrowser.stop();
|
||||
this.nativeBrowser = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
10
bridge/src/dns_sd/native/addon.cc
Normal file
10
bridge/src/dns_sd/native/addon.cc
Normal file
@@ -0,0 +1,10 @@
|
||||
#include "dns_sd_browser.h"
|
||||
|
||||
// Module init
|
||||
Napi::Object init(Napi::Env env, Napi::Object exports)
|
||||
{
|
||||
DnsSdBrowser::init(env, exports);
|
||||
return exports;
|
||||
}
|
||||
|
||||
NODE_API_MODULE(dns_sd, init)
|
||||
119
bridge/src/dns_sd/native/dns_sd_browser.cc
Normal file
119
bridge/src/dns_sd/native/dns_sd_browser.cc
Normal file
@@ -0,0 +1,119 @@
|
||||
#include "dns_sd_browser.h"
|
||||
|
||||
DnsSdBrowser::DnsSdBrowser(const Napi::CallbackInfo& info)
|
||||
: Napi::ObjectWrap<DnsSdBrowser>(info)
|
||||
, browser_(nullptr)
|
||||
, started_(false)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsFunction()) {
|
||||
Napi::TypeError::New(env, "Expected (serviceType: string, callback: Function)")
|
||||
.ThrowAsJavaScriptException();
|
||||
return;
|
||||
}
|
||||
service_type_ = info[0].As<Napi::String>().Utf8Value();
|
||||
tsfn_ = Napi::ThreadSafeFunction::New(
|
||||
env, info[1].As<Napi::Function>(), "DnsSdBrowserCallback", 0, 1);
|
||||
}
|
||||
|
||||
DnsSdBrowser::~DnsSdBrowser()
|
||||
{
|
||||
if (browser_) {
|
||||
browser_->stop();
|
||||
browser_.reset();
|
||||
}
|
||||
if (started_) {
|
||||
tsfn_.Release();
|
||||
started_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
Napi::Object DnsSdBrowser::init(Napi::Env env, Napi::Object exports)
|
||||
{
|
||||
Napi::Function func = DefineClass(env, "DnsSdBrowser",
|
||||
{
|
||||
InstanceMethod("start", &DnsSdBrowser::start),
|
||||
InstanceMethod("stop", &DnsSdBrowser::stop),
|
||||
});
|
||||
|
||||
exports.Set("DnsSdBrowser", func);
|
||||
return exports;
|
||||
}
|
||||
|
||||
Napi::Value DnsSdBrowser::start(const Napi::CallbackInfo& info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (started_) {
|
||||
return env.Undefined();
|
||||
}
|
||||
started_ = true;
|
||||
|
||||
tsfn_.Unref(env);
|
||||
|
||||
browser_ = std::make_unique<DnsSdPlatformBrowser>(service_type_, *this);
|
||||
browser_->start();
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
Napi::Value DnsSdBrowser::stop(const Napi::CallbackInfo& info)
|
||||
{
|
||||
Napi::Env env = info.Env();
|
||||
|
||||
if (!started_) {
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
if (browser_) {
|
||||
browser_->stop();
|
||||
browser_.reset();
|
||||
}
|
||||
|
||||
tsfn_.Release();
|
||||
started_ = false;
|
||||
|
||||
return env.Undefined();
|
||||
}
|
||||
|
||||
void DnsSdBrowser::on_service_up(const DnsSdService& service)
|
||||
{
|
||||
auto data = std::make_unique<DnsSdService>(service);
|
||||
|
||||
napi_status status = tsfn_.NonBlockingCall(
|
||||
data.get(), [](Napi::Env env, Napi::Function js_callback, DnsSdService* raw) {
|
||||
std::unique_ptr<DnsSdService> owned(raw);
|
||||
|
||||
Napi::Object obj = Napi::Object::New(env);
|
||||
obj.Set("name", Napi::String::New(env, owned->name));
|
||||
obj.Set("host", Napi::String::New(env, owned->host));
|
||||
obj.Set("port", Napi::Number::New(env, owned->port));
|
||||
if (!owned->address4.empty())
|
||||
obj.Set("address4", Napi::String::New(env, owned->address4));
|
||||
if (!owned->address6.empty())
|
||||
obj.Set("address6", Napi::String::New(env, owned->address6));
|
||||
|
||||
Napi::Object txt = Napi::Object::New(env);
|
||||
for (const auto& [key, value] : owned->txt_record) {
|
||||
txt.Set(key, Napi::String::New(env, value));
|
||||
}
|
||||
obj.Set("txtRecord", txt);
|
||||
|
||||
js_callback.Call({ Napi::String::New(env, "serviceUp"), obj });
|
||||
});
|
||||
if (status == napi_ok)
|
||||
data.release();
|
||||
}
|
||||
|
||||
void DnsSdBrowser::on_service_down(const std::string& name)
|
||||
{
|
||||
auto data = std::make_unique<std::string>(name);
|
||||
|
||||
napi_status status = tsfn_.NonBlockingCall(
|
||||
data.get(), [](Napi::Env env, Napi::Function js_callback, std::string* raw) {
|
||||
std::unique_ptr<std::string> owned(raw);
|
||||
js_callback.Call(
|
||||
{ Napi::String::New(env, "serviceDown"), Napi::String::New(env, *owned) });
|
||||
});
|
||||
if (status == napi_ok)
|
||||
data.release();
|
||||
}
|
||||
30
bridge/src/dns_sd/native/dns_sd_browser.h
Normal file
30
bridge/src/dns_sd/native/dns_sd_browser.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#ifndef DNS_SD_BROWSER_H
|
||||
#define DNS_SD_BROWSER_H
|
||||
|
||||
#include "dns_sd_platform_browser.h"
|
||||
|
||||
#include <memory>
|
||||
#include <napi.h>
|
||||
#include <string>
|
||||
|
||||
class DnsSdBrowser : public Napi::ObjectWrap<DnsSdBrowser>, public DnsSdPlatformBrowserDelegate {
|
||||
public:
|
||||
static Napi::Object init(Napi::Env env, Napi::Object exports);
|
||||
DnsSdBrowser(const Napi::CallbackInfo& info);
|
||||
~DnsSdBrowser();
|
||||
|
||||
// DnsSdPlatformBrowserDelegate
|
||||
void on_service_up(const DnsSdService& service) override;
|
||||
void on_service_down(const std::string& name) override;
|
||||
|
||||
private:
|
||||
Napi::Value start(const Napi::CallbackInfo& info);
|
||||
Napi::Value stop(const Napi::CallbackInfo& info);
|
||||
|
||||
std::string service_type_;
|
||||
Napi::ThreadSafeFunction tsfn_;
|
||||
std::unique_ptr<DnsSdPlatformBrowser> browser_;
|
||||
bool started_;
|
||||
};
|
||||
|
||||
#endif // DNS_SD_BROWSER_H
|
||||
49
bridge/src/dns_sd/native/dns_sd_platform_browser.h
Normal file
49
bridge/src/dns_sd/native/dns_sd_platform_browser.h
Normal file
@@ -0,0 +1,49 @@
|
||||
#ifndef DNS_SD_PLATFORM_BROWSER_H
|
||||
#define DNS_SD_PLATFORM_BROWSER_H
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Represents a resolved DNS-SD service.
|
||||
*/
|
||||
struct DnsSdService {
|
||||
std::string name;
|
||||
std::string host;
|
||||
uint16_t port = 0;
|
||||
std::string address4;
|
||||
std::string address6;
|
||||
std::map<std::string, std::string> txt_record;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delegate interface for receiving DNS-SD browser events.
|
||||
*/
|
||||
class DnsSdPlatformBrowserDelegate {
|
||||
public:
|
||||
virtual ~DnsSdPlatformBrowserDelegate() = default;
|
||||
virtual void on_service_up(const DnsSdService& service) = 0;
|
||||
virtual void on_service_down(const std::string& name) = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Platform-specific DNS-SD browser.
|
||||
* Implemented in dns_sd_platform_browser_unix.cc (macOS/Linux) and
|
||||
* dns_sd_platform_browser_win.cc (Windows).
|
||||
*/
|
||||
class DnsSdPlatformBrowser {
|
||||
public:
|
||||
DnsSdPlatformBrowser(const std::string& service_type, DnsSdPlatformBrowserDelegate& delegate);
|
||||
~DnsSdPlatformBrowser();
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
#endif // DNS_SD_PLATFORM_BROWSER_H
|
||||
262
bridge/src/dns_sd/native/dns_sd_platform_browser_unix.cc
Normal file
262
bridge/src/dns_sd/native/dns_sd_platform_browser_unix.cc
Normal file
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* DNS-SD browser implementation for macOS and Linux.
|
||||
*
|
||||
* Uses the dns_sd.h API (Apple's mDNSResponder on macOS and Avahi's compatibility layer for that
|
||||
* API (libdns_sd) on Linux) to browse for and resolve DNS-SD services.
|
||||
*
|
||||
* Address resolution uses POSIX getaddrinfo() on both platforms rather than DNSServiceGetAddrInfo,
|
||||
* which is not available in libdns_sd.
|
||||
*/
|
||||
|
||||
#include "dns_sd_platform_browser.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <dns_sd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
struct DnsSdPlatformBrowser::Impl {
|
||||
struct ResolveContext;
|
||||
|
||||
std::string service_type;
|
||||
DnsSdPlatformBrowserDelegate& delegate;
|
||||
|
||||
DNSServiceRef browse_ref;
|
||||
std::atomic<bool> is_started;
|
||||
std::thread event_loop_thread;
|
||||
|
||||
std::mutex pending_resolves_mutex;
|
||||
std::set<ResolveContext*> pending_resolves;
|
||||
|
||||
Impl(const std::string& service_type, DnsSdPlatformBrowserDelegate& delegate)
|
||||
: service_type(service_type)
|
||||
, delegate(delegate)
|
||||
, browse_ref(nullptr)
|
||||
, is_started(false)
|
||||
{
|
||||
}
|
||||
|
||||
~Impl() { stop(); }
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void event_loop();
|
||||
|
||||
// dns_sd callbacks
|
||||
static void DNSSD_API browse_callback(DNSServiceRef, DNSServiceFlags, uint32_t,
|
||||
DNSServiceErrorType, const char*, const char*, const char*, void*);
|
||||
|
||||
static void DNSSD_API resolve_callback(DNSServiceRef, DNSServiceFlags, uint32_t,
|
||||
DNSServiceErrorType, const char*, const char*, uint16_t, uint16_t, const unsigned char*,
|
||||
void*);
|
||||
};
|
||||
|
||||
struct DnsSdPlatformBrowser::Impl::ResolveContext {
|
||||
Impl* impl;
|
||||
std::string service_name;
|
||||
DNSServiceRef resolve_ref;
|
||||
bool destroyed;
|
||||
|
||||
ResolveContext()
|
||||
: impl(nullptr)
|
||||
, resolve_ref(nullptr)
|
||||
, destroyed(false)
|
||||
{
|
||||
}
|
||||
~ResolveContext()
|
||||
{
|
||||
if (resolve_ref)
|
||||
DNSServiceRefDeallocate(resolve_ref);
|
||||
}
|
||||
};
|
||||
|
||||
DnsSdPlatformBrowser::DnsSdPlatformBrowser(
|
||||
const std::string& service_type, DnsSdPlatformBrowserDelegate& delegate)
|
||||
: impl_(std::make_unique<Impl>(service_type, delegate))
|
||||
{
|
||||
}
|
||||
|
||||
DnsSdPlatformBrowser::~DnsSdPlatformBrowser() = default;
|
||||
|
||||
void DnsSdPlatformBrowser::start() { impl_->start(); }
|
||||
|
||||
void DnsSdPlatformBrowser::stop() { impl_->stop(); }
|
||||
|
||||
void DnsSdPlatformBrowser::Impl::start()
|
||||
{
|
||||
if (is_started)
|
||||
return;
|
||||
is_started = true;
|
||||
|
||||
DNSServiceErrorType err = DNSServiceBrowse(&browse_ref, 0, kDNSServiceInterfaceIndexAny,
|
||||
service_type.c_str(), nullptr, browse_callback, this);
|
||||
|
||||
if (err != kDNSServiceErr_NoError) {
|
||||
ERROR_LOG("browse failed with error %d", err);
|
||||
is_started = false;
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOG("browse started for %s", service_type.c_str());
|
||||
// Poll on background thread
|
||||
event_loop_thread = std::thread(&Impl::event_loop, this);
|
||||
}
|
||||
|
||||
void DnsSdPlatformBrowser::Impl::stop()
|
||||
{
|
||||
if (!is_started)
|
||||
return;
|
||||
is_started = false;
|
||||
|
||||
if (event_loop_thread.joinable()) {
|
||||
event_loop_thread.join();
|
||||
}
|
||||
|
||||
// Clean up pending resolves
|
||||
{
|
||||
std::scoped_lock lock(pending_resolves_mutex);
|
||||
for (auto* ctx : pending_resolves) {
|
||||
delete ctx;
|
||||
}
|
||||
pending_resolves.clear();
|
||||
}
|
||||
|
||||
DNSServiceRefDeallocate(browse_ref);
|
||||
}
|
||||
|
||||
/**
|
||||
* Background thread that waits for DNS-SD socket events. When data is available, calls
|
||||
* DNSServiceProcessResult to trigger the registered callbacks.
|
||||
*/
|
||||
void DnsSdPlatformBrowser::Impl::event_loop()
|
||||
{
|
||||
while (is_started) {
|
||||
int max_fd = 0;
|
||||
fd_set read_fds;
|
||||
FD_ZERO(&read_fds);
|
||||
|
||||
// Add the browse socket
|
||||
int browse_fd = DNSServiceRefSockFD(browse_ref);
|
||||
FD_SET(browse_fd, &read_fds);
|
||||
max_fd = browse_fd;
|
||||
|
||||
// Add resolve sockets
|
||||
{
|
||||
std::scoped_lock lock(pending_resolves_mutex);
|
||||
for (auto* ctx : pending_resolves) {
|
||||
int fd = DNSServiceRefSockFD(ctx->resolve_ref);
|
||||
FD_SET(fd, &read_fds);
|
||||
if (fd > max_fd)
|
||||
max_fd = fd;
|
||||
}
|
||||
}
|
||||
|
||||
struct timeval tv {
|
||||
.tv_sec = 0, .tv_usec = 250000
|
||||
}; // 250ms
|
||||
int result = select(max_fd + 1, &read_fds, nullptr, nullptr, &tv);
|
||||
|
||||
if (result <= 0 || !is_started)
|
||||
continue;
|
||||
|
||||
// Process browse ref
|
||||
if (FD_ISSET(browse_fd, &read_fds)) {
|
||||
DNSServiceProcessResult(browse_ref);
|
||||
}
|
||||
|
||||
// Process resolve refs
|
||||
{
|
||||
std::scoped_lock lock(pending_resolves_mutex);
|
||||
for (auto it = pending_resolves.begin(); it != pending_resolves.end();) {
|
||||
auto* ctx = *it;
|
||||
int fd = DNSServiceRefSockFD(ctx->resolve_ref);
|
||||
if (FD_ISSET(fd, &read_fds)) {
|
||||
DNSServiceProcessResult(ctx->resolve_ref);
|
||||
}
|
||||
if (ctx->destroyed) {
|
||||
it = pending_resolves.erase(it);
|
||||
delete ctx;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DNSSD_API DnsSdPlatformBrowser::Impl::browse_callback(DNSServiceRef, DNSServiceFlags flags,
|
||||
uint32_t interface_index, DNSServiceErrorType error_code, const char* service_name,
|
||||
const char* reg_type, const char* reply_domain, void* context)
|
||||
{
|
||||
if (error_code != kDNSServiceErr_NoError)
|
||||
return;
|
||||
|
||||
auto* impl = static_cast<Impl*>(context);
|
||||
if (!impl->is_started)
|
||||
return;
|
||||
|
||||
if (flags & kDNSServiceFlagsAdd) {
|
||||
DEBUG_LOG("browse found: %s (ifindex=%u)", service_name, interface_index);
|
||||
// New service found, resolve it
|
||||
auto* ctx = new ResolveContext();
|
||||
ctx->impl = impl;
|
||||
ctx->service_name = service_name;
|
||||
|
||||
if (DNSServiceResolve(&ctx->resolve_ref, 0, interface_index, service_name, reg_type,
|
||||
reply_domain, resolve_callback, ctx)
|
||||
!= kDNSServiceErr_NoError) {
|
||||
delete ctx;
|
||||
return;
|
||||
}
|
||||
std::scoped_lock lock(impl->pending_resolves_mutex);
|
||||
impl->pending_resolves.insert(ctx);
|
||||
} else {
|
||||
// Service disappeared
|
||||
DEBUG_LOG("service removed: %s", service_name);
|
||||
impl->delegate.on_service_down(service_name);
|
||||
}
|
||||
}
|
||||
|
||||
void DNSSD_API DnsSdPlatformBrowser::Impl::resolve_callback(DNSServiceRef, DNSServiceFlags,
|
||||
uint32_t, DNSServiceErrorType error_code, const char*, const char* hosttarget, uint16_t port,
|
||||
uint16_t txt_len, const unsigned char* txt_record, void* context)
|
||||
{
|
||||
auto* ctx = static_cast<ResolveContext*>(context);
|
||||
if (error_code != kDNSServiceErr_NoError || !ctx->impl->is_started) {
|
||||
ctx->destroyed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
DnsSdService info;
|
||||
info.name = ctx->service_name;
|
||||
info.host = hosttarget;
|
||||
info.port = ntohs(port);
|
||||
|
||||
// Parse TXT record into key-value map
|
||||
{
|
||||
uint16_t count = TXTRecordGetCount(txt_len, txt_record);
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
char key[256];
|
||||
uint8_t value_len = 0;
|
||||
const void* value = nullptr;
|
||||
auto err = TXTRecordGetItemAtIndex(
|
||||
txt_len, txt_record, i, sizeof(key), key, &value_len, &value);
|
||||
if (err == kDNSServiceErr_NoError) {
|
||||
info.txt_record[key] = (value && value_len > 0)
|
||||
? std::string(static_cast<const char*>(value), value_len)
|
||||
: "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve v4/v6 addresses via getaddrinfo
|
||||
resolve_addresses(hosttarget, info.address4, info.address6);
|
||||
|
||||
DEBUG_LOG("resolved: %s -> %s:%d (%s / %s)", info.name.c_str(), info.host.c_str(), info.port,
|
||||
info.address4.c_str(), info.address6.c_str());
|
||||
ctx->impl->delegate.on_service_up(info);
|
||||
ctx->destroyed = true;
|
||||
}
|
||||
362
bridge/src/dns_sd/native/dns_sd_platform_browser_win.cc
Normal file
362
bridge/src/dns_sd/native/dns_sd_platform_browser_win.cc
Normal file
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* DNS-SD browser implementation for Windows.
|
||||
*
|
||||
* Uses the DNS-SD functions of the Windows DNS API (Windns.h) available on Windows 10+ without any
|
||||
* third-party dependencies like Bonjour.
|
||||
*/
|
||||
|
||||
#include "dns_sd_platform_browser.h"
|
||||
|
||||
#define NOMINMAX
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#include <windns.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
/** Convert a wide string to a UTF-8 std::string. */
|
||||
std::string wide_to_utf8(const wchar_t* wide)
|
||||
{
|
||||
if (!wide)
|
||||
return "";
|
||||
int len = WideCharToMultiByte(CP_UTF8, 0, wide, -1, nullptr, 0, nullptr, nullptr);
|
||||
if (len <= 0)
|
||||
return "";
|
||||
std::string result(len - 1, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide, -1, &result[0], len, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Convert a UTF-8 std::string to a wide string. */
|
||||
std::wstring utf8_to_wide(const std::string& utf8)
|
||||
{
|
||||
if (utf8.empty())
|
||||
return L"";
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, nullptr, 0);
|
||||
if (len <= 0)
|
||||
return L"";
|
||||
std::wstring result(len - 1, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, &result[0], len);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
struct DnsSdPlatformBrowser::Impl {
|
||||
/* Service type for browse operation. */
|
||||
std::string service_type;
|
||||
/* Delegate to receive browser events. */
|
||||
DnsSdPlatformBrowserDelegate& delegate;
|
||||
|
||||
/** Represents the current browse operation. */
|
||||
struct BrowseContext {
|
||||
/** Query name (owns storage for `request.QueryName`). */
|
||||
std::wstring query_name;
|
||||
DNS_SERVICE_BROWSE_REQUEST request;
|
||||
DNS_SERVICE_CANCEL cancel;
|
||||
|
||||
BrowseContext()
|
||||
: request {}
|
||||
, cancel {}
|
||||
{
|
||||
}
|
||||
};
|
||||
/* Whether browse operation is ongoing. */
|
||||
std::atomic<bool> is_started;
|
||||
BrowseContext browse;
|
||||
|
||||
/** Represents a resolve operation triggered by the current browse operation. */
|
||||
struct ResolveContext {
|
||||
Impl* impl;
|
||||
bool cancelled;
|
||||
DWORD ttl;
|
||||
std::string service_name;
|
||||
std::wstring query_name;
|
||||
DNS_SERVICE_RESOLVE_REQUEST resolve_request;
|
||||
DNS_SERVICE_CANCEL resolve_cancel;
|
||||
|
||||
ResolveContext()
|
||||
: impl(nullptr)
|
||||
, cancelled(false)
|
||||
, ttl(0)
|
||||
, resolve_request {}
|
||||
, resolve_cancel {}
|
||||
{
|
||||
}
|
||||
};
|
||||
// Stored contexts for ongoing resolve operations
|
||||
std::set<ResolveContext*> active_resolves;
|
||||
std::mutex active_resolves_mutex;
|
||||
|
||||
// WinDNS doesn't have a builtin mechanism to notify us when a service disappears, so we
|
||||
// keep a record of found services and expire them (emitting a `service_down` event) based on
|
||||
// their TTL unless they're refreshed by a subsequent browse callback.
|
||||
std::map<std::string, std::chrono::steady_clock::time_point> expiring_services;
|
||||
std::mutex expiring_services_mutex;
|
||||
std::thread expiry_thread;
|
||||
std::condition_variable expiry_cv;
|
||||
|
||||
Impl(const std::string& type, DnsSdPlatformBrowserDelegate& del)
|
||||
: service_type(type)
|
||||
, delegate(del)
|
||||
, is_started(false)
|
||||
{
|
||||
}
|
||||
|
||||
~Impl() { stop(); }
|
||||
|
||||
void start();
|
||||
void stop();
|
||||
void expiry_loop();
|
||||
|
||||
static void WINAPI browse_callback(DWORD status, PVOID context, PDNS_RECORD query_results);
|
||||
|
||||
static void WINAPI resolve_callback(
|
||||
DWORD status, PVOID context, PDNS_SERVICE_INSTANCE service_instance);
|
||||
};
|
||||
|
||||
DnsSdPlatformBrowser::DnsSdPlatformBrowser(
|
||||
const std::string& service_type, DnsSdPlatformBrowserDelegate& delegate)
|
||||
: impl_(std::make_unique<Impl>(service_type, delegate))
|
||||
{
|
||||
}
|
||||
|
||||
DnsSdPlatformBrowser::~DnsSdPlatformBrowser() = default;
|
||||
|
||||
void DnsSdPlatformBrowser::start() { impl_->start(); }
|
||||
|
||||
void DnsSdPlatformBrowser::stop() { impl_->stop(); }
|
||||
|
||||
void DnsSdPlatformBrowser::Impl::start()
|
||||
{
|
||||
if (is_started)
|
||||
return;
|
||||
is_started = true;
|
||||
|
||||
WSADATA wsa_data;
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||
|
||||
// Windns expects service name with a .local suffix
|
||||
browse.query_name = utf8_to_wide(service_type + ".local");
|
||||
|
||||
browse.request.Version = DNS_QUERY_REQUEST_VERSION1;
|
||||
browse.request.InterfaceIndex = 0;
|
||||
browse.request.QueryName = browse.query_name.c_str();
|
||||
browse.request.pBrowseCallback = browse_callback;
|
||||
browse.request.pQueryContext = this;
|
||||
|
||||
DNS_STATUS status = DnsServiceBrowse(&browse.request, &browse.cancel);
|
||||
if (status != DNS_REQUEST_PENDING && status != ERROR_SUCCESS) {
|
||||
ERROR_LOG("browse failed with status %lu", status);
|
||||
is_started = false;
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_LOG("browse started for %s", service_type.c_str());
|
||||
|
||||
// Start expiry loop on background thread
|
||||
expiry_thread = std::thread(&Impl::expiry_loop, this);
|
||||
}
|
||||
|
||||
void DnsSdPlatformBrowser::Impl::stop()
|
||||
{
|
||||
if (!is_started)
|
||||
return;
|
||||
is_started = false;
|
||||
|
||||
// Cancel browse operation
|
||||
DnsServiceBrowseCancel(&browse.cancel);
|
||||
|
||||
// Cancel and cleanup active resolves
|
||||
{
|
||||
std::scoped_lock lock(active_resolves_mutex);
|
||||
for (auto* ctx : active_resolves) {
|
||||
if (!ctx->cancelled) {
|
||||
ctx->cancelled = true;
|
||||
DnsServiceResolveCancel(&ctx->resolve_cancel);
|
||||
}
|
||||
}
|
||||
active_resolves.clear();
|
||||
}
|
||||
|
||||
// Wake and join expiry thread
|
||||
expiry_cv.notify_all();
|
||||
if (expiry_thread.joinable())
|
||||
expiry_thread.join();
|
||||
{
|
||||
std::scoped_lock lock(expiring_services_mutex);
|
||||
expiring_services.clear();
|
||||
}
|
||||
|
||||
WSACleanup();
|
||||
}
|
||||
|
||||
void DnsSdPlatformBrowser::Impl::expiry_loop()
|
||||
{
|
||||
std::unique_lock lock(expiring_services_mutex);
|
||||
while (is_started) {
|
||||
// Check for expired services and calculate the next expiry time (if any) from the tracked
|
||||
// expiring services.
|
||||
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
std::chrono::steady_clock::time_point next_expiry
|
||||
= std::chrono::steady_clock::time_point::max();
|
||||
for (auto it = expiring_services.begin(); it != expiring_services.end();) {
|
||||
auto& [name, expires_at] = *it;
|
||||
if (expires_at <= now) {
|
||||
// Service expired without a new browse result, so we should treat this as the
|
||||
// service becoming unavailable and emit a `service_down` event.
|
||||
DEBUG_LOG("service expired: %s", name.c_str());
|
||||
std::string expired_name = name;
|
||||
it = expiring_services.erase(it);
|
||||
lock.unlock();
|
||||
delegate.on_service_down(expired_name);
|
||||
lock.lock();
|
||||
} else {
|
||||
// Update expiry time if this service expires sooner than the current next_expiry.
|
||||
auto expiry_s
|
||||
= std::chrono::duration_cast<std::chrono::seconds>(expires_at - now).count();
|
||||
DEBUG_LOG("service %s expires in %llds", name.c_str(), expiry_s);
|
||||
if (expires_at < next_expiry)
|
||||
next_expiry = expires_at;
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if (next_expiry == std::chrono::steady_clock::time_point::max()) {
|
||||
// Wait until browse operation stopped (which subsequently ends the loop at this
|
||||
// iteration) or a new service is added (which updates the expiry time).
|
||||
expiry_cv.wait(lock, [&] { return !is_started || !expiring_services.empty(); });
|
||||
} else {
|
||||
// Wait until the next service expiry time
|
||||
expiry_cv.wait_until(lock, next_expiry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WINAPI DnsSdPlatformBrowser::Impl::browse_callback(
|
||||
DWORD status, PVOID context, PDNS_RECORD query_results)
|
||||
{
|
||||
auto* impl = static_cast<Impl*>(context);
|
||||
|
||||
ScopeGuard free_records { [&] {
|
||||
if (query_results)
|
||||
DnsRecordListFree(query_results, DnsFreeRecordList);
|
||||
} };
|
||||
|
||||
if (!impl->is_started)
|
||||
return;
|
||||
if (status != ERROR_SUCCESS || !query_results)
|
||||
return;
|
||||
|
||||
// Walk the record chain for PTR records (representing service instances)
|
||||
for (PDNS_RECORD record = query_results; record; record = record->pNext) {
|
||||
if (record->wType != DNS_TYPE_PTR)
|
||||
continue;
|
||||
|
||||
auto* resolve_ctx = new ResolveContext();
|
||||
resolve_ctx->impl = impl;
|
||||
resolve_ctx->ttl = record->dwTtl;
|
||||
|
||||
std::string instance_name = wide_to_utf8(record->Data.PTR.pNameHost);
|
||||
DEBUG_LOG("browse found: %s (ttl=%lu)", instance_name.c_str(), record->dwTtl);
|
||||
|
||||
// Strip everything after (and including) the first dot to get the service name
|
||||
size_t dot = instance_name.find('.');
|
||||
resolve_ctx->service_name
|
||||
= (dot != std::string::npos) ? instance_name.substr(0, dot) : instance_name;
|
||||
|
||||
resolve_ctx->query_name = utf8_to_wide(instance_name);
|
||||
|
||||
// Populate resolve request
|
||||
resolve_ctx->resolve_request.Version = DNS_QUERY_REQUEST_VERSION1;
|
||||
resolve_ctx->resolve_request.InterfaceIndex = 0;
|
||||
resolve_ctx->resolve_request.QueryName = resolve_ctx->query_name.data();
|
||||
resolve_ctx->resolve_request.pResolveCompletionCallback = resolve_callback;
|
||||
resolve_ctx->resolve_request.pQueryContext = resolve_ctx;
|
||||
|
||||
// Start the resolve operation
|
||||
{
|
||||
std::scoped_lock lock(impl->active_resolves_mutex);
|
||||
DNS_STATUS resolve_status
|
||||
= DnsServiceResolve(&resolve_ctx->resolve_request, &resolve_ctx->resolve_cancel);
|
||||
switch (resolve_status) {
|
||||
case ERROR_SUCCESS:
|
||||
case DNS_REQUEST_PENDING:
|
||||
impl->active_resolves.insert(resolve_ctx);
|
||||
break;
|
||||
default:
|
||||
delete resolve_ctx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WINAPI DnsSdPlatformBrowser::Impl::resolve_callback(
|
||||
DWORD status, PVOID context, PDNS_SERVICE_INSTANCE service_instance)
|
||||
{
|
||||
auto* resolve_ctx = static_cast<ResolveContext*>(context);
|
||||
auto* impl = resolve_ctx->impl;
|
||||
|
||||
ScopeGuard defer { [&] {
|
||||
if (service_instance)
|
||||
DnsServiceFreeInstance(service_instance);
|
||||
{
|
||||
std::scoped_lock lock(impl->active_resolves_mutex);
|
||||
impl->active_resolves.erase(resolve_ctx);
|
||||
}
|
||||
delete resolve_ctx;
|
||||
} };
|
||||
|
||||
// If the browse operation is still active, the resolve operation was not cancelled, and we got
|
||||
// a valid result, emit a service_up event
|
||||
if (impl->is_started && !resolve_ctx->cancelled && status == ERROR_SUCCESS
|
||||
&& service_instance) {
|
||||
DnsSdService service;
|
||||
service.name = resolve_ctx->service_name;
|
||||
service.host = wide_to_utf8(service_instance->pszHostName);
|
||||
service.port = service_instance->wPort;
|
||||
|
||||
// Extract TXT record key-value pairs
|
||||
if (service_instance->dwPropertyCount > 0 && service_instance->keys
|
||||
&& service_instance->values) {
|
||||
for (DWORD i = 0; i < service_instance->dwPropertyCount; i++) {
|
||||
if (service_instance->keys[i]) {
|
||||
std::string key = wide_to_utf8(service_instance->keys[i]);
|
||||
std::string value;
|
||||
if (service_instance->values[i])
|
||||
value = wide_to_utf8(service_instance->values[i]);
|
||||
service.txt_record[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve v4/v6 addresses via getaddrinfo
|
||||
resolve_addresses(service.host, service.address4, service.address6);
|
||||
|
||||
if (impl->is_started) {
|
||||
// Schedule service expiry
|
||||
{
|
||||
std::scoped_lock svc_lock(impl->expiring_services_mutex);
|
||||
impl->expiring_services[service.name]
|
||||
= std::chrono::steady_clock::now() + std::chrono::seconds(resolve_ctx->ttl);
|
||||
}
|
||||
impl->expiry_cv.notify_one();
|
||||
|
||||
DEBUG_LOG("resolved: %s -> %s:%d (%s / %s, ttl=%lus)", service.name.c_str(),
|
||||
service.host.c_str(), service.port, service.address4.c_str(),
|
||||
service.address6.c_str(), resolve_ctx->ttl);
|
||||
|
||||
// Emit service_up event with merged addresses
|
||||
impl->delegate.on_service_up(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
bridge/src/dns_sd/native/utils.h
Normal file
55
bridge/src/dns_sd/native/utils.h
Normal file
@@ -0,0 +1,55 @@
|
||||
#ifndef DNS_SD_UTILS_H_
|
||||
#define DNS_SD_UTILS_H_
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#else
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include <netinet/in.h>
|
||||
#endif
|
||||
|
||||
/** Defer-cleanup util class. */
|
||||
template <typename F> struct [[nodiscard]] ScopeGuard {
|
||||
F fn;
|
||||
~ScopeGuard() { fn(); }
|
||||
};
|
||||
|
||||
#ifdef DNS_SD_DEBUG
|
||||
#define DEBUG_LOG(fmt, ...) std::fprintf(stderr, "[dns_sd] " fmt "\n", ##__VA_ARGS__)
|
||||
#else
|
||||
#define DEBUG_LOG(fmt, ...) ((void)0)
|
||||
#endif
|
||||
|
||||
#define ERROR_LOG(fmt, ...) std::fprintf(stderr, "[dns_sd] " fmt "\n", ##__VA_ARGS__)
|
||||
|
||||
/** Resolves a hostname to IPv4/v6 address strings via getaddrinfo. */
|
||||
inline void resolve_addresses(
|
||||
const std::string& hostname, std::string& out_ipv4, std::string& out_ipv6)
|
||||
{
|
||||
addrinfo hints { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM };
|
||||
addrinfo* result = nullptr;
|
||||
if (getaddrinfo(hostname.c_str(), nullptr, &hints, &result) != 0)
|
||||
return;
|
||||
|
||||
for (addrinfo* p = result; p; p = p->ai_next) {
|
||||
if (p->ai_family == AF_INET && out_ipv4.empty()) {
|
||||
char buf[INET_ADDRSTRLEN];
|
||||
auto* addr = reinterpret_cast<sockaddr_in*>(p->ai_addr);
|
||||
if (inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf)))
|
||||
out_ipv4 = buf;
|
||||
} else if (p->ai_family == AF_INET6 && out_ipv6.empty()) {
|
||||
char buf[INET6_ADDRSTRLEN];
|
||||
auto* addr = reinterpret_cast<sockaddr_in6*>(p->ai_addr);
|
||||
if (inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf)))
|
||||
out_ipv6 = buf;
|
||||
}
|
||||
}
|
||||
freeaddrinfo(result);
|
||||
}
|
||||
|
||||
#endif // DNS_SD_UTILS_H_
|
||||
Reference in New Issue
Block a user