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/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
bridge/node_modules/
|
bridge/node_modules/
|
||||||
bridge/build
|
bridge/build/
|
||||||
extension/node_modules/
|
extension/node_modules/
|
||||||
test/ChromeProfile/
|
test/ChromeProfile/
|
||||||
.idea/
|
.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 __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
const ROOT_PATH = path.join(__dirname, "..");
|
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 = {
|
const spawnOptions = {
|
||||||
shell: true,
|
shell: true,
|
||||||
@@ -70,15 +70,14 @@ const spawnOptions = {
|
|||||||
* build directories, just in case.
|
* build directories, just in case.
|
||||||
*/
|
*/
|
||||||
fs.rmSync(BUILD_PATH, { force: true, recursive: true });
|
fs.rmSync(BUILD_PATH, { force: true, recursive: true });
|
||||||
fs.rmSync(paths.DIST_PATH, { force: true, recursive: true });
|
|
||||||
fs.mkdirSync(BUILD_PATH, { recursive: true });
|
fs.mkdirSync(BUILD_PATH, { recursive: true });
|
||||||
fs.mkdirSync(paths.DIST_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(
|
const NATIVE_BINDING_PATH = path.join(ROOT_PATH, "build/Release");
|
||||||
__dirname,
|
const NATIVE_BINDING_NAME = "dns_sd.node";
|
||||||
"../node_modules/mdns/build/Release/"
|
|
||||||
);
|
|
||||||
const MDNS_BINDING_NAME = "dns_sd_bindings.node";
|
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
// Run tsc
|
// Run tsc
|
||||||
@@ -140,8 +139,8 @@ async function build() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
fs.copySync(
|
fs.copySync(
|
||||||
path.join(MDNS_BINDING_PATH, MDNS_BINDING_NAME),
|
path.join(NATIVE_BINDING_PATH, NATIVE_BINDING_NAME),
|
||||||
path.join(BUILD_PATH, MDNS_BINDING_NAME)
|
path.join(BUILD_PATH, NATIVE_BINDING_NAME)
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.rmSync(path.join(BUILD_PATH, "src"), {
|
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));
|
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
|
// Write app manifest
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
||||||
@@ -318,8 +329,8 @@ function packageDarwin(
|
|||||||
path.join(rootExecutableDirectory, platformExecutableName)
|
path.join(rootExecutableDirectory, platformExecutableName)
|
||||||
);
|
);
|
||||||
fs.moveSync(
|
fs.moveSync(
|
||||||
path.join(BUILD_PATH, MDNS_BINDING_NAME),
|
path.join(BUILD_PATH, NATIVE_BINDING_NAME),
|
||||||
path.join(rootExecutableDirectory, MDNS_BINDING_NAME)
|
path.join(rootExecutableDirectory, NATIVE_BINDING_NAME)
|
||||||
);
|
);
|
||||||
fs.moveSync(
|
fs.moveSync(
|
||||||
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
||||||
@@ -416,8 +427,8 @@ function packageLinuxDeb(
|
|||||||
path.join(rootExecutableDirectory, platformExecutableName)
|
path.join(rootExecutableDirectory, platformExecutableName)
|
||||||
);
|
);
|
||||||
fs.moveSync(
|
fs.moveSync(
|
||||||
path.join(BUILD_PATH, MDNS_BINDING_NAME),
|
path.join(BUILD_PATH, NATIVE_BINDING_NAME),
|
||||||
path.join(rootExecutableDirectory, MDNS_BINDING_NAME)
|
path.join(rootExecutableDirectory, NATIVE_BINDING_NAME)
|
||||||
);
|
);
|
||||||
fs.moveSync(
|
fs.moveSync(
|
||||||
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
path.join(BUILD_PATH, paths.MANIFEST_NAME),
|
||||||
@@ -490,7 +501,7 @@ function packageLinuxRpm(
|
|||||||
manifestPath: platformManifestDirectory,
|
manifestPath: platformManifestDirectory,
|
||||||
executableName: platformExecutableName,
|
executableName: platformExecutableName,
|
||||||
manifestName: paths.MANIFEST_NAME,
|
manifestName: paths.MANIFEST_NAME,
|
||||||
bindingName: MDNS_BINDING_NAME
|
bindingName: NATIVE_BINDING_NAME
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
@@ -539,7 +550,7 @@ function packageWin32(
|
|||||||
executableName: platformExecutableName,
|
executableName: platformExecutableName,
|
||||||
executablePath: platformExecutableDirectory,
|
executablePath: platformExecutableDirectory,
|
||||||
manifestName: paths.MANIFEST_NAME,
|
manifestName: paths.MANIFEST_NAME,
|
||||||
bindingName: MDNS_BINDING_NAME,
|
bindingName: NATIVE_BINDING_NAME,
|
||||||
winRegistryKey: paths.REGISTRY_KEY,
|
winRegistryKey: paths.REGISTRY_KEY,
|
||||||
outputName,
|
outputName,
|
||||||
licensePath: paths.LICENSE_PATH,
|
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,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
"bplist-creator": "^0.1.0",
|
"bplist-creator": "^0.1.0",
|
||||||
"bplist-parser": "^0.3.1",
|
"bplist-parser": "^0.3.1",
|
||||||
"castv2": "^0.1.10",
|
"castv2": "^0.1.10",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"fast-srp-hap": "^2.0.4",
|
"fast-srp-hap": "^2.0.4",
|
||||||
"mdns": "^2.7.2",
|
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
"node-addon-api": "^8.0.0",
|
||||||
"node-fetch": "^3.2.10",
|
"node-fetch": "^3.2.10",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"ws": "^8.5.0",
|
"ws": "^8.5.0",
|
||||||
"yargs": "^17.5.1"
|
"yargs": "^17.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mdns": "^0.0.34",
|
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
@@ -32,6 +33,14 @@
|
|||||||
"typescript": "^5.7.0"
|
"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": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.16.7",
|
"version": "7.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||||
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
"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": {
|
"node_modules/@types/mime-types": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
||||||
@@ -328,9 +328,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bindings": {
|
"node_modules/bindings": {
|
||||||
"version": "1.2.1",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
"integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/bl": {
|
"node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
@@ -776,6 +780,12 @@
|
|||||||
"node": "^12.20 || >= 14.13"
|
"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": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -1115,16 +1125,6 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -1242,11 +1242,6 @@
|
|||||||
"mustache": "bin/mustache"
|
"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": {
|
"node_modules/napi-build-utils": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
||||||
@@ -1271,6 +1266,15 @@
|
|||||||
"semver": "bin/semver"
|
"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": {
|
"node_modules/node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
|
||||||
"integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
|
"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": {
|
"@types/mime-types": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
|
||||||
@@ -2522,9 +2517,12 @@
|
|||||||
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
|
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
|
||||||
},
|
},
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"version": "1.2.1",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||||
"integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE="
|
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||||
|
"requires": {
|
||||||
|
"file-uri-to-path": "1.0.0"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"bl": {
|
"bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
@@ -2848,6 +2846,11 @@
|
|||||||
"web-streams-polyfill": "^3.0.3"
|
"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": {
|
"fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -3108,15 +3111,6 @@
|
|||||||
"yallist": "^4.0.0"
|
"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": {
|
"merge2": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||||
@@ -3198,11 +3192,6 @@
|
|||||||
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
|
||||||
"dev": true
|
"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": {
|
"napi-build-utils": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz",
|
"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": {
|
"node-domexception": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
|||||||
@@ -2,25 +2,26 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node bin/build.js",
|
"build": "node bin/build.js",
|
||||||
|
"install": "node-gyp rebuild",
|
||||||
"package": "node bin/build.js --package",
|
"package": "node bin/build.js --package",
|
||||||
"install-manifest": "node bin/install-manifest.js",
|
"install-manifest": "node bin/install-manifest.js",
|
||||||
"remove-manifest": "node bin/install-manifest.js --remove"
|
"remove-manifest": "node bin/install-manifest.js --remove"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bindings": "^1.5.0",
|
||||||
"bplist-creator": "^0.1.0",
|
"bplist-creator": "^0.1.0",
|
||||||
"bplist-parser": "^0.3.1",
|
"bplist-parser": "^0.3.1",
|
||||||
"castv2": "^0.1.10",
|
"castv2": "^0.1.10",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"fast-srp-hap": "^2.0.4",
|
"fast-srp-hap": "^2.0.4",
|
||||||
"mdns": "^2.7.2",
|
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
|
"node-addon-api": "^8.0.0",
|
||||||
"node-fetch": "^3.2.10",
|
"node-fetch": "^3.2.10",
|
||||||
"tweetnacl": "^1.0.3",
|
"tweetnacl": "^1.0.3",
|
||||||
"ws": "^8.5.0",
|
"ws": "^8.5.0",
|
||||||
"yargs": "^17.5.1"
|
"yargs": "^17.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/mdns": "^0.0.34",
|
|
||||||
"@types/mime-types": "^2.1.1",
|
"@types/mime-types": "^2.1.1",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
|||||||
@@ -35,16 +35,12 @@ SetCompressor /SOLID LZMA
|
|||||||
!insertmacro MUI_LANGUAGE "German"
|
!insertmacro MUI_LANGUAGE "German"
|
||||||
|
|
||||||
# lang:en
|
# lang:en
|
||||||
LangString MSG__INSTALL_BONJOUR ${LANG_ENGLISH} \
|
|
||||||
"Install Bonjour dependency?"
|
|
||||||
LangString MSG__FIREFOX_OPEN ${LANG_ENGLISH} \
|
LangString MSG__FIREFOX_OPEN ${LANG_ENGLISH} \
|
||||||
"Firefox must be closed during uninstallation if the extension is \
|
"Firefox must be closed during uninstallation if the extension is \
|
||||||
installed. Close Firefox and click $\"Retry$\", click $\"Ignore$\" \
|
installed. Close Firefox and click $\"Retry$\", click $\"Ignore$\" \
|
||||||
to force close or $\"Abort$\" to cancel uninstallation."
|
to force close or $\"Abort$\" to cancel uninstallation."
|
||||||
|
|
||||||
# lang:es
|
# lang:es
|
||||||
LangString MSG__INSTALL_BONJOUR ${LANG_SPANISH} \
|
|
||||||
"¿Instalar dependencia Bonjour?"
|
|
||||||
LangString MSG__FIREFOX_OPEN ${LANG_SPANISH} \
|
LangString MSG__FIREFOX_OPEN ${LANG_SPANISH} \
|
||||||
"Firefox debe estar cerrado durante la desinstalación si la extensión \
|
"Firefox debe estar cerrado durante la desinstalación si la extensión \
|
||||||
está instalada. Cierra Firefox y aprieta $\"Reintentar$\", aprieta \
|
está instalada. Cierra Firefox y aprieta $\"Reintentar$\", aprieta \
|
||||||
@@ -52,8 +48,6 @@ LangString MSG__FIREFOX_OPEN ${LANG_SPANISH} \
|
|||||||
desinstalación."
|
desinstalación."
|
||||||
|
|
||||||
# lang:de
|
# lang:de
|
||||||
LangString MSG__INSTALL_BONJOUR ${LANG_GERMAN} \
|
|
||||||
"Bonjour installieren?"
|
|
||||||
LangString MSG__FIREFOX_OPEN ${LANG_GERMAN} \
|
LangString MSG__FIREFOX_OPEN ${LANG_GERMAN} \
|
||||||
"Firefox muss während der Deinstallation geschlossen werden, wenn die \
|
"Firefox muss während der Deinstallation geschlossen werden, wenn die \
|
||||||
Erweiterung installiert ist. Schließen Sie Firefox und klicken Sie auf \
|
Erweiterung installiert ist. Schließen Sie Firefox und klicken Sie auf \
|
||||||
@@ -86,23 +80,6 @@ Section
|
|||||||
File "{{bindingName}}"
|
File "{{bindingName}}"
|
||||||
File "{{manifestName}}"
|
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
|
# Native manifest key
|
||||||
WriteRegStr HKLM "${KEY_MANIFEST}" "" "$INSTDIR\{{manifestName}}"
|
WriteRegStr HKLM "${KEY_MANIFEST}" "" "$INSTDIR\{{manifestName}}"
|
||||||
|
|
||||||
|
|||||||
@@ -180,7 +180,8 @@ export default class Session extends CastClient {
|
|||||||
type: "LAUNCH",
|
type: "LAUNCH",
|
||||||
appId: this.appId
|
appId: this.appId
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
// Handle client connection closed
|
// Handle client connection closed
|
||||||
this.client.on("close", () => {
|
this.client.on("close", () => {
|
||||||
|
|||||||
@@ -66,8 +66,19 @@ export default class CastClient {
|
|||||||
*/
|
*/
|
||||||
connect(host: string, options?: CastClientConnectOptions) {
|
connect(host: string, options?: CastClientConnectOptions) {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
let connected = false;
|
||||||
|
|
||||||
// Handle errors
|
// 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", () => {
|
this.client.on("close", () => {
|
||||||
if (this.heartbeatChannel && this.heartbeatIntervalId) {
|
if (this.heartbeatChannel && this.heartbeatIntervalId) {
|
||||||
clearInterval(this.heartbeatIntervalId);
|
clearInterval(this.heartbeatIntervalId);
|
||||||
@@ -84,6 +95,7 @@ export default class CastClient {
|
|||||||
},
|
},
|
||||||
// On connection callback
|
// On connection callback
|
||||||
() => {
|
() => {
|
||||||
|
connected = true;
|
||||||
this.connectionChannel = this.createChannel(NS_CONNECTION);
|
this.connectionChannel = this.createChannel(NS_CONNECTION);
|
||||||
this.heartbeatChannel = this.createChannel(NS_HEARTBEAT);
|
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";
|
import type { ReceiverDevice } from "../../messagingTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Chromecast TXT record
|
* Chromecast TXT record fields.
|
||||||
*/
|
*/
|
||||||
interface CastRecord {
|
interface CastRecord {
|
||||||
// Device ID
|
// Device ID
|
||||||
@@ -27,54 +27,47 @@ interface CastRecord {
|
|||||||
rs: string;
|
rs: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DiscoveryOptions {
|
export default class CastDeviceBrowser extends EventEmitter<{
|
||||||
onDeviceFound(device: ReceiverDevice): void;
|
deviceUp: [device: ReceiverDevice];
|
||||||
onDeviceDown(deviceId: string): void;
|
deviceDown: [deviceId: string];
|
||||||
}
|
}> {
|
||||||
|
browser = new DnsSdBrowser("_googlecast._tcp");
|
||||||
|
|
||||||
export default class Discovery {
|
constructor() {
|
||||||
browser = mdns.createBrowser(mdns.tcp("googlecast"), {
|
super();
|
||||||
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) {
|
|
||||||
/**
|
/**
|
||||||
* When a service is found, gather device info from service
|
* When a service is found, gather device info from service object and
|
||||||
* object and TXT record, then send a `main:deviceUp` message.
|
* TXT record, then send a `main:deviceUp` message.
|
||||||
*/
|
*/
|
||||||
this.browser.on("serviceUp", service => {
|
this.browser.on("serviceUp", service => {
|
||||||
// Filter invalid results
|
// Filter invalid results
|
||||||
if (!service.txtRecord || !service.name) return;
|
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 = {
|
const device: ReceiverDevice = {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
friendlyName: record.fn,
|
friendlyName: record.fn,
|
||||||
modelName: record.md,
|
modelName: record.md,
|
||||||
capabilities: parseInt(record.ca),
|
capabilities: parseInt(record.ca),
|
||||||
host: service.addresses[0],
|
host: address,
|
||||||
port: service.port
|
port: service.port
|
||||||
};
|
};
|
||||||
|
|
||||||
opts.onDeviceFound(device);
|
this.emit("deviceUp", device);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a service is lost, send a `main:deviceDown` message with
|
* When a service is lost, send a `main:deviceDown` message with the
|
||||||
* the service name as the `deviceId`.
|
* service name as the `deviceId`.
|
||||||
*/
|
*/
|
||||||
this.browser.on("serviceDown", service => {
|
this.browser.on("serviceDown", name => {
|
||||||
// Filter invalid results
|
// 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(() => {
|
.then(() => {
|
||||||
this.sendReceiverMessage({ type: "GET_STATUS" });
|
this.sendReceiverMessage({ type: "GET_STATUS" });
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
@@ -85,7 +86,8 @@ export default class Remote extends CastClient {
|
|||||||
type: "GET_STATUS",
|
type: "GET_STATUS",
|
||||||
requestId: 0
|
requestId: 0
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
this.options?.onApplicationFound?.();
|
this.options?.onApplicationFound?.();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Messenger, Message } from "./messaging";
|
import type { Messenger, Message } from "./messaging";
|
||||||
|
|
||||||
import { handleCastMessage } from "./components/cast";
|
import { handleCastMessage } from "./components/cast";
|
||||||
import Discovery from "./components/cast/discovery";
|
import CastDeviceBrowser from "./components/cast/deviceBrowser";
|
||||||
import Remote from "./components/cast/remote";
|
import Remote from "./components/cast/remote";
|
||||||
|
|
||||||
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
||||||
@@ -9,7 +9,7 @@ import { startMediaServer, stopMediaServer } from "./components/mediaServer";
|
|||||||
import { applicationVersion } from "../../config.json";
|
import { applicationVersion } from "../../config.json";
|
||||||
|
|
||||||
process.on("SIGTERM", async () => {
|
process.on("SIGTERM", async () => {
|
||||||
discovery?.stop();
|
deviceBrowser?.stop();
|
||||||
try {
|
try {
|
||||||
await stopMediaServer();
|
await stopMediaServer();
|
||||||
} catch (err) {
|
} 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>();
|
const remotes = new Map<string, Remote>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle incoming messages from the extension and forward
|
* Handle incoming messages from the extension and forward them to the
|
||||||
* them to the appropriate handlers.
|
* appropriate handlers.
|
||||||
*
|
*
|
||||||
* Initializes the counterpart objects and is responsible
|
* Initializes the counterpart objects and is responsible for managing existing
|
||||||
* for managing existing ones.
|
* ones.
|
||||||
*/
|
*/
|
||||||
export function run(messaging: Messenger) {
|
export function run(messaging: Messenger) {
|
||||||
messaging.on("message", (message: Message) => {
|
messaging.on("message", (message: Message) => {
|
||||||
@@ -41,66 +41,66 @@ export function run(messaging: Messenger) {
|
|||||||
case "bridge:startDiscovery": {
|
case "bridge:startDiscovery": {
|
||||||
const { shouldWatchStatus } = message.data;
|
const { shouldWatchStatus } = message.data;
|
||||||
|
|
||||||
discovery = new Discovery({
|
deviceBrowser = new CastDeviceBrowser();
|
||||||
onDeviceFound(device) {
|
|
||||||
messaging.sendMessage({
|
|
||||||
subject: "main:deviceUp",
|
|
||||||
data: {
|
|
||||||
deviceId: device.id,
|
|
||||||
deviceInfo: device
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldWatchStatus) {
|
deviceBrowser.on("deviceUp", device => {
|
||||||
remotes.set(
|
messaging.sendMessage({
|
||||||
device.id,
|
subject: "main:deviceUp",
|
||||||
new Remote(device.host, {
|
data: {
|
||||||
port: device.port,
|
deviceId: device.id,
|
||||||
// RECEIVER_STATUS
|
deviceInfo: device
|
||||||
onReceiverStatusUpdate(status) {
|
|
||||||
messaging.sendMessage({
|
|
||||||
subject:
|
|
||||||
"main:receiverDeviceStatusUpdated",
|
|
||||||
data: {
|
|
||||||
deviceId: device.id,
|
|
||||||
status
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// MEDIA_STATUS
|
|
||||||
onMediaStatusUpdate(status) {
|
|
||||||
if (!status) return;
|
|
||||||
|
|
||||||
messaging.sendMessage({
|
|
||||||
subject:
|
|
||||||
"main:receiverDeviceMediaStatusUpdated",
|
|
||||||
data: {
|
|
||||||
deviceId: device.id,
|
|
||||||
status
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
onDeviceDown(deviceId) {
|
|
||||||
messaging.sendMessage({
|
|
||||||
subject: "main:deviceDown",
|
|
||||||
data: { deviceId }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (shouldWatchStatus) {
|
if (shouldWatchStatus) {
|
||||||
if (remotes.has(deviceId)) {
|
remotes.set(
|
||||||
remotes.get(deviceId)?.disconnect();
|
device.id,
|
||||||
remotes.delete(deviceId);
|
new Remote(device.host, {
|
||||||
}
|
port: device.port,
|
||||||
|
// RECEIVER_STATUS
|
||||||
|
onReceiverStatusUpdate(status) {
|
||||||
|
messaging.sendMessage({
|
||||||
|
subject:
|
||||||
|
"main:receiverDeviceStatusUpdated",
|
||||||
|
data: {
|
||||||
|
deviceId: device.id,
|
||||||
|
status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// MEDIA_STATUS
|
||||||
|
onMediaStatusUpdate(status) {
|
||||||
|
if (!status) return;
|
||||||
|
|
||||||
|
messaging.sendMessage({
|
||||||
|
subject:
|
||||||
|
"main:receiverDeviceMediaStatusUpdated",
|
||||||
|
data: {
|
||||||
|
deviceId: device.id,
|
||||||
|
status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
deviceBrowser.on("deviceDown", deviceId => {
|
||||||
|
messaging.sendMessage({
|
||||||
|
subject: "main:deviceDown",
|
||||||
|
data: { deviceId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldWatchStatus) {
|
||||||
|
if (remotes.has(deviceId)) {
|
||||||
|
remotes.get(deviceId)?.disconnect();
|
||||||
|
remotes.delete(deviceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
discovery.start();
|
deviceBrowser.start();
|
||||||
|
|
||||||
break;
|
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