├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── rustfmt.toml ├── .github ├── FUNDING.yml ├── renovate.json └── workflows │ └── CI.yml ├── __test__ ├── package.json ├── tsconfig.json └── index.spec.ts ├── benchmark ├── package.json ├── tsconfig.json └── bench.ts ├── .yarnrc.yml ├── .cargo └── config.toml ├── .taplo.toml ├── simple-test.windows.mjs ├── .editorconfig ├── tsconfig.json ├── .gitattributes ├── simple-test.linux.mjs ├── simple-test.mjs ├── README.md ├── LICENSE ├── Cargo.toml ├── src ├── lib.rs ├── macos.rs ├── linux.rs └── windows.rs ├── index.d.ts ├── .gitignore ├── package.json ├── .eslintrc.yml └── index.js /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | target 2 | .yarn -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [Brooooooklyn] 2 | -------------------------------------------------------------------------------- /__test__/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | npmAuditRegistry: "https://registry.npmjs.org" 4 | 5 | yarnPath: .yarn/releases/yarn-4.9.2.cjs 6 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.aarch64-unknown-linux-musl] 2 | linker = "aarch64-linux-musl-gcc" 3 | rustflags = ["-C", "target-feature=-crt-static"] 4 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | exclude = ["node_modules/**/*.toml"] 2 | 3 | # https://taplo.tamasfe.dev/configuration/formatter-options.html 4 | [formatting] 5 | align_entries = true 6 | indent_tables = true 7 | reorder_keys = true 8 | -------------------------------------------------------------------------------- /benchmark/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "NodeNext", 5 | "module": "NodeNext", 6 | "outDir": "lib" 7 | }, 8 | "include": ["."], 9 | "exclude": ["lib"] 10 | } 11 | -------------------------------------------------------------------------------- /__test__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "moduleResolution": "Bundler", 6 | "outDir": "lib", 7 | "rootDir": "." 8 | }, 9 | "include": ["*.ts"], 10 | "exclude": ["lib"] 11 | } 12 | -------------------------------------------------------------------------------- /__test__/index.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import { NwPathMonitor } from '../index.js' 4 | 5 | test('should not throw while listening', (t) => { 6 | t.notThrows(() => { 7 | const pm = new NwPathMonitor() 8 | pm.start((path) => { 9 | console.info(path) 10 | pm.stop() 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /simple-test.windows.mjs: -------------------------------------------------------------------------------- 1 | import { InternetMonitor } from './index.js' 2 | 3 | const pm = new InternetMonitor() 4 | 5 | console.log(pm.current()) 6 | 7 | pm.start((path) => { 8 | console.log(`Network status: `, path) 9 | }) 10 | 11 | setTimeout(() => { 12 | // ref the pm, so that it doesn't get GCed 13 | pm.stop() 14 | }, 1000000) 15 | -------------------------------------------------------------------------------- /benchmark/bench.ts: -------------------------------------------------------------------------------- 1 | import { Bench } from 'tinybench' 2 | 3 | import { plus100 } from '../index.js' 4 | 5 | function add(a: number) { 6 | return a + 100 7 | } 8 | 9 | const b = new Bench() 10 | 11 | b.add('Native a + 100', () => { 12 | plus100(10) 13 | }) 14 | 15 | b.add('JavaScript a + 100', () => { 16 | add(10) 17 | }) 18 | 19 | await b.run() 20 | 21 | console.table(b.table()) 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors or IDEs 3 | # http://editorconfig.org 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "strict": true, 5 | "moduleResolution": "node", 6 | "module": "CommonJS", 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": ["."], 13 | "exclude": ["node_modules", "bench", "__test__"] 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | 5 | *.ts text eol=lf merge=union 6 | *.tsx text eol=lf merge=union 7 | *.rs text eol=lf merge=union 8 | *.js text eol=lf merge=union 9 | *.json text eol=lf merge=union 10 | *.debug text eol=lf merge=union 11 | 12 | # Generated codes 13 | index.js linguist-detectable=false 14 | index.d.ts linguist-detectable=false -------------------------------------------------------------------------------- /simple-test.linux.mjs: -------------------------------------------------------------------------------- 1 | import { InternetMonitor } from './index.js' 2 | 3 | const pm = new InternetMonitor() 4 | 5 | console.log(`Network status: `, pm.current()) 6 | 7 | pm.start((path) => { 8 | console.log(`Network status: `, path) 9 | pm.stop(); 10 | pm.start((path) => { 11 | console.log(`Network status inner: `, path) 12 | }) 13 | }) 14 | 15 | setTimeout(() => { 16 | // ref the pm, so that it doesn't get GCed 17 | pm.stop() 18 | }, 1000000) 19 | -------------------------------------------------------------------------------- /simple-test.mjs: -------------------------------------------------------------------------------- 1 | import { eq } from 'lodash-es' 2 | 3 | import { NwPathMonitor } from './index.js' 4 | 5 | const pm = new NwPathMonitor() 6 | 7 | /** 8 | * @type {import('./index.js').NwPath} 9 | */ 10 | const currentPath = { 11 | status: 'Unsatisfied', 12 | isExpensive: false, 13 | isConstrained: false, 14 | hasDns: false, 15 | hasIpv4: false, 16 | hasIpv6: false, 17 | } 18 | 19 | pm.start((path) => { 20 | if (!eq(path, currentPath)) { 21 | Object.assign(currentPath, path) 22 | console.log(`Network status: `, currentPath) 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", "group:allNonMajor", ":preserveSemverRanges", ":disablePeerDependencies"], 4 | "labels": ["dependencies"], 5 | "packageRules": [ 6 | { 7 | "matchPackageNames": ["@napi/cli", "napi", "napi-build", "napi-derive"], 8 | "addLabels": ["napi-rs"], 9 | "groupName": "napi-rs" 10 | } 11 | ], 12 | "commitMessagePrefix": "chore: ", 13 | "commitMessageAction": "bump up", 14 | "commitMessageTopic": "{{depName}} version", 15 | "ignoreDeps": [] 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@napi-rs/network-change` 2 | 3 | ![https://github.com/Brooooooklyn/network-change/actions](https://github.com/Brooooooklyn/network-change/workflows/CI/badge.svg) 4 | 5 | **Observe network change event in Node.js.** 6 | 7 | > [!IMPORTANT] 8 | > This package is working in progress, and only support Windows and macOS now. 9 | 10 | ## Install 11 | 12 | ``` 13 | yarn add @napi-rs/network-change 14 | ``` 15 | 16 | ``` 17 | pnpm add @napi-rs/network-change 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```typescript 23 | import { NwPathMonitor } from '@napi-rs/network-change'; 24 | 25 | 26 | const monitor = new NwPathMonitor(); 27 | monitor.start((path) => { 28 | console.log('network change', path); 29 | }); 30 | ``` 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 N-API for Rust 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["LongYinan "] 3 | edition = "2021" 4 | name = "network-change" 5 | version = "0.1.0" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [lib] 10 | crate-type = ["cdylib"] 11 | 12 | [dependencies] 13 | napi = { version = "3.0.0-alpha.13", features = ["napi4"] } 14 | napi-derive = "3.0.0-alpha.13" 15 | 16 | [target.'cfg(target_os = "macos")'.dependencies] 17 | block2 = "0.6" 18 | 19 | [target.'cfg(target_os = "windows")'.dependencies] 20 | bitflags = "2" 21 | bytes = "1" 22 | windows = { version = "0.61.0", features = [ 23 | # for INetworkListManager 24 | "Win32_Networking_NetworkListManager", 25 | "Win32_NetworkManagement", 26 | "Win32_NetworkManagement_IpHelper", 27 | "Win32_NetworkManagement_Ndis", 28 | "Win32_Networking_WinSock", 29 | # for COM interfaces 30 | "Win32_System_Com", 31 | # for error handling 32 | "Win32_System_Ole", 33 | # for implementing INetworkListManagerEvents 34 | "implement", 35 | ] } 36 | windows-core = "0.61.0" 37 | 38 | [build-dependencies] 39 | napi-build = "2" 40 | 41 | [profile.release] 42 | lto = true 43 | strip = "symbols" 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use napi_derive::napi; 2 | 3 | #[cfg(target_os = "macos")] 4 | mod macos; 5 | 6 | #[cfg(target_os = "macos")] 7 | pub use macos::*; 8 | 9 | #[cfg(target_os = "windows")] 10 | mod windows; 11 | 12 | #[cfg(target_os = "windows")] 13 | pub use windows::*; 14 | 15 | #[cfg(target_os = "linux")] 16 | mod linux; 17 | 18 | #[cfg(target_os = "linux")] 19 | pub use linux::*; 20 | 21 | #[napi(string_enum)] 22 | #[repr(u8)] 23 | #[derive(Debug, Clone, Copy)] 24 | /// A network path status indicates if there is a usable route available upon which to send and receive data. 25 | pub enum NetworkStatus { 26 | /// nw_path_status_invalid The path is not valid 27 | Invalid, 28 | /// nw_path_status_satisfied The path is valid and satisfies the required constraints 29 | Satisfied, 30 | /// nw_path_status_unsatisfied The path is valid, but does not satisfy the required constraints 31 | Unsatisfied, 32 | /// nw_path_status_satisfiable The path is potentially valid, but a connection is required 33 | Satisfiable, 34 | /// Reserved for future use 35 | Unknown, 36 | } 37 | 38 | #[napi(object, object_from_js = false)] 39 | #[derive(Debug, Clone)] 40 | pub struct NetworkInfo { 41 | pub status: NetworkStatus, 42 | pub is_expensive: bool, 43 | pub is_low_data_mode: bool, 44 | pub has_ipv4: bool, 45 | pub has_ipv6: bool, 46 | pub has_dns: bool, 47 | } 48 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /* auto-generated by NAPI-RS */ 2 | /* eslint-disable */ 3 | export declare class InternetMonitor { 4 | constructor() 5 | current(): NetworkInfo 6 | /** Start the InternetMonitor, it will keep the Node.js alive unless you call stop on it. */ 7 | start(onUpdate: (arg: NetworkInfo) => void): void 8 | /** Start the InternetMonitor with weak reference, it will not keep the Node.js alive. */ 9 | startWeak(onUpdate: (arg: NetworkInfo) => void): void 10 | /** 11 | * Stop the InternetMonitor. 12 | * 13 | * If you don't call this method and leave the monitor alone, it will be stopped automatically when it is GC. 14 | */ 15 | stop(): void 16 | } 17 | 18 | export interface NetworkInfo { 19 | status: NetworkStatus 20 | isExpensive: boolean 21 | isLowDataMode: boolean 22 | hasIpv4: boolean 23 | hasIpv6: boolean 24 | hasDns: boolean 25 | } 26 | 27 | /** A network path status indicates if there is a usable route available upon which to send and receive data. */ 28 | export type NetworkStatus = /** nw_path_status_invalid The path is not valid */ 29 | 'Invalid'| 30 | /** nw_path_status_satisfied The path is valid and satisfies the required constraints */ 31 | 'Satisfied'| 32 | /** nw_path_status_unsatisfied The path is valid| but does not satisfy the required constraints */ 33 | 'Unsatisfied'| 34 | /** nw_path_status_satisfiable The path is potentially valid| but a connection is required */ 35 | 'Satisfiable'| 36 | /** Reserved for future use */ 37 | 'Unknown'; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # End of https://www.toptal.com/developers/gitignore/api/node 115 | 116 | 117 | #Added by cargo 118 | 119 | /target 120 | Cargo.lock 121 | 122 | *.node 123 | .pnp.* 124 | .yarn/* 125 | !.yarn/patches 126 | !.yarn/plugins 127 | !.yarn/releases 128 | !.yarn/sdks 129 | !.yarn/versions -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@napi-rs/network-change", 3 | "version": "0.0.0", 4 | "description": "Template project for writing node package with napi-rs", 5 | "main": "index.js", 6 | "repository": "git@github.com:napi-rs/package-template.git", 7 | "license": "MIT", 8 | "keywords": [ 9 | "napi-rs", 10 | "NAPI", 11 | "N-API", 12 | "Rust", 13 | "node-addon", 14 | "node-addon-api" 15 | ], 16 | "files": [ 17 | "index.d.ts", 18 | "index.js" 19 | ], 20 | "napi": { 21 | "binaryName": "network-change", 22 | "constEnum": false, 23 | "targets": [ 24 | "x86_64-apple-darwin", 25 | "x86_64-unknown-linux-gnu", 26 | "x86_64-pc-windows-msvc", 27 | "x86_64-unknown-linux-musl", 28 | "x86_64-unknown-freebsd", 29 | "i686-pc-windows-msvc", 30 | "aarch64-unknown-linux-gnu", 31 | "aarch64-apple-darwin", 32 | "aarch64-linux-android", 33 | "aarch64-unknown-linux-musl", 34 | "aarch64-pc-windows-msvc", 35 | "armv7-unknown-linux-gnueabihf", 36 | "armv7-linux-androideabi", 37 | "powerpc64le-unknown-linux-gnu", 38 | "s390x-unknown-linux-gnu" 39 | ] 40 | }, 41 | "engines": { 42 | "node": ">= 10" 43 | }, 44 | "publishConfig": { 45 | "registry": "https://registry.npmjs.org/", 46 | "access": "public" 47 | }, 48 | "scripts": { 49 | "artifacts": "napi artifacts", 50 | "bench": "node --import @oxc-node/core/register benchmark/bench.ts", 51 | "build": "napi build --platform --release", 52 | "build:debug": "napi build --platform", 53 | "format": "run-p format:prettier format:rs format:toml", 54 | "format:prettier": "prettier . -w", 55 | "format:toml": "taplo format", 56 | "format:rs": "cargo fmt", 57 | "lint": "oxlint .", 58 | "prepublishOnly": "napi prepublish -t npm", 59 | "test": "ava", 60 | "version": "napi version", 61 | "prepare": "husky" 62 | }, 63 | "devDependencies": { 64 | "@napi-rs/cli": "^3.0.0-alpha.63", 65 | "@oxc-node/core": "^0.0.29", 66 | "@taplo/cli": "^0.7.0", 67 | "@types/lodash-es": "^4", 68 | "ava": "^6.1.3", 69 | "chalk": "^5.3.0", 70 | "husky": "^9.1.6", 71 | "lint-staged": "^16.0.0", 72 | "lodash-es": "^4.17.21", 73 | "npm-run-all2": "^8.0.0", 74 | "oxlint": "^1.0.0", 75 | "prettier": "^3.3.3", 76 | "tinybench": "^4.0.0", 77 | "typescript": "^5.6.2" 78 | }, 79 | "lint-staged": { 80 | "*.@(js|ts|tsx)": [ 81 | "oxlint --fix" 82 | ], 83 | "*.@(js|ts|tsx|yml|yaml|md|json)": [ 84 | "prettier --write" 85 | ], 86 | "*.toml": [ 87 | "taplo format" 88 | ] 89 | }, 90 | "ava": { 91 | "extensions": { 92 | "ts": "module" 93 | }, 94 | "timeout": "2m", 95 | "workerThreads": false, 96 | "environmentVariables": { 97 | "TS_NODE_PROJECT": "./tsconfig.json" 98 | }, 99 | "nodeArguments": [ 100 | "--import", 101 | "@oxc-node/core/register" 102 | ] 103 | }, 104 | "prettier": { 105 | "printWidth": 120, 106 | "semi": false, 107 | "trailingComma": "all", 108 | "singleQuote": true, 109 | "arrowParens": "always" 110 | }, 111 | "packageManager": "yarn@4.9.2", 112 | "funding": { 113 | "type": "github", 114 | "url": "https://github.com/sponsors/Brooooooklyn/" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parser: '@typescript-eslint/parser' 2 | 3 | parserOptions: 4 | ecmaFeatures: 5 | jsx: true 6 | ecmaVersion: latest 7 | sourceType: module 8 | project: ./tsconfig.json 9 | 10 | env: 11 | browser: true 12 | es6: true 13 | node: true 14 | jest: true 15 | 16 | ignorePatterns: ['index.js'] 17 | 18 | plugins: 19 | - import 20 | - '@typescript-eslint' 21 | 22 | extends: 23 | - eslint:recommended 24 | - plugin:prettier/recommended 25 | 26 | rules: 27 | # 0 = off, 1 = warn, 2 = error 28 | 'space-before-function-paren': 0 29 | 'no-useless-constructor': 0 30 | 'no-undef': 2 31 | 'no-console': [2, { allow: ['error', 'warn', 'info', 'assert'] }] 32 | 'comma-dangle': ['error', 'only-multiline'] 33 | 'no-unused-vars': 0 34 | 'no-var': 2 35 | 'one-var-declaration-per-line': 2 36 | 'prefer-const': 2 37 | 'no-const-assign': 2 38 | 'no-duplicate-imports': 2 39 | 'no-use-before-define': [2, { 'functions': false, 'classes': false }] 40 | 'eqeqeq': [2, 'always', { 'null': 'ignore' }] 41 | 'no-case-declarations': 0 42 | 'no-restricted-syntax': 43 | [ 44 | 2, 45 | { 46 | 'selector': 'BinaryExpression[operator=/(==|===|!=|!==)/][left.raw=true], BinaryExpression[operator=/(==|===|!=|!==)/][right.raw=true]', 47 | 'message': Don't compare for equality against boolean literals, 48 | }, 49 | ] 50 | 51 | # https://github.com/benmosher/eslint-plugin-import/pull/334 52 | 'import/no-duplicates': 2 53 | 'import/first': 2 54 | 'import/newline-after-import': 2 55 | 'import/order': 56 | [ 57 | 2, 58 | { 59 | 'newlines-between': 'always', 60 | 'alphabetize': { 'order': 'asc' }, 61 | 'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'], 62 | }, 63 | ] 64 | 65 | overrides: 66 | - files: 67 | - ./**/*{.ts,.tsx} 68 | rules: 69 | 'no-unused-vars': [2, { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }] 70 | 'no-undef': 0 71 | # TypeScript declare merge 72 | 'no-redeclare': 0 73 | 'no-useless-constructor': 0 74 | 'no-dupe-class-members': 0 75 | 'no-case-declarations': 0 76 | 'no-duplicate-imports': 0 77 | # TypeScript Interface and Type 78 | 'no-use-before-define': 0 79 | 80 | '@typescript-eslint/adjacent-overload-signatures': 2 81 | '@typescript-eslint/await-thenable': 2 82 | '@typescript-eslint/consistent-type-assertions': 2 83 | '@typescript-eslint/ban-types': 84 | [ 85 | 'error', 86 | { 87 | 'types': 88 | { 89 | 'String': { 'message': 'Use string instead', 'fixWith': 'string' }, 90 | 'Number': { 'message': 'Use number instead', 'fixWith': 'number' }, 91 | 'Boolean': { 'message': 'Use boolean instead', 'fixWith': 'boolean' }, 92 | 'Function': { 'message': 'Use explicit type instead' }, 93 | }, 94 | }, 95 | ] 96 | '@typescript-eslint/explicit-member-accessibility': 97 | [ 98 | 'error', 99 | { 100 | accessibility: 'explicit', 101 | overrides: 102 | { 103 | accessors: 'no-public', 104 | constructors: 'no-public', 105 | methods: 'no-public', 106 | properties: 'no-public', 107 | parameterProperties: 'explicit', 108 | }, 109 | }, 110 | ] 111 | '@typescript-eslint/method-signature-style': 2 112 | '@typescript-eslint/no-floating-promises': 2 113 | '@typescript-eslint/no-implied-eval': 2 114 | '@typescript-eslint/no-for-in-array': 2 115 | '@typescript-eslint/no-inferrable-types': 2 116 | '@typescript-eslint/no-invalid-void-type': 2 117 | '@typescript-eslint/no-misused-new': 2 118 | '@typescript-eslint/no-misused-promises': 2 119 | '@typescript-eslint/no-namespace': 2 120 | '@typescript-eslint/no-non-null-asserted-optional-chain': 2 121 | '@typescript-eslint/no-throw-literal': 2 122 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': 2 123 | '@typescript-eslint/prefer-for-of': 2 124 | '@typescript-eslint/prefer-nullish-coalescing': 2 125 | '@typescript-eslint/switch-exhaustiveness-check': 2 126 | '@typescript-eslint/prefer-optional-chain': 2 127 | '@typescript-eslint/prefer-readonly': 2 128 | '@typescript-eslint/prefer-string-starts-ends-with': 0 129 | '@typescript-eslint/no-array-constructor': 2 130 | '@typescript-eslint/require-await': 2 131 | '@typescript-eslint/return-await': 2 132 | '@typescript-eslint/ban-ts-comment': 133 | [2, { 'ts-expect-error': false, 'ts-ignore': true, 'ts-nocheck': true, 'ts-check': false }] 134 | '@typescript-eslint/naming-convention': 135 | [ 136 | 2, 137 | { 138 | selector: 'memberLike', 139 | format: ['camelCase', 'PascalCase'], 140 | modifiers: ['private'], 141 | leadingUnderscore: 'forbid', 142 | }, 143 | ] 144 | '@typescript-eslint/no-unused-vars': 145 | [2, { varsIgnorePattern: '^_', argsIgnorePattern: '^_', ignoreRestSiblings: true }] 146 | '@typescript-eslint/member-ordering': 147 | [ 148 | 2, 149 | { 150 | default: 151 | [ 152 | 'public-static-field', 153 | 'protected-static-field', 154 | 'private-static-field', 155 | 'public-static-method', 156 | 'protected-static-method', 157 | 'private-static-method', 158 | 'public-instance-field', 159 | 'protected-instance-field', 160 | 'private-instance-field', 161 | 'public-constructor', 162 | 'protected-constructor', 163 | 'private-constructor', 164 | 'public-instance-method', 165 | 'protected-instance-method', 166 | 'private-instance-method', 167 | ], 168 | }, 169 | ] 170 | -------------------------------------------------------------------------------- /src/macos.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::c_void; 2 | 3 | use block2::RcBlock; 4 | use napi::bindgen_prelude::*; 5 | use napi::threadsafe_function::{ThreadsafeCallContext, ThreadsafeFunctionCallMode}; 6 | use napi_derive::napi; 7 | 8 | use crate::{NetworkInfo as NWPath, NetworkStatus as NWPathStatus}; 9 | 10 | #[napi] 11 | /// Interface types represent the underlying media for a network link, such as Wi-Fi or Cellular. 12 | pub enum NWInterfaceType { 13 | /// nw_interface_type_other A virtual or otherwise unknown interface type 14 | Other, 15 | /// nw_interface_type_wifi A Wi-Fi link 16 | Wifi, 17 | /// nw_interface_type_wifi A Cellular link 18 | Cellular, 19 | /// nw_interface_type_wired A Wired Ethernet link 20 | Wired, 21 | /// nw_interface_type_loopback A Loopback link 22 | Loopback, 23 | } 24 | 25 | impl From for NWPathStatus { 26 | fn from(status: ffi::nw_path_status_t) -> Self { 27 | match status { 28 | ffi::nw_path_status_t::NW_PATH_STATUS_INVALID => NWPathStatus::Invalid, 29 | ffi::nw_path_status_t::NW_PATH_STATUS_SATISFIED => NWPathStatus::Satisfied, 30 | ffi::nw_path_status_t::NW_PATH_STATUS_UNSATISFIED => NWPathStatus::Unsatisfied, 31 | ffi::nw_path_status_t::NW_PATH_STATUS_SATISFIABLE => NWPathStatus::Satisfiable, 32 | _ => NWPathStatus::Unknown, 33 | } 34 | } 35 | } 36 | 37 | impl From for ffi::nw_interface_type_t { 38 | fn from(interface_type: NWInterfaceType) -> Self { 39 | match interface_type { 40 | NWInterfaceType::Other => 0, 41 | NWInterfaceType::Wifi => 1, 42 | NWInterfaceType::Cellular => 2, 43 | NWInterfaceType::Wired => 3, 44 | NWInterfaceType::Loopback => 4, 45 | } 46 | } 47 | } 48 | 49 | #[napi] 50 | /// A monitor that watches for changes in network path status. 51 | pub struct NWPathMonitor { 52 | pm: ffi::nw_path_monitor_t, 53 | } 54 | 55 | #[napi] 56 | impl NWPathMonitor { 57 | #[napi(constructor)] 58 | #[allow(clippy::new_without_default)] 59 | pub fn new() -> Self { 60 | let monitor = unsafe { ffi::nw_path_monitor_create() }; 61 | let queue = 62 | unsafe { ffi::dispatch_get_global_queue(ffi::dispatch_qos_class_t::QOS_CLASS_DEFAULT, 0) }; 63 | unsafe { ffi::nw_path_monitor_set_queue(monitor, queue.cast()) }; 64 | Self { pm: monitor } 65 | } 66 | 67 | #[napi(factory)] 68 | /// Create a new path monitor with the specified interface type. 69 | pub fn new_with_type(interface_type: NWInterfaceType) -> Self { 70 | let monitor = unsafe { ffi::nw_path_monitor_create_with_type(interface_type.into()) }; 71 | let queue = 72 | unsafe { ffi::dispatch_get_global_queue(ffi::dispatch_qos_class_t::QOS_CLASS_DEFAULT, 0) }; 73 | unsafe { ffi::nw_path_monitor_set_queue(monitor, queue.cast()) }; 74 | Self { pm: monitor } 75 | } 76 | 77 | #[napi] 78 | /// Start the path monitor, it will keep the Node.js alive unless you call stop on it. 79 | pub fn start(&mut self, on_update: Function) -> Result<()> { 80 | let change_handler = on_update 81 | .build_threadsafe_function() 82 | .callee_handled::() 83 | .weak::() 84 | .build_callback(ctx_to_path)?; 85 | let cb = move |path: *mut c_void| { 86 | change_handler.call(path.cast(), ThreadsafeFunctionCallMode::NonBlocking); 87 | }; 88 | unsafe { 89 | ffi::nw_path_monitor_set_update_handler(self.pm, &RcBlock::new(cb)); 90 | }; 91 | unsafe { ffi::nw_path_monitor_start(self.pm) }; 92 | Ok(()) 93 | } 94 | 95 | #[napi] 96 | /// Start the path monitor with weak reference, it will not keep the Node.js alive. 97 | pub fn start_weak(&mut self, on_update: Function) -> Result<()> { 98 | let change_handler = on_update 99 | .build_threadsafe_function() 100 | .callee_handled::() 101 | .weak::() 102 | .build_callback(ctx_to_path)?; 103 | let cb = move |path: *mut c_void| { 104 | change_handler.call(path.cast(), ThreadsafeFunctionCallMode::NonBlocking); 105 | }; 106 | unsafe { 107 | ffi::nw_path_monitor_set_update_handler(self.pm, &RcBlock::new(cb)); 108 | }; 109 | unsafe { ffi::nw_path_monitor_start(self.pm) }; 110 | Ok(()) 111 | } 112 | 113 | #[napi] 114 | /// Stop the path monitor. 115 | /// 116 | /// If you don't call this method and leave the monitor alone, it will be stopped automatically when it is GC. 117 | pub fn stop(&mut self) -> Result<()> { 118 | unsafe { ffi::nw_path_monitor_cancel(self.pm) }; 119 | Ok(()) 120 | } 121 | } 122 | 123 | #[inline] 124 | fn ctx_to_path(ctx: ThreadsafeCallContext) -> Result { 125 | Ok(NWPath { 126 | status: unsafe { ffi::nw_path_get_status(ctx.value).into() }, 127 | is_expensive: unsafe { ffi::nw_path_is_expensive(ctx.value) }, 128 | is_low_data_mode: unsafe { ffi::nw_path_is_constrained(ctx.value) }, 129 | has_ipv4: unsafe { ffi::nw_path_has_ipv4(ctx.value) }, 130 | has_ipv6: unsafe { ffi::nw_path_has_ipv6(ctx.value) }, 131 | has_dns: unsafe { ffi::nw_path_has_dns(ctx.value) }, 132 | }) 133 | } 134 | 135 | #[allow(non_camel_case_types)] 136 | #[allow(unused)] 137 | mod ffi { 138 | use core::ffi::{c_int, c_uint, c_void}; 139 | 140 | use block2::Block; 141 | 142 | macro_rules! enum_with_val { 143 | ($(#[$meta:meta])* $vis:vis struct $ident:ident($innervis:vis $ty:ty) { 144 | $($(#[$varmeta:meta])* $variant:ident = $num:expr),* $(,)* 145 | }) => { 146 | $(#[$meta])* 147 | #[repr(transparent)] 148 | $vis struct $ident($innervis $ty); 149 | impl $ident { 150 | $($(#[$varmeta])* $vis const $variant: $ident = $ident($num);)* 151 | } 152 | 153 | impl ::core::fmt::Debug for $ident { 154 | fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 155 | match self { 156 | $(&$ident::$variant => write!(f, "{}::{}", stringify!($ident), stringify!($variant)),)* 157 | &$ident(v) => write!(f, "UNKNOWN({})", v), 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | enum_with_val! { 165 | /// Quality-of-service classes that specify the priorities for executing tasks. 166 | #[derive(PartialEq, Eq, Clone, Copy)] 167 | pub struct dispatch_qos_class_t(pub c_uint) { 168 | QOS_CLASS_USER_INTERACTIVE = 0x21, 169 | QOS_CLASS_USER_INITIATED = 0x19, 170 | QOS_CLASS_DEFAULT = 0x15, 171 | QOS_CLASS_UTILITY = 0x11, 172 | QOS_CLASS_BACKGROUND = 0x09, 173 | QOS_CLASS_UNSPECIFIED = 0x00, 174 | } 175 | } 176 | 177 | enum_with_val! { 178 | /// A network path status indicates if there is a usable route available upon which to send and receive data. 179 | #[derive(PartialEq, Eq, Clone, Copy)] 180 | pub struct nw_path_status_t(pub c_uint) { 181 | NW_PATH_STATUS_INVALID = 0, 182 | NW_PATH_STATUS_SATISFIED = 1, 183 | NW_PATH_STATUS_UNSATISFIED = 2, 184 | NW_PATH_STATUS_SATISFIABLE = 3, 185 | } 186 | } 187 | 188 | #[repr(C)] 189 | // Dispatch.Framework 190 | // https://developer.apple.com/documentation/dispatch/dispatch_queue_t 191 | pub struct dispatch_queue { 192 | _unused: [u8; 0], 193 | } 194 | pub type dispatch_queue_t = *mut dispatch_queue; 195 | 196 | #[repr(C)] 197 | pub struct dispatch_queue_global { 198 | _unused: [u8; 0], 199 | } 200 | 201 | pub type dispatch_queue_global_t = *mut dispatch_queue_global; 202 | 203 | #[repr(C)] 204 | pub struct nw_interface { 205 | _unused: [u8; 0], 206 | } 207 | 208 | pub type nw_interface_t = *mut nw_interface; 209 | 210 | pub type nw_interface_type_t = c_int; 211 | 212 | #[repr(C)] 213 | pub struct nw_path { 214 | _unused: [u8; 0], 215 | } 216 | pub type nw_path_t = *mut nw_path; 217 | 218 | #[repr(C)] 219 | pub struct nw_path_monitor { 220 | _unused: [u8; 0], 221 | } 222 | pub type nw_path_monitor_t = *mut nw_path_monitor; 223 | 224 | #[cfg_attr( 225 | any( 226 | target_os = "macos", 227 | target_os = "ios", 228 | target_os = "tvos", 229 | target_os = "watchos", 230 | target_os = "visionos" 231 | ), 232 | link(name = "System", kind = "dylib") 233 | )] 234 | extern "C" { 235 | pub static _dispatch_main_q: dispatch_queue; 236 | /// Returns a system-defined global concurrent queue with the specified quality-of-service class. 237 | pub fn dispatch_get_global_queue( 238 | identifier: dispatch_qos_class_t, 239 | flags: usize, 240 | ) -> dispatch_queue_global_t; 241 | } 242 | #[cfg_attr( 243 | any( 244 | target_os = "macos", 245 | target_os = "ios", 246 | target_os = "tvos", 247 | target_os = "watchos", 248 | target_os = "visionos" 249 | ), 250 | link(name = "Network", kind = "framework") 251 | )] 252 | extern "C" { 253 | pub fn nw_path_monitor_create() -> nw_path_monitor_t; 254 | pub fn nw_path_monitor_create_with_type( 255 | required_interface_type: nw_interface_type_t, 256 | ) -> nw_path_monitor_t; 257 | 258 | // pub fn nw_path_monitor_set_cancel_handler( 259 | // monitor: nw_path_monitor_t, 260 | // cancel_handler: nw_path_monitor_cancel_handler_t, 261 | // ); 262 | 263 | pub fn nw_path_monitor_set_update_handler( 264 | monitor: nw_path_monitor_t, 265 | update_handler: &Block, 266 | ); 267 | pub fn nw_path_monitor_set_queue(monitor: nw_path_monitor_t, queue: dispatch_queue_t); 268 | pub fn nw_path_monitor_start(monitor: nw_path_monitor_t); 269 | pub fn nw_path_monitor_cancel(monitor: nw_path_monitor_t); 270 | pub fn nw_path_monitor_copy_current_path(monitor: nw_path_monitor_t) -> nw_path_t; 271 | 272 | pub fn nw_release(obj: *mut c_void); 273 | 274 | pub fn nw_path_get_status(path: nw_path_t) -> nw_path_status_t; 275 | pub fn nw_path_is_expensive(path: nw_path_t) -> bool; 276 | pub fn nw_path_is_constrained(path: nw_path_t) -> bool; 277 | pub fn nw_path_has_ipv4(path: nw_path_t) -> bool; 278 | pub fn nw_path_has_ipv6(path: nw_path_t) -> bool; 279 | pub fn nw_path_has_dns(path: nw_path_t) -> bool; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // prettier-ignore 2 | /* eslint-disable */ 3 | /* auto-generated by NAPI-RS */ 4 | 5 | const { readFileSync } = require('fs') 6 | 7 | let nativeBinding = null 8 | const loadErrors = [] 9 | 10 | const isMusl = () => { 11 | let musl = false 12 | if (process.platform === 'linux') { 13 | musl = isMuslFromFilesystem() 14 | if (musl === null) { 15 | musl = isMuslFromReport() 16 | } 17 | if (musl === null) { 18 | musl = isMuslFromChildProcess() 19 | } 20 | } 21 | return musl 22 | } 23 | 24 | const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-') 25 | 26 | const isMuslFromFilesystem = () => { 27 | try { 28 | return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl') 29 | } catch { 30 | return null 31 | } 32 | } 33 | 34 | const isMuslFromReport = () => { 35 | const report = typeof process.report.getReport === 'function' ? process.report.getReport() : null 36 | if (!report) { 37 | return null 38 | } 39 | if (report.header && report.header.glibcVersionRuntime) { 40 | return false 41 | } 42 | if (Array.isArray(report.sharedObjects)) { 43 | if (report.sharedObjects.some(isFileMusl)) { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | const isMuslFromChildProcess = () => { 51 | try { 52 | return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl') 53 | } catch (e) { 54 | // If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false 55 | return false 56 | } 57 | } 58 | 59 | function requireNative() { 60 | if (process.platform === 'android') { 61 | if (process.arch === 'arm64') { 62 | try { 63 | return require('./network-change.android-arm64.node') 64 | } catch (e) { 65 | loadErrors.push(e) 66 | } 67 | try { 68 | return require('@napi-rs/network-change-android-arm64') 69 | } catch (e) { 70 | loadErrors.push(e) 71 | } 72 | 73 | } else if (process.arch === 'arm') { 74 | try { 75 | return require('./network-change.android-arm-eabi.node') 76 | } catch (e) { 77 | loadErrors.push(e) 78 | } 79 | try { 80 | return require('@napi-rs/network-change-android-arm-eabi') 81 | } catch (e) { 82 | loadErrors.push(e) 83 | } 84 | 85 | } else { 86 | loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`)) 87 | } 88 | } else if (process.platform === 'win32') { 89 | if (process.arch === 'x64') { 90 | try { 91 | return require('./network-change.win32-x64-msvc.node') 92 | } catch (e) { 93 | loadErrors.push(e) 94 | } 95 | try { 96 | return require('@napi-rs/network-change-win32-x64-msvc') 97 | } catch (e) { 98 | loadErrors.push(e) 99 | } 100 | 101 | } else if (process.arch === 'ia32') { 102 | try { 103 | return require('./network-change.win32-ia32-msvc.node') 104 | } catch (e) { 105 | loadErrors.push(e) 106 | } 107 | try { 108 | return require('@napi-rs/network-change-win32-ia32-msvc') 109 | } catch (e) { 110 | loadErrors.push(e) 111 | } 112 | 113 | } else if (process.arch === 'arm64') { 114 | try { 115 | return require('./network-change.win32-arm64-msvc.node') 116 | } catch (e) { 117 | loadErrors.push(e) 118 | } 119 | try { 120 | return require('@napi-rs/network-change-win32-arm64-msvc') 121 | } catch (e) { 122 | loadErrors.push(e) 123 | } 124 | 125 | } else { 126 | loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`)) 127 | } 128 | } else if (process.platform === 'darwin') { 129 | try { 130 | return require('./network-change.darwin-universal.node') 131 | } catch (e) { 132 | loadErrors.push(e) 133 | } 134 | try { 135 | return require('@napi-rs/network-change-darwin-universal') 136 | } catch (e) { 137 | loadErrors.push(e) 138 | } 139 | 140 | if (process.arch === 'x64') { 141 | try { 142 | return require('./network-change.darwin-x64.node') 143 | } catch (e) { 144 | loadErrors.push(e) 145 | } 146 | try { 147 | return require('@napi-rs/network-change-darwin-x64') 148 | } catch (e) { 149 | loadErrors.push(e) 150 | } 151 | 152 | } else if (process.arch === 'arm64') { 153 | try { 154 | return require('./network-change.darwin-arm64.node') 155 | } catch (e) { 156 | loadErrors.push(e) 157 | } 158 | try { 159 | return require('@napi-rs/network-change-darwin-arm64') 160 | } catch (e) { 161 | loadErrors.push(e) 162 | } 163 | 164 | } else { 165 | loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`)) 166 | } 167 | } else if (process.platform === 'freebsd') { 168 | if (process.arch === 'x64') { 169 | try { 170 | return require('./network-change.freebsd-x64.node') 171 | } catch (e) { 172 | loadErrors.push(e) 173 | } 174 | try { 175 | return require('@napi-rs/network-change-freebsd-x64') 176 | } catch (e) { 177 | loadErrors.push(e) 178 | } 179 | 180 | } else if (process.arch === 'arm64') { 181 | try { 182 | return require('./network-change.freebsd-arm64.node') 183 | } catch (e) { 184 | loadErrors.push(e) 185 | } 186 | try { 187 | return require('@napi-rs/network-change-freebsd-arm64') 188 | } catch (e) { 189 | loadErrors.push(e) 190 | } 191 | 192 | } else { 193 | loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`)) 194 | } 195 | } else if (process.platform === 'linux') { 196 | if (process.arch === 'x64') { 197 | if (isMusl()) { 198 | try { 199 | return require('./network-change.linux-x64-musl.node') 200 | } catch (e) { 201 | loadErrors.push(e) 202 | } 203 | try { 204 | return require('@napi-rs/network-change-linux-x64-musl') 205 | } catch (e) { 206 | loadErrors.push(e) 207 | } 208 | 209 | } else { 210 | try { 211 | return require('./network-change.linux-x64-gnu.node') 212 | } catch (e) { 213 | loadErrors.push(e) 214 | } 215 | try { 216 | return require('@napi-rs/network-change-linux-x64-gnu') 217 | } catch (e) { 218 | loadErrors.push(e) 219 | } 220 | 221 | } 222 | } else if (process.arch === 'arm64') { 223 | if (isMusl()) { 224 | try { 225 | return require('./network-change.linux-arm64-musl.node') 226 | } catch (e) { 227 | loadErrors.push(e) 228 | } 229 | try { 230 | return require('@napi-rs/network-change-linux-arm64-musl') 231 | } catch (e) { 232 | loadErrors.push(e) 233 | } 234 | 235 | } else { 236 | try { 237 | return require('./network-change.linux-arm64-gnu.node') 238 | } catch (e) { 239 | loadErrors.push(e) 240 | } 241 | try { 242 | return require('@napi-rs/network-change-linux-arm64-gnu') 243 | } catch (e) { 244 | loadErrors.push(e) 245 | } 246 | 247 | } 248 | } else if (process.arch === 'arm') { 249 | if (isMusl()) { 250 | try { 251 | return require('./network-change.linux-arm-musleabihf.node') 252 | } catch (e) { 253 | loadErrors.push(e) 254 | } 255 | try { 256 | return require('@napi-rs/network-change-linux-arm-musleabihf') 257 | } catch (e) { 258 | loadErrors.push(e) 259 | } 260 | 261 | } else { 262 | try { 263 | return require('./network-change.linux-arm-gnueabihf.node') 264 | } catch (e) { 265 | loadErrors.push(e) 266 | } 267 | try { 268 | return require('@napi-rs/network-change-linux-arm-gnueabihf') 269 | } catch (e) { 270 | loadErrors.push(e) 271 | } 272 | 273 | } 274 | } else if (process.arch === 'riscv64') { 275 | if (isMusl()) { 276 | try { 277 | return require('./network-change.linux-riscv64-musl.node') 278 | } catch (e) { 279 | loadErrors.push(e) 280 | } 281 | try { 282 | return require('@napi-rs/network-change-linux-riscv64-musl') 283 | } catch (e) { 284 | loadErrors.push(e) 285 | } 286 | 287 | } else { 288 | try { 289 | return require('./network-change.linux-riscv64-gnu.node') 290 | } catch (e) { 291 | loadErrors.push(e) 292 | } 293 | try { 294 | return require('@napi-rs/network-change-linux-riscv64-gnu') 295 | } catch (e) { 296 | loadErrors.push(e) 297 | } 298 | 299 | } 300 | } else if (process.arch === 'ppc64') { 301 | try { 302 | return require('./network-change.linux-ppc64-gnu.node') 303 | } catch (e) { 304 | loadErrors.push(e) 305 | } 306 | try { 307 | return require('@napi-rs/network-change-linux-ppc64-gnu') 308 | } catch (e) { 309 | loadErrors.push(e) 310 | } 311 | 312 | } else if (process.arch === 's390x') { 313 | try { 314 | return require('./network-change.linux-s390x-gnu.node') 315 | } catch (e) { 316 | loadErrors.push(e) 317 | } 318 | try { 319 | return require('@napi-rs/network-change-linux-s390x-gnu') 320 | } catch (e) { 321 | loadErrors.push(e) 322 | } 323 | 324 | } else { 325 | loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`)) 326 | } 327 | } else { 328 | loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`)) 329 | } 330 | } 331 | 332 | nativeBinding = requireNative() 333 | 334 | if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) { 335 | try { 336 | nativeBinding = require('./network-change.wasi.cjs') 337 | } catch (err) { 338 | if (process.env.NAPI_RS_FORCE_WASI) { 339 | loadErrors.push(err) 340 | } 341 | } 342 | if (!nativeBinding) { 343 | try { 344 | nativeBinding = require('@napi-rs/network-change-wasm32-wasi') 345 | } catch (err) { 346 | if (process.env.NAPI_RS_FORCE_WASI) { 347 | loadErrors.push(err) 348 | } 349 | } 350 | } 351 | } 352 | 353 | if (!nativeBinding) { 354 | if (loadErrors.length > 0) { 355 | // TODO Link to documentation with potential fixes 356 | // - The package owner could build/publish bindings for this arch 357 | // - The user may need to bundle the correct files 358 | // - The user may need to re-install node_modules to get new packages 359 | throw new Error('Failed to load native binding', { cause: loadErrors }) 360 | } 361 | throw new Error(`Failed to load native binding`) 362 | } 363 | 364 | module.exports.InternetMonitor = nativeBinding.InternetMonitor 365 | module.exports.NetworkStatus = nativeBinding.NetworkStatus 366 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | env: 3 | DEBUG: napi:* 4 | APP_NAME: network-change 5 | MACOSX_DEPLOYMENT_TARGET: '10.13' 6 | CARGO_INCREMENTAL: '1' 7 | permissions: 8 | contents: write 9 | id-token: write 10 | 'on': 11 | push: 12 | branches: 13 | - main 14 | tags-ignore: 15 | - '**' 16 | pull_request: null 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | cancel-in-progress: true 20 | jobs: 21 | lint: 22 | name: Lint 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | 27 | - name: Setup node 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 20 31 | cache: 'yarn' 32 | 33 | - name: Install 34 | uses: dtolnay/rust-toolchain@stable 35 | with: 36 | components: clippy, rustfmt 37 | 38 | - name: Install dependencies 39 | run: yarn install 40 | 41 | - name: ESLint 42 | run: yarn lint 43 | 44 | - name: Cargo fmt 45 | run: cargo fmt -- --check 46 | 47 | - name: Clippy 48 | run: cargo clippy 49 | build: 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | settings: 54 | - host: macos-latest 55 | target: x86_64-apple-darwin 56 | build: yarn build --target x86_64-apple-darwin 57 | - host: windows-latest 58 | build: yarn build --target x86_64-pc-windows-msvc 59 | target: x86_64-pc-windows-msvc 60 | - host: windows-latest 61 | build: | 62 | yarn build --target i686-pc-windows-msvc 63 | yarn test 64 | target: i686-pc-windows-msvc 65 | - host: ubuntu-latest 66 | target: x86_64-unknown-linux-gnu 67 | build: CC=clang yarn build --target x86_64-unknown-linux-gnu --use-napi-cross 68 | - host: ubuntu-latest 69 | target: x86_64-unknown-linux-musl 70 | build: yarn build --target x86_64-unknown-linux-musl -x 71 | - host: macos-latest 72 | target: aarch64-apple-darwin 73 | build: yarn build --target aarch64-apple-darwin 74 | - host: ubuntu-latest 75 | target: aarch64-unknown-linux-gnu 76 | build: CC=clang yarn build --target aarch64-unknown-linux-gnu --use-napi-cross 77 | - host: ubuntu-latest 78 | target: armv7-unknown-linux-gnueabihf 79 | build: CC=clang yarn build --target armv7-unknown-linux-gnueabihf --use-napi-cross 80 | - host: ubuntu-latest 81 | target: aarch64-linux-android 82 | build: yarn build --target aarch64-linux-android 83 | - host: ubuntu-latest 84 | target: armv7-linux-androideabi 85 | build: yarn build --target armv7-linux-androideabi 86 | - host: ubuntu-latest 87 | target: aarch64-unknown-linux-musl 88 | build: yarn build --target aarch64-unknown-linux-musl -x 89 | - host: ubuntu-latest 90 | target: powerpc64le-unknown-linux-gnu 91 | build: | 92 | sudo apt-get update 93 | sudo apt-get install -y gcc-powerpc64le-linux-gnu 94 | yarn build --target powerpc64le-unknown-linux-gnu 95 | - host: ubuntu-latest 96 | target: s390x-unknown-linux-gnu 97 | build: | 98 | sudo apt-get update 99 | sudo apt-get install -y gcc-s390x-linux-gnu 100 | yarn build --target s390x-unknown-linux-gnu 101 | - host: windows-latest 102 | target: aarch64-pc-windows-msvc 103 | build: yarn build --target aarch64-pc-windows-msvc 104 | name: stable - ${{ matrix.settings.target }} - node@20 105 | runs-on: ${{ matrix.settings.host }} 106 | steps: 107 | - uses: actions/checkout@v4 108 | - name: Setup node 109 | uses: actions/setup-node@v4 110 | if: ${{ !matrix.settings.docker }} 111 | with: 112 | node-version: 20 113 | cache: yarn 114 | - name: Install 115 | uses: dtolnay/rust-toolchain@stable 116 | with: 117 | toolchain: stable 118 | targets: ${{ matrix.settings.target }} 119 | - name: Cache cargo 120 | uses: actions/cache@v4 121 | with: 122 | path: | 123 | ~/.cargo 124 | ~/.napi-rs 125 | target/ 126 | key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }} 127 | - uses: goto-bus-stop/setup-zig@v2 128 | if: ${{ contains(matrix.settings.target, 'musl') }} 129 | with: 130 | version: 0.13.0 131 | - name: Install cargo-zigbuild 132 | uses: taiki-e/install-action@v2 133 | if: ${{ contains(matrix.settings.target, 'musl') }} 134 | env: 135 | GITHUB_TOKEN: ${{ github.token }} 136 | with: 137 | tool: cargo-zigbuild 138 | - name: Setup toolchain 139 | run: ${{ matrix.settings.setup }} 140 | if: ${{ matrix.settings.setup }} 141 | shell: bash 142 | - name: Setup node x86 143 | if: matrix.settings.target == 'i686-pc-windows-msvc' 144 | run: yarn config set supportedArchitectures.cpu "ia32" 145 | shell: bash 146 | - name: Install dependencies 147 | run: yarn install 148 | - name: Setup node x86 149 | uses: actions/setup-node@v4 150 | if: matrix.settings.target == 'i686-pc-windows-msvc' 151 | with: 152 | node-version: 20 153 | architecture: x86 154 | - name: Build 155 | run: ${{ matrix.settings.build }} 156 | shell: bash 157 | - name: Upload artifact 158 | uses: actions/upload-artifact@v4 159 | with: 160 | name: bindings-${{ matrix.settings.target }} 161 | path: | 162 | ${{ env.APP_NAME }}.*.node 163 | if-no-files-found: error 164 | build-freebsd: 165 | runs-on: ubuntu-latest 166 | name: Build FreeBSD 167 | steps: 168 | - uses: actions/checkout@v4 169 | - name: Build 170 | id: build 171 | uses: cross-platform-actions/action@v0.28.0 172 | env: 173 | DEBUG: napi:* 174 | RUSTUP_IO_THREADS: 1 175 | with: 176 | operating_system: freebsd 177 | version: '14.0' 178 | memory: 8G 179 | cpu_count: 3 180 | environment_variables: 'DEBUG RUSTUP_IO_THREADS' 181 | shell: bash 182 | run: | 183 | sudo pkg install -y -f curl node libnghttp2 npm 184 | sudo npm install -g yarn --ignore-scripts 185 | curl https://sh.rustup.rs -sSf --output rustup.sh 186 | sh rustup.sh -y --profile minimal --default-toolchain beta 187 | source "$HOME/.cargo/env" 188 | echo "~~~~ rustc --version ~~~~" 189 | rustc --version 190 | echo "~~~~ node -v ~~~~" 191 | node -v 192 | echo "~~~~ yarn --version ~~~~" 193 | yarn --version 194 | pwd 195 | ls -lah 196 | whoami 197 | env 198 | freebsd-version 199 | yarn install 200 | yarn build 201 | rm -rf node_modules 202 | rm -rf target 203 | rm -rf .yarn/cache 204 | - name: Upload artifact 205 | uses: actions/upload-artifact@v4 206 | with: 207 | name: bindings-freebsd 208 | path: ${{ env.APP_NAME }}.*.node 209 | if-no-files-found: error 210 | test-macOS-windows-binding: 211 | name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} 212 | needs: 213 | - build 214 | strategy: 215 | fail-fast: false 216 | matrix: 217 | settings: 218 | - host: windows-latest 219 | target: x86_64-pc-windows-msvc 220 | architecture: x64 221 | - host: macos-latest 222 | target: aarch64-apple-darwin 223 | architecture: arm64 224 | - host: macos-latest 225 | target: x86_64-apple-darwin 226 | architecture: x64 227 | node: 228 | - '18' 229 | - '20' 230 | runs-on: ${{ matrix.settings.host }} 231 | steps: 232 | - uses: actions/checkout@v4 233 | - name: Setup node 234 | uses: actions/setup-node@v4 235 | with: 236 | node-version: ${{ matrix.node }} 237 | cache: yarn 238 | architecture: ${{ matrix.settings.architecture }} 239 | - name: Install dependencies 240 | run: yarn install 241 | - name: Download artifacts 242 | uses: actions/download-artifact@v4 243 | with: 244 | name: bindings-${{ matrix.settings.target }} 245 | path: . 246 | - name: List packages 247 | run: ls -R . 248 | shell: bash 249 | - name: Test bindings 250 | run: yarn test 251 | test-linux-binding: 252 | name: Test ${{ matrix.target }} - node@${{ matrix.node }} 253 | needs: 254 | - build 255 | strategy: 256 | fail-fast: false 257 | matrix: 258 | target: 259 | - x86_64-unknown-linux-gnu 260 | - x86_64-unknown-linux-musl 261 | - aarch64-unknown-linux-gnu 262 | - aarch64-unknown-linux-musl 263 | - armv7-unknown-linux-gnueabihf 264 | - s390x-unknown-linux-gnu 265 | - powerpc64le-unknown-linux-gnu 266 | node: 267 | - '18' 268 | - '20' 269 | exclude: 270 | # too slow 271 | - target: aarch64-unknown-linux-gnu 272 | node: '18' 273 | - target: s390x-unknown-linux-gnu 274 | node: '18' 275 | runs-on: ubuntu-latest 276 | steps: 277 | - uses: actions/checkout@v4 278 | - name: Setup node 279 | uses: actions/setup-node@v4 280 | with: 281 | node-version: ${{ matrix.node }} 282 | cache: yarn 283 | - name: Output docker params 284 | id: docker 285 | run: | 286 | node -e " 287 | if ('${{ matrix.target }}'.startsWith('aarch64')) { 288 | console.log('PLATFORM=linux/arm64') 289 | } else if ('${{ matrix.target }}'.startsWith('armv7')) { 290 | console.log('PLATFORM=linux/arm/v7') 291 | } else if ('${{ matrix.target }}'.startsWith('powerpc64le')) { 292 | console.log('PLATFORM=linux/ppc64le') 293 | } else if ('${{ matrix.target }}'.startsWith('s390x')) { 294 | console.log('PLATFORM=linux/s390x') 295 | } else { 296 | console.log('PLATFORM=linux/amd64') 297 | } 298 | " >> $GITHUB_OUTPUT 299 | node -e " 300 | if ('${{ matrix.target }}'.endsWith('-musl')) { 301 | console.log('IMAGE=node:${{ matrix.node }}-alpine') 302 | } else { 303 | console.log('IMAGE=node:${{ matrix.node }}-slim') 304 | } 305 | " >> $GITHUB_OUTPUT 306 | - name: Install dependencies 307 | run: | 308 | yarn config set --json supportedArchitectures.os '["current", "linux"]' 309 | yarn config set --json supportedArchitectures.cpu '["current", "arm64", "arm", "ppc64", "s390x", "x64"]' 310 | yarn config set --json supportedArchitectures.libc '["current", "glibc", "musl"]' 311 | yarn install 312 | - name: Download artifacts 313 | uses: actions/download-artifact@v4 314 | with: 315 | name: bindings-${{ matrix.target }} 316 | path: . 317 | - name: List packages 318 | run: ls -R . 319 | shell: bash 320 | - name: Set up QEMU 321 | uses: docker/setup-qemu-action@v3 322 | with: 323 | platforms: all 324 | - run: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 325 | - name: Test bindings 326 | uses: addnab/docker-run-action@v3 327 | with: 328 | image: ${{ steps.docker.outputs.IMAGE }} 329 | options: -v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} --platform ${{ steps.docker.outputs.PLATFORM }} 330 | run: yarn run test 331 | publish: 332 | name: Publish 333 | runs-on: ubuntu-latest 334 | needs: 335 | - lint 336 | - build-freebsd 337 | - test-macOS-windows-binding 338 | - test-linux-binding 339 | steps: 340 | - uses: actions/checkout@v4 341 | - name: Setup node 342 | uses: actions/setup-node@v4 343 | with: 344 | node-version: 20 345 | cache: yarn 346 | - name: Install dependencies 347 | run: yarn install 348 | - name: Download all artifacts 349 | uses: actions/download-artifact@v4 350 | with: 351 | path: artifacts 352 | - name: create npm dirs 353 | run: yarn napi create-npm-dirs 354 | - name: Move artifacts 355 | run: yarn artifacts 356 | - name: List packages 357 | run: ls -R ./npm 358 | shell: bash 359 | - name: Publish 360 | run: | 361 | npm config set provenance true 362 | if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; 363 | then 364 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 365 | npm publish --access public 366 | elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; 367 | then 368 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 369 | npm publish --tag next --access public 370 | else 371 | echo "Not a release, skipping publish" 372 | fi 373 | env: 374 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 375 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 376 | -------------------------------------------------------------------------------- /src/linux.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, LazyLock, Mutex}; 2 | 3 | use crate::NetworkInfo; 4 | use crate::NetworkStatus; 5 | use napi::bindgen_prelude::*; 6 | use napi::threadsafe_function::{ 7 | ThreadsafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, 8 | }; 9 | use napi_derive::napi; 10 | 11 | const SIGNAL_NAME: &std::ffi::CStr = c"notify::connectivity"; 12 | 13 | static NETWORK_INFO: LazyLock> = LazyLock::new(|| { 14 | Mutex::new(NetworkInfo { 15 | status: NetworkStatus::Invalid, 16 | is_expensive: false, 17 | is_low_data_mode: false, 18 | has_ipv4: false, 19 | has_ipv6: false, 20 | has_dns: false, 21 | }) 22 | }); 23 | 24 | #[allow(clippy::type_complexity)] 25 | static GLOBAL_HANDLER: LazyLock>>> = 26 | LazyLock::new(|| Mutex::new(None)); 27 | 28 | #[derive(Clone, Copy)] 29 | struct MainLoopWrapper(*mut ffi::GMainLoop); 30 | unsafe impl Send for MainLoopWrapper {} 31 | unsafe impl Sync for MainLoopWrapper {} 32 | 33 | #[napi] 34 | pub struct InternetMonitor { 35 | client: *mut ffi::NMClient, 36 | signal_id: Arc>>, 37 | thread_handle: Option>, 38 | lo: MainLoopWrapper, 39 | } 40 | 41 | impl Drop for InternetMonitor { 42 | fn drop(&mut self) { 43 | println!("Dropping InternetMonitor"); 44 | self.stop(); 45 | unsafe { 46 | ffi::g_main_loop_quit(self.lo.0); 47 | } 48 | if let Some(thread_handle) = self.thread_handle.take() { 49 | thread_handle.join().unwrap(); 50 | } 51 | } 52 | } 53 | 54 | #[napi] 55 | impl InternetMonitor { 56 | #[napi(constructor)] 57 | pub fn new() -> Result { 58 | let client = unsafe { ffi::nm_client_new(std::ptr::null_mut(), std::ptr::null_mut()) }; 59 | if client.is_null() { 60 | return Err(Error::new( 61 | Status::GenericFailure, 62 | "Error initializing NetworkManager client.", 63 | )); 64 | } 65 | 66 | network_changed_cb(client, std::ptr::null_mut(), std::ptr::null_mut()); 67 | 68 | let lo = MainLoopWrapper(unsafe { ffi::g_main_loop_new(core::ptr::null_mut(), 0) }); 69 | let thread_handle = std::thread::spawn(move || { 70 | let l = lo; 71 | // SAFETY: we know we already init it before AND no other thread will access it. 72 | unsafe { ffi::g_main_loop_run(l.0) } 73 | }); 74 | 75 | Ok(Self { 76 | client, 77 | signal_id: Arc::new(Mutex::new(None)), 78 | thread_handle: Some(thread_handle), 79 | lo, 80 | }) 81 | } 82 | 83 | #[napi] 84 | pub fn current(&self) -> NetworkInfo { 85 | NETWORK_INFO.lock().unwrap().clone() 86 | } 87 | 88 | #[napi] 89 | /// Start the InternetMonitor, it will keep the Node.js alive unless you call stop on it. 90 | pub fn start(&mut self, on_update: Function) -> Result<()> { 91 | let change_handler = Arc::new( 92 | on_update 93 | .build_threadsafe_function() 94 | .callee_handled::() 95 | .weak::() 96 | .build_callback(ctx_to_path)?, 97 | ); 98 | self.start_inner::(change_handler) 99 | } 100 | 101 | #[napi] 102 | /// Start the InternetMonitor with weak reference, it will not keep the Node.js alive. 103 | pub fn start_weak(&mut self, on_update: Function) -> Result<()> { 104 | let change_handler = Arc::new( 105 | on_update 106 | .build_threadsafe_function() 107 | .callee_handled::() 108 | .weak::() 109 | .build_callback(ctx_to_path)?, 110 | ); 111 | self.start_inner::(change_handler) 112 | } 113 | 114 | fn start_inner( 115 | &mut self, 116 | change_handler: Arc>, 117 | ) -> Result<()> { 118 | let change_handler_for_cost = change_handler.clone(); 119 | 120 | GLOBAL_HANDLER 121 | .lock() 122 | .unwrap() 123 | .replace(Box::new(move |info| { 124 | change_handler_for_cost.call(info, ThreadsafeFunctionCallMode::Blocking); 125 | })); 126 | 127 | let signal_id = self.signal_id.clone(); 128 | unsafe { 129 | signal_id.lock().unwrap().replace(ffi::g_signal_connect( 130 | self.client, 131 | SIGNAL_NAME.as_ptr(), 132 | network_changed_cb, 133 | std::ptr::null_mut(), 134 | )); 135 | } 136 | 137 | Ok(()) 138 | } 139 | 140 | #[napi] 141 | /// Stop the InternetMonitor. 142 | /// 143 | /// If you don't call this method and leave the monitor alone, it will be stopped automatically when it is GC. 144 | pub fn stop(&mut self) { 145 | let signal_id = self.signal_id.lock().unwrap().take(); 146 | unsafe { 147 | if let Some(signal_id) = signal_id { 148 | ffi::g_signal_handler_disconnect(self.client, signal_id); 149 | } 150 | } 151 | } 152 | } 153 | 154 | #[inline] 155 | fn ctx_to_path(ctx: ThreadsafeCallContext) -> Result { 156 | Ok(ctx.value) 157 | } 158 | 159 | extern "C" fn network_changed_cb( 160 | client: *mut ffi::NMClient, 161 | _: *mut core::ffi::c_void, 162 | _: *mut core::ffi::c_void, 163 | ) { 164 | let mut info = NETWORK_INFO.lock().unwrap(); 165 | 166 | let metered = unsafe { ffi::nm_client_get_metered(client) }; 167 | info.is_low_data_mode = matches!( 168 | metered, 169 | ffi::NMMetered::NM_METERED_YES | ffi::NMMetered::NM_METERED_GUESS_YES 170 | ); 171 | 172 | let devices = unsafe { &*ffi::nm_client_get_devices(client) }; 173 | for i in 0..devices.len { 174 | let device = unsafe { (devices.pdata as *mut *mut ffi::NMDevice).add(i as usize) }; 175 | let device_type = unsafe { ffi::nm_device_get_device_type(*device) }; 176 | 177 | // Check if the connection is expensive (e.g., mobile broadband) 178 | if device_type == ffi::NMDeviceType::NM_DEVICE_TYPE_MODEM { 179 | info.is_expensive = true; 180 | } 181 | 182 | // Check for IPv4 connectivity 183 | let ip4_config = unsafe { ffi::nm_device_get_ip4_config(*device) }; 184 | if !ip4_config.is_null() { 185 | info.has_ipv4 = true; 186 | } 187 | 188 | // Check for IPv6 connectivity 189 | let ip6_config = unsafe { ffi::nm_device_get_ip6_config(*device) }; 190 | if !ip6_config.is_null() { 191 | info.has_ipv6 = true; 192 | } 193 | } 194 | 195 | // Check DNS configuration from global NM settings 196 | let active_conn = unsafe { ffi::nm_client_get_primary_connection(client) }; 197 | if !active_conn.is_null() { 198 | let ip_config = unsafe { ffi::nm_active_connection_get_ip4_config(active_conn) }; 199 | if !ip_config.is_null() && !unsafe { ffi::nm_ip_config_get_nameservers(ip_config) }.is_null() { 200 | info.has_dns = true; 201 | } 202 | } 203 | 204 | // Determine network status 205 | let connectivity = unsafe { ffi::nm_client_get_connectivity(client) }; 206 | match connectivity { 207 | ffi::NMConnectivityState::NM_CONNECTIVITY_FULL => { 208 | info.status = NetworkStatus::Satisfied; 209 | } 210 | ffi::NMConnectivityState::NM_CONNECTIVITY_LIMITED 211 | | ffi::NMConnectivityState::NM_CONNECTIVITY_PORTAL => { 212 | info.status = NetworkStatus::Satisfiable; 213 | } 214 | ffi::NMConnectivityState::NM_CONNECTIVITY_NONE => { 215 | info.status = NetworkStatus::Unsatisfied; 216 | } 217 | _ => { 218 | info.status = NetworkStatus::Invalid; 219 | } 220 | } 221 | 222 | if let Some(f) = GLOBAL_HANDLER.lock().unwrap().as_ref() { 223 | f(info.clone()) 224 | } 225 | } 226 | 227 | #[allow(non_camel_case_types)] 228 | #[allow(non_snake_case)] 229 | #[allow(unused)] 230 | mod ffi { 231 | pub use std::ffi::{c_char, c_int, c_ulong, c_void}; 232 | 233 | macro_rules! enum_with_val { 234 | ($(#[$meta:meta])* $vis:vis struct $ident:ident($innervis:vis $ty:ty) { 235 | $($(#[$varmeta:meta])* $variant:ident = $num:expr),* $(,)* 236 | }) => { 237 | $(#[$meta])* 238 | #[repr(transparent)] 239 | $vis struct $ident($innervis $ty); 240 | impl $ident { 241 | $($(#[$varmeta])* $vis const $variant: $ident = $ident($num);)* 242 | } 243 | 244 | impl ::core::fmt::Debug for $ident { 245 | fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { 246 | match self { 247 | $(&$ident::$variant => write!(f, "{}::{}", stringify!($ident), stringify!($variant)),)* 248 | &$ident(v) => write!(f, "UNKNOWN({})", v), 249 | } 250 | } 251 | } 252 | } 253 | } 254 | 255 | enum_with_val! { 256 | #[derive(PartialEq, Eq, Clone, Copy)] 257 | pub struct NMConnectivityState(pub c_int) { 258 | NM_CONNECTIVITY_UNKNOWN = 0, 259 | NM_CONNECTIVITY_NONE = 1, 260 | NM_CONNECTIVITY_PORTAL = 2, 261 | NM_CONNECTIVITY_LIMITED = 3, 262 | NM_CONNECTIVITY_FULL = 4, 263 | } 264 | } 265 | 266 | enum_with_val! { 267 | #[derive(PartialEq, Eq, Clone, Copy)] 268 | pub struct NMMetered(pub c_int) { 269 | NM_METERED_UNKNOWN = 0, 270 | NM_METERED_YES = 1, 271 | NM_METERED_NO = 2, 272 | NM_METERED_GUESS_YES = 3, 273 | NM_METERED_GUESS_NO = 4, 274 | } 275 | } 276 | 277 | pub type NMClient = *mut c_void; 278 | 279 | type gpointer = *mut c_void; 280 | type guint = u32; 281 | 282 | #[repr(C)] 283 | pub struct GPtrArray { 284 | pub pdata: *mut gpointer, 285 | pub len: guint, 286 | } 287 | 288 | #[repr(C)] 289 | pub struct NMDevice { 290 | _unused: [u8; 0], 291 | } 292 | 293 | #[repr(C)] 294 | pub struct NMActiveConnection { 295 | _unused: [u8; 0], 296 | } 297 | 298 | #[repr(C)] 299 | pub struct Cancellable { 300 | _unused: [u8; 0], 301 | } 302 | 303 | #[repr(C)] 304 | pub struct GError { 305 | _unused: [u8; 0], 306 | } 307 | 308 | enum_with_val! { 309 | #[derive(PartialEq, Eq, Clone, Copy)] 310 | pub struct NMDeviceType(c_int) { 311 | NM_DEVICE_TYPE_UNKNOWN = 0, 312 | NM_DEVICE_TYPE_ETHERNET = 1, 313 | NM_DEVICE_TYPE_WIFI = 2, 314 | NM_DEVICE_TYPE_UNUSED1 = 3, 315 | NM_DEVICE_TYPE_UNUSED2 = 4, 316 | NM_DEVICE_TYPE_BT = 5, /* Bluetooth */ 317 | NM_DEVICE_TYPE_OLPC_MESH = 6, 318 | NM_DEVICE_TYPE_WIMAX = 7, 319 | NM_DEVICE_TYPE_MODEM = 8, 320 | NM_DEVICE_TYPE_INFINIBAND = 9, 321 | NM_DEVICE_TYPE_BOND = 10, 322 | NM_DEVICE_TYPE_VLAN = 11, 323 | NM_DEVICE_TYPE_ADSL = 12, 324 | NM_DEVICE_TYPE_BRIDGE = 13, 325 | NM_DEVICE_TYPE_GENERIC = 14, 326 | NM_DEVICE_TYPE_TEAM = 15, 327 | NM_DEVICE_TYPE_TUN = 16, 328 | NM_DEVICE_TYPE_IP_TUNNEL = 17, 329 | NM_DEVICE_TYPE_MACVLAN = 18, 330 | NM_DEVICE_TYPE_VXLAN = 19, 331 | NM_DEVICE_TYPE_VETH = 20, 332 | NM_DEVICE_TYPE_MACSEC = 21, 333 | NM_DEVICE_TYPE_DUMMY = 22, 334 | NM_DEVICE_TYPE_PPP = 23, 335 | NM_DEVICE_TYPE_OVS_INTERFACE = 24, 336 | NM_DEVICE_TYPE_OVS_PORT = 25, 337 | NM_DEVICE_TYPE_OVS_BRIDGE = 26, 338 | NM_DEVICE_TYPE_WPAN = 27, 339 | NM_DEVICE_TYPE_6LOWPAN = 28, 340 | NM_DEVICE_TYPE_WIREGUARD = 29, 341 | NM_DEVICE_TYPE_WIFI_P2P = 30, 342 | NM_DEVICE_TYPE_VRF = 31, 343 | } 344 | } 345 | 346 | #[repr(C)] 347 | pub struct NMIPConfig { 348 | _unused: [u8; 0], 349 | } 350 | 351 | #[cfg_attr(any(target_os = "linux",), link(name = "nm", kind = "dylib"))] 352 | extern "C" { 353 | pub fn nm_client_new(callcellable: *mut Cancellable, error: *mut GError) -> *mut NMClient; 354 | 355 | pub fn nm_client_get_devices(client: *mut NMClient) -> *mut GPtrArray; 356 | pub fn nm_device_get_device_type(device: *mut NMDevice) -> NMDeviceType; 357 | pub fn nm_device_get_ip4_config(device: *mut NMDevice) -> *mut NMIPConfig; 358 | pub fn nm_device_get_ip6_config(device: *mut NMDevice) -> *mut NMIPConfig; 359 | pub fn nm_client_get_primary_connection(device: *mut NMClient) -> *mut NMActiveConnection; 360 | pub fn nm_active_connection_get_ip4_config(device: *mut NMActiveConnection) -> *mut NMIPConfig; 361 | pub fn nm_ip_config_get_nameservers(ip_config: *mut NMIPConfig) -> *mut GPtrArray; 362 | pub fn nm_client_get_connectivity(client: *mut NMClient) -> NMConnectivityState; 363 | pub fn nm_client_get_metered(client: *mut NMClient) -> NMMetered; 364 | } 365 | 366 | pub type gchar = c_char; 367 | pub type gulong = c_ulong; 368 | pub type gint = c_int; 369 | pub type GClosureNotify = extern "C" fn(); 370 | pub type gboolean = gint; 371 | 372 | #[repr(C)] 373 | pub struct GMainContext { 374 | _unused: [u8; 0], 375 | } 376 | 377 | #[repr(C)] 378 | pub struct GMainLoop { 379 | _unused: [u8; 0], 380 | } 381 | #[cfg_attr(any(target_os = "linux",), link(name = "glib-2.0", kind = "dylib"))] 382 | extern "C" { 383 | fn g_signal_connect_data( 384 | instance: *mut NMClient, 385 | detailed_signal: *const gchar, 386 | c_handler: extern "C" fn(client: *mut NMClient, _: *mut c_void, user_data: *mut c_void), 387 | data: *mut c_void, 388 | destroy_data: Option, 389 | connect_flags: c_int, 390 | ) -> gulong; 391 | pub fn g_signal_handler_disconnect(instance: *mut NMClient, signal_id: gulong); 392 | 393 | pub fn g_main_loop_new(context: *mut GMainContext, is_running: gboolean) -> *mut GMainLoop; 394 | pub fn g_main_loop_run(lo: *mut GMainLoop); 395 | pub fn g_main_loop_quit(lo: *mut GMainLoop); 396 | } 397 | 398 | pub unsafe fn g_signal_connect( 399 | instance: *mut NMClient, 400 | detailed_signal: *const gchar, 401 | c_handler: extern "C" fn(client: *mut NMClient, _: *mut c_void, user_data: *mut c_void), 402 | data: *mut c_void, 403 | ) -> gulong { 404 | g_signal_connect_data(instance, detailed_signal, c_handler, data, None, 0) 405 | } 406 | } 407 | -------------------------------------------------------------------------------- /src/windows.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::mem::MaybeUninit; 3 | use std::rc::Rc; 4 | use std::sync::{ 5 | atomic::{AtomicBool, AtomicU8, Ordering}, 6 | Arc, 7 | }; 8 | 9 | use bitflags::bitflags; 10 | use napi::bindgen_prelude::*; 11 | use napi::threadsafe_function::{ 12 | ThreadsafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, 13 | }; 14 | use napi_derive::napi; 15 | use windows::Win32::Foundation::{self, ERROR_BUFFER_OVERFLOW}; 16 | use windows::Win32::NetworkManagement::Ndis::IfOperStatusUp; 17 | use windows::Win32::Networking::NetworkListManager::*; 18 | use windows::Win32::System::{self, Com::*}; 19 | use windows_core::{implement, IUnknown, Interface, HRESULT}; 20 | 21 | use crate::{NetworkInfo, NetworkStatus}; 22 | 23 | #[napi] 24 | pub struct InternetMonitor { 25 | network_events_manager: INetworkEvents, 26 | cost_event_manager: INetworkCostManagerEvents, 27 | advise_network_list_manager_cookie: u32, 28 | advise_cost_manager_cookie: u32, 29 | network_list_manager: Rc, 30 | network_list_manager_events_connection_point: IConnectionPoint, 31 | network_cost_manager: Rc, 32 | network_cost_manager_events_connection_point: IConnectionPoint, 33 | is_expensive: Arc, 34 | is_low_data_mode: Arc, 35 | has_ipv4: Arc, 36 | has_ipv6: Arc, 37 | has_dns: Arc, 38 | status: Arc, 39 | } 40 | 41 | #[napi::module_init] 42 | fn init() { 43 | unsafe { 44 | // https://stackoverflow.com/a/2979671 45 | CoInitializeEx(None, COINIT_MULTITHREADED) 46 | .ok() 47 | .expect("CoInitializeEx failed"); 48 | } 49 | } 50 | 51 | #[napi] 52 | impl InternetMonitor { 53 | #[napi(constructor)] 54 | pub fn new() -> Result { 55 | // SAFETY: Windows API requires unsafe block 56 | unsafe { 57 | let network_list_manager: Rc = Rc::new( 58 | CoCreateInstance(&NetworkListManager, None, CLSCTX_ALL).map_err(|_| { 59 | Error::new( 60 | Status::GenericFailure, 61 | "CoCreateInstance::CoCreateInstance INetworkListManager failed", 62 | ) 63 | })?, 64 | ); 65 | 66 | let network_cost_manager: Rc = Rc::new( 67 | CoCreateInstance(&NetworkListManager, None, CLSCTX_ALL).map_err(|_| { 68 | Error::new( 69 | Status::GenericFailure, 70 | "CoCreateInstance::CoCreateInstance INetworkCostManager failed", 71 | ) 72 | })?, 73 | ); 74 | 75 | let mut network_list_manager_connection_point_container: MaybeUninit< 76 | IConnectionPointContainer, 77 | > = MaybeUninit::uninit(); 78 | network_list_manager 79 | .query( 80 | &IConnectionPointContainer::IID, 81 | network_list_manager_connection_point_container 82 | .as_mut_ptr() 83 | .cast(), 84 | ) 85 | .ok() 86 | .map_err(|_| { 87 | Error::new( 88 | Status::GenericFailure, 89 | "INetworkListManager::QueryInterface failed", 90 | ) 91 | })?; 92 | 93 | let mut network_cost_manager_connection_point_container: MaybeUninit< 94 | IConnectionPointContainer, 95 | > = MaybeUninit::uninit(); 96 | network_cost_manager 97 | .query( 98 | &IConnectionPointContainer::IID, 99 | network_cost_manager_connection_point_container 100 | .as_mut_ptr() 101 | .cast(), 102 | ) 103 | .ok() 104 | .map_err(|_| { 105 | Error::new( 106 | Status::GenericFailure, 107 | "INetworkCostManager::QueryInterface failed", 108 | ) 109 | })?; 110 | 111 | // SAFETY: network_list_manager_connection_point_container is initialized when query is successful 112 | let network_list_manager_connection_point_container = 113 | network_list_manager_connection_point_container.assume_init(); 114 | 115 | let network_list_manager_events_connection_point = 116 | network_list_manager_connection_point_container 117 | .FindConnectionPoint(&INetworkEvents::IID) 118 | .map_err(|_| { 119 | Error::new( 120 | Status::GenericFailure, 121 | "FindConnectionPoint::FindConnectionPoint(INetworkListManagerEvents) failed", 122 | ) 123 | })?; 124 | 125 | // SAFETY: network_cost_manager_connection_point_container is initialized when query is successful 126 | let network_cost_manager_connection_point_container = 127 | network_cost_manager_connection_point_container.assume_init(); 128 | 129 | let network_cost_manager_events_connection_point = 130 | network_cost_manager_connection_point_container 131 | .FindConnectionPoint(&INetworkCostManagerEvents::IID) 132 | .map_err(|_| { 133 | Error::new( 134 | Status::GenericFailure, 135 | "FindConnectionPoint::FindConnectionPoint(INetworkCostManagerEvents) failed", 136 | ) 137 | })?; 138 | 139 | let mut network_info = NetworkInfo { 140 | has_ipv4: false, 141 | has_ipv6: false, 142 | has_dns: false, 143 | is_low_data_mode: false, 144 | is_expensive: false, 145 | status: NetworkStatus::Invalid, 146 | }; 147 | 148 | let is_expensive = Arc::new(AtomicBool::new(false)); 149 | let is_low_data_mode = Arc::new(AtomicBool::new(network_info.is_low_data_mode)); 150 | let status = Arc::new(AtomicU8::new(network_info.status as u8)); 151 | let mut get_network_info = || { 152 | { 153 | let connectivity = network_list_manager.GetConnectivity()?; 154 | 155 | let connections = network_list_manager.GetNetworkConnections()?; 156 | let mut all_connections = [None]; 157 | connections.Next(&mut all_connections, None)?; 158 | if let Some(Some(connection)) = all_connections.first() { 159 | let mut network_connection_cost: MaybeUninit = 160 | MaybeUninit::uninit(); 161 | connection 162 | .query( 163 | &INetworkConnectionCost::IID, 164 | network_connection_cost.as_mut_ptr().cast(), 165 | ) 166 | .ok()?; 167 | let network_connection_cost = network_connection_cost.assume_init(); 168 | let cost = network_connection_cost.GetCost()?; 169 | let mut data_plan = NLM_DATAPLAN_STATUS::default(); 170 | network_connection_cost.GetDataPlanStatus(&mut data_plan)?; 171 | is_expensive.store(data_plan.DataLimitInMegabytes != u32::MAX, Ordering::SeqCst); 172 | is_low_data_mode.store( 173 | cost > NlmConnectionCost::UNRESTRICTED.bits(), 174 | Ordering::SeqCst, 175 | ); 176 | network_info = get_network_info( 177 | connectivity, 178 | &is_expensive, 179 | &is_low_data_mode, 180 | &status, 181 | &network_list_manager, 182 | )?; 183 | } 184 | } 185 | Ok::<(), windows_core::Error>(()) 186 | }; 187 | 188 | get_network_info().map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))?; 189 | 190 | let has_ipv4 = Arc::new(AtomicBool::new(network_info.has_ipv4)); 191 | let has_ipv6 = Arc::new(AtomicBool::new(network_info.has_ipv6)); 192 | let has_dns = Arc::new(AtomicBool::new(network_info.has_dns)); 193 | 194 | Ok(Self { 195 | network_events_manager: NetworkEventsHandler { 196 | inner: Box::new(move |_status| {}), 197 | network_list_manager: network_list_manager.clone(), 198 | is_expensive: is_expensive.clone(), 199 | is_low_data_mode: is_low_data_mode.clone(), 200 | status: status.clone(), 201 | } 202 | .into(), 203 | cost_event_manager: NetworkCostEventsHandler { 204 | inner: Box::new(move |_status| {}), 205 | network_cost_manager: network_cost_manager.clone(), 206 | is_expensive: is_expensive.clone(), 207 | is_low_data_mode: is_low_data_mode.clone(), 208 | has_ipv4: has_ipv4.clone(), 209 | has_ipv6: has_ipv6.clone(), 210 | has_dns: has_dns.clone(), 211 | status: status.clone(), 212 | } 213 | .into(), 214 | advise_network_list_manager_cookie: 0, 215 | advise_cost_manager_cookie: 0, 216 | network_list_manager, 217 | network_list_manager_events_connection_point, 218 | network_cost_manager, 219 | network_cost_manager_events_connection_point, 220 | is_expensive, 221 | is_low_data_mode, 222 | has_ipv4, 223 | has_ipv6, 224 | has_dns, 225 | status, 226 | }) 227 | } 228 | } 229 | 230 | #[napi] 231 | pub fn current(&self) -> NetworkInfo { 232 | NetworkInfo { 233 | is_expensive: self.is_expensive.load(Ordering::SeqCst), 234 | is_low_data_mode: self.is_low_data_mode.load(Ordering::SeqCst), 235 | has_ipv4: self.has_ipv4.load(Ordering::SeqCst), 236 | has_ipv6: self.has_ipv6.load(Ordering::SeqCst), 237 | has_dns: self.has_dns.load(Ordering::SeqCst), 238 | status: match self.status.load(Ordering::SeqCst) { 239 | 0 => NetworkStatus::Invalid, 240 | 1 => NetworkStatus::Satisfied, 241 | 2 => NetworkStatus::Unsatisfied, 242 | 3 => NetworkStatus::Satisfiable, 243 | 4 => NetworkStatus::Unknown, 244 | _ => NetworkStatus::Invalid, 245 | }, 246 | } 247 | } 248 | 249 | #[napi] 250 | /// Start the path monitor, it will keep the Node.js alive unless you call stop on it. 251 | pub fn start(&mut self, on_update: Function) -> Result<()> { 252 | let change_handler = Arc::new( 253 | on_update 254 | .build_threadsafe_function() 255 | .callee_handled::() 256 | .weak::() 257 | .build_callback(ctx_to_path)?, 258 | ); 259 | self.start_inner::(change_handler) 260 | } 261 | 262 | #[napi] 263 | /// Start the path monitor with weak reference, it will not keep the Node.js alive. 264 | pub fn start_weak(&mut self, on_update: Function) -> Result<()> { 265 | let change_handler = Arc::new( 266 | on_update 267 | .build_threadsafe_function() 268 | .callee_handled::() 269 | .weak::() 270 | .build_callback(ctx_to_path)?, 271 | ); 272 | self.start_inner::(change_handler) 273 | } 274 | 275 | fn start_inner( 276 | &mut self, 277 | change_handler: Arc>, 278 | ) -> Result<()> { 279 | let change_handler_for_cost = change_handler.clone(); 280 | 281 | // SAFETY: Windows API requires unsafe block 282 | unsafe { 283 | let network_event: INetworkEvents = NetworkEventsHandler { 284 | inner: Box::new(move |status| { 285 | change_handler.call(status, ThreadsafeFunctionCallMode::NonBlocking); 286 | }), 287 | network_list_manager: self.network_list_manager.clone(), 288 | is_expensive: self.is_expensive.clone(), 289 | is_low_data_mode: self.is_low_data_mode.clone(), 290 | status: self.status.clone(), 291 | } 292 | .into(); 293 | let cost_event: INetworkCostManagerEvents = NetworkCostEventsHandler { 294 | inner: Box::new(move |status| { 295 | change_handler_for_cost.call(status, ThreadsafeFunctionCallMode::NonBlocking); 296 | }), 297 | network_cost_manager: self.network_cost_manager.clone(), 298 | is_expensive: self.is_expensive.clone(), 299 | is_low_data_mode: self.is_low_data_mode.clone(), 300 | has_ipv4: self.has_ipv4.clone(), 301 | has_ipv6: self.has_ipv6.clone(), 302 | has_dns: self.has_dns.clone(), 303 | status: self.status.clone(), 304 | } 305 | .into(); 306 | let mut cost_event_handler = MaybeUninit::::uninit(); 307 | cost_event 308 | .query(&IUnknown::IID, cost_event_handler.as_mut_ptr().cast()) 309 | .ok() 310 | .map_err(|_| { 311 | Error::new( 312 | Status::GenericFailure, 313 | "Failed to query IUnknown::IID on INetworkConnectionCostEvents", 314 | ) 315 | })?; 316 | let cost_event_handler = cost_event_handler.assume_init(); 317 | let advise_network_list_manager_cookie = self 318 | .network_list_manager_events_connection_point 319 | .Advise(&network_event) 320 | .map_err(handle_advise_error)?; 321 | let advise_cost_manager_cookie = self 322 | .network_cost_manager_events_connection_point 323 | .Advise(&cost_event_handler) 324 | .map_err(handle_advise_error)?; 325 | self.network_events_manager = network_event; 326 | self.cost_event_manager = cost_event; 327 | self.advise_network_list_manager_cookie = advise_network_list_manager_cookie; 328 | self.advise_cost_manager_cookie = advise_cost_manager_cookie; 329 | } 330 | Ok(()) 331 | } 332 | 333 | #[napi] 334 | /// Stop the path monitor. 335 | /// 336 | /// If you don't call this method and leave the monitor alone, it will be stopped automatically when it is GC. 337 | pub fn stop(&mut self) -> Result<()> { 338 | // SAFETY: Windows API requires unsafe block 339 | unsafe { 340 | if self.advise_network_list_manager_cookie != 0 { 341 | self 342 | .network_list_manager_events_connection_point 343 | .Unadvise(self.advise_network_list_manager_cookie) 344 | .map_err(|_| { 345 | Error::new( 346 | Status::GenericFailure, 347 | "IConnectionPoint::Unadvise INetworkListManagerEvents failed", 348 | ) 349 | })?; 350 | } 351 | 352 | if self.advise_cost_manager_cookie != 0 { 353 | self 354 | .network_cost_manager_events_connection_point 355 | .Unadvise(self.advise_cost_manager_cookie) 356 | .map_err(|_| { 357 | Error::new( 358 | Status::GenericFailure, 359 | "IConnectionPoint::Unadvise INetworkListManagerEvents failed", 360 | ) 361 | })?; 362 | } 363 | 364 | // unref the ThreadsafeFunction 365 | self.network_events_manager = NetworkEventsHandler { 366 | inner: Box::new(move |_status| {}), 367 | network_list_manager: self.network_list_manager.clone(), 368 | is_expensive: self.is_expensive.clone(), 369 | is_low_data_mode: self.is_low_data_mode.clone(), 370 | status: self.status.clone(), 371 | } 372 | .into(); 373 | 374 | // unref the ThreadsafeFunction 375 | self.cost_event_manager = NetworkCostEventsHandler { 376 | inner: Box::new(move |_status| {}), 377 | network_cost_manager: self.network_cost_manager.clone(), 378 | is_expensive: self.is_expensive.clone(), 379 | is_low_data_mode: self.is_low_data_mode.clone(), 380 | has_ipv4: self.has_ipv4.clone(), 381 | has_ipv6: self.has_ipv6.clone(), 382 | has_dns: self.has_dns.clone(), 383 | status: self.status.clone(), 384 | } 385 | .into(); 386 | } 387 | Ok(()) 388 | } 389 | } 390 | 391 | #[inline] 392 | fn ctx_to_path(ctx: ThreadsafeCallContext) -> Result { 393 | Ok(ctx.value) 394 | } 395 | 396 | fn handle_advise_error(err: windows_core::Error) -> Error { 397 | let message = match err.code() { 398 | Foundation::E_POINTER => Cow::Borrowed("The value in pUnkSink or pdwCookie is not valid. For example, either pointer may be NULL. "), 399 | System::Ole::CONNECT_E_ADVISELIMIT => { 400 | Cow::Borrowed("The connection point has already reached its limit of connections and cannot accept any more.") 401 | } 402 | System::Ole::CONNECT_E_CANNOTCONNECT => { 403 | Cow::Borrowed("The sink does not support the interface required by this connection point.") 404 | } 405 | _ => Cow::Owned(format!("{err}")), 406 | }; 407 | Error::new( 408 | Status::GenericFailure, 409 | format!("IConnectionPoint::Advise INetworkConnectionCostEvents failed {message}",), 410 | ) 411 | } 412 | 413 | #[implement(INetworkEvents)] 414 | struct NetworkEventsHandler { 415 | inner: Box, 416 | is_expensive: Arc, 417 | is_low_data_mode: Arc, 418 | status: Arc, 419 | network_list_manager: Rc, 420 | } 421 | 422 | #[implement(INetworkCostManagerEvents)] 423 | struct NetworkCostEventsHandler { 424 | inner: Box, 425 | network_cost_manager: Rc, 426 | is_expensive: Arc, 427 | is_low_data_mode: Arc, 428 | has_ipv4: Arc, 429 | has_ipv6: Arc, 430 | has_dns: Arc, 431 | status: Arc, 432 | } 433 | 434 | impl INetworkEvents_Impl for NetworkEventsHandler_Impl { 435 | fn NetworkAdded(&self, _networkid: &windows_core::GUID) -> windows_core::Result<()> { 436 | Ok(()) 437 | } 438 | 439 | fn NetworkDeleted(&self, _networkid: &windows_core::GUID) -> windows_core::Result<()> { 440 | Ok(()) 441 | } 442 | 443 | fn NetworkConnectivityChanged( 444 | &self, 445 | _: &windows_core::GUID, 446 | new_connectivity: NLM_CONNECTIVITY, 447 | ) -> windows_core::Result<()> { 448 | (self.inner)(get_network_info( 449 | new_connectivity, 450 | &self.is_expensive, 451 | &self.is_low_data_mode, 452 | &self.status, 453 | &self.network_list_manager, 454 | )?); 455 | 456 | Ok(()) 457 | } 458 | 459 | fn NetworkPropertyChanged( 460 | &self, 461 | _networkid: &windows_core::GUID, 462 | _flags: NLM_NETWORK_PROPERTY_CHANGE, 463 | ) -> windows_core::Result<()> { 464 | Ok(()) 465 | } 466 | } 467 | 468 | bitflags! { 469 | #[derive(Debug)] 470 | pub struct NlmConnectionCost: u32 { 471 | const UNKNOWN = 0; 472 | const UNRESTRICTED = 0x1; 473 | const FIXED = 0x2; 474 | const VARIABLE = 0x4; 475 | const OVERDATALIMIT = 0x10000; 476 | const CONGESTED = 0x20000; 477 | const ROAMING = 0x40000; 478 | const APPROACHINGDATALIMIT = 0x80000; 479 | } 480 | } 481 | 482 | impl INetworkCostManagerEvents_Impl for NetworkCostEventsHandler_Impl { 483 | fn CostChanged(&self, newcost: u32, _pdestaddr: *const NLM_SOCKADDR) -> windows_core::Result<()> { 484 | let is_low_data_mode = newcost > NlmConnectionCost::UNRESTRICTED.bits(); 485 | self 486 | .is_low_data_mode 487 | .store(is_low_data_mode, Ordering::SeqCst); 488 | (self.inner)(NetworkInfo { 489 | is_expensive: self.is_expensive.load(Ordering::SeqCst), 490 | is_low_data_mode, 491 | has_ipv4: self.has_ipv4.load(Ordering::SeqCst), 492 | has_ipv6: self.has_ipv6.load(Ordering::SeqCst), 493 | has_dns: self.has_dns.load(Ordering::SeqCst), 494 | status: match self.status.load(Ordering::SeqCst) { 495 | 0 => NetworkStatus::Invalid, 496 | 1 => NetworkStatus::Satisfied, 497 | 2 => NetworkStatus::Unsatisfied, 498 | 3 => NetworkStatus::Satisfiable, 499 | 4 => NetworkStatus::Unknown, 500 | _ => return Err(windows_core::Error::empty()), 501 | }, 502 | }); 503 | Ok(()) 504 | } 505 | 506 | fn DataPlanStatusChanged(&self, pdestaddr: *const NLM_SOCKADDR) -> windows_core::Result<()> { 507 | let mut data_plan_status = NLM_DATAPLAN_STATUS::default(); 508 | unsafe { 509 | self 510 | .network_cost_manager 511 | .GetDataPlanStatus(&mut data_plan_status, pdestaddr)? 512 | }; 513 | let is_unlimited = data_plan_status.DataLimitInMegabytes == u32::MAX; 514 | if is_unlimited { 515 | self.is_expensive.store(false, Ordering::SeqCst); 516 | self.is_low_data_mode.store(false, Ordering::SeqCst); 517 | } 518 | self.is_expensive.store(!is_unlimited, Ordering::SeqCst); 519 | (self.inner)(NetworkInfo { 520 | is_expensive: !is_unlimited, 521 | is_low_data_mode: self.is_low_data_mode.load(Ordering::SeqCst), 522 | has_ipv4: self.has_ipv4.load(Ordering::SeqCst), 523 | has_ipv6: self.has_ipv6.load(Ordering::SeqCst), 524 | has_dns: self.has_dns.load(Ordering::SeqCst), 525 | status: match self.status.load(Ordering::SeqCst) { 526 | 0 => NetworkStatus::Invalid, 527 | 1 => NetworkStatus::Satisfied, 528 | 2 => NetworkStatus::Unsatisfied, 529 | 3 => NetworkStatus::Satisfiable, 530 | 4 => NetworkStatus::Unknown, 531 | _ => NetworkStatus::Invalid, 532 | }, 533 | }); 534 | Ok(()) 535 | } 536 | } 537 | 538 | fn get_available_connections< 539 | F: FnMut( 540 | &windows::Win32::NetworkManagement::IpHelper::IP_ADAPTER_ADDRESSES_LH, 541 | ) -> windows_core::Result, 542 | >( 543 | mut callback: F, 544 | ) -> windows_core::Result<()> { 545 | use windows::Win32::NetworkManagement::IpHelper::{ 546 | GetAdaptersAddresses, GAA_FLAG_INCLUDE_ALL_INTERFACES, IP_ADAPTER_ADDRESSES_LH, 547 | }; 548 | use windows::Win32::Networking::WinSock::AF_UNSPEC; 549 | 550 | unsafe { 551 | let mut buffer_length = 0; 552 | let code = GetAdaptersAddresses( 553 | AF_UNSPEC.0 as u32, 554 | GAA_FLAG_INCLUDE_ALL_INTERFACES, 555 | None, 556 | None, 557 | &mut buffer_length, 558 | ); 559 | 560 | // https://github.com/microsoft/windows-rs/issues/2832#issuecomment-1922306953 561 | // ERROR_BUFFER_OVERFLOW is expected because the buffer length is initially 0 562 | if code != 0x00000000 && code != ERROR_BUFFER_OVERFLOW.0 { 563 | return HRESULT::from_win32(code).ok(); 564 | } 565 | 566 | let mut buffer = vec![0u8; buffer_length as usize]; 567 | let addresses = buffer.as_mut_ptr() as *mut IP_ADAPTER_ADDRESSES_LH; 568 | let code = GetAdaptersAddresses( 569 | AF_UNSPEC.0 as u32, 570 | GAA_FLAG_INCLUDE_ALL_INTERFACES, 571 | None, 572 | Some(addresses), 573 | &mut buffer_length, 574 | ); 575 | if code != 0x00000000 { 576 | return HRESULT::from_win32(code).ok(); 577 | } 578 | let mut current_addresses = addresses; 579 | while !current_addresses.is_null() { 580 | let adapter = &*current_addresses; 581 | if !callback(adapter)? { 582 | return Ok(()); 583 | } 584 | current_addresses = adapter.Next; 585 | } 586 | Ok(()) 587 | } 588 | } 589 | 590 | fn has_available_connections() -> windows_core::Result { 591 | let mut available = false; 592 | get_available_connections(|adapter| { 593 | if adapter.OperStatus == IfOperStatusUp { 594 | // break the iterator 595 | available = true; 596 | Ok(false) 597 | } else { 598 | Ok(true) 599 | } 600 | })?; 601 | Ok(available) 602 | } 603 | 604 | fn has_dns() -> windows_core::Result { 605 | let mut has_dns = false; 606 | get_available_connections(|adapter| { 607 | if adapter.OperStatus == IfOperStatusUp { 608 | // break the iterator 609 | has_dns = !adapter.FirstDnsServerAddress.is_null(); 610 | Ok(false) 611 | } else { 612 | Ok(true) 613 | } 614 | })?; 615 | Ok(has_dns) 616 | } 617 | 618 | fn get_network_info( 619 | connectivity: NLM_CONNECTIVITY, 620 | is_expensive: &Arc, 621 | is_low_data_mode: &Arc, 622 | network_status: &Arc, 623 | network_list_manager: &Rc, 624 | ) -> windows_core::Result { 625 | let ipv4_internet = 626 | connectivity.0 & NLM_CONNECTIVITY_IPV4_INTERNET.0 == NLM_CONNECTIVITY_IPV4_INTERNET.0; 627 | let ipv4_no_traffic = 628 | connectivity.0 & NLM_CONNECTIVITY_IPV4_NOTRAFFIC.0 == NLM_CONNECTIVITY_IPV4_NOTRAFFIC.0; 629 | let ipv6_internet = 630 | connectivity.0 & NLM_CONNECTIVITY_IPV6_INTERNET.0 == NLM_CONNECTIVITY_IPV6_INTERNET.0; 631 | let ipv6_no_traffic = 632 | connectivity.0 & NLM_CONNECTIVITY_IPV6_NOTRAFFIC.0 == NLM_CONNECTIVITY_IPV6_NOTRAFFIC.0; 633 | let is_connected_to_internet = unsafe { network_list_manager.IsConnectedToInternet()? }; 634 | let is_connected = unsafe { network_list_manager.IsConnected()? }; 635 | let status = if is_connected_to_internet == true { 636 | NetworkStatus::Satisfied 637 | } else if is_connected == true && (ipv4_no_traffic || ipv6_no_traffic) { 638 | NetworkStatus::Unsatisfied 639 | } else if has_available_connections()? { 640 | NetworkStatus::Satisfiable 641 | } else { 642 | NetworkStatus::Invalid 643 | }; 644 | network_status.store(status as u8, Ordering::SeqCst); 645 | Ok(NetworkInfo { 646 | has_ipv4: ipv4_internet, 647 | has_ipv6: ipv6_internet, 648 | has_dns: has_dns()?, 649 | is_low_data_mode: is_low_data_mode.load(Ordering::SeqCst), 650 | is_expensive: is_expensive.load(Ordering::SeqCst), 651 | status, 652 | }) 653 | } 654 | --------------------------------------------------------------------------------