Repository: Ashfaaq18/OpenNetMeter
Branch: main
Commit: d94ec569cb39
Files: 154
Total size: 698.1 KB
Directory structure:
gitextract_0pgbfx2f/
├── .github/
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ └── workflows/
│ └── dotnet.yml
├── .gitignore
├── .vscode/
│ └── launch.json
├── Directory.Build.props
├── Installer/
│ ├── OpenNetMeter-Installer.wixproj
│ ├── Product.wxs
│ ├── Strings_en-us.wxl
│ └── WixUIFeatureTree.wxs
├── LICENSE
├── NOTICE
├── OpenNetMeter/
│ ├── App.axaml
│ ├── App.axaml.cs
│ ├── Compat/
│ │ ├── Models/
│ │ │ ├── ApplicationDB.cs
│ │ │ ├── MyProcess_Small.cs
│ │ │ ├── NetworkProcess.TestHooks.cs
│ │ │ └── NetworkProcess.cs
│ │ ├── Properties/
│ │ │ ├── AppSettings.cs
│ │ │ ├── Global.cs
│ │ │ └── SettingsManager.cs
│ │ └── Utilities/
│ │ ├── ByteArray.cs
│ │ ├── EventLogger.cs
│ │ └── PeriodicWork.cs
│ ├── OpenNetMeter.csproj
│ ├── Program.cs
│ ├── Services/
│ │ ├── AvaloniaThemeService.cs
│ │ ├── AvaloniaWindowService.cs
│ │ ├── ExternalLinkService.cs
│ │ ├── IMiniWidgetService.cs
│ │ ├── IThemeService.cs
│ │ ├── ITrayNotificationService.cs
│ │ ├── ITrayService.cs
│ │ ├── NoOpThemeService.cs
│ │ ├── PlaceholderMiniWidgetService.cs
│ │ ├── PlaceholderNetworkCaptureService.cs
│ │ ├── PlaceholderProcessIconService.cs
│ │ ├── PlaceholderStartupRegistrationService.cs
│ │ ├── PlaceholderTrayNotificationService.cs
│ │ ├── PlaceholderTrayService.cs
│ │ ├── UpdateChecker.cs
│ │ ├── WindowsMiniWidgetService.cs
│ │ ├── WindowsNetworkCaptureService.cs
│ │ ├── WindowsProcessIconService.cs
│ │ ├── WindowsStartupRegistrationService.cs
│ │ ├── WindowsTrayNotificationService.cs
│ │ ├── WindowsTrayService.cs
│ │ └── WindowsWidgetZOrderHelper.cs
│ ├── ViewModels/
│ │ ├── ByteSizeFormatter.cs
│ │ ├── HistoryViewModel.cs
│ │ ├── MainWindowViewModel.cs
│ │ ├── MiniWidgetViewModel.cs
│ │ ├── RelayCommand.cs
│ │ ├── SettingsViewModel.cs
│ │ └── SummaryViewModel.cs
│ ├── Views/
│ │ ├── MainWindow/
│ │ │ ├── History.axaml
│ │ │ ├── History.axaml.cs
│ │ │ ├── Settings.axaml
│ │ │ ├── Settings.axaml.cs
│ │ │ ├── Summary.axaml
│ │ │ └── Summary.axaml.cs
│ │ ├── MainWindow.axaml
│ │ ├── MainWindow.axaml.cs
│ │ ├── MiniWidgetWindow.axaml
│ │ ├── MiniWidgetWindow.axaml.cs
│ │ └── Themes.axaml
│ └── app.manifest
├── OpenNetMeter.Core/
│ ├── OpenNetMeter.Core.csproj
│ └── ViewModels/
│ ├── ConfirmationDialogVM.cs
│ └── MainShellTabsViewModel.cs
├── OpenNetMeter.Old/
│ ├── DatabaseEngine/
│ │ ├── Connection.cs
│ │ ├── Database.cs
│ │ └── DatabaseEngine.csproj
│ └── OpenNetMeter/
│ ├── App.xaml
│ ├── App.xaml.cs
│ ├── AssemblyInfo.cs
│ ├── Models/
│ │ ├── ApplicationDB.cs
│ │ ├── MyProcess.cs
│ │ ├── NativeMethods.cs
│ │ ├── NetworkProcess.TestHooks.cs
│ │ ├── NetworkProcess.cs
│ │ └── SpeedGraph.cs
│ ├── OpenNetMeter.Old.csproj
│ ├── Properties/
│ │ ├── AppSettings.cs
│ │ ├── Global.cs
│ │ ├── InternalsVisibleTo.cs
│ │ ├── Resources.Designer.cs
│ │ ├── Resources.resx
│ │ └── SettingsManager.cs
│ ├── Utilities/
│ │ ├── BaseCommand.cs
│ │ ├── ByteArray.cs
│ │ ├── DataSizeSuffix.cs
│ │ ├── EventLogger.cs
│ │ ├── IconToImgSource.cs
│ │ ├── JsonHelper.cs
│ │ ├── PeriodicWork.cs
│ │ ├── ProcessIconCache.cs
│ │ ├── UIMeasure.cs
│ │ ├── UpdateChecker.cs
│ │ ├── WindowsNetworkCaptureService.cs
│ │ ├── WindowsProcessIconService.cs
│ │ ├── WindowsStartupRegistrationService.cs
│ │ └── WpfUiDispatcher.cs
│ ├── ViewModels/
│ │ ├── DataUsageHistoryVM.cs
│ │ ├── DataUsageSummaryVM.cs
│ │ ├── MainWindowVM.cs
│ │ ├── MiniWidgetVM.cs
│ │ └── SettingsVM.cs
│ ├── Views/
│ │ ├── AboutWindow.xaml
│ │ ├── AboutWindow.xaml.cs
│ │ ├── ConfirmationDialog.xaml
│ │ ├── ConfirmationDialog.xaml.cs
│ │ ├── Converters/
│ │ │ ├── BitmapToImageConverter.cs
│ │ │ ├── NetSpeedFormatConverter.cs
│ │ │ ├── RadioBoolToIntConverter.cs
│ │ │ ├── UiVisibilityToWpfVisibilityConverter.cs
│ │ │ └── UnitConverterBytes.cs
│ │ ├── CustomSystemTray.cs
│ │ ├── MainWindow.xaml
│ │ ├── MainWindow.xaml.cs
│ │ ├── MainWindowTabs/
│ │ │ ├── DataUsageHistoryV.xaml
│ │ │ ├── DataUsageHistoryV.xaml.cs
│ │ │ ├── DataUsageSummaryV.xaml
│ │ │ ├── DataUsageSummaryV.xaml.cs
│ │ │ ├── SettingsV.xaml
│ │ │ └── SettingsV.xaml.cs
│ │ ├── MiniWidgetV.xaml
│ │ ├── MiniWidgetV.xaml.cs
│ │ └── ResourceDictionaries/
│ │ ├── CustomDatePicker.xaml
│ │ ├── Theme.Colors.xaml
│ │ ├── Theme.ComboBox.xaml
│ │ ├── Theme.ContextMenu.xaml
│ │ ├── Theme.DataGrid.xaml
│ │ ├── Theme.MainWindowTabs.xaml
│ │ ├── Theme.Styles.xaml
│ │ ├── Theme.SummaryPage.xaml
│ │ └── ThemeResources.xaml
│ └── app.manifest
├── OpenNetMeter.PlatformAbstractions/
│ ├── IExternalLinkService.cs
│ ├── INetworkCaptureService.cs
│ ├── IProcessIconService.cs
│ ├── IStartupRegistrationService.cs
│ ├── IUiDispatcher.cs
│ ├── IWindowService.cs
│ ├── OpenNetMeter.PlatformAbstractions.csproj
│ └── UiVisibility.cs
├── OpenNetMeter.Tests/
│ ├── NetworkProcessTests.cs
│ └── OpenNetMeter.Tests.csproj
├── OpenNetMeter.sln
├── README.md
├── Resources/
│ └── documentation/
│ └── README.md
└── scripts/
├── build-avalonia-msi.ps1
└── publish-avalonia-rc.ps1
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: ashfaaq18
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**App and Windows Version (please complete the following information):**
- Windows Specs: [Select Start > Settings > System > About. Open About settings.]
- OpenNetMeter Version:
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/workflows/dotnet.yml
================================================
name: Build OpenNetMeter MSI and Push to Single Draft Release
on:
workflow_dispatch:
permissions:
contents: write
jobs:
draft-msi:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Build Avalonia MSI
shell: pwsh
run: |
powershell -ExecutionPolicy Bypass -File .\scripts\build-avalonia-msi.ps1 -Runtime win-x64 -Configuration Release
- name: Find built MSI
id: find_msi
shell: pwsh
run: |
$msi = Get-ChildItem `
"$Env:GITHUB_WORKSPACE\Installer\bin" `
-Filter 'OpenNetMeter-*.msi' `
-Recurse `
| Sort-Object LastWriteTimeUtc -Descending `
| Select-Object -First 1
if (-not $msi) { throw 'No MSI found after build.' }
"msi_path=$($msi.FullName)" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Create or Update Draft Release and Upload MSI
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: opennetmeter-draft
name: OpenNetMeter (Draft Release)
draft: true
allowUpdates: true
artifacts: ${{ steps.find_msi.outputs.msi_path }}
- name: Echo MSI path
shell: pwsh
env:
MSI_PATH: ${{ steps.find_msi.outputs.msi_path }}
run: |
Write-Host "Built MSI path was: $Env:MSI_PATH"
================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
[Ll]og/
[Ll]ogs/
_rc/
_verify_build/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
#My stuff
MyStuff/
================================================
FILE: .vscode/launch.json
================================================
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET_CoreAttach",
"type": "coreclr",
"request": "attach"
}
]
}
================================================
FILE: Directory.Build.props
================================================
0.15.0
OpenNetMeter
Ashfaaq18
================================================
FILE: Installer/OpenNetMeter-Installer.wixproj
================================================
net8.0-windows10.0.19041.0
$(MSBuildProjectDirectory)\..\
$(DefineConstants);
RepoRoot=$(RepoRoot);
TargetFramework=$(TargetFramework);
Manufacturer=$(Manufacturer);
ProductName=$(ProductName);
ProductVersion=$(ProductVersion);
$(ProductName)-$(ProductVersion)
================================================
FILE: Installer/Product.wxs
================================================
================================================
FILE: Installer/Strings_en-us.wxl
================================================
================================================
FILE: Installer/WixUIFeatureTree.wxs
================================================
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021-2026 Ashfaaq Riphque
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: NOTICE
================================================
OpenNetMeter
Copyright 2021-2026 Ashfaaq Riphque
================================================
FILE: OpenNetMeter/App.axaml
================================================
================================================
FILE: OpenNetMeter/App.axaml.cs
================================================
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using OpenNetMeter.Services;
using OpenNetMeter.ViewModels;
using OpenNetMeter.Views;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
namespace OpenNetMeter;
public partial class App : Application
{
private static bool unhandledHandlersRegistered;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
RegisterUnhandledExceptionLoggingOnce();
EventLogger.Info("Application starting");
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.ShutdownMode = global::Avalonia.Controls.ShutdownMode.OnExplicitShutdown;
bool startMinimized = HasStartMinimizedArgument();
IMiniWidgetService miniWidgetService = new PlaceholderMiniWidgetService();
ITrayService trayService = new PlaceholderTrayService();
ITrayNotificationService trayNotificationService = new PlaceholderTrayNotificationService();
desktop.Exit += (_, _) =>
{
EventLogger.Info("Application exiting");
trayService.Dispose();
trayNotificationService.Dispose();
miniWidgetService.Dispose();
SettingsManager.Save();
};
var windowService = new AvaloniaWindowService();
IExternalLinkService externalLinkService = new ExternalLinkService();
IThemeService themeService = new AvaloniaThemeService(this);
var miniWidgetViewModel = new MiniWidgetViewModel();
IStartupRegistrationService startupRegistrationService = OperatingSystem.IsWindows()
? new WindowsStartupRegistrationService()
: new PlaceholderStartupRegistrationService();
INetworkCaptureService networkCaptureService = OperatingSystem.IsWindows()
? new WindowsNetworkCaptureService()
: new PlaceholderNetworkCaptureService();
IProcessIconService processIconService = OperatingSystem.IsWindows()
? new WindowsProcessIconService()
: new PlaceholderProcessIconService();
var mainWindow = new MainWindow();
miniWidgetService = OperatingSystem.IsWindows()
? new WindowsMiniWidgetService(miniWidgetViewModel, mainWindow)
: new PlaceholderMiniWidgetService();
trayNotificationService = OperatingSystem.IsWindows()
? new WindowsTrayNotificationService()
: new PlaceholderTrayNotificationService();
mainWindow.InitializeWindowState(miniWidgetService, trayNotificationService);
mainWindow.DataContext = new MainWindowViewModel(windowService, networkCaptureService, processIconService, externalLinkService, miniWidgetViewModel, miniWidgetService, startupRegistrationService, themeService);
desktop.MainWindow = mainWindow;
trayService = OperatingSystem.IsWindows()
? new WindowsTrayService(this, desktop, mainWindow, miniWidgetService)
: new PlaceholderTrayService();
if (startMinimized)
mainWindow.Hide();
if (OperatingSystem.IsWindows() && SettingsManager.Current.MiniWidgetVisibility)
miniWidgetService.Show();
}
base.OnFrameworkInitializationCompleted();
}
private static void RegisterUnhandledExceptionLoggingOnce()
{
if (unhandledHandlersRegistered)
return;
unhandledHandlersRegistered = true;
AppDomain.CurrentDomain.UnhandledException += (_, e) =>
{
if (e.ExceptionObject is Exception ex)
{
EventLogger.Error("Unhandled exception", ex);
}
else
{
EventLogger.Error($"Unhandled exception object: {e.ExceptionObject}");
}
};
}
private static bool HasStartMinimizedArgument()
{
foreach (string arg in Environment.GetCommandLineArgs())
{
if (string.Equals(arg, "/StartMinimized", StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}
}
================================================
FILE: OpenNetMeter/Compat/Models/ApplicationDB.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Data.Sqlite;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Models;
internal sealed class ApplicationDB : IDisposable
{
private static readonly object DbLock = new();
private static SqliteConnection? sharedConnection;
private static int refCount;
private readonly string adapterName;
private bool disposed;
private static string LogTime() => DateTime.Now.ToString("HH:mm:ss.fff");
public const string UnifiedDBFileName = "OpenNetMeter";
public const int DataStoragePeriodInDays = 60;
public ApplicationDB(string dBFileName)
{
adapterName = dBFileName;
lock (DbLock)
{
if (sharedConnection == null)
{
Directory.CreateDirectory(Properties.Global.GetFilePath());
var builder = new SqliteConnectionStringBuilder
{
DataSource = GetUnifiedDBFullPath(),
Mode = SqliteOpenMode.ReadWriteCreate,
Cache = SqliteCacheMode.Shared
};
sharedConnection = new SqliteConnection(builder.ToString());
sharedConnection.Open();
RunNonQuery(sharedConnection, "PRAGMA journal_mode=WAL;");
RunNonQuery(sharedConnection, "PRAGMA busy_timeout=5000;");
RunNonQuery(sharedConnection, "PRAGMA synchronous=NORMAL;");
RunNonQuery(sharedConnection, "PRAGMA foreign_keys=ON;");
}
refCount++;
}
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] ApplicationDB created adapter='{dBFileName}'");
}
public static string GetUnifiedDBFullPath()
{
return Path.Combine(Properties.Global.GetFilePath(), UnifiedDBFileName + ".sqlite");
}
public static void CloseSharedConnection()
{
lock (DbLock)
{
sharedConnection?.Dispose();
sharedConnection = null;
refCount = 0;
}
}
public int CreateTable()
{
lock (DbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] CreateTable adapter='{adapterName}' db='{GetUnifiedDBFullPath()}'");
return CreateAdapterTable() >> 31 |
CreateProcessTable() >> 31 |
CreateDateTable() >> 31 |
CreateProcessDateTable() >> 31;
}
}
public int InsertUniqueRow_AdapterTable(string adapter)
{
lock (DbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] InsertUniqueRow_AdapterTable adapter='{adapter}'");
return RunNonQuery(
"INSERT OR IGNORE INTO Adapter(Name) VALUES(@Name)",
("@Name", adapter));
}
}
public void UpdateDatesInDB()
{
lock (DbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] UpdateDatesInDB adapter='{adapterName}' today='{DateTime.Today:yyyy-MM-dd}' retentionDays={DataStoragePeriodInDays}");
InsertUniqueRow_DateTable(DateTime.Today);
RemoveOldDate();
RemoveOldProcess();
}
}
public void PushToDB(string processName, long totalDataRecv, long totalDataSend)
{
lock (DbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] PushToDB adapter='{adapterName}' process='{processName}' recv={totalDataRecv} sent={totalDataSend}");
InsertUniqueRow_ProcessTable(processName);
InsertUniqueRow_AdapterTable(adapterName);
long dateID = GetID_DateTable(DateTime.Today);
long processID = GetID_ProcessTable(processName);
long adapterID = GetID_AdapterTable_Internal(adapterName);
if (InsertUniqueRow_ProcessDateTable(processID, dateID, adapterID, totalDataRecv, totalDataSend) < 1)
{
UpdateRow_ProcessDateTable(processID, dateID, adapterID, totalDataRecv, totalDataSend);
}
}
}
public List> GetDataSum_ProcessDateTable(DateTime date1, DateTime date2)
{
lock (DbLock)
{
return GetMultipleCellData(
"SELECT p1.Name, SUM(pd1.DataReceived), SUM(pd1.DataSent) " +
"FROM ProcessDate pd1 " +
"JOIN Process p1 ON p1.ID = pd1.ProcessID " +
"WHERE DateID IN " +
"(SELECT ID FROM Date WHERE " +
"(Year * 10000 + Month * 100 + Day) " +
"BETWEEN " +
$"({date1.Year * 10000 + date1.Month * 100 + date1.Day}) " +
"AND " +
$"({date2.Year * 10000 + date2.Month * 100 + date2.Day})) " +
"AND AdapterID = @AdapterID " +
"GROUP BY ProcessID",
("@AdapterID", GetID_AdapterTable_Internal(adapterName).ToString()));
}
}
public (long, long) GetDataSumBetweenDates(DateTime startDate, DateTime endDate)
{
DateTime fromDate = startDate.Date;
DateTime toDate = endDate.Date;
if (toDate < fromDate)
{
(fromDate, toDate) = (toDate, fromDate);
}
lock (DbLock)
{
var sum = GetMultipleCellData(
"SELECT SUM(DataReceived), SUM(DataSent) FROM ProcessDate " +
"WHERE DateID IN " +
"(SELECT ID FROM Date " +
"WHERE (Year * 10000 + Month * 100 + Day) " +
"BETWEEN " +
$"({fromDate.Year * 10000 + fromDate.Month * 100 + fromDate.Day}) " +
"AND " +
$"({toDate.Year * 10000 + toDate.Month * 100 + toDate.Day})) " +
"AND AdapterID = @AdapterID",
("@AdapterID", GetID_AdapterTable_Internal(adapterName).ToString()));
if (sum.Count == 1 && sum[0].Count == 2)
{
long download = Convert.IsDBNull(sum[0][0]) ? 0 : Convert.ToInt64(sum[0][0]);
long upload = Convert.IsDBNull(sum[0][1]) ? 0 : Convert.ToInt64(sum[0][1]);
return (download, upload);
}
}
return (0, 0);
}
public (long, long) GetTodayDataSum_ProcessDateTable()
{
lock (DbLock)
{
var sum = GetMultipleCellData(
"SELECT SUM(DataReceived), SUM(DataSent) FROM ProcessDate " +
"WHERE DateID IN " +
"(SELECT ID FROM Date " +
"WHERE (Year * 10000 + Month * 100 + Day) = (@Year * 10000 + @Month * 100 + @Day)) " +
"AND AdapterID = @AdapterID",
("@Year", DateTime.Today.Year.ToString()),
("@Month", DateTime.Today.Month.ToString()),
("@Day", DateTime.Today.Day.ToString()),
("@AdapterID", GetID_AdapterTable_Internal(adapterName).ToString()));
if (sum.Count == 1 && sum[0].Count == 2 &&
!Convert.IsDBNull(sum[0][0]) &&
!Convert.IsDBNull(sum[0][1]))
{
return (Convert.ToInt64(sum[0][0]), Convert.ToInt64(sum[0][1]));
}
}
return (0, 0);
}
public long GetID_AdapterTable(string adapter)
{
lock (DbLock)
{
return GetID_AdapterTable_Internal(adapter);
}
}
public List GetAllAdapters()
{
var adapters = new List();
lock (DbLock)
{
var rows = GetMultipleCellData("SELECT Name FROM Adapter ORDER BY Name");
foreach (var row in rows)
{
if (row.Count > 0 && !Convert.IsDBNull(row[0]))
{
adapters.Add(Convert.ToString(row[0])!);
}
}
}
return adapters;
}
public void Dispose()
{
if (disposed)
{
return;
}
disposed = true;
lock (DbLock)
{
refCount--;
if (refCount == 0 && sharedConnection != null)
{
sharedConnection.Dispose();
sharedConnection = null;
}
}
}
private static SqliteConnection Connection =>
sharedConnection ?? throw new InvalidOperationException("Database not initialized");
private int CreateProcessTable()
{
return RunNonQuery(
"CREATE TABLE IF NOT EXISTS Process(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"Name TEXT NOT NULL UNIQUE)");
}
private int CreateDateTable()
{
return RunNonQuery(
"CREATE TABLE IF NOT EXISTS Date(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"Year INTEGER NOT NULL, " +
"Month INTEGER NOT NULL, " +
"Day INTEGER NOT NULL, " +
"UNIQUE (Year, Month, Day) ON CONFLICT IGNORE)");
}
private int CreateProcessDateTable()
{
return RunNonQuery(
"CREATE TABLE IF NOT EXISTS ProcessDate(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"ProcessID INTEGER NOT NULL, " +
"DateID INTEGER NOT NULL, " +
"AdapterID INTEGER NOT NULL, " +
"DataReceived INTEGER NOT NULL, " +
"DataSent INTEGER NOT NULL, " +
"FOREIGN KEY(ProcessID) REFERENCES Process(ID) ON DELETE CASCADE, " +
"FOREIGN KEY(DateID) REFERENCES Date(ID) ON DELETE CASCADE, " +
"FOREIGN KEY(AdapterID) REFERENCES Adapter(ID) ON DELETE CASCADE, " +
"UNIQUE (ProcessID, DateID, AdapterID) ON CONFLICT IGNORE)");
}
private int CreateAdapterTable()
{
return RunNonQuery(
"CREATE TABLE IF NOT EXISTS Adapter(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"Name TEXT NOT NULL UNIQUE)");
}
private int InsertUniqueRow_ProcessTable(string appName)
{
return RunNonQuery(
"INSERT OR IGNORE INTO Process(Name) VALUES(@Name)",
("@Name", appName));
}
private int InsertUniqueRow_DateTable(DateTime date)
{
return RunNonQuery(
"INSERT OR IGNORE INTO Date(Year, Month, Day) VALUES(@Year, @Month, @Day)",
("@Year", date.Year.ToString()),
("@Month", date.Month.ToString()),
("@Day", date.Day.ToString()));
}
private void RemoveOldDate()
{
var cutoff = DateTime.Now.AddDays(-DataStoragePeriodInDays);
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] RemoveOldDate cutoff='{cutoff:yyyy-MM-dd}'");
RunNonQuery(
"DELETE FROM Date WHERE (Year * 10000 + Month * 100 + Day) < " +
$"({cutoff.Year * 10000 + cutoff.Month * 100 + cutoff.Day})");
}
private void RemoveOldProcess()
{
Console.WriteLine($"[{LogTime()}] [SQLite][Avalonia] RemoveOldProcess");
RunNonQuery(
"DELETE FROM Process WHERE ID NOT IN " +
"(SELECT DISTINCT ProcessID FROM ProcessDate)");
}
private int InsertUniqueRow_ProcessDateTable(long processID, long dateID, long adapterID, long dataReceived, long dataSent)
{
return RunNonQuery(
"INSERT OR IGNORE INTO ProcessDate(ProcessID, DateID, AdapterID, DataReceived, DataSent) " +
"VALUES(@ProcessID, @DateID, @AdapterID, @DataReceived, @DataSent)",
("@ProcessID", processID.ToString()),
("@DateID", dateID.ToString()),
("@AdapterID", adapterID.ToString()),
("@DataReceived", dataReceived.ToString()),
("@DataSent", dataSent.ToString()));
}
private int UpdateRow_ProcessDateTable(long processID, long dateID, long adapterID, long dataReceived, long dataSent)
{
return RunNonQuery(
"UPDATE ProcessDate SET " +
"DataReceived = DataReceived + @DataReceived, " +
"DataSent = DataSent + @DataSent " +
"WHERE ProcessID = @ProcessID AND DateID = @DateID AND AdapterID = @AdapterID",
("@DataReceived", dataReceived.ToString()),
("@DataSent", dataSent.ToString()),
("@ProcessID", processID.ToString()),
("@DateID", dateID.ToString()),
("@AdapterID", adapterID.ToString()));
}
private long GetID_DateTable(DateTime time)
{
var result = GetSingleCellData(
"SELECT ID FROM Date WHERE Year = @Year AND Month = @Month AND Day = @Day",
("@Year", time.Year.ToString()),
("@Month", time.Month.ToString()),
("@Day", time.Day.ToString()));
return Convert.ToInt64(result ?? -1);
}
private long GetID_ProcessTable(string appName)
{
var result = GetSingleCellData(
"SELECT ID FROM Process WHERE Name = @Name",
("@Name", appName));
return Convert.ToInt64(result ?? -1);
}
private long GetID_AdapterTable_Internal(string adapter)
{
var result = GetSingleCellData(
"SELECT ID FROM Adapter WHERE Name = @Name",
("@Name", adapter));
return Convert.ToInt64(result ?? -1);
}
private static int RunNonQuery(string query, params (string Name, string Value)[] parameters)
{
return RunNonQuery(Connection, query, parameters);
}
private static int RunNonQuery(SqliteConnection connection, string query, params (string Name, string Value)[] parameters)
{
try
{
using var command = connection.CreateCommand();
command.CommandText = query;
foreach (var parameter in parameters)
{
command.Parameters.AddWithValue(parameter.Name, parameter.Value);
}
return command.ExecuteNonQuery();
}
catch (Exception ex)
{
EventLogger.Error($"SQLite non-query failed. Query='{query}'", ex);
return -1;
}
}
private static List> GetMultipleCellData(string query, params (string Name, string Value)[] parameters)
{
var result = new List>();
try
{
using var command = Connection.CreateCommand();
command.CommandText = query;
foreach (var parameter in parameters)
{
command.Parameters.AddWithValue(parameter.Name, parameter.Value);
}
using var reader = command.ExecuteReader();
while (reader.Read())
{
var row = new List();
for (int i = 0; i < reader.FieldCount; i++)
{
row.Add(reader.GetValue(i));
}
result.Add(row);
}
}
catch (Exception ex)
{
EventLogger.Error($"SQLite multi-cell read failed. Query='{query}'", ex);
return result;
}
return result;
}
private static object? GetSingleCellData(string query, params (string Name, string Value)[] parameters)
{
try
{
using var command = Connection.CreateCommand();
command.CommandText = query;
foreach (var parameter in parameters)
{
command.Parameters.AddWithValue(parameter.Name, parameter.Value);
}
return command.ExecuteScalar();
}
catch (Exception ex)
{
EventLogger.Error($"SQLite single-cell read failed. Query='{query}'", ex);
return null;
}
}
}
================================================
FILE: OpenNetMeter/Compat/Models/MyProcess_Small.cs
================================================
namespace OpenNetMeter.Models;
public class MyProcess_Small
{
public string? Name { get; set; }
public long CurrentDataRecv { get; set; }
public long CurrentDataSend { get; set; }
public MyProcess_Small(string nameP, long currentDataRecvP, long currentDataSendP)
{
Name = nameP;
CurrentDataRecv = currentDataRecvP;
CurrentDataSend = currentDataSendP;
}
}
================================================
FILE: OpenNetMeter/Compat/Models/NetworkProcess.TestHooks.cs
================================================
using System.Net;
namespace OpenNetMeter.Models
{
// Test-only helpers kept internal to avoid exposing implementation details publicly.
public partial class NetworkProcess
{
internal void TestSetLocalIPs(byte[] ipv4, byte[] ipv6)
{
localIPv4 = ipv4;
localIPv6 = ipv6;
}
internal void TestInvokeRecvProcess(IPAddress src, IPAddress dest, int size, string name)
{
ProcessPacket(src, dest, size, name, isRecv: true);
}
internal void TestInvokeSendProcess(IPAddress src, IPAddress dest, int size, string name)
{
ProcessPacket(src, dest, size, name, isRecv: false);
}
}
}
================================================
FILE: OpenNetMeter/Compat/Models/NetworkProcess.cs
================================================
using System;
using System.Net;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
using Microsoft.Diagnostics.Tracing.Session;
using System.Threading;
using System.Net.Sockets;
using System.Collections.Generic;
using OpenNetMeter.Utilities;
using OpenNetMeter.Properties;
using System.Diagnostics.Eventing.Reader;
using System.Runtime.Versioning;
using System.Text.RegularExpressions;
namespace OpenNetMeter.Models
{
[SupportedOSPlatform("windows")]
public partial class NetworkProcess : IDisposable
{
//---------- Constants ------------//
private const int OneSec = 1000;
private const int DebounceDelayMs = 300; // Delay before processing network change events
// Default IP values indicating "not connected"
private static readonly byte[] DefaultIPv4 = { 0, 0, 0, 0 };
private static readonly byte[] DefaultIPv6 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
//---------- Network State ------------//
///
/// Immutable snapshot of current network connection state.
/// Used to detect connection changes without race conditions.
///
private sealed record NetworkSnapshot(
string AdapterName, // Display name, includes SSID for Wi-Fi
string AdapterId, // GUID for the adapter
byte[] IPv4, // Assigned IPv4 address bytes
byte[] IPv6 // Assigned IPv6 address bytes
);
// Lock for thread-safe access to local IP addresses
private readonly object stateLock = new object();
// Currently assigned local IP addresses (used for packet filtering)
private byte[] localIPv4 = DefaultIPv4;
private byte[] localIPv6 = DefaultIPv6;
//---------- Debounce & Synchronization ------------//
// Timer-based debounce: each network change event resets the timer.
// When it finally fires (after DebounceDelayMs of silence), HandleNetworkChange runs.
private Timer? debounceTimer;
private volatile bool isDisposed;
// Lock to ensure only one HandleNetworkChange runs at a time
private readonly object networkChangeLock = new object();
//---------- Periodic Tasks ------------//
// Periodic task for updating network speed display
private PeriodicWork? networkSpeedWork;
// Periodic task for pushing process data to database
private PeriodicWork? dbPushWork;
//---------- ETW Session ------------//
// ETW kernel session for capturing network packets
private TraceEventSession? kernelSession;
private const string KernelSessionName = "OpenNetMeter-Kernel";
// Task running the ETW packet capture loop
public Task? PacketTask;
//---------- Public State ------------//
// Current network adapter name (includes SSID for Wi-Fi connections)
public string AdapterName { get; private set; } = "";
// Current adapter's unique ID (GUID) - used for comparison to detect changes
private string currentAdapterId = "";
public string CurrentAdapterId => currentAdapterId;
///
/// Primary buffer for storing process network data.
/// Alternates with MyProcessesBuffer to allow lock-free reading.
///
public Dictionary MyProcesses { get; } = new();
///
/// Secondary buffer for storing process network data.
/// While GUI reads from MyProcesses, new data goes here (and vice versa).
/// This double-buffering prevents lock contention with the UI thread.
///
public Dictionary MyProcessesBuffer { get; } = new();
///
/// Buffer for data pending database write.
/// Cleared after each successful DB push.
///
public Dictionary PushToDBBuffer { get; } = new();
///
/// Controls which buffer receives incoming packet data.
/// true = write to MyProcessesBuffer, read from MyProcesses
/// false = write to MyProcesses, read from MyProcessesBuffer
/// Toggled by the UI layer when extracting data for display.
///
public bool IsBufferTime { get; set; }
// Running totals for current session (reset on adapter change)
public long CurrentSessionDownloadData;
public long CurrentSessionUploadData;
public long UploadSpeed;
//---------- Properties with Change Notification ------------//
private long downloadSpeed;
public long DownloadSpeed
{
get => downloadSpeed;
set { downloadSpeed = value; OnPropertyChanged(nameof(DownloadSpeed)); }
}
private string isNetworkOnline = "Disconnected";
///
/// Current connection status. Either "Disconnected" or the adapter name.
///
public string IsNetworkOnline
{
get => isNetworkOnline;
private set { isNetworkOnline = value; OnPropertyChanged(nameof(IsNetworkOnline)); }
}
//---------- Initialization ------------//
///
/// Call after subscribing to property handlers in MainWindowVM.
/// Sets up network change monitoring and performs initial connection check.
///
public void Initialize()
{
IsNetworkOnline = "Disconnected";
// Create the debounce timer (initially not running)
debounceTimer = new Timer(_ => HandleNetworkChange(), null, Timeout.Infinite, Timeout.Infinite);
// Subscribe to network address changes (fires on connect/disconnect/IP change)
NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
// Check current connection state on startup
HandleNetworkChange();
}
//---------- Network Detection (Snapshot-Based) ------------//
///
/// Queries the system for the current active network connection.
/// Returns an immutable snapshot of the connection, or null if disconnected.
///
/// This replaces the old socket-based GetLocalIP() approach which was unreliable
/// during network transitions (socket connect to 8.8.8.8 would fail transiently).
///
private NetworkSnapshot? GetCurrentNetworkSnapshot()
{
var adapters = NetworkInterface.GetAllNetworkInterfaces();
foreach (var adapter in adapters)
{
// Skip adapters that aren't connected
if (adapter.OperationalStatus != OperationalStatus.Up)
continue;
// Skip loopback (127.0.0.1)
if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback)
continue;
var props = adapter.GetIPProperties();
// No gateway = not a real internet connection (e.g., virtual adapters)
if (props.GatewayAddresses.Count == 0)
continue;
byte[]? ipv4 = null;
byte[]? ipv6 = null;
// Extract assigned IP addresses
foreach (var unicast in props.UnicastAddresses)
{
var addr = unicast.Address;
if (addr.AddressFamily == AddressFamily.InterNetwork)
{
ipv4 = addr.GetAddressBytes();
}
else if (addr.AddressFamily == AddressFamily.InterNetworkV6
&& !addr.IsIPv6LinkLocal) // Skip link-local (fe80::) addresses
{
ipv6 = addr.GetAddressBytes();
}
}
// Need at least one usable IP to consider this a valid connection
if (ipv4 == null && ipv6 == null)
continue;
// Build display name (include SSID for Wi-Fi)
var name = adapter.Name;
if (adapter.NetworkInterfaceType == NetworkInterfaceType.Wireless80211)
{
var ssid = GetConnectedSsid(adapter.Id);
if (!string.IsNullOrEmpty(ssid))
name += $"({ssid})";
}
Debug.WriteLine($"{adapter.Name} is up, IP: {(ipv4 != null ? new IPAddress(ipv4) : new IPAddress(ipv6!))}");
return new NetworkSnapshot(
name,
adapter.Id,
ipv4 ?? DefaultIPv4,
ipv6 ?? DefaultIPv6
);
}
return null; // No valid connection found
}
///
/// Gets the SSID of the currently connected Wi-Fi network by reading
/// from the Windows WLAN AutoConfig event log.
///
/// This is more reliable than the WinRT API on Windows 11 which requires
/// location permissions to retrieve SSID.
///
private static string? GetConnectedSsid(string adapterGuid)
{
try
{
// Query for EventID 8001 (successful connection) in WLAN-AutoConfig log
var query = new EventLogQuery(
"Microsoft-Windows-WLAN-AutoConfig/Operational",
PathType.LogName,
"*[System[EventID=8001]]"
)
{
ReverseDirection = true // Get most recent first
};
using var reader = new EventLogReader(query);
if (reader.ReadEvent() is EventRecord evt)
{
var message = evt.FormatDescription();
var match = Regex.Match(message, @"^SSID:\s*(.+)$", RegexOptions.Multiline);
if (match.Success)
return match.Groups[1].Value.Trim();
}
}
catch (Exception ex)
{
EventLogger.Error("Failed to read connected SSID", ex);
}
return null;
}
//---------- Network Change Handling ------------//
///
/// Event handler for NetworkChange.NetworkAddressChanged.
/// Resets the debounce timer so that HandleNetworkChange only runs
/// after DebounceDelayMs of silence (no rapid-fire events).
///
private void OnNetworkAddressChanged(object? sender, EventArgs? e)
{
if (isDisposed)
return;
// Reset the timer countdown. If another event arrives before it fires,
// the timer resets again. No allocations, no CTS, no race conditions.
try
{
debounceTimer?.Change(DebounceDelayMs, Timeout.Infinite);
}
catch (ObjectDisposedException)
{
EventLogger.Warn("Debounce timer was disposed while handling network address change");
}
}
///
/// Core network state machine. Compares current network state to tracked state
/// and triggers appropriate actions (connect/disconnect/switch/refresh).
///
/// State transitions:
/// - Disconnected -> Connected: Start monitoring
/// - Connected -> Disconnected: Stop monitoring
/// - Connected -> Different adapter: Stop then start monitoring
/// - Connected -> Same adapter:
/// - IP/name changed: Refresh tracked snapshot (no restart)
/// - No meaningful change: Ignore transient event
///
/// Protected by networkChangeLock to prevent concurrent execution.
///
private void HandleNetworkChange()
{
if (isDisposed)
return;
lock (networkChangeLock)
{
if (isDisposed)
return;
var snapshot = GetCurrentNetworkSnapshot();
if (snapshot == null)
{
// No valid connection found
if (IsNetworkOnline != "Disconnected")
{
Debug.WriteLine("Ash: Connection lost");
EndNetworkProcess();
}
return;
}
// Valid connection found - determine if it's new or changed
if (IsNetworkOnline == "Disconnected")
{
// Was disconnected, now connected
Debug.WriteLine("Ash: Connection established");
ApplySnapshot(snapshot);
StartNetworkProcess();
}
else if (currentAdapterId != snapshot.AdapterId)
{
// Was connected to different adapter (e.g., switched Wi-Fi networks)
// Compare by ID, not name, because SSID retrieval can race with event log
Debug.WriteLine("Ash: Network adapter changed");
EndNetworkProcess();
ApplySnapshot(snapshot);
StartNetworkProcess();
}
else
{
// Same adapter can still receive DHCP/IPv6 address changes or an SSID/name update.
// Refresh tracked snapshot so packet filtering keeps matching local traffic.
bool ipChanged = HasSnapshotAddressChanged(snapshot);
bool nameChanged = !string.Equals(AdapterName, snapshot.AdapterName, StringComparison.Ordinal);
if (ipChanged || nameChanged)
{
Debug.WriteLine("Ash: Adapter IP/name changed on same adapter - refreshing snapshot");
ApplySnapshot(snapshot);
// Keep status text in sync when SSID/name changes without adapter GUID change.
if (nameChanged)
IsNetworkOnline = AdapterName;
}
}
}
}
private bool HasSnapshotAddressChanged(NetworkSnapshot snapshot)
{
lock (stateLock)
{
return !ByteArray.Compare(localIPv4, snapshot.IPv4) ||
!ByteArray.Compare(localIPv6, snapshot.IPv6);
}
}
///
/// Updates tracked state from a network snapshot.
/// Called when establishing a new connection, switching adapters,
/// or refreshing state on same-adapter IP/name changes.
///
private void ApplySnapshot(NetworkSnapshot snapshot)
{
lock (stateLock)
{
localIPv4 = snapshot.IPv4;
localIPv6 = snapshot.IPv6;
}
currentAdapterId = snapshot.AdapterId;
AdapterName = snapshot.AdapterName;
}
//---------- Start/Stop Network Monitoring ------------//
///
/// Starts all network monitoring components:
/// - Database table setup
/// - ETW packet capture
/// - Speed monitoring periodic task
/// - Database push periodic task
///
public void StartNetworkProcess()
{
// Ensure database table exists for this adapter
using (var db = new ApplicationDB(AdapterName))
{
if (db.CreateTable() < 0)
{
EventLogger.Error("Error: Create table");
}
else
{
db.InsertUniqueRow_AdapterTable(AdapterName);
db.UpdateDatesInDB();
}
}
TraceEventSession.GetActiveSession(KernelSessionName)?.Stop();
// Start packet capture and periodic tasks
CaptureNetworkPackets();
StartSpeedMonitoring();
StartDbPush();
// Update connection status (triggers UI update via PropertyChanged)
IsNetworkOnline = AdapterName;
}
///
/// Stops all network monitoring components and resets state.
/// Called on disconnect or before switching to a different adapter.
///
public void EndNetworkProcess()
{
// Stop ETW session first (generates most activity)
StopKernelSession();
// Stop periodic tasks
StopPeriodicWork(ref networkSpeedWork, "Network speed");
StopPeriodicWork(ref dbPushWork, "DB push");
FlushPendingDbWrites();
// Clear process data buffers
lock (MyProcesses) MyProcesses.Clear();
lock (MyProcessesBuffer) MyProcessesBuffer.Clear();
// Reset speed counters
CurrentSessionDownloadData = 0;
CurrentSessionUploadData = 0;
UploadSpeed = 0;
DownloadSpeed = 0;
// Reset adapter tracking
currentAdapterId = "";
IsNetworkOnline = "Disconnected";
}
///
/// Stops the ETW kernel session and waits for the capture task to complete.
///
private void StopKernelSession()
{
// Disposing the session causes Source.Process() to return
kernelSession?.Dispose();
kernelSession = null;
var task = PacketTask;
if (task != null)
{
try
{
task.Wait(TimeSpan.FromMilliseconds(OneSec));
}
catch (AggregateException ex)
{
EventLogger.Error("Packet capture stop error", ex);
}
finally
{
if (task.IsCompleted)
task.Dispose();
else
task.ContinueWith(t => t.Dispose(), TaskScheduler.Default);
PacketTask = null;
}
}
}
///
/// Safely stops a periodic work task with error handling.
///
private void StopPeriodicWork(ref PeriodicWork? work, string name)
{
try
{
work?.Stop();
}
catch (Exception ex)
{
EventLogger.Error($"{name} stop error", ex);
}
finally
{
work = null;
}
}
private void FlushPendingDbWrites()
{
lock (PushToDBBuffer)
{
if (PushToDBBuffer.Count <= 0 || string.IsNullOrWhiteSpace(AdapterName))
{
PushToDBBuffer.Clear();
return;
}
using var db = new ApplicationDB(AdapterName);
foreach (var (key, proc) in PushToDBBuffer)
{
if (proc != null)
db.PushToDB(key, proc.CurrentDataRecv, proc.CurrentDataSend);
}
PushToDBBuffer.Clear();
}
}
//---------- Periodic Tasks ------------//
///
/// Starts the periodic task that calculates current network speed.
/// Runs every second and computes speed as delta from previous totals.
///
private void StartSpeedMonitoring()
{
long tempDownload = 0;
long tempUpload = 0;
networkSpeedWork = new PeriodicWork("Network speed", TimeSpan.FromSeconds(1));
networkSpeedWork.Start(_ =>
{
// Read current totals (atomic reads)
long currentDown = Interlocked.Read(ref CurrentSessionDownloadData);
long currentUp = Interlocked.Read(ref CurrentSessionUploadData);
// Calculate speed based on user's preferred format
if (SettingsManager.Current.NetworkSpeedFormat == 0)
{
// Bits per second
DownloadSpeed = (currentDown - tempDownload) * 8;
UploadSpeed = (currentUp - tempUpload) * 8;
}
else
{
// Bytes per second
DownloadSpeed = currentDown - tempDownload;
UploadSpeed = currentUp - tempUpload;
}
// Store for next iteration's delta calculation
tempDownload = currentDown;
tempUpload = currentUp;
return Task.CompletedTask;
});
}
///
/// Starts the periodic task that pushes accumulated process data to the database.
/// Runs every 5 seconds to batch writes and reduce I/O.
///
private void StartDbPush()
{
dbPushWork = new PeriodicWork("DB push", TimeSpan.FromSeconds(5));
dbPushWork.Start(_ =>
{
#if DEBUG
var sw = Stopwatch.StartNew();
#endif
lock (PushToDBBuffer)
{
if (PushToDBBuffer.Count > 0)
{
using var db = new ApplicationDB(AdapterName);
foreach (var (key, proc) in PushToDBBuffer)
{
if (proc != null)
db.PushToDB(key, proc.CurrentDataRecv, proc.CurrentDataSend);
}
PushToDBBuffer.Clear();
}
}
#if DEBUG
sw.Stop();
Debug.WriteLine($"elapsed time (DBpush): {sw.ElapsedMilliseconds} | time {DateTime.Now:O}");
#endif
return Task.CompletedTask;
});
}
//---------- ETW Packet Capture ------------//
///
/// Starts the ETW (Event Tracing for Windows) kernel session to capture
/// all TCP/IP network packets on the system.
///
/// Uses the NT Kernel Logger session which requires admin privileges.
/// Packets are filtered to only count those matching our local IP.
///
private void CaptureNetworkPackets()
{
PacketTask = Task.Run(() =>
{
if (kernelSession != null) return;
try
{
// Create kernel trace session (requires admin)
kernelSession = new TraceEventSession(KernelSessionName);
kernelSession.EnableKernelProvider(KernelTraceEventParser.Keywords.NetworkTCPIP);
// Subscribe to all TCP/UDP send/receive events
// All events funnel through ProcessPacket for unified handling
var kernel = kernelSession.Source.Kernel;
// Receive events (download)
kernel.TcpIpRecv += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
kernel.TcpIpRecvIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
kernel.UdpIpRecv += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
kernel.UdpIpRecvIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
// Send events (upload)
kernel.TcpIpSend += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
kernel.TcpIpSendIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
kernel.UdpIpSend += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
kernel.UdpIpSendIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
// Blocks until session is disposed
kernelSession.Source.Process();
}
catch (Exception ex)
{
EventLogger.Error("Packet capture loop error", ex);
}
});
}
///
/// Processes a single network packet captured by ETW.
/// Filters packets to only count those involving our local IP,
/// then records to the appropriate buffer based on direction.
///
/// Source IP address
/// Destination IP address
/// Packet size in bytes
/// Name of the process that sent/received the packet
/// True for download, false for upload
private void ProcessPacket(IPAddress src, IPAddress dest, int size, string processName, bool isRecv)
{
// Get current local IP for this address family
byte[] localIp;
lock (stateLock)
{
localIp = src.AddressFamily == AddressFamily.InterNetwork ? localIPv4 : localIPv6;
}
// Cache address bytes to avoid repeated allocations
var srcBytes = src.GetAddressBytes();
var destBytes = dest.GetAddressBytes();
// Check if packet involves our local IP
bool isSrc = ByteArray.Compare(srcBytes, localIp);
bool isDest = ByteArray.Compare(destBytes, localIp);
// XOR: exactly one should match (either we sent it or received it)
// If both match (loopback) or neither match (other adapter), skip
if (!(isSrc ^ isDest))
return;
// Apply network type filter (private/public/both)
if (!ShouldProcessByNetworkType(isSrc, src, dest))
return;
// Record the packet to appropriate counter and buffer
if (isRecv)
RecordRecv(processName, size);
else
RecordSend(processName, size);
}
///
/// Checks if a packet should be counted based on the user's network type filter setting.
///
/// True if local IP is the source (upload), false if destination (download)
/// Source IP
/// Destination IP
/// True if packet should be counted
private bool ShouldProcessByNetworkType(bool isLocalSrc, IPAddress src, IPAddress dest)
{
// Remote IP is the one that isn't ours
var remoteIp = isLocalSrc ? dest : src;
return SettingsManager.Current.NetworkType switch
{
0 => IsPrivateIP(remoteIp), // Private only (LAN traffic)
1 => !IsPrivateIP(remoteIp), // Public only (internet traffic)
2 => true, // Both
_ => false
};
}
///
/// Records a received (download) packet.
///
private void RecordRecv(string name, int size)
{
// Thread-safe increment of running total
Interlocked.Add(ref CurrentSessionDownloadData, size);
RecordToBuffer(name, size, isRecv: true);
}
///
/// Records a sent (upload) packet.
///
private void RecordSend(string name, int size)
{
// Thread-safe increment of running total
Interlocked.Add(ref CurrentSessionUploadData, size);
RecordToBuffer(name, size, isRecv: false);
}
///
/// Records packet data to the active process buffer.
/// Uses double-buffering to minimize lock contention with UI reads.
///
private void RecordToBuffer(string name, int size, bool isRecv)
{
// Empty process name means kernel/system traffic
if (string.IsNullOrEmpty(name))
name = "System";
// Select buffer based on current phase
var dict = IsBufferTime ? MyProcessesBuffer : MyProcesses;
lock (dict)
{
// Get or create process entry (single lookup)
if (!dict.TryGetValue(name, out var proc))
{
proc = new MyProcess_Small(name, 0, 0);
dict[name] = proc;
}
// Accumulate data
if (isRecv)
proc!.CurrentDataRecv += size;
else
proc!.CurrentDataSend += size;
}
}
//---------- IP Classification ------------//
///
/// Determines if an IP address is in a private (non-routable) range.
/// Used for filtering traffic by network type (LAN vs internet).
///
/// Private ranges:
/// - IPv4: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
/// - IPv6: Link-local (fe80::), site-local, unique-local (fc00::/7)
///
private bool IsPrivateIP(IPAddress ip)
{
// Handle IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)
if (ip.IsIPv4MappedToIPv6)
ip = ip.MapToIPv4();
// Loopback is always private
if (IPAddress.IsLoopback(ip))
return true;
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
var bytes = ip.GetAddressBytes();
return bytes[0] == 10 || // 10.0.0.0/8 (Class A)
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) || // 172.16.0.0/12 (Class B)
(bytes[0] == 192 && bytes[1] == 168) || // 192.168.0.0/16 (Class C)
(bytes[0] == 169 && bytes[1] == 254); // 169.254.0.0/16 (link-local/APIPA)
}
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
return ip.IsIPv6LinkLocal || // fe80::/10
#if NET6_0_OR_GREATER
ip.IsIPv6UniqueLocal || // fc00::/7 (only available in .NET 6+)
#endif
ip.IsIPv6SiteLocal; // fec0::/10 (deprecated but still checked)
}
return false;
}
//---------- Property Changed ------------//
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
//---------- Dispose ------------//
///
/// Cleans up all resources: unsubscribes from events, stops monitoring,
/// and flushes any pending data to the database.
///
public void Dispose()
{
isDisposed = true;
// Unsubscribe from network change events
NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
// Dispose the debounce timer (prevents any pending callback from firing)
debounceTimer?.Dispose();
debounceTimer = null;
// Stop monitoring if active
if (IsNetworkOnline != "Disconnected")
EndNetworkProcess();
// Flush any remaining buffered data to database
FlushPendingDbWrites();
}
}
}
================================================
FILE: OpenNetMeter/Compat/Properties/AppSettings.cs
================================================
namespace OpenNetMeter.Properties;
public class AppSettings
{
public bool DarkMode { get; set; }
public bool StartWithWin { get; set; }
public bool MinimizeOnStart { get; set; } = true;
public int WinPosX { get; set; }
public int WinPosY { get; set; }
public int WinWidth { get; set; } = 900;
public int WinHeight { get; set; } = 600;
public bool MainWindowPositionInitialized { get; set; }
public bool MiniWidgetVisibility { get; set; } = true;
public bool MiniWidgetPinned { get; set; }
public int MiniWidgetPosX { get; set; }
public int MiniWidgetPosY { get; set; }
public bool MiniWidgetPositionInitialized { get; set; }
public int MiniWidgetTransparentSlider { get; set; } = 20;
public int NetworkType { get; set; } = 2;
public int NetworkSpeedFormat { get; set; } = 0;
public int NetworkSpeedMagnitude { get; set; } = 0;
}
================================================
FILE: OpenNetMeter/Compat/Properties/Global.cs
================================================
using System;
using System.IO;
namespace OpenNetMeter.Properties;
internal class Global
{
public const string AppName = "OpenNetMeter";
public static string GetFilePath()
{
var path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
path = Path.Combine(path, AppName);
Directory.CreateDirectory(path);
return path;
}
}
================================================
FILE: OpenNetMeter/Compat/Properties/SettingsManager.cs
================================================
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Properties;
internal static class SettingsManager
{
private static readonly string filePath;
private static readonly object sync = new();
public static AppSettings Current { get; } = new();
static SettingsManager()
{
filePath = Path.Combine(Global.GetFilePath(), "settings.json");
Load();
}
public static void Load()
{
lock (sync)
{
if (!File.Exists(filePath))
return;
try
{
var json = File.ReadAllText(filePath);
var root = JsonNode.Parse(json) as JsonObject;
if (root == null)
return;
Current.DarkMode = GetBool(root, nameof(AppSettings.DarkMode), Current.DarkMode);
Current.StartWithWin = GetBool(root, nameof(AppSettings.StartWithWin), Current.StartWithWin);
Current.MinimizeOnStart = GetBool(root, nameof(AppSettings.MinimizeOnStart), Current.MinimizeOnStart);
Current.WinPosX = GetInt(root, nameof(AppSettings.WinPosX), Current.WinPosX);
Current.WinPosY = GetInt(root, nameof(AppSettings.WinPosY), Current.WinPosY);
Current.WinWidth = GetInt(root, nameof(AppSettings.WinWidth), Current.WinWidth);
Current.WinHeight = GetInt(root, nameof(AppSettings.WinHeight), Current.WinHeight);
Current.MainWindowPositionInitialized = GetBool(root, nameof(AppSettings.MainWindowPositionInitialized), Current.MainWindowPositionInitialized);
Current.MiniWidgetVisibility = GetBool(root, nameof(AppSettings.MiniWidgetVisibility), Current.MiniWidgetVisibility);
Current.MiniWidgetPinned = GetBool(root, nameof(AppSettings.MiniWidgetPinned), Current.MiniWidgetPinned);
Current.MiniWidgetPosX = GetInt(root, nameof(AppSettings.MiniWidgetPosX), Current.MiniWidgetPosX);
Current.MiniWidgetPosY = GetInt(root, nameof(AppSettings.MiniWidgetPosY), Current.MiniWidgetPosY);
Current.MiniWidgetPositionInitialized = GetBool(root, nameof(AppSettings.MiniWidgetPositionInitialized), Current.MiniWidgetPositionInitialized);
Current.MiniWidgetTransparentSlider = GetInt(root, nameof(AppSettings.MiniWidgetTransparentSlider), Current.MiniWidgetTransparentSlider);
Current.NetworkType = GetInt(root, nameof(AppSettings.NetworkType), Current.NetworkType);
Current.NetworkSpeedFormat = GetInt(root, nameof(AppSettings.NetworkSpeedFormat), Current.NetworkSpeedFormat);
Current.NetworkSpeedMagnitude = GetInt(root, nameof(AppSettings.NetworkSpeedMagnitude), Current.NetworkSpeedMagnitude);
}
catch (Exception ex)
{
EventLogger.Error("Error loading settings", ex);
}
}
}
public static void Save()
{
lock (sync)
{
JsonObject root;
if (File.Exists(filePath))
{
try
{
root = JsonNode.Parse(File.ReadAllText(filePath)) as JsonObject ?? [];
}
catch (Exception ex)
{
EventLogger.Error("Error parsing existing settings file before save; creating new settings root", ex);
root = [];
}
}
else
{
root = [];
}
root[nameof(AppSettings.DarkMode)] = Current.DarkMode;
root[nameof(AppSettings.StartWithWin)] = Current.StartWithWin;
root[nameof(AppSettings.MinimizeOnStart)] = Current.MinimizeOnStart;
root[nameof(AppSettings.WinPosX)] = Current.WinPosX;
root[nameof(AppSettings.WinPosY)] = Current.WinPosY;
root[nameof(AppSettings.WinWidth)] = Current.WinWidth;
root[nameof(AppSettings.WinHeight)] = Current.WinHeight;
root[nameof(AppSettings.MainWindowPositionInitialized)] = Current.MainWindowPositionInitialized;
root[nameof(AppSettings.MiniWidgetVisibility)] = Current.MiniWidgetVisibility;
root[nameof(AppSettings.MiniWidgetPinned)] = Current.MiniWidgetPinned;
root[nameof(AppSettings.MiniWidgetPosX)] = Current.MiniWidgetPosX;
root[nameof(AppSettings.MiniWidgetPosY)] = Current.MiniWidgetPosY;
root[nameof(AppSettings.MiniWidgetPositionInitialized)] = Current.MiniWidgetPositionInitialized;
root[nameof(AppSettings.MiniWidgetTransparentSlider)] = Current.MiniWidgetTransparentSlider;
root[nameof(AppSettings.NetworkType)] = Current.NetworkType;
root[nameof(AppSettings.NetworkSpeedFormat)] = Current.NetworkSpeedFormat;
root[nameof(AppSettings.NetworkSpeedMagnitude)] = Current.NetworkSpeedMagnitude;
var json = root.ToJsonString(new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(filePath, json);
}
}
private static bool GetBool(JsonObject root, string key, bool fallback)
{
if (!root.TryGetPropertyValue(key, out var node) || node == null)
return fallback;
try
{
return node.GetValue();
}
catch (Exception ex)
{
EventLogger.Error($"Error reading bool setting '{key}'", ex);
return fallback;
}
}
private static int GetInt(JsonObject root, string key, int fallback)
{
if (!root.TryGetPropertyValue(key, out var node) || node == null)
return fallback;
try
{
return node.GetValue();
}
catch (Exception ex)
{
EventLogger.Error($"Error reading int setting '{key}'", ex);
return fallback;
}
}
}
================================================
FILE: OpenNetMeter/Compat/Utilities/ByteArray.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenNetMeter.Utilities
{
internal class ByteArray
{
public static bool Compare(ReadOnlySpan a1, ReadOnlySpan a2)
{
return a1.SequenceEqual(a2);
}
}
}
================================================
FILE: OpenNetMeter/Compat/Utilities/EventLogger.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
namespace OpenNetMeter.Utilities;
public static class EventLogger
{
private const string EventSourceName = "OpenNetMeter";
private const int MaxEventLogMessageLength = 31839;
private enum LogLevel
{
Information,
Warning,
Error
}
public static void Info(string message, int eventId = 1000, short category = 0)
{
WriteEntrySafe(message, LogLevel.Information, eventId, category);
}
public static void Warn(string message, int eventId = 2000, short category = 0)
{
WriteEntrySafe(message, LogLevel.Warning, eventId, category);
}
public static void Error(
string message,
int eventId = 3000,
short category = 0,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
string logMessage = AddCallerContext(message, memberName, filePath, lineNumber);
WriteEntrySafe(logMessage, LogLevel.Error, eventId, category);
}
public static void Error(
Exception ex,
int eventId = 3001,
short category = 0,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Error("Unhandled exception", ex, eventId, category, memberName, filePath, lineNumber);
}
public static void Error(
string message,
Exception ex,
int eventId = 3001,
short category = 0,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
string exceptionText = ex is AggregateException aggregateEx
? aggregateEx.Flatten().ToString()
: ex.ToString();
string errorMessage = $"{message}{Environment.NewLine}{exceptionText}";
string logMessage = AddCallerContext(errorMessage, memberName, filePath, lineNumber);
WriteEntrySafe(logMessage, LogLevel.Error, eventId, category);
}
private static string AddCallerContext(string message, string memberName, string filePath, int lineNumber)
{
string fileName = string.IsNullOrWhiteSpace(filePath) ? "unknown" : Path.GetFileName(filePath);
return $"[{fileName}:{lineNumber} in {memberName}] {message}";
}
private static void WriteEntrySafe(string message, LogLevel level, int eventId, short category)
{
string safeMessage = Truncate(message);
Debug.WriteLine(safeMessage);
if (!OperatingSystem.IsWindows())
return;
try
{
WriteEntryWindows(safeMessage, level, eventId, category);
}
catch (Exception writeEx)
{
Debug.WriteLine($"[EventLogger fallback] Failed to write to Windows Event Log: {writeEx}");
}
}
[SupportedOSPlatform("windows")]
private static void WriteEntryWindows(string message, LogLevel level, int eventId, short category)
{
var entryType = level switch
{
LogLevel.Information => EventLogEntryType.Information,
LogLevel.Warning => EventLogEntryType.Warning,
_ => EventLogEntryType.Error
};
EventLog.WriteEntry(EventSourceName, message, entryType, eventId, category);
}
private static string Truncate(string message)
{
if (message.Length <= MaxEventLogMessageLength)
return message;
const string suffix = "... [truncated]";
int maxLength = MaxEventLogMessageLength - suffix.Length;
return message[..Math.Max(0, maxLength)] + suffix;
}
}
================================================
FILE: OpenNetMeter/Compat/Utilities/PeriodicWork.cs
================================================
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace OpenNetMeter.Utilities
{
internal sealed class PeriodicWork : IDisposable, IAsyncDisposable
{
private readonly string name;
private readonly TimeSpan interval;
private readonly SemaphoreSlim stopLock = new SemaphoreSlim(1, 1);
private Task? runTask;
private PeriodicTimer? timer;
private CancellationTokenSource? cts;
public PeriodicWork(string name, TimeSpan interval)
{
this.name = name;
this.interval = interval;
}
public void Start(Func onTick)
{
if (runTask != null)
return; // already running
cts = new CancellationTokenSource();
timer = new PeriodicTimer(interval);
runTask = RunAsync(onTick, cts.Token, timer);
}
private async Task RunAsync(Func onTick, CancellationToken token, PeriodicTimer timer)
{
try
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
try
{
await onTick(token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
EventLogger.Warn($"{name} tick cancelled");
throw;
}
catch (Exception ex)
{
EventLogger.Error($"{name} tick error", ex);
}
}
}
catch (OperationCanceledException)
{
Debug.WriteLine($"{name} task cancelled");
EventLogger.Warn($"{name} task cancelled");
}
}
public async Task StopAsync()
{
await stopLock.WaitAsync().ConfigureAwait(false);
try
{
if (runTask == null)
return;
cts?.Cancel();
try
{
await runTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
EventLogger.Warn($"{name} stop cancelled");
}
}
finally
{
timer?.Dispose();
cts?.Dispose();
runTask = null;
timer = null;
cts = null;
stopLock.Release();
}
}
public void Stop()
{
StopAsync().GetAwaiter().GetResult();
}
public void Dispose()
{
Stop();
}
public ValueTask DisposeAsync()
{
return new ValueTask(StopAsync());
}
}
}
================================================
FILE: OpenNetMeter/OpenNetMeter.csproj
================================================
WinExe
net8.0
enable
$(ProductVersion)
$(ProductName)
app.manifest
..\Resources\AppIcon.ico
================================================
FILE: OpenNetMeter/Program.cs
================================================
using System;
using Avalonia;
namespace OpenNetMeter;
internal sealed class Program
{
[STAThread]
public static void Main(string[] args)
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
public static AppBuilder BuildAvaloniaApp()
{
return AppBuilder.Configure()
.UsePlatformDetect()
.LogToTrace();
}
}
================================================
FILE: OpenNetMeter/Services/AvaloniaThemeService.cs
================================================
using Avalonia;
using Avalonia.Styling;
namespace OpenNetMeter.Services;
public sealed class AvaloniaThemeService : IThemeService
{
private readonly Application application;
public AvaloniaThemeService(Application application)
{
this.application = application;
}
public void ApplyDarkMode(bool enabled)
{
application.RequestedThemeVariant = enabled
? ThemeVariant.Dark
: ThemeVariant.Light;
}
}
================================================
FILE: OpenNetMeter/Services/AvaloniaWindowService.cs
================================================
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Services;
public sealed class AvaloniaWindowService : IWindowService
{
public void MinimizeMainWindow()
{
if (GetMainWindow() is { } window)
window.WindowState = WindowState.Minimized;
}
public void CloseMainWindow()
{
if (GetMainWindow() is { } window)
window.Close();
}
public void ShowAbout()
{
// Placeholder until About dialog content is migrated to Avalonia.
}
private static Window? GetMainWindow()
{
return (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
}
}
================================================
FILE: OpenNetMeter/Services/ExternalLinkService.cs
================================================
using System;
using System.Diagnostics;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Services;
public sealed class ExternalLinkService : IExternalLinkService
{
public void Open(string uri)
{
if (string.IsNullOrWhiteSpace(uri))
return;
try
{
Process.Start(new ProcessStartInfo
{
FileName = uri,
UseShellExecute = true
});
}
catch (Exception ex)
{
EventLogger.Error($"Failed to open external link '{uri}'", ex);
}
}
}
================================================
FILE: OpenNetMeter/Services/IMiniWidgetService.cs
================================================
using Avalonia.Controls;
namespace OpenNetMeter.Services;
public interface IMiniWidgetService : System.IDisposable
{
event System.Action? VisibilityChanged;
void Show();
void Hide();
void RefreshAppearance(bool darkMode, int transparency);
void ResetPosition(Window mainWindow);
void EnsurePositionOnScreen(Window mainWindow);
}
================================================
FILE: OpenNetMeter/Services/IThemeService.cs
================================================
namespace OpenNetMeter.Services;
public interface IThemeService
{
void ApplyDarkMode(bool enabled);
}
================================================
FILE: OpenNetMeter/Services/ITrayNotificationService.cs
================================================
using System;
using Avalonia.Controls;
namespace OpenNetMeter.Services;
public interface ITrayNotificationService : IDisposable
{
void ShowMinimizedToTrayOnce(Window mainWindow);
}
================================================
FILE: OpenNetMeter/Services/ITrayService.cs
================================================
namespace OpenNetMeter.Services;
public interface ITrayService : System.IDisposable
{
}
================================================
FILE: OpenNetMeter/Services/NoOpThemeService.cs
================================================
namespace OpenNetMeter.Services;
public sealed class NoOpThemeService : IThemeService
{
public void ApplyDarkMode(bool enabled)
{
}
}
================================================
FILE: OpenNetMeter/Services/PlaceholderMiniWidgetService.cs
================================================
using Avalonia.Controls;
namespace OpenNetMeter.Services;
public sealed class PlaceholderMiniWidgetService : IMiniWidgetService
{
public event System.Action? VisibilityChanged
{
add { }
remove { }
}
public void Show()
{
}
public void Hide()
{
}
public void RefreshAppearance(bool darkMode, int transparency)
{
}
public void ResetPosition(Window mainWindow)
{
}
public void EnsurePositionOnScreen(Window mainWindow)
{
}
public void Dispose()
{
}
}
================================================
FILE: OpenNetMeter/Services/PlaceholderNetworkCaptureService.cs
================================================
using System;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Services;
public sealed class PlaceholderNetworkCaptureService : INetworkCaptureService
{
private bool started;
public event EventHandler? NetworkChanged;
public event EventHandler? TrafficObserved
{
add { }
remove { }
}
public void Start()
{
if (started)
return;
started = true;
NetworkChanged?.Invoke(this, new NetworkSnapshotChangedEventArgs(string.Empty, string.Empty));
}
public void Stop()
{
started = false;
}
public void Dispose()
{
Stop();
}
}
================================================
FILE: OpenNetMeter/Services/PlaceholderProcessIconService.cs
================================================
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Services;
public sealed class PlaceholderProcessIconService : IProcessIconService
{
public object? GetProcessIcon(string processName) => null;
}
================================================
FILE: OpenNetMeter/Services/PlaceholderStartupRegistrationService.cs
================================================
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Services;
public sealed class PlaceholderStartupRegistrationService : IStartupRegistrationService
{
public bool IsEnabled() => false;
public void SetEnabled(bool enabled, bool startMinimized)
{
}
}
================================================
FILE: OpenNetMeter/Services/PlaceholderTrayNotificationService.cs
================================================
using Avalonia.Controls;
namespace OpenNetMeter.Services;
public sealed class PlaceholderTrayNotificationService : ITrayNotificationService
{
public void ShowMinimizedToTrayOnce(Window mainWindow)
{
}
public void Dispose()
{
}
}
================================================
FILE: OpenNetMeter/Services/PlaceholderTrayService.cs
================================================
namespace OpenNetMeter.Services;
public sealed class PlaceholderTrayService : ITrayService
{
public void Dispose()
{
}
}
================================================
FILE: OpenNetMeter/Services/UpdateChecker.cs
================================================
using System;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
namespace OpenNetMeter.Services;
public static class UpdateChecker
{
public static async Task<(Version? latestVersion, string? downloadUrl)> CheckForUpdatesAsync()
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "OpenNetMeter");
using var response = await client.GetAsync($"https://api.github.com/repos/Ashfaaq18/OpenNetMeter/releases/latest");
if (!response.IsSuccessStatusCode)
{
return (null, null);
}
await using var stream = await response.Content.ReadAsStreamAsync();
using var document = await JsonDocument.ParseAsync(stream);
JsonElement root = document.RootElement;
if (!root.TryGetProperty("tag_name", out JsonElement tagElement))
{
return (null, null);
}
string? latestTag = tagElement.GetString();
if (string.IsNullOrWhiteSpace(latestTag))
{
return (null, null);
}
string normalizedVersion = latestTag.Trim();
if (normalizedVersion.StartsWith('v') || normalizedVersion.StartsWith('V'))
{
normalizedVersion = normalizedVersion[1..];
}
if (!Version.TryParse(normalizedVersion, out Version? latestVersion))
{
return (null, null);
}
string? downloadUrl = null;
if (root.TryGetProperty("assets", out JsonElement assetsElement) &&
assetsElement.ValueKind == JsonValueKind.Array &&
assetsElement.GetArrayLength() > 0)
{
JsonElement firstAsset = assetsElement[0];
if (firstAsset.TryGetProperty("browser_download_url", out JsonElement downloadUrlElement))
{
downloadUrl = downloadUrlElement.GetString();
}
}
return (latestVersion, downloadUrl);
}
}
================================================
FILE: OpenNetMeter/Services/WindowsMiniWidgetService.cs
================================================
using System;
using Avalonia;
using Avalonia.Controls;
using OpenNetMeter.ViewModels;
using OpenNetMeter.Views;
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Services;
public sealed class WindowsMiniWidgetService : IMiniWidgetService
{
private readonly Window mainWindow;
private readonly MiniWidgetViewModel viewModel;
private readonly MiniWidgetWindow window;
private readonly WindowsWidgetZOrderHelper zOrderHelper;
private bool positionTrackingEnabled;
private bool restoringPosition;
public event Action? VisibilityChanged;
public WindowsMiniWidgetService(MiniWidgetViewModel viewModel, Window mainWindow)
{
this.mainWindow = mainWindow;
this.viewModel = viewModel;
window = new MiniWidgetWindow
{
DataContext = viewModel
};
zOrderHelper = new WindowsWidgetZOrderHelper(window);
viewModel.SetActions(OpenMainWindow, Hide);
window.Opened += Window_Opened;
window.PositionChanged += Window_PositionChanged;
}
public void Show()
{
try
{
if (!window.IsVisible)
window.Show();
else
window.Activate();
zOrderHelper.Start();
VisibilityChanged?.Invoke(true);
}
catch (Exception ex)
{
EventLogger.Error("Failed to show mini widget window", ex);
}
}
public void Hide()
{
try
{
if (window.IsVisible)
window.Hide();
zOrderHelper.Stop();
VisibilityChanged?.Invoke(false);
}
catch (Exception ex)
{
EventLogger.Error("Failed to hide mini widget window", ex);
}
}
public void RefreshAppearance(bool darkMode, int transparency)
{
viewModel.RefreshBackground(darkMode, transparency);
}
public void ResetPosition(Window mainWindow)
{
try
{
restoringPosition = true;
var mainWidth = Math.Max(1, (int)Math.Round(mainWindow.Bounds.Width));
var mainHeight = Math.Max(1, (int)Math.Round(mainWindow.Bounds.Height));
var widgetWidth = Math.Max(1, (int)Math.Round(window.Bounds.Width > 0 ? window.Bounds.Width : window.Width));
var widgetHeight = Math.Max(1, (int)Math.Round(window.Bounds.Height > 0 ? window.Bounds.Height : window.Height));
var x = mainWindow.Position.X + (mainWidth / 2) - (widgetWidth / 2);
var y = mainWindow.Position.Y + (mainHeight / 2) - (widgetHeight / 2);
window.Position = new PixelPoint(x, y);
SaveWindowPosition();
}
catch (Exception ex)
{
EventLogger.Error("Failed to reset mini widget window position", ex);
}
finally
{
restoringPosition = false;
}
}
public void EnsurePositionOnScreen(Window mainWindow)
{
try
{
if (!SettingsManager.Current.MiniWidgetPositionInitialized || !IsWindowInBounds(window))
ResetPosition(mainWindow);
}
catch (Exception ex)
{
EventLogger.Error("Failed to validate mini widget window position", ex);
}
}
public void Dispose()
{
try
{
zOrderHelper.Dispose();
window.Close();
}
catch (Exception ex)
{
EventLogger.Error("Failed to dispose mini widget window", ex);
}
}
private void OpenMainWindow()
{
try
{
if (!mainWindow.IsVisible)
mainWindow.Show();
if (mainWindow.WindowState == WindowState.Minimized)
mainWindow.WindowState = WindowState.Normal;
mainWindow.Activate();
}
catch (Exception ex)
{
EventLogger.Error("Failed to open main window from mini widget", ex);
}
}
private void Window_Opened(object? sender, EventArgs e)
{
try
{
if (SettingsManager.Current.MiniWidgetPositionInitialized)
{
restoringPosition = true;
window.Position = new PixelPoint(SettingsManager.Current.MiniWidgetPosX, SettingsManager.Current.MiniWidgetPosY);
restoringPosition = false;
}
else
{
SaveWindowPosition();
}
positionTrackingEnabled = true;
}
catch (Exception ex)
{
restoringPosition = false;
EventLogger.Error("Failed to restore mini widget window position", ex);
}
}
private void Window_PositionChanged(object? sender, PixelPointEventArgs e)
{
if (restoringPosition || !positionTrackingEnabled)
return;
try
{
SaveWindowPosition();
}
catch (Exception ex)
{
EventLogger.Error("Failed to save mini widget window position", ex);
}
}
private void SaveWindowPosition()
{
SettingsManager.Current.MiniWidgetPosX = window.Position.X;
SettingsManager.Current.MiniWidgetPosY = window.Position.Y;
SettingsManager.Current.MiniWidgetPositionInitialized = true;
SettingsManager.Save();
}
private static bool IsWindowInBounds(Window target)
{
var screens = target.Screens?.All;
if (screens is null || screens.Count == 0)
return true;
var width = Math.Max(1, (int)Math.Round(target.Bounds.Width > 0 ? target.Bounds.Width : target.Width));
var height = Math.Max(1, (int)Math.Round(target.Bounds.Height > 0 ? target.Bounds.Height : target.Height));
const int margin = 32;
foreach (var screen in screens)
{
// Use full screen bounds here, not WorkingArea, so a widget intentionally
// positioned over the taskbar is still considered a valid persisted position.
var area = screen.Bounds;
var areaRight = area.X + area.Width;
var areaBottom = area.Y + area.Height;
var targetRight = target.Position.X + width;
var targetBottom = target.Position.Y + height;
if (area.X < targetRight - margin &&
areaRight > target.Position.X + margin &&
area.Y < targetBottom - margin &&
areaBottom > target.Position.Y + margin)
{
return true;
}
}
return false;
}
}
================================================
FILE: OpenNetMeter/Services/WindowsNetworkCaptureService.cs
================================================
using System;
using System.ComponentModel;
using System.Runtime.Versioning;
using OpenNetMeter.Models;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Services;
[SupportedOSPlatform("windows")]
public sealed class WindowsNetworkCaptureService : INetworkCaptureService
{
private NetworkProcess? networkProcess;
private readonly object syncLock = new();
private bool disposed;
public event EventHandler? NetworkChanged;
public event EventHandler? TrafficObserved;
public void Start()
{
lock (syncLock)
{
ThrowIfDisposed();
if (networkProcess != null)
return;
networkProcess = new NetworkProcess();
networkProcess.PropertyChanged += NetworkProcess_PropertyChanged;
networkProcess.Initialize();
}
}
public void Stop()
{
lock (syncLock)
{
if (networkProcess == null)
return;
networkProcess.PropertyChanged -= NetworkProcess_PropertyChanged;
networkProcess.Dispose();
networkProcess = null;
}
}
public void Dispose()
{
lock (syncLock)
{
if (disposed)
return;
Stop();
disposed = true;
}
}
private void NetworkProcess_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (networkProcess == null)
return;
switch (e.PropertyName)
{
case nameof(NetworkProcess.IsNetworkOnline):
var isDisconnected = string.Equals(
networkProcess.IsNetworkOnline,
"Disconnected",
StringComparison.OrdinalIgnoreCase);
NetworkChanged?.Invoke(
this,
new NetworkSnapshotChangedEventArgs(
isDisconnected ? string.Empty : networkProcess.IsNetworkOnline,
isDisconnected ? string.Empty : networkProcess.CurrentAdapterId));
break;
case nameof(NetworkProcess.DownloadSpeed):
EmitProcessTraffic();
break;
}
}
private void EmitProcessTraffic()
{
if (networkProcess == null)
return;
networkProcess.IsBufferTime = true;
lock (networkProcess.MyProcesses)
{
foreach (var app in networkProcess.MyProcesses)
{
if (app.Value == null)
continue;
if (app.Value.CurrentDataRecv > 0)
TrafficObserved?.Invoke(this, new NetworkTrafficEventArgs(app.Key, app.Value.CurrentDataRecv, isReceive: true));
if (app.Value.CurrentDataSend > 0)
TrafficObserved?.Invoke(this, new NetworkTrafficEventArgs(app.Key, app.Value.CurrentDataSend, isReceive: false));
StageForDatabase(app.Key, app.Value.CurrentDataRecv, app.Value.CurrentDataSend);
}
networkProcess.MyProcesses.Clear();
}
networkProcess.IsBufferTime = false;
lock (networkProcess.MyProcessesBuffer)
{
foreach (var app in networkProcess.MyProcessesBuffer)
{
if (app.Value == null)
continue;
if (app.Value.CurrentDataRecv > 0)
TrafficObserved?.Invoke(this, new NetworkTrafficEventArgs(app.Key, app.Value.CurrentDataRecv, isReceive: true));
if (app.Value.CurrentDataSend > 0)
TrafficObserved?.Invoke(this, new NetworkTrafficEventArgs(app.Key, app.Value.CurrentDataSend, isReceive: false));
StageForDatabase(app.Key, app.Value.CurrentDataRecv, app.Value.CurrentDataSend);
}
networkProcess.MyProcessesBuffer.Clear();
}
}
private void StageForDatabase(string processName, long receivedBytes, long sentBytes)
{
if (networkProcess == null)
return;
if (receivedBytes <= 0 && sentBytes <= 0)
return;
lock (networkProcess.PushToDBBuffer)
{
if (!networkProcess.PushToDBBuffer.TryGetValue(processName, out var pending) || pending == null)
{
pending = new MyProcess_Small(processName, 0, 0);
networkProcess.PushToDBBuffer[processName] = pending;
}
pending.CurrentDataRecv += receivedBytes;
pending.CurrentDataSend += sentBytes;
}
}
private void ThrowIfDisposed()
{
if (disposed)
throw new ObjectDisposedException(nameof(WindowsNetworkCaptureService));
}
}
================================================
FILE: OpenNetMeter/Services/WindowsProcessIconService.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.Versioning;
using Avalonia.Media.Imaging;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Utilities;
using AvaloniaBitmap = Avalonia.Media.Imaging.Bitmap;
namespace OpenNetMeter.Services;
[SupportedOSPlatform("windows")]
public sealed class WindowsProcessIconService : IProcessIconService
{
private readonly ConcurrentDictionary cache = new(StringComparer.OrdinalIgnoreCase);
private static readonly AvaloniaBitmap? DefaultIcon = CreateDefaultIcon();
public object? GetProcessIcon(string processName)
{
if (string.IsNullOrWhiteSpace(processName))
return DefaultIcon;
return cache.GetOrAdd(processName, LoadIcon);
}
private static AvaloniaBitmap? LoadIcon(string processName)
{
var nameWithoutExtension = Path.GetFileNameWithoutExtension(processName);
if (string.IsNullOrWhiteSpace(nameWithoutExtension))
return DefaultIcon;
try
{
var processes = Process.GetProcessesByName(nameWithoutExtension);
try
{
foreach (var process in processes)
{
try
{
var path = process.MainModule?.FileName;
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
continue;
using Icon? icon = Icon.ExtractAssociatedIcon(path);
if (icon == null)
continue;
using var bitmap = icon.ToBitmap();
using var ms = new MemoryStream();
bitmap.Save(ms, ImageFormat.Png);
ms.Position = 0;
return new AvaloniaBitmap(ms);
}
catch (Exception ex)
{
EventLogger.Error($"Failed to fetch icon for process '{processName}' from candidate instance", ex);
}
}
}
finally
{
foreach (var process in processes)
process.Dispose();
}
}
catch (Exception ex)
{
EventLogger.Error($"Failed to resolve process icon for '{processName}'", ex);
}
return DefaultIcon;
}
private static AvaloniaBitmap? CreateDefaultIcon()
{
try
{
using var icon = (Icon)SystemIcons.Application.Clone();
using var bitmap = icon.ToBitmap();
using var ms = new MemoryStream();
bitmap.Save(ms, ImageFormat.Png);
ms.Position = 0;
return new AvaloniaBitmap(ms);
}
catch (Exception ex)
{
EventLogger.Error("Failed to create default process icon", ex);
return null;
}
}
}
================================================
FILE: OpenNetMeter/Services/WindowsStartupRegistrationService.cs
================================================
using System;
using System.IO;
using System.Runtime.Versioning;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Utilities;
using TaskScheduler = Microsoft.Win32.TaskScheduler;
namespace OpenNetMeter.Services;
[SupportedOSPlatform("windows")]
public sealed class WindowsStartupRegistrationService : IStartupRegistrationService
{
private const string TaskFolder = "OpenNetMeter";
private const string TaskName = "OpenNetMeterLogon";
public bool IsEnabled()
{
try
{
TaskScheduler.TaskFolder folder = TaskScheduler.TaskService.Instance.RootFolder.SubFolders[TaskFolder];
return folder.Tasks.Exists(TaskName);
}
catch
{
return false;
}
}
public void SetEnabled(bool enabled, bool startMinimized)
{
try
{
TaskScheduler.TaskFolder folder = TaskScheduler.TaskService.Instance.RootFolder.SubFolders[TaskFolder];
if (!enabled)
{
for (int i = 0; i < folder.Tasks.Count; i++)
{
folder.DeleteTask(folder.Tasks[i].Name);
}
TaskScheduler.TaskService.Instance.RootFolder.DeleteFolder(TaskFolder);
return;
}
}
catch (Exception ex)
{
EventLogger.Error("Error while updating startup task registration", ex);
}
if (enabled)
{
try
{
TaskScheduler.TaskService.Instance.RootFolder.CreateFolder(TaskFolder);
CreateTask(startMinimized);
}
catch (Exception ex)
{
EventLogger.Error("Error creating startup task folder/definition", ex);
}
}
}
private static void CreateTask(bool startMinimized)
{
try
{
TaskScheduler.TaskDefinition td = TaskScheduler.TaskService.Instance.NewTask();
td.RegistrationInfo.Description = "Run OpenNetMeter Avalonia on system log on";
td.Principal.RunLevel = TaskScheduler.TaskRunLevel.Highest;
td.Principal.LogonType = TaskScheduler.TaskLogonType.InteractiveToken;
td.Settings.DisallowStartIfOnBatteries = false;
td.Settings.StopIfGoingOnBatteries = false;
td.Settings.Compatibility = TaskScheduler.TaskCompatibility.V2_3;
TaskScheduler.LogonTrigger logonTrigger = new TaskScheduler.LogonTrigger
{
Enabled = true,
UserId = null
};
td.Triggers.Add(logonTrigger);
(string path, string? arguments) = ResolveLaunchCommand(startMinimized);
TaskScheduler.ExecAction action = new TaskScheduler.ExecAction
{
Path = path,
Arguments = arguments ?? string.Empty
};
td.Actions.Add(action);
TaskScheduler.TaskService.Instance.RootFolder.SubFolders[TaskFolder].RegisterTaskDefinition(
TaskName,
td,
TaskScheduler.TaskCreation.CreateOrUpdate,
null,
null,
td.Principal.LogonType);
}
catch (Exception ex)
{
EventLogger.Error("Error creating startup task", ex);
}
}
private static (string path, string? arguments) ResolveLaunchCommand(bool startMinimized)
{
string? processPath = Environment.ProcessPath;
string minimizedArgument = startMinimized ? " /StartMinimized" : string.Empty;
if (!string.IsNullOrWhiteSpace(processPath) &&
!string.Equals(Path.GetFileName(processPath), "dotnet.exe", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(Path.GetFileName(processPath), "dotnet", StringComparison.OrdinalIgnoreCase))
{
return (processPath, startMinimized ? "/StartMinimized" : null);
}
string baseDirectory = AppContext.BaseDirectory;
string assemblyFileName = $"{AppDomain.CurrentDomain.FriendlyName}.dll";
string managedEntryPoint = Path.Combine(baseDirectory, assemblyFileName);
if (File.Exists(managedEntryPoint))
{
return ("dotnet", $"\"{managedEntryPoint}\"{minimizedArgument}");
}
string fallbackExe = Path.Combine(AppContext.BaseDirectory, "OpenNetMeter.exe");
return (fallbackExe, startMinimized ? "/StartMinimized" : null);
}
}
================================================
FILE: OpenNetMeter/Services/WindowsTrayNotificationService.cs
================================================
using System;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Avalonia.Controls;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Services;
[SupportedOSPlatform("windows")]
public sealed class WindowsTrayNotificationService : ITrayNotificationService
{
private const uint NIM_ADD = 0x00000000;
private const uint NIM_MODIFY = 0x00000001;
private const uint NIM_DELETE = 0x00000002;
private const uint NIF_ICON = 0x00000002;
private const uint NIF_TIP = 0x00000004;
private const uint NIF_INFO = 0x00000010;
private const uint NIIF_NONE = 0x00000000;
private const uint BalloonIconId = 0x4F4E4D;
private bool hasShownMinimizedNotification;
private Icon? balloonIcon;
public void ShowMinimizedToTrayOnce(Window mainWindow)
{
if (hasShownMinimizedNotification)
return;
try
{
var handle = mainWindow.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
if (handle == IntPtr.Zero)
return;
balloonIcon?.Dispose();
balloonIcon = LoadNotificationIcon();
var addData = CreateNotifyIconData(handle, balloonIcon.Handle);
addData.uFlags = NIF_ICON | NIF_TIP;
addData.szTip = "OpenNetMeter";
if (!Shell_NotifyIcon(NIM_ADD, ref addData))
return;
var infoData = CreateNotifyIconData(handle, balloonIcon.Handle);
infoData.uFlags = NIF_INFO;
infoData.szInfo = "Minimized to system tray";
infoData.szInfoTitle = string.Empty;
infoData.dwInfoFlags = NIIF_NONE;
infoData.uTimeoutOrVersion = 1000;
Shell_NotifyIcon(NIM_MODIFY, ref infoData);
hasShownMinimizedNotification = true;
_ = RemoveTemporaryIconAsync(handle);
}
catch (Exception ex)
{
EventLogger.Error("Failed to show minimized-to-tray notification", ex);
}
}
public void Dispose()
{
try
{
balloonIcon?.Dispose();
balloonIcon = null;
}
catch (Exception ex)
{
EventLogger.Error("Failed to dispose tray notification icon", ex);
}
}
private async Task RemoveTemporaryIconAsync(IntPtr handle)
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5));
var deleteData = CreateNotifyIconData(handle, IntPtr.Zero);
Shell_NotifyIcon(NIM_DELETE, ref deleteData);
}
catch (Exception ex)
{
EventLogger.Error("Failed to remove tray notification icon", ex);
}
}
private static Icon LoadNotificationIcon()
{
string? processPath = Environment.ProcessPath;
if (!string.IsNullOrWhiteSpace(processPath) && System.IO.File.Exists(processPath))
{
var icon = Icon.ExtractAssociatedIcon(processPath);
if (icon != null)
return (Icon)icon.Clone();
}
return (Icon)SystemIcons.Application.Clone();
}
private static NOTIFYICONDATA CreateNotifyIconData(IntPtr handle, IntPtr iconHandle)
{
return new NOTIFYICONDATA
{
cbSize = (uint)Marshal.SizeOf(),
hWnd = handle,
uID = BalloonIconId,
hIcon = iconHandle
};
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern bool Shell_NotifyIcon(uint dwMessage, ref NOTIFYICONDATA lpData);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct NOTIFYICONDATA
{
public uint cbSize;
public IntPtr hWnd;
public uint uID;
public uint uFlags;
public uint uCallbackMessage;
public IntPtr hIcon;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szTip;
public uint dwState;
public uint dwStateMask;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
public string szInfo;
public uint uTimeoutOrVersion;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
public string szInfoTitle;
public uint dwInfoFlags;
public Guid guidItem;
public IntPtr hBalloonIcon;
}
}
================================================
FILE: OpenNetMeter/Services/WindowsTrayService.cs
================================================
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform;
using OpenNetMeter.Views;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Services;
public sealed class WindowsTrayService : ITrayService
{
private readonly TrayIcon trayIcon;
public WindowsTrayService(
Application application,
IClassicDesktopStyleApplicationLifetime desktop,
MainWindow mainWindow,
IMiniWidgetService miniWidgetService)
{
var menu = new NativeMenu();
var resetPositionsItem = new NativeMenuItem("Reset all window positions");
resetPositionsItem.Click += (_, _) => mainWindow.ResetWindowPositions();
menu.Add(resetPositionsItem);
var showMiniWidgetItem = new NativeMenuItem("Show Mini Widget");
showMiniWidgetItem.Click += (_, _) => miniWidgetService.Show();
menu.Add(showMiniWidgetItem);
menu.Add(new NativeMenuItemSeparator());
var openItem = new NativeMenuItem("Open");
openItem.Click += (_, _) => mainWindow.OpenFromTray();
menu.Add(openItem);
var exitItem = new NativeMenuItem("Exit");
exitItem.Click += (_, _) =>
{
mainWindow.PrepareForExit();
desktop.Shutdown();
};
menu.Add(exitItem);
using var iconStream = AssetLoader.Open(new Uri("avares://OpenNetMeter/Assets/x48.png"));
trayIcon = new TrayIcon
{
ToolTipText = "OpenNetMeter",
Menu = menu,
Icon = new WindowIcon(iconStream),
IsVisible = true
};
trayIcon.Clicked += (_, _) => mainWindow.OpenFromTray();
TrayIcon.SetIcons(application, new TrayIcons { trayIcon });
}
public void Dispose()
{
try
{
trayIcon.IsVisible = false;
trayIcon.Dispose();
}
catch (Exception ex)
{
EventLogger.Error("Failed to dispose tray icon", ex);
}
}
}
================================================
FILE: OpenNetMeter/Services/WindowsWidgetZOrderHelper.cs
================================================
using System;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Threading;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Services;
internal sealed class WindowsWidgetZOrderHelper : IDisposable
{
private static readonly IntPtr HwndTopMost = new(-1);
private const int GwlpHwndParent = -8;
private const string ShellTrayWindowClass = "Shell_TrayWnd";
private readonly Window window;
private readonly DispatcherTimer timer;
public WindowsWidgetZOrderHelper(Window window)
{
this.window = window;
timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
timer.Tick += Timer_Tick;
}
public void Start()
{
timer.Start();
}
public void Stop()
{
timer.Stop();
}
public void Dispose()
{
timer.Stop();
timer.Tick -= Timer_Tick;
}
private void Timer_Tick(object? sender, EventArgs e)
{
try
{
var windowHandle = window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero;
if (windowHandle == IntPtr.Zero)
return;
var shellTray = FindWindowEx(IntPtr.Zero, IntPtr.Zero, ShellTrayWindowClass, string.Empty);
if (shellTray == IntPtr.Zero)
return;
var owner = GetWindowLongPtr(windowHandle, GwlpHwndParent);
if (owner != shellTray)
SetWindowLongPtr(windowHandle, GwlpHwndParent, shellTray);
for (var current = windowHandle; current != IntPtr.Zero; current = GetWindow(current, 3))
{
if (current != shellTray)
continue;
SetWindowPos(
windowHandle,
HwndTopMost,
0,
0,
0,
0,
0x4000 | 0x0010 | 0x0002 | 0x0001);
break;
}
}
catch (Exception ex)
{
EventLogger.Error("Failed to maintain mini widget z-order", ex);
}
}
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("user32.dll", SetLastError = true)]
private static extern IntPtr GetWindow(IntPtr hwnd, uint cmd);
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
private static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int wFlags);
[DllImport("user32.dll", EntryPoint = "GetWindowLongPtrW", SetLastError = true)]
private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "GetWindowLongW", SetLastError = true)]
private static extern int GetWindowLong32(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
private static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong);
[DllImport("user32.dll", EntryPoint = "SetWindowLongW", SetLastError = true)]
private static extern int SetWindowLong32(IntPtr hWnd, int nIndex, int dwNewLong);
private static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex)
{
return IntPtr.Size == 8
? GetWindowLongPtr64(hWnd, nIndex)
: new IntPtr(GetWindowLong32(hWnd, nIndex));
}
private static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong)
{
return IntPtr.Size == 8
? SetWindowLongPtr64(hWnd, nIndex, dwNewLong)
: new IntPtr(SetWindowLong32(hWnd, nIndex, dwNewLong.ToInt32()));
}
}
================================================
FILE: OpenNetMeter/ViewModels/ByteSizeFormatter.cs
================================================
namespace OpenNetMeter.ViewModels;
internal static class ByteSizeFormatter
{
public static string FormatBytes(long value)
{
string[] suffix = { "B", "KB", "MB", "GB", "TB" };
var current = (double)value;
var unit = 0;
while (current >= 1024 && unit < suffix.Length - 1)
{
current /= 1024;
unit++;
}
return $"{current:0.##} {suffix[unit]}";
}
}
================================================
FILE: OpenNetMeter/ViewModels/HistoryViewModel.cs
================================================
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows.Input;
using Avalonia.Media;
using Microsoft.Data.Sqlite;
using OpenNetMeter.Models;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.ViewModels;
public sealed class HistoryViewModel : INotifyPropertyChanged
{
private readonly string dbPath;
private readonly IProcessIconService processIconService;
private string? selectedProfile;
private DateTimeOffset? dateStart;
private DateTimeOffset? dateEnd;
private long totalDownload;
private long totalUpload;
private string? currentSortColumn;
private bool sortDescending;
public HistoryViewModel()
: this(new NoOpProcessIconService())
{
}
public HistoryViewModel(IProcessIconService processIconService)
{
this.processIconService = processIconService;
dbPath = ResolveDatabasePath();
Profiles = [];
Rows = [];
FilterCommand = new RelayCommand(ApplyFilter);
SortRowsCommand = new ParameterRelayCommand(parameter =>
{
var column = parameter?.ToString();
if (string.IsNullOrWhiteSpace(column))
return;
SortRows(column);
});
DateStart = DateTimeOffset.Now.Date.AddDays(-7);
DateEnd = DateTimeOffset.Now.Date;
LoadProfiles();
if (Profiles.Count > 0)
{
SelectedProfile = Profiles[0];
ApplyFilter();
}
}
public ObservableCollection Profiles { get; }
public string? SelectedProfile
{
get => selectedProfile;
set
{
if (selectedProfile == value)
return;
selectedProfile = value;
OnPropertyChanged(nameof(SelectedProfile));
}
}
public DateTimeOffset? DateStart
{
get => dateStart;
set
{
if (dateStart == value)
return;
dateStart = value;
OnPropertyChanged(nameof(DateStart));
}
}
public DateTimeOffset? DateEnd
{
get => dateEnd;
set
{
if (dateEnd == value)
return;
dateEnd = value;
OnPropertyChanged(nameof(DateEnd));
}
}
public ObservableCollection Rows { get; }
public long TotalDownload
{
get => totalDownload;
private set
{
if (totalDownload == value)
return;
totalDownload = value;
OnPropertyChanged(nameof(TotalDownload));
OnPropertyChanged(nameof(TotalDownloadText));
}
}
public long TotalUpload
{
get => totalUpload;
private set
{
if (totalUpload == value)
return;
totalUpload = value;
OnPropertyChanged(nameof(TotalUpload));
OnPropertyChanged(nameof(TotalUploadText));
}
}
public string TotalDownloadText => ByteSizeFormatter.FormatBytes(TotalDownload);
public string TotalUploadText => ByteSizeFormatter.FormatBytes(TotalUpload);
public ICommand FilterCommand { get; }
public ICommand SortRowsCommand { get; }
public event PropertyChangedEventHandler? PropertyChanged;
public void ReloadProfiles()
{
string? previousSelection = SelectedProfile;
LoadProfiles();
if (!string.IsNullOrWhiteSpace(previousSelection) && Profiles.Contains(previousSelection))
{
SelectedProfile = previousSelection;
}
else
{
SelectedProfile = Profiles.Count > 0 ? Profiles[0] : null;
}
}
public void DeleteAllDbFiles()
{
try
{
ApplicationDB.CloseSharedConnection();
DeleteIfExists(dbPath);
DeleteIfExists($"{dbPath}-wal");
DeleteIfExists($"{dbPath}-shm");
DeleteIfExists($"{dbPath}-journal");
Profiles.Clear();
Rows.Clear();
SelectedProfile = null;
TotalDownload = 0;
TotalUpload = 0;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
EventLogger.Error("Failed to delete usage database file", ex);
}
}
private void LoadProfiles()
{
Profiles.Clear();
if (!File.Exists(dbPath))
return;
using var connection = OpenReadOnlyConnection(dbPath);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT Name FROM Adapter ORDER BY Name";
using var reader = command.ExecuteReader();
while (reader.Read())
{
if (!reader.IsDBNull(0))
Profiles.Add(reader.GetString(0));
}
}
private void ApplyFilter()
{
Rows.Clear();
TotalDownload = 0;
TotalUpload = 0;
if (string.IsNullOrWhiteSpace(SelectedProfile) || !File.Exists(dbPath))
return;
var start = (DateStart ?? DateTimeOffset.Now.Date).Date;
var end = (DateEnd ?? DateTimeOffset.Now.Date).Date;
if (end < start)
(start, end) = (end, start);
using var connection = OpenReadOnlyConnection(dbPath);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText =
"SELECT p.Name, SUM(pd.DataReceived) AS TotalRecv, SUM(pd.DataSent) AS TotalSent " +
"FROM ProcessDate pd " +
"JOIN Process p ON p.ID = pd.ProcessID " +
"JOIN Adapter a ON a.ID = pd.AdapterID " +
"JOIN Date d ON d.ID = pd.DateID " +
"WHERE a.Name = @AdapterName " +
"AND (d.Year * 10000 + d.Month * 100 + d.Day) BETWEEN @StartDate AND @EndDate " +
"GROUP BY p.ID, p.Name " +
"ORDER BY p.Name";
command.Parameters.AddWithValue("@AdapterName", SelectedProfile);
command.Parameters.AddWithValue("@StartDate", ToDateInt(start));
command.Parameters.AddWithValue("@EndDate", ToDateInt(end));
using var reader = command.ExecuteReader();
while (reader.Read())
{
var processName = reader.IsDBNull(0) ? string.Empty : reader.GetString(0);
var download = reader.IsDBNull(1) ? 0 : reader.GetInt64(1);
var upload = reader.IsDBNull(2) ? 0 : reader.GetInt64(2);
var icon = processIconService.GetProcessIcon(processName) as IImage;
Rows.Add(new HistoryRowViewModel(processName, download, upload, icon));
TotalDownload += download;
TotalUpload += upload;
}
}
private void SortRows(string column)
{
if (string.Equals(currentSortColumn, column, StringComparison.Ordinal))
{
sortDescending = !sortDescending;
}
else
{
currentSortColumn = column;
sortDescending = false;
}
var sorted = column switch
{
"Download" => sortDescending
? Rows.OrderByDescending(r => r.DownloadBytes).ToList()
: Rows.OrderBy(r => r.DownloadBytes).ToList(),
"Upload" => sortDescending
? Rows.OrderByDescending(r => r.UploadBytes).ToList()
: Rows.OrderBy(r => r.UploadBytes).ToList(),
_ => sortDescending
? Rows.OrderByDescending(r => r.ProcessName).ToList()
: Rows.OrderBy(r => r.ProcessName).ToList()
};
Rows.Clear();
foreach (var row in sorted)
Rows.Add(row);
}
private static SqliteConnection OpenReadOnlyConnection(string path)
{
var csb = new SqliteConnectionStringBuilder
{
DataSource = path,
Mode = SqliteOpenMode.ReadOnly
};
return new SqliteConnection(csb.ToString());
}
private static int ToDateInt(DateTime date)
{
return (date.Year * 10000) + (date.Month * 100) + date.Day;
}
private static string ResolveDatabasePath()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var appFolder = Path.Combine(localAppData, "OpenNetMeter");
return Path.Combine(appFolder, "OpenNetMeter.sqlite");
}
private static void DeleteIfExists(string path)
{
if (File.Exists(path))
File.Delete(path);
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private sealed class NoOpProcessIconService : IProcessIconService
{
public object? GetProcessIcon(string processName) => null;
}
}
public sealed class HistoryRowViewModel
{
public HistoryRowViewModel(string processName, long downloadBytes, long uploadBytes, IImage? icon = null)
{
ProcessName = processName;
DownloadBytes = downloadBytes;
UploadBytes = uploadBytes;
Icon = icon;
}
public IImage? Icon { get; }
public string ProcessName { get; }
public long DownloadBytes { get; }
public long UploadBytes { get; }
public string DownloadText => ByteSizeFormatter.FormatBytes(DownloadBytes);
public string UploadText => ByteSizeFormatter.FormatBytes(UploadBytes);
}
================================================
FILE: OpenNetMeter/ViewModels/MainWindowViewModel.cs
================================================
using System;
using System.Reflection;
using System.Windows.Input;
using OpenNetMeter.Services;
using OpenNetMeter.Core.ViewModels;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.ViewModels;
public sealed class MainWindowViewModel : MainShellTabsViewModel, IDisposable
{
private const string AboutRepositoryUri = "https://github.com/Ashfaaq18/OpenNetMeter";
private readonly IWindowService windowService;
private readonly INetworkCaptureService networkCaptureService;
private readonly IExternalLinkService externalLinkService;
private readonly MiniWidgetViewModel miniWidget;
private string networkStatus = "Disconnected";
private bool isAboutOpen;
public MainWindowViewModel()
: this(new NoOpWindowService(), new NoOpNetworkCaptureService(), new NoOpProcessIconService(), new NoOpExternalLinkService(), new MiniWidgetViewModel(), new PlaceholderMiniWidgetService(), new PlaceholderStartupRegistrationService(), new NoOpThemeService())
{
}
public MainWindowViewModel(
IWindowService windowService,
INetworkCaptureService networkCaptureService,
IProcessIconService processIconService,
IExternalLinkService externalLinkService,
MiniWidgetViewModel miniWidget,
IMiniWidgetService miniWidgetService,
IStartupRegistrationService startupRegistrationService,
IThemeService themeService)
{
this.windowService = windowService;
this.networkCaptureService = networkCaptureService;
this.externalLinkService = externalLinkService;
this.miniWidget = miniWidget;
Summary = new SummaryViewModel(this.networkCaptureService, processIconService);
History = new HistoryViewModel(processIconService);
Settings = new SettingsViewModel(miniWidget, miniWidgetService, startupRegistrationService, externalLinkService, themeService);
Settings.PropertyChanged += Settings_PropertyChanged;
Settings.DeleteAllDataConfirmed += Settings_DeleteAllDataConfirmed;
Summary.PropertyChanged += Summary_PropertyChanged;
SwitchTabCommand = new ParameterRelayCommand(parameter =>
{
if (parameter is null)
return;
if (int.TryParse(parameter.ToString(), out var nextIndex))
SelectedTabIndex = nextIndex;
});
AboutCommand = new RelayCommand(() => IsAboutOpen = true);
CloseAboutCommand = new RelayCommand(() => IsAboutOpen = false);
OpenAboutRepositoryCommand = new RelayCommand(() => this.externalLinkService.Open(AboutRepositoryUri));
MinimizeWindowCommand = new RelayCommand(() => this.windowService.MinimizeMainWindow());
CloseWindowCommand = new RelayCommand(() => this.windowService.CloseMainWindow());
this.networkCaptureService.NetworkChanged += OnNetworkChanged;
this.networkCaptureService.Start();
History.ReloadProfiles();
SyncMiniWidgetFromSummary();
}
public SummaryViewModel Summary { get; }
public HistoryViewModel History { get; }
public SettingsViewModel Settings { get; }
public string AboutVersionText { get; } = $"Version: {Assembly.GetExecutingAssembly()?.GetName().Version}";
public string AboutRepositoryUrl { get; } = AboutRepositoryUri;
public ICommand SwitchTabCommand { get; }
public ICommand AboutCommand { get; }
public ICommand CloseAboutCommand { get; }
public ICommand OpenAboutRepositoryCommand { get; }
public ICommand MinimizeWindowCommand { get; }
public ICommand CloseWindowCommand { get; }
public bool IsSummaryTab => SelectedTabIndex == 0;
public bool IsHistoryTab => SelectedTabIndex == 1;
public bool IsSettingsTab => SelectedTabIndex == 2;
public string NetworkStatus
{
get => networkStatus;
private set
{
if (networkStatus == value)
return;
networkStatus = value;
OnPropertyChanged(nameof(NetworkStatus));
}
}
public bool IsAboutOpen
{
get => isAboutOpen;
private set
{
if (isAboutOpen == value)
return;
isAboutOpen = value;
OnPropertyChanged(nameof(IsAboutOpen));
}
}
public override int SelectedTabIndex
{
get => base.SelectedTabIndex;
set
{
if (base.SelectedTabIndex == value)
return;
base.SelectedTabIndex = value;
OnPropertyChanged(nameof(IsSummaryTab));
OnPropertyChanged(nameof(IsHistoryTab));
OnPropertyChanged(nameof(IsSettingsTab));
}
}
public void Dispose()
{
Settings.PropertyChanged -= Settings_PropertyChanged;
Settings.DeleteAllDataConfirmed -= Settings_DeleteAllDataConfirmed;
Summary.PropertyChanged -= Summary_PropertyChanged;
Summary.Dispose();
networkCaptureService.NetworkChanged -= OnNetworkChanged;
networkCaptureService.Dispose();
}
private void OnNetworkChanged(object? sender, NetworkSnapshotChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.AdapterName))
{
NetworkStatus = "Disconnected";
Summary.ClearOnDisconnect();
return;
}
NetworkStatus = $"Connected : {e.AdapterName}";
Summary.SetActiveAdapter(e.AdapterName);
History.ReloadProfiles();
}
private void Settings_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(SettingsViewModel.SelectedSpeedMagnitudeIndex) ||
e.PropertyName == nameof(SettingsViewModel.SelectedSpeedUnitIndex))
{
Summary.RefreshSpeedDisplayFormat();
}
}
private void Settings_DeleteAllDataConfirmed()
{
bool wasConnected = !string.Equals(NetworkStatus, "Disconnected", StringComparison.OrdinalIgnoreCase);
if (wasConnected)
{
networkCaptureService.Stop();
Summary.ClearOnDisconnect();
NetworkStatus = "Disconnected";
}
History.DeleteAllDbFiles();
if (wasConnected)
{
networkCaptureService.Start();
}
History.ReloadProfiles();
}
private void Summary_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(SummaryViewModel.DownloadSpeedText):
case nameof(SummaryViewModel.UploadSpeedText):
case nameof(SummaryViewModel.CurrentSessionDownloadText):
case nameof(SummaryViewModel.CurrentSessionUploadText):
SyncMiniWidgetFromSummary();
break;
}
}
private void SyncMiniWidgetFromSummary()
{
miniWidget.DownloadSpeedText = Summary.DownloadSpeedText;
miniWidget.UploadSpeedText = Summary.UploadSpeedText;
miniWidget.CurrentSessionDownloadText = Summary.CurrentSessionDownloadText;
miniWidget.CurrentSessionUploadText = Summary.CurrentSessionUploadText;
}
private sealed class NoOpWindowService : IWindowService
{
public void MinimizeMainWindow() { }
public void CloseMainWindow() { }
public void ShowAbout() { }
}
private sealed class NoOpNetworkCaptureService : INetworkCaptureService
{
public event EventHandler? NetworkChanged
{
add { }
remove { }
}
public event EventHandler? TrafficObserved
{
add { }
remove { }
}
public void Start() { }
public void Stop() { }
public void Dispose() { }
}
private sealed class NoOpProcessIconService : IProcessIconService
{
public object? GetProcessIcon(string processName) => null;
}
private sealed class NoOpExternalLinkService : IExternalLinkService
{
public void Open(string uri) { }
}
}
================================================
FILE: OpenNetMeter/ViewModels/MiniWidgetViewModel.cs
================================================
using System;
using System.ComponentModel;
using System.Windows.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.ViewModels;
public sealed class MiniWidgetViewModel : INotifyPropertyChanged
{
private static readonly Bitmap PinLightImage = LoadBitmap("avares://OpenNetMeter/Assets/pin/pin.png");
private static readonly Bitmap PinDarkImage = LoadBitmap("avares://OpenNetMeter/Assets/pin/pin-dark.png");
private static readonly Bitmap UnpinLightImage = LoadBitmap("avares://OpenNetMeter/Assets/pin/unpin.png");
private static readonly Bitmap UnpinDarkImage = LoadBitmap("avares://OpenNetMeter/Assets/pin/unpin-dark.png");
private string downloadSpeedText = "35.2 Mbps";
private string uploadSpeedText = "4.8 Mbps";
private string currentSessionDownloadText = "1.24 GB";
private string currentSessionUploadText = "98.4 MB";
private string backgroundColor = "#cc252525";
private bool isPinned;
private bool darkMode;
private ICommand togglePinnedCommand;
public string DownloadSpeedText
{
get => downloadSpeedText;
set
{
if (downloadSpeedText == value)
return;
downloadSpeedText = value;
OnPropertyChanged(nameof(DownloadSpeedText));
}
}
public string UploadSpeedText
{
get => uploadSpeedText;
set
{
if (uploadSpeedText == value)
return;
uploadSpeedText = value;
OnPropertyChanged(nameof(UploadSpeedText));
}
}
public string CurrentSessionDownloadText
{
get => currentSessionDownloadText;
set
{
if (currentSessionDownloadText == value)
return;
currentSessionDownloadText = value;
OnPropertyChanged(nameof(CurrentSessionDownloadText));
}
}
public string CurrentSessionUploadText
{
get => currentSessionUploadText;
set
{
if (currentSessionUploadText == value)
return;
currentSessionUploadText = value;
OnPropertyChanged(nameof(CurrentSessionUploadText));
}
}
public bool IsPinned
{
get => isPinned;
set
{
if (isPinned == value)
return;
isPinned = value;
SettingsManager.Current.MiniWidgetPinned = value;
SettingsManager.Save();
OnPropertyChanged(nameof(IsPinned));
OnPropertyChanged(nameof(PinIconSource));
OnPropertyChanged(nameof(PinToolTip));
}
}
public string BackgroundColor
{
get => backgroundColor;
private set
{
if (backgroundColor == value)
return;
backgroundColor = value;
OnPropertyChanged(nameof(BackgroundColor));
}
}
public IImage PinIconSource => IsPinned
? (darkMode ? UnpinDarkImage : UnpinLightImage)
: (darkMode ? PinDarkImage : PinLightImage);
public string PinToolTip => IsPinned ? "Unpin" : "Pin";
public ICommand OpenMainWindowCommand { get; private set; } = new RelayCommand(() => { });
public ICommand HideWidgetCommand { get; private set; } = new RelayCommand(() => { });
public ICommand TogglePinnedCommand => togglePinnedCommand;
public MiniWidgetViewModel()
{
togglePinnedCommand = new RelayCommand(() => IsPinned = !IsPinned);
isPinned = SettingsManager.Current.MiniWidgetPinned;
RefreshBackground(SettingsManager.Current.DarkMode, SettingsManager.Current.MiniWidgetTransparentSlider);
}
public void SetActions(Action openMainWindow, Action hideWidget)
{
OpenMainWindowCommand = new RelayCommand(openMainWindow);
HideWidgetCommand = new RelayCommand(hideWidget);
OnPropertyChanged(nameof(OpenMainWindowCommand));
OnPropertyChanged(nameof(HideWidgetCommand));
}
public void RefreshBackground(bool darkMode, int transparency)
{
var clampedTransparency = Math.Clamp(transparency, 0, 100);
var alpha = ((100 - clampedTransparency) * 255) / 100;
this.darkMode = darkMode;
BackgroundColor = darkMode
? $"#{alpha:x2}252525"
: $"#{alpha:x2}f1f1f1";
OnPropertyChanged(nameof(PinIconSource));
}
public event PropertyChangedEventHandler? PropertyChanged;
private static Bitmap LoadBitmap(string assetUri)
{
try
{
using var assetStream = AssetLoader.Open(new Uri(assetUri));
return new Bitmap(assetStream);
}
catch (Exception ex)
{
EventLogger.Error($"Failed to load mini widget pin asset '{assetUri}'", ex);
throw;
}
}
private void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
================================================
FILE: OpenNetMeter/ViewModels/RelayCommand.cs
================================================
using System;
using System.Windows.Input;
namespace OpenNetMeter.ViewModels;
public sealed class RelayCommand : ICommand
{
private readonly Action execute;
private readonly Func? canExecute;
public RelayCommand(Action execute, Func? canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return canExecute?.Invoke() ?? true;
}
public void Execute(object? parameter)
{
execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
public sealed class ParameterRelayCommand : ICommand
{
private readonly Action execute;
private readonly Func? canExecute;
public ParameterRelayCommand(Action execute, Func? canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter)
{
return canExecute?.Invoke(parameter) ?? true;
}
public void Execute(object? parameter)
{
execute(parameter);
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
================================================
FILE: OpenNetMeter/ViewModels/SettingsViewModel.cs
================================================
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
using OpenNetMeter.Services;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.ViewModels;
public sealed class SettingsViewModel : INotifyPropertyChanged
{
private readonly IMiniWidgetService miniWidgetService;
private readonly IStartupRegistrationService startupRegistrationService;
private readonly IExternalLinkService externalLinkService;
private readonly IThemeService themeService;
private bool startWithWindows;
private bool minimizeOnStart;
private bool darkMode;
private bool miniWidgetVisible = true;
private double miniWidgetTransparency = 20;
private int selectedNetworkTargetIndex = 2;
private int selectedSpeedMagnitudeIndex;
private int selectedSpeedUnitIndex;
private bool isCheckingForUpdates;
private bool isUpdateAvailable;
private string updateStatusMessage = "Click here to check for new updates";
private string downloadUrl = string.Empty;
private bool isDeleteConfirmationOpen;
public SettingsViewModel(MiniWidgetViewModel miniWidgetViewModel, IMiniWidgetService miniWidgetService, IStartupRegistrationService startupRegistrationService, IExternalLinkService externalLinkService, IThemeService themeService)
{
this.miniWidgetService = miniWidgetService;
this.startupRegistrationService = startupRegistrationService;
this.externalLinkService = externalLinkService;
this.themeService = themeService;
var settings = SettingsManager.Current;
startWithWindows = settings.StartWithWin;
minimizeOnStart = settings.MinimizeOnStart;
darkMode = settings.DarkMode;
miniWidgetVisible = settings.MiniWidgetVisibility;
miniWidgetTransparency = settings.MiniWidgetTransparentSlider;
selectedNetworkTargetIndex = settings.NetworkType;
selectedSpeedMagnitudeIndex = settings.NetworkSpeedMagnitude;
selectedSpeedUnitIndex = settings.NetworkSpeedFormat;
ResetDataCommand = new RelayCommand(ResetData);
CheckForUpdatesCommand = new RelayCommand(async () => await CheckForUpdatesAsync());
DownloadUpdateCommand = new RelayCommand(DownloadUpdate);
ConfirmDeleteAllDataCommand = new RelayCommand(ConfirmDeleteAllData);
CancelDeleteAllDataCommand = new RelayCommand(CancelDeleteAllData);
this.miniWidgetService.VisibilityChanged += SyncMiniWidgetVisibility;
this.themeService.ApplyDarkMode(darkMode);
this.miniWidgetService.RefreshAppearance(darkMode, (int)Math.Round(miniWidgetTransparency));
}
public SettingsViewModel()
: this(new MiniWidgetViewModel(), new PlaceholderMiniWidgetService(), new PlaceholderStartupRegistrationService(), new NoOpExternalLinkService(), new NoOpThemeService())
{
}
public bool StartWithWindows
{
get => startWithWindows;
set
{
if (startWithWindows == value)
return;
startWithWindows = value;
SettingsManager.Current.StartWithWin = value;
SettingsManager.Save();
startupRegistrationService.SetEnabled(value, MinimizeOnStart);
OnPropertyChanged(nameof(StartWithWindows));
OnPropertyChanged(nameof(CanChangeMinimizeOnStart));
}
}
public bool MinimizeOnStart
{
get => minimizeOnStart;
set
{
if (minimizeOnStart == value)
return;
minimizeOnStart = value;
SettingsManager.Current.MinimizeOnStart = value;
SettingsManager.Save();
OnPropertyChanged(nameof(MinimizeOnStart));
}
}
public bool CanChangeMinimizeOnStart => !StartWithWindows;
public bool DarkMode
{
get => darkMode;
set
{
if (darkMode == value)
return;
darkMode = value;
SettingsManager.Current.DarkMode = value;
SettingsManager.Save();
themeService.ApplyDarkMode(value);
miniWidgetService.RefreshAppearance(value, (int)Math.Round(MiniWidgetTransparency));
OnPropertyChanged(nameof(DarkMode));
}
}
public bool MiniWidgetVisible
{
get => miniWidgetVisible;
set
{
if (miniWidgetVisible == value)
return;
miniWidgetVisible = value;
SettingsManager.Current.MiniWidgetVisibility = value;
SettingsManager.Save();
if (value)
miniWidgetService.Show();
else
miniWidgetService.Hide();
OnPropertyChanged(nameof(MiniWidgetVisible));
}
}
public double MiniWidgetTransparency
{
get => miniWidgetTransparency;
set
{
if (miniWidgetTransparency.Equals(value))
return;
miniWidgetTransparency = value;
SettingsManager.Current.MiniWidgetTransparentSlider = (int)Math.Round(value);
SettingsManager.Save();
miniWidgetService.RefreshAppearance(DarkMode, (int)Math.Round(value));
OnPropertyChanged(nameof(MiniWidgetTransparency));
}
}
public string[] NetworkTargets { get; } = ["Private", "Public", "Both"];
public int SelectedNetworkTargetIndex
{
get => selectedNetworkTargetIndex;
set
{
if (selectedNetworkTargetIndex == value)
return;
selectedNetworkTargetIndex = value;
SettingsManager.Current.NetworkType = value;
SettingsManager.Save();
OnPropertyChanged(nameof(SelectedNetworkTargetIndex));
OnPropertyChanged(nameof(IsNetworkTargetPrivate));
OnPropertyChanged(nameof(IsNetworkTargetPublic));
OnPropertyChanged(nameof(IsNetworkTargetBoth));
}
}
public bool IsNetworkTargetPrivate
{
get => SelectedNetworkTargetIndex == 0;
set
{
if (value)
SelectedNetworkTargetIndex = 0;
}
}
public bool IsNetworkTargetPublic
{
get => SelectedNetworkTargetIndex == 1;
set
{
if (value)
SelectedNetworkTargetIndex = 1;
}
}
public bool IsNetworkTargetBoth
{
get => SelectedNetworkTargetIndex == 2;
set
{
if (value)
SelectedNetworkTargetIndex = 2;
}
}
public string[] SpeedMagnitudes { get; } = ["Auto", "Kilo", "Mega", "Giga"];
public int SelectedSpeedMagnitudeIndex
{
get => selectedSpeedMagnitudeIndex;
set
{
if (selectedSpeedMagnitudeIndex == value)
return;
selectedSpeedMagnitudeIndex = value;
SettingsManager.Current.NetworkSpeedMagnitude = value;
SettingsManager.Save();
OnPropertyChanged(nameof(SelectedSpeedMagnitudeIndex));
}
}
public string[] SpeedUnits { get; } = ["bps (bits/sec)", "Bps (bytes/sec)"];
public int SelectedSpeedUnitIndex
{
get => selectedSpeedUnitIndex;
set
{
if (selectedSpeedUnitIndex == value)
return;
selectedSpeedUnitIndex = value;
SettingsManager.Current.NetworkSpeedFormat = value;
SettingsManager.Save();
OnPropertyChanged(nameof(SelectedSpeedUnitIndex));
}
}
public bool IsCheckingForUpdates
{
get => isCheckingForUpdates;
private set
{
if (isCheckingForUpdates == value)
return;
isCheckingForUpdates = value;
OnPropertyChanged(nameof(IsCheckingForUpdates));
}
}
public bool IsUpdateAvailable
{
get => isUpdateAvailable;
private set
{
if (isUpdateAvailable == value)
return;
isUpdateAvailable = value;
OnPropertyChanged(nameof(IsUpdateAvailable));
}
}
public string UpdateStatusMessage
{
get => updateStatusMessage;
private set
{
if (updateStatusMessage == value)
return;
updateStatusMessage = value;
OnPropertyChanged(nameof(UpdateStatusMessage));
}
}
public bool IsDeleteConfirmationOpen
{
get => isDeleteConfirmationOpen;
private set
{
if (isDeleteConfirmationOpen == value)
return;
isDeleteConfirmationOpen = value;
OnPropertyChanged(nameof(IsDeleteConfirmationOpen));
}
}
public string DeleteConfirmationMessage { get; } =
"Warning!!! This will delete all saved profiles.\nDo you still want to continue?";
public ICommand ResetDataCommand { get; }
public ICommand CheckForUpdatesCommand { get; }
public ICommand DownloadUpdateCommand { get; }
public ICommand ConfirmDeleteAllDataCommand { get; }
public ICommand CancelDeleteAllDataCommand { get; }
public event PropertyChangedEventHandler? PropertyChanged;
public event Action? DeleteAllDataConfirmed;
public void SyncMiniWidgetVisibility(bool isVisible)
{
if (miniWidgetVisible == isVisible)
return;
miniWidgetVisible = isVisible;
SettingsManager.Current.MiniWidgetVisibility = isVisible;
SettingsManager.Save();
OnPropertyChanged(nameof(MiniWidgetVisible));
}
private void ResetData()
{
IsDeleteConfirmationOpen = true;
}
private async Task CheckForUpdatesAsync()
{
IsCheckingForUpdates = true;
UpdateStatusMessage = "Checking for updates...";
IsUpdateAvailable = false;
downloadUrl = string.Empty;
string statusMessage = string.Empty;
const int minDisplayTimeMs = 2000;
var stopwatch = Stopwatch.StartNew();
try
{
(Version? latestVersion, string? nextDownloadUrl) = await UpdateChecker.CheckForUpdatesAsync();
if (latestVersion != null && nextDownloadUrl != null)
{
Version? currentVersion = Assembly.GetExecutingAssembly()?.GetName()?.Version;
if (currentVersion != null && latestVersion > currentVersion)
{
downloadUrl = nextDownloadUrl;
statusMessage = $"A new version {latestVersion} is available!";
IsUpdateAvailable = true;
}
else
{
statusMessage = "You have the latest version.";
}
}
else
{
statusMessage = "Error checking for updates.";
}
}
catch (Exception ex)
{
statusMessage = "Error checking for updates.";
EventLogger.Error("Error checking for updates", ex);
}
finally
{
stopwatch.Stop();
int remainingTime = minDisplayTimeMs - (int)stopwatch.ElapsedMilliseconds;
if (remainingTime > 0)
{
await Task.Delay(remainingTime);
}
IsCheckingForUpdates = false;
UpdateStatusMessage = statusMessage;
}
}
private void DownloadUpdate()
{
if (string.IsNullOrWhiteSpace(downloadUrl))
{
UpdateStatusMessage = "No update download is available.";
return;
}
try
{
externalLinkService.Open(downloadUrl);
}
catch (Exception ex)
{
EventLogger.Error("Error launching update download URL", ex);
UpdateStatusMessage = "Error launching update download URL.";
}
}
private void ConfirmDeleteAllData()
{
IsDeleteConfirmationOpen = false;
DeleteAllDataConfirmed?.Invoke();
}
private void CancelDeleteAllData()
{
IsDeleteConfirmationOpen = false;
}
private void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
private sealed class NoOpExternalLinkService : IExternalLinkService
{
public void Open(string uri)
{
}
}
}
================================================
FILE: OpenNetMeter/ViewModels/SummaryViewModel.cs
================================================
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Windows.Input;
using Avalonia.Threading;
using Avalonia.Media;
using LiveChartsCore;
using LiveChartsCore.Defaults;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using Microsoft.Data.Sqlite;
using OpenNetMeter.Models;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
using SkiaSharp;
namespace OpenNetMeter.ViewModels;
public sealed class SummaryViewModel : INotifyPropertyChanged, IDisposable
{
private const double GraphLogBase = 10d;
private const int MaxSpeedMagnitude = 6;
private readonly INetworkCaptureService networkCaptureService;
private readonly IProcessIconService processIconService;
private readonly ObservableCollection dlValues = new();
private readonly ObservableCollection ulValues = new();
private readonly Dictionary processIndex = new(StringComparer.OrdinalIgnoreCase);
private readonly object pendingLock = new();
private readonly Dictionary pendingByProcess = new(StringComparer.OrdinalIgnoreCase);
private readonly DispatcherTimer flushTimer;
private string? currentSortColumn;
private bool sortDescending;
private string activeAdapterName = string.Empty;
private long sinceDateDbDownloadBaseline;
private long sinceDateDbUploadBaseline;
private long sinceDateSessionDownloadBaseline;
private long sinceDateSessionUploadBaseline;
private const int WindowSize = 35;
private int tickCount;
private long currentSessionDownload;
private long currentSessionUpload;
private long totalFromDateDownload;
private long totalFromDateUpload;
private double latestDownloadMbps;
private double latestUploadMbps;
private DateTimeOffset? sinceDate;
private long pendingDownloadBytes;
private long pendingUploadBytes;
private long latestDownloadBytesPerSecond;
private long latestUploadBytesPerSecond;
private int graphAxisMagnitude;
public SummaryViewModel(INetworkCaptureService networkCaptureService, IProcessIconService processIconService)
{
this.networkCaptureService = networkCaptureService;
this.processIconService = processIconService;
// Match WPF dark theme accents:
// Download -> #367061, Upload -> #D98868
var dlColor = new SKColor(0x36, 0x70, 0x61);
var ulColor = new SKColor(0xD9, 0x88, 0x68);
GraphSeries =
[
new LineSeries
{
Values = dlValues,
Stroke = new SolidColorPaint(dlColor, 2),
GeometrySize = 0,
GeometryStroke = null,
GeometryFill = null,
Fill = new SolidColorPaint(dlColor.WithAlpha(0x33)),
LineSmoothness = 0.3,
Name = "Download"
},
new LineSeries
{
Values = ulValues,
Stroke = new SolidColorPaint(ulColor, 2),
GeometrySize = 0,
GeometryStroke = null,
GeometryFill = null,
Fill = new SolidColorPaint(ulColor.WithAlpha(0x33)),
LineSmoothness = 0.3,
Name = "Upload"
}
];
GraphXAxes =
[
new Axis
{
ShowSeparatorLines = false,
IsVisible = false,
MinLimit = 0,
MaxLimit = WindowSize
}
];
GraphYAxes = CreateGraphYAxes();
ActiveProcesses = [];
SortProcessesCommand = new ParameterRelayCommand(parameter =>
{
var column = parameter?.ToString();
if (string.IsNullOrWhiteSpace(column))
return;
SortProcesses(column);
});
DateMax = DateTime.Today;
DateMin = DateMax.AddDays(-ApplicationDB.DataStoragePeriodInDays);
sinceDate = DateMax;
this.networkCaptureService.TrafficObserved += OnTrafficObserved;
flushTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
flushTimer.Tick += (_, _) => FlushPendingTraffic();
flushTimer.Start();
}
public ISeries[] GraphSeries { get; }
public Axis[] GraphXAxes { get; }
public Axis[] GraphYAxes { get; private set; }
public ObservableCollection ActiveProcesses { get; }
public ICommand SortProcessesCommand { get; }
public string CurrentSessionDownloadText => ByteSizeFormatter.FormatBytes(currentSessionDownload);
public string CurrentSessionUploadText => ByteSizeFormatter.FormatBytes(currentSessionUpload);
public string TotalFromDateDownloadText => ByteSizeFormatter.FormatBytes(totalFromDateDownload);
public string TotalFromDateUploadText => ByteSizeFormatter.FormatBytes(totalFromDateUpload);
public string DownloadSpeedText => $"{FormatSpeed(latestDownloadBytesPerSecond)}ps";
public string UploadSpeedText => $"{FormatSpeed(latestUploadBytesPerSecond)}ps";
public int ProcessCount => ActiveProcesses.Count;
public DateTime DateMin { get; }
public DateTime DateMax { get; }
public DateTimeOffset? SinceDate
{
get => sinceDate;
set
{
var normalized = NormalizeSinceDate(value);
if (sinceDate == normalized)
return;
sinceDate = normalized;
RefreshSinceDateBaseline();
OnPropertyChanged(nameof(SinceDate));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
public void Dispose()
{
networkCaptureService.TrafficObserved -= OnTrafficObserved;
flushTimer.Stop();
}
public void ClearOnDisconnect()
{
lock (pendingLock)
{
pendingDownloadBytes = 0;
pendingUploadBytes = 0;
pendingByProcess.Clear();
}
latestDownloadMbps = 0;
latestUploadMbps = 0;
latestDownloadBytesPerSecond = 0;
latestUploadBytesPerSecond = 0;
currentSessionDownload = 0;
currentSessionUpload = 0;
totalFromDateDownload = 0;
totalFromDateUpload = 0;
sinceDateDbDownloadBaseline = 0;
sinceDateDbUploadBaseline = 0;
sinceDateSessionDownloadBaseline = 0;
sinceDateSessionUploadBaseline = 0;
activeAdapterName = string.Empty;
dlValues.Clear();
ulValues.Clear();
tickCount = 0;
GraphXAxes[0].MinLimit = 0;
GraphXAxes[0].MaxLimit = WindowSize;
ActiveProcesses.Clear();
processIndex.Clear();
OnPropertyChanged(nameof(ProcessCount));
UpdateGraphAxisLabelScale();
OnPropertyChanged(nameof(CurrentSessionDownloadText));
OnPropertyChanged(nameof(CurrentSessionUploadText));
OnPropertyChanged(nameof(TotalFromDateDownloadText));
OnPropertyChanged(nameof(TotalFromDateUploadText));
OnPropertyChanged(nameof(DownloadSpeedText));
OnPropertyChanged(nameof(UploadSpeedText));
}
public void SetActiveAdapter(string adapterName)
{
var normalized = adapterName?.Trim() ?? string.Empty;
if (string.Equals(activeAdapterName, normalized, StringComparison.Ordinal))
return;
activeAdapterName = normalized;
RefreshSinceDateBaseline();
}
private void OnTrafficObserved(object? sender, NetworkTrafficEventArgs e)
{
lock (pendingLock)
{
if (!pendingByProcess.TryGetValue(e.ProcessName, out var pending))
{
pending = new PendingTraffic();
pendingByProcess[e.ProcessName] = pending;
}
if (e.IsReceive)
{
pending.DownloadBytes += e.Bytes;
pendingDownloadBytes += e.Bytes;
}
else
{
pending.UploadBytes += e.Bytes;
pendingUploadBytes += e.Bytes;
}
}
}
private void FlushPendingTraffic()
{
Dictionary pendingSnapshot;
long secondDownloadBytes;
long secondUploadBytes;
lock (pendingLock)
{
secondDownloadBytes = pendingDownloadBytes;
secondUploadBytes = pendingUploadBytes;
pendingDownloadBytes = 0;
pendingUploadBytes = 0;
pendingSnapshot = new Dictionary(pendingByProcess, StringComparer.OrdinalIgnoreCase);
pendingByProcess.Clear();
}
latestDownloadMbps = secondDownloadBytes * 8d / 1_000_000d;
latestUploadMbps = secondUploadBytes * 8d / 1_000_000d;
latestDownloadBytesPerSecond = secondDownloadBytes;
latestUploadBytesPerSecond = secondUploadBytes;
currentSessionDownload += secondDownloadBytes;
currentSessionUpload += secondUploadBytes;
UpdateTotalFromDateFromBaselines();
AppendGraphPoint();
ApplyProcessTick(pendingSnapshot);
OnPropertyChanged(nameof(CurrentSessionDownloadText));
OnPropertyChanged(nameof(CurrentSessionUploadText));
OnPropertyChanged(nameof(TotalFromDateDownloadText));
OnPropertyChanged(nameof(TotalFromDateUploadText));
OnPropertyChanged(nameof(DownloadSpeedText));
OnPropertyChanged(nameof(UploadSpeedText));
}
private void RefreshSinceDateBaseline()
{
sinceDateSessionDownloadBaseline = currentSessionDownload;
sinceDateSessionUploadBaseline = currentSessionUpload;
if (string.IsNullOrWhiteSpace(activeAdapterName))
{
sinceDateDbDownloadBaseline = 0;
sinceDateDbUploadBaseline = 0;
totalFromDateDownload = 0;
totalFromDateUpload = 0;
}
else
{
var fromDate = (sinceDate ?? DateTimeOffset.Now.Date).Date;
var totals = ReadDbTotals(activeAdapterName, fromDate, DateTime.Today);
sinceDateDbDownloadBaseline = totals.download;
sinceDateDbUploadBaseline = totals.upload;
UpdateTotalFromDateFromBaselines();
}
OnPropertyChanged(nameof(TotalFromDateDownloadText));
OnPropertyChanged(nameof(TotalFromDateUploadText));
}
private void UpdateTotalFromDateFromBaselines()
{
var sessionDownloadDelta = currentSessionDownload - sinceDateSessionDownloadBaseline;
var sessionUploadDelta = currentSessionUpload - sinceDateSessionUploadBaseline;
if (sessionDownloadDelta < 0)
sessionDownloadDelta = 0;
if (sessionUploadDelta < 0)
sessionUploadDelta = 0;
totalFromDateDownload = sinceDateDbDownloadBaseline + sessionDownloadDelta;
totalFromDateUpload = sinceDateDbUploadBaseline + sessionUploadDelta;
}
private static (long download, long upload) ReadDbTotals(string adapterName, DateTime startDate, DateTime endDate)
{
try
{
var dbPath = ResolveDatabasePath();
if (!File.Exists(dbPath))
return (0, 0);
var fromDate = startDate.Date;
var toDate = endDate.Date;
if (toDate < fromDate)
(fromDate, toDate) = (toDate, fromDate);
using var connection = OpenReadOnlyConnection(dbPath);
connection.Open();
using var command = connection.CreateCommand();
command.CommandText =
"SELECT SUM(pd.DataReceived) AS TotalRecv, SUM(pd.DataSent) AS TotalSent " +
"FROM ProcessDate pd " +
"JOIN Adapter a ON a.ID = pd.AdapterID " +
"JOIN Date d ON d.ID = pd.DateID " +
"WHERE a.Name = @AdapterName " +
"AND (d.Year * 10000 + d.Month * 100 + d.Day) BETWEEN @StartDate AND @EndDate";
command.Parameters.AddWithValue("@AdapterName", adapterName);
command.Parameters.AddWithValue("@StartDate", ToDateInt(fromDate));
command.Parameters.AddWithValue("@EndDate", ToDateInt(toDate));
using var reader = command.ExecuteReader();
if (reader.Read())
{
var download = reader.IsDBNull(0) ? 0 : reader.GetInt64(0);
var upload = reader.IsDBNull(1) ? 0 : reader.GetInt64(1);
return (download, upload);
}
}
catch (Exception ex)
{
EventLogger.Error($"Failed to read summary totals from database for adapter '{adapterName}'", ex);
}
return (0, 0);
}
private static SqliteConnection OpenReadOnlyConnection(string path)
{
var csb = new SqliteConnectionStringBuilder
{
DataSource = path,
Mode = SqliteOpenMode.ReadOnly
};
return new SqliteConnection(csb.ToString());
}
private static int ToDateInt(DateTime date)
{
return (date.Year * 10000) + (date.Month * 100) + date.Day;
}
private static string ResolveDatabasePath()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var appFolder = Path.Combine(localAppData, "OpenNetMeter");
return Path.Combine(appFolder, "OpenNetMeter.sqlite");
}
private DateTimeOffset NormalizeSinceDate(DateTimeOffset? value)
{
var candidate = (value ?? DateTimeOffset.Now.Date).Date;
if (candidate > DateMax)
candidate = DateMax;
if (candidate < DateMin)
candidate = DateMin;
return candidate;
}
private void AppendGraphPoint()
{
dlValues.Add(new ObservablePoint(tickCount, MbpsToGraphValue(latestDownloadMbps)));
ulValues.Add(new ObservablePoint(tickCount, MbpsToGraphValue(latestUploadMbps)));
while (dlValues.Count > WindowSize)
dlValues.RemoveAt(0);
while (ulValues.Count > WindowSize)
ulValues.RemoveAt(0);
if (tickCount >= WindowSize)
{
GraphXAxes[0].MinLimit = tickCount - WindowSize;
GraphXAxes[0].MaxLimit = tickCount;
}
UpdateGraphAxisLabelScale();
tickCount++;
}
private Axis[] CreateGraphYAxes()
{
return
[
new Axis
{
MinLimit = 0,
ShowSeparatorLines = true,
// Match dark divider/text tones from MainWindow resources
SeparatorsPaint = new SolidColorPaint(new SKColor(0x55, 0x55, 0x55)) { StrokeThickness = 1 },
LabelsPaint = new SolidColorPaint(new SKColor(0xA9, 0xAB, 0xAB)),
TextSize = 10,
Labeler = FormatGraphAxisLabel
}
];
}
private static double MbpsToGraphValue(double mbps)
{
if (mbps <= 0)
return 0;
// Plot the graph on a logarithmic curve while preserving zero traffic.
return Math.Log(mbps + 1, GraphLogBase);
}
private static double GraphValueToMbps(double graphValue)
{
if (graphValue <= 0)
return 0;
return Math.Pow(GraphLogBase, graphValue) - 1;
}
private static long GraphValueToBytesPerSecond(double graphValue)
{
if (graphValue <= 0)
return 0;
return (long)Math.Round(GraphValueToMbps(graphValue) * 1_000_000d / 8d);
}
private void UpdateGraphAxisLabelScale()
{
var useBytes = SettingsManager.Current.NetworkSpeedFormat != 0;
long maxBytesPerSecond = 0;
if (dlValues.Count > 0)
maxBytesPerSecond = Math.Max(maxBytesPerSecond, GraphValueToBytesPerSecond(dlValues.Max(point => point.Y ?? 0d)));
if (ulValues.Count > 0)
maxBytesPerSecond = Math.Max(maxBytesPerSecond, GraphValueToBytesPerSecond(ulValues.Max(point => point.Y ?? 0d)));
var displayValue = useBytes ? maxBytesPerSecond : maxBytesPerSecond * 8;
var (_, magnitude) = GetAdjustedSize(displayValue, SpeedMagnitude.Auto);
if (graphAxisMagnitude == magnitude)
return;
graphAxisMagnitude = magnitude;
}
private string FormatGraphAxisLabel(double graphValue)
{
var bytesPerSecond = GraphValueToBytesPerSecond(graphValue);
var useBytes = SettingsManager.Current.NetworkSpeedFormat != 0;
var displayValue = useBytes ? bytesPerSecond : bytesPerSecond * 8;
var adjustedSize = ScaleToMagnitude(displayValue, graphAxisMagnitude);
var suffix = useBytes ? BytesSuffix(graphAxisMagnitude) : BitsSuffix(graphAxisMagnitude);
return $"{FormatGraphAxisValue(adjustedSize)} {suffix}/s";
}
private void ApplyProcessTick(Dictionary pendingSnapshot)
{
var touched = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in pendingSnapshot)
{
touched.Add(kvp.Key);
if (!processIndex.TryGetValue(kvp.Key, out var row))
{
row = new SummaryProcessRowViewModel(kvp.Key, processIconService.GetProcessIcon(kvp.Key) as IImage);
processIndex[kvp.Key] = row;
ActiveProcesses.Add(row);
OnPropertyChanged(nameof(ProcessCount));
}
row.ApplyTick(kvp.Value.DownloadBytes, kvp.Value.UploadBytes);
}
foreach (var kvp in processIndex)
{
if (!touched.Contains(kvp.Key))
kvp.Value.ResetCurrent();
}
}
private void SortProcesses(string column)
{
if (string.Equals(currentSortColumn, column, StringComparison.Ordinal))
{
sortDescending = !sortDescending;
}
else
{
currentSortColumn = column;
sortDescending = false;
}
Func selector = column switch
{
"Name" => p => p.ProcessName,
"CurrentDownload" => p => p.CurrentDownloadBytes,
"CurrentUpload" => p => p.CurrentUploadBytes,
"TotalDownload" => p => p.TotalDownloadBytes,
"TotalUpload" => p => p.TotalUploadBytes,
_ => p => p.ProcessName
};
var sorted = sortDescending
? ActiveProcesses.OrderByDescending(selector).ToList()
: ActiveProcesses.OrderBy(selector).ToList();
ActiveProcesses.Clear();
foreach (var item in sorted)
ActiveProcesses.Add(item);
}
public void RefreshSpeedDisplayFormat()
{
UpdateGraphAxisLabelScale();
GraphYAxes = CreateGraphYAxes();
OnPropertyChanged(nameof(GraphYAxes));
OnPropertyChanged(nameof(DownloadSpeedText));
OnPropertyChanged(nameof(UploadSpeedText));
}
private static string FormatSpeed(long bytesPerSecond)
{
var useBytes = SettingsManager.Current.NetworkSpeedFormat != 0;
var magnitude = NormalizeMagnitude(SettingsManager.Current.NetworkSpeedMagnitude);
var value = useBytes ? bytesPerSecond : bytesPerSecond * 8;
var (adjustedSize, mag) = GetAdjustedSize(value, magnitude);
return decimal.Round(adjustedSize, 2).ToString() + (useBytes ? BytesSuffix(mag) : BitsSuffix(mag));
}
private static decimal ScaleToMagnitude(long value, int magnitude)
{
var clampedMagnitude = Math.Clamp(magnitude, 0, MaxSpeedMagnitude);
return (decimal)value / (1L << (clampedMagnitude * 10));
}
private static string FormatGraphAxisValue(decimal adjustedSize)
{
if (adjustedSize <= 0)
return "0";
var rounded = adjustedSize < 10
? decimal.Round(adjustedSize, 1)
: decimal.Round(adjustedSize, 0);
return rounded == decimal.Truncate(rounded)
? rounded.ToString("0")
: rounded.ToString("0.#");
}
private static SpeedMagnitude NormalizeMagnitude(int magnitude)
{
return Enum.IsDefined(typeof(SpeedMagnitude), magnitude)
? (SpeedMagnitude)magnitude
: SpeedMagnitude.Auto;
}
private static (decimal adjustedSize, int mag) GetAdjustedSize(long value, SpeedMagnitude magnitude)
{
int mag;
decimal adjustedSize;
if (magnitude == SpeedMagnitude.Auto)
{
mag = value > 0 ? (int)Math.Log(value, 1024) : 0;
mag = Math.Clamp(mag, 0, 6);
adjustedSize = (decimal)value / (1L << (mag * 10));
if (Math.Round(adjustedSize, 1) >= 1000 && mag < 6)
{
mag += 1;
adjustedSize /= 1024;
}
}
else
{
mag = Math.Clamp((int)magnitude, 0, 6);
adjustedSize = (decimal)value / (1L << (mag * 10));
}
return (adjustedSize, mag);
}
private static string BytesSuffix(int value)
{
return value switch
{
0 => "B",
1 => "KB",
2 => "MB",
3 => "GB",
4 => "TB",
5 => "PB",
6 => "EB",
_ => "B"
};
}
private static string BitsSuffix(int value)
{
return value switch
{
0 => "b",
1 => "Kb",
2 => "Mb",
3 => "Gb",
4 => "Tb",
5 => "Pb",
6 => "Eb",
_ => "b"
};
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private sealed class PendingTraffic
{
public long DownloadBytes;
public long UploadBytes;
}
}
internal enum SpeedMagnitude
{
Auto = 0,
Kilo = 1,
Mega = 2,
Giga = 3
}
public sealed class SummaryProcessRowViewModel : INotifyPropertyChanged
{
private long currentDownloadBytes;
private long currentUploadBytes;
private long totalDownloadBytes;
private long totalUploadBytes;
public SummaryProcessRowViewModel(string processName, IImage? icon = null)
{
ProcessName = processName;
Icon = icon;
}
public IImage? Icon { get; }
public string ProcessName { get; }
public long CurrentDownloadBytes => currentDownloadBytes;
public long CurrentUploadBytes => currentUploadBytes;
public long TotalDownloadBytes => totalDownloadBytes;
public long TotalUploadBytes => totalUploadBytes;
public string CurrentDownload => ByteSizeFormatter.FormatBytes(currentDownloadBytes);
public string CurrentUpload => ByteSizeFormatter.FormatBytes(currentUploadBytes);
public string TotalDownload => ByteSizeFormatter.FormatBytes(totalDownloadBytes);
public string TotalUpload => ByteSizeFormatter.FormatBytes(totalUploadBytes);
public event PropertyChangedEventHandler? PropertyChanged;
public void ApplyTick(long secondDownloadBytes, long secondUploadBytes)
{
currentDownloadBytes = secondDownloadBytes;
currentUploadBytes = secondUploadBytes;
totalDownloadBytes += secondDownloadBytes;
totalUploadBytes += secondUploadBytes;
OnPropertyChanged(nameof(CurrentDownloadBytes));
OnPropertyChanged(nameof(CurrentUploadBytes));
OnPropertyChanged(nameof(TotalDownloadBytes));
OnPropertyChanged(nameof(TotalUploadBytes));
OnPropertyChanged(nameof(CurrentDownload));
OnPropertyChanged(nameof(CurrentUpload));
OnPropertyChanged(nameof(TotalDownload));
OnPropertyChanged(nameof(TotalUpload));
}
public void ResetCurrent()
{
if (currentDownloadBytes == 0 && currentUploadBytes == 0)
return;
currentDownloadBytes = 0;
currentUploadBytes = 0;
OnPropertyChanged(nameof(CurrentDownloadBytes));
OnPropertyChanged(nameof(CurrentUploadBytes));
OnPropertyChanged(nameof(CurrentDownload));
OnPropertyChanged(nameof(CurrentUpload));
}
public void ApplyTraffic(long bytes, bool isReceive)
{
if (isReceive) // kept for compatibility with older call sites
{
currentDownloadBytes = bytes;
totalDownloadBytes += bytes;
OnPropertyChanged(nameof(CurrentDownloadBytes));
OnPropertyChanged(nameof(TotalDownloadBytes));
OnPropertyChanged(nameof(CurrentDownload));
OnPropertyChanged(nameof(TotalDownload));
}
else
{
currentUploadBytes = bytes;
totalUploadBytes += bytes;
OnPropertyChanged(nameof(CurrentUploadBytes));
OnPropertyChanged(nameof(TotalUploadBytes));
OnPropertyChanged(nameof(CurrentUpload));
OnPropertyChanged(nameof(TotalUpload));
}
}
private void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
================================================
FILE: OpenNetMeter/Views/MainWindow/History.axaml
================================================
================================================
FILE: OpenNetMeter/Views/MainWindow/History.axaml.cs
================================================
using Avalonia.Controls;
namespace OpenNetMeter.Views.MainWindowTabs;
public partial class HistoryView : UserControl
{
public HistoryView()
{
InitializeComponent();
}
}
================================================
FILE: OpenNetMeter/Views/MainWindow/Settings.axaml
================================================
================================================
FILE: OpenNetMeter/Views/MainWindow/Settings.axaml.cs
================================================
using Avalonia.Controls;
namespace OpenNetMeter.Views.MainWindowTabs;
public partial class SettingsView : UserControl
{
public SettingsView()
{
InitializeComponent();
}
}
================================================
FILE: OpenNetMeter/Views/MainWindow/Summary.axaml
================================================
================================================
FILE: OpenNetMeter/Views/MainWindow/Summary.axaml.cs
================================================
using Avalonia.Controls;
namespace OpenNetMeter.Views.MainWindowTabs;
public partial class SummaryView : UserControl
{
public SummaryView()
{
InitializeComponent();
}
}
================================================
FILE: OpenNetMeter/Views/MainWindow.axaml
================================================
================================================
FILE: OpenNetMeter/Views/MainWindow.axaml.cs
================================================
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using OpenNetMeter.Services;
using OpenNetMeter.ViewModels;
using OpenNetMeter.Properties;
namespace OpenNetMeter.Views;
public partial class MainWindow : Window
{
private readonly DispatcherTimer resizeTimer;
private readonly DispatcherTimer relocationTimer;
private IMiniWidgetService? miniWidgetService;
private ITrayNotificationService? trayNotificationService;
private bool allowClose;
public MainWindow()
{
InitializeComponent();
resizeTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
relocationTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
resizeTimer.Tick += ResizeTimer_Tick;
relocationTimer.Tick += RelocationTimer_Tick;
Opened += MainWindow_Opened;
Closing += MainWindow_Closing;
PositionChanged += MainWindow_PositionChanged;
SizeChanged += MainWindow_SizeChanged;
}
public void InitializeWindowState(IMiniWidgetService miniWidgetService, ITrayNotificationService trayNotificationService)
{
this.miniWidgetService = miniWidgetService;
this.trayNotificationService = trayNotificationService;
if (SettingsManager.Current.MainWindowPositionInitialized)
{
WindowStartupLocation = WindowStartupLocation.Manual;
Width = Math.Max(MinWidth, SettingsManager.Current.WinWidth);
Height = Math.Max(MinHeight, SettingsManager.Current.WinHeight);
Position = new PixelPoint(SettingsManager.Current.WinPosX, SettingsManager.Current.WinPosY);
return;
}
WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
public void OpenFromTray()
{
if (!IsVisible)
Show();
if (WindowState == WindowState.Minimized)
WindowState = WindowState.Normal;
Activate();
}
public void PrepareForExit()
{
allowClose = true;
}
public void ResetWindowPositions()
{
CenterOnPrimaryScreen();
SaveWindowGeometry();
miniWidgetService?.ResetPosition(this);
}
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
BeginMoveDrag(e);
}
private void ResizeTop_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.North, e);
private void ResizeBottom_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.South, e);
private void ResizeLeft_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.West, e);
private void ResizeRight_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.East, e);
private void ResizeTopLeft_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.NorthWest, e);
private void ResizeTopRight_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.NorthEast, e);
private void ResizeBottomLeft_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.SouthWest, e);
private void ResizeBottomRight_PointerPressed(object? sender, PointerPressedEventArgs e) => TryBeginResize(WindowEdge.SouthEast, e);
private void TryBeginResize(WindowEdge edge, PointerPressedEventArgs e)
{
if (WindowState == WindowState.Normal)
BeginResizeDrag(edge, e);
}
protected override void OnClosed(System.EventArgs e)
{
resizeTimer.Stop();
relocationTimer.Stop();
if (DataContext is MainWindowViewModel vm)
vm.Dispose();
base.OnClosed(e);
}
private void MainWindow_Closing(object? sender, WindowClosingEventArgs e)
{
if (allowClose)
return;
e.Cancel = true;
trayNotificationService?.ShowMinimizedToTrayOnce(this);
Hide();
}
private void MainWindow_Opened(object? sender, EventArgs e)
{
if (!SettingsManager.Current.MainWindowPositionInitialized)
{
SaveWindowGeometry();
miniWidgetService?.ResetPosition(this);
return;
}
if (!IsWindowInBounds(this))
{
CenterOnPrimaryScreen();
SaveWindowGeometry();
}
miniWidgetService?.EnsurePositionOnScreen(this);
}
private void MainWindow_PositionChanged(object? sender, PixelPointEventArgs e)
{
if (WindowState != WindowState.Normal)
return;
RestartTimer(relocationTimer);
}
private void MainWindow_SizeChanged(object? sender, SizeChangedEventArgs e)
{
if (WindowState != WindowState.Normal)
return;
RestartTimer(resizeTimer);
}
private void ResizeTimer_Tick(object? sender, EventArgs e)
{
resizeTimer.Stop();
SaveWindowGeometry();
}
private void RelocationTimer_Tick(object? sender, EventArgs e)
{
relocationTimer.Stop();
SaveWindowGeometry();
}
private void SaveWindowGeometry()
{
if (WindowState != WindowState.Normal)
return;
SettingsManager.Current.WinPosX = Position.X;
SettingsManager.Current.WinPosY = Position.Y;
SettingsManager.Current.WinWidth = Math.Max((int)MinWidth, (int)Math.Round(Bounds.Width > 0 ? Bounds.Width : Width));
SettingsManager.Current.WinHeight = Math.Max((int)MinHeight, (int)Math.Round(Bounds.Height > 0 ? Bounds.Height : Height));
SettingsManager.Current.MainWindowPositionInitialized = true;
SettingsManager.Save();
}
private void CenterOnPrimaryScreen()
{
var screen = Screens?.Primary ?? Screens?.All[0];
if (screen == null)
return;
var width = Math.Max((int)MinWidth, (int)Math.Round(Bounds.Width > 0 ? Bounds.Width : Width));
var height = Math.Max((int)MinHeight, (int)Math.Round(Bounds.Height > 0 ? Bounds.Height : Height));
var area = screen.WorkingArea;
Position = new PixelPoint(
area.X + Math.Max(0, (area.Width - width) / 2),
area.Y + Math.Max(0, (area.Height - height) / 2));
}
private static void RestartTimer(DispatcherTimer timer)
{
timer.Stop();
timer.Start();
}
private static bool IsWindowInBounds(Window target)
{
var screens = target.Screens?.All;
if (screens is null || screens.Count == 0)
return true;
var width = Math.Max(1, (int)Math.Round(target.Bounds.Width > 0 ? target.Bounds.Width : target.Width));
var height = Math.Max(1, (int)Math.Round(target.Bounds.Height > 0 ? target.Bounds.Height : target.Height));
const int margin = 32;
foreach (var screen in screens)
{
var area = screen.WorkingArea;
var areaRight = area.X + area.Width;
var areaBottom = area.Y + area.Height;
var targetRight = target.Position.X + width;
var targetBottom = target.Position.Y + height;
if (area.X < targetRight - margin &&
areaRight > target.Position.X + margin &&
area.Y < targetBottom - margin &&
areaBottom > target.Position.Y + margin)
{
return true;
}
}
return false;
}
}
================================================
FILE: OpenNetMeter/Views/MiniWidgetWindow.axaml
================================================
================================================
FILE: OpenNetMeter/Views/MiniWidgetWindow.axaml.cs
================================================
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using OpenNetMeter.ViewModels;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Views;
public partial class MiniWidgetWindow : Window
{
public MiniWidgetWindow()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void WidgetChrome_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
if (DataContext is MiniWidgetViewModel { IsPinned: true })
return;
try
{
BeginMoveDrag(e);
}
catch (System.Exception ex)
{
EventLogger.Error("Failed to drag mini widget window", ex);
}
}
}
================================================
FILE: OpenNetMeter/Views/Themes.axaml
================================================
0 2 10 0 #33000000
0 1 4 0 #20000000
0 2 10 0 #1ADDDDDD
0 1 4 0 #14000000
10
11
12
13
================================================
FILE: OpenNetMeter/app.manifest
================================================
================================================
FILE: OpenNetMeter.Core/OpenNetMeter.Core.csproj
================================================
net8.0
enable
================================================
FILE: OpenNetMeter.Core/ViewModels/ConfirmationDialogVM.cs
================================================
using System.ComponentModel;
using System.Windows.Input;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.ViewModels
{
public class ConfirmationDialogVM : INotifyPropertyChanged
{
private UiVisibility isVisible;
public UiVisibility IsVisible
{
get => isVisible;
set
{
isVisible = value;
OnPropertyChanged(nameof(IsVisible));
}
}
public string? DialogMessage { get; set; }
public ICommand? BtnCommand { get; set; }
public ConfirmationDialogVM()
{
IsVisible = UiVisibility.Hidden;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
================================================
FILE: OpenNetMeter.Core/ViewModels/MainShellTabsViewModel.cs
================================================
using System.ComponentModel;
namespace OpenNetMeter.Core.ViewModels;
public class MainShellTabsViewModel : INotifyPropertyChanged
{
private int selectedTabIndex;
public virtual int SelectedTabIndex
{
get => selectedTabIndex;
set
{
if (selectedTabIndex == value)
return;
selectedTabIndex = value;
OnPropertyChanged(nameof(SelectedTabIndex));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string propertyName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
================================================
FILE: OpenNetMeter.Old/DatabaseEngine/Connection.cs
================================================
using System.Data.SQLite;
using System.IO;
namespace DatabaseEngine
{
internal class Connection
{
public string ConnectionString { get; set; }
public Connection(string path, string dbFileName)
{
var filePath = Path.Combine(path, dbFileName + ".sqlite");
ConnectionString = @"Data Source=" + Path.Combine(path, dbFileName + ".sqlite;" + " foreign keys=true");
if (!File.Exists(filePath))
{
SQLiteConnection.CreateFile(filePath);
}
}
}
}
================================================
FILE: OpenNetMeter.Old/DatabaseEngine/Database.cs
================================================
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Diagnostics;
namespace DatabaseEngine
{
public class Database : IDisposable
{
private SQLiteConnection? connection;
private bool disposed = false;
///
/// Creates and opens a database connection.
///
/// Directory path for the database file
/// Database file name (without extension)
/// Additional connection string parameters
public Database(string path, string dbFileName, string[]? extraParams = null)
{
string connectionString = new Connection(path, dbFileName).ConnectionString;
// Append any extra parameters to connection string
if (extraParams != null)
{
foreach (var param in extraParams)
{
connectionString += $";{param}";
}
}
connection = new SQLiteConnection(connectionString);
connection.Open();
}
///
/// Configures the database for optimal concurrent access.
/// Call once after opening the connection.
///
public void ConfigureForConcurrency()
{
// WAL mode allows concurrent reads during writes
RunSQLiteNonQuery("PRAGMA journal_mode=WAL;");
// Wait up to 5 seconds if database is locked instead of failing immediately
RunSQLiteNonQuery("PRAGMA busy_timeout=5000;");
// Good balance of durability and performance with WAL
RunSQLiteNonQuery("PRAGMA synchronous=NORMAL;");
}
///
/// Executes a non-query SQL command (INSERT, UPDATE, DELETE, CREATE, etc.)
///
/// SQL query string
/// Number of rows affected, or negative value on error
public int RunSQLiteNonQuery(string query)
{
if (connection == null) return -2;
try
{
using var command = new SQLiteCommand(query, connection);
return command.ExecuteNonQuery();
}
catch (Exception ex)
{
Debug.WriteLine($"SQLite Error: {ex.Message}");
return -1;
}
}
///
/// Executes a non-query SQL command with parameters.
///
/// SQL query string with parameter placeholders
/// 2D array of parameter names and values
/// Number of rows affected, or negative value on error
public int RunSQLiteNonQuery(string query, string[,] paramAndValue)
{
if (connection == null) return -2;
try
{
using var command = new SQLiteCommand(query, connection);
for (int i = 0; i < paramAndValue.GetLength(0); i++)
{
command.Parameters.AddWithValue(paramAndValue[i, 0], paramAndValue[i, 1]);
}
command.Prepare();
return command.ExecuteNonQuery();
}
catch (Exception ex)
{
Debug.WriteLine($"SQLite Error: {ex.Message}");
return -1;
}
}
///
/// Executes multiple non-query commands within a single transaction.
/// More efficient for batch operations.
///
/// Action that performs the database operations
/// True if transaction committed successfully
public bool RunInTransaction(Action action)
{
if (connection == null) return false;
using var transaction = connection.BeginTransaction();
try
{
action(this);
transaction.Commit();
return true;
}
catch (Exception ex)
{
Debug.WriteLine($"SQLite Transaction Error: {ex.Message}");
transaction.Rollback();
return false;
}
}
///
/// Executes a query and returns all rows/columns as a list of lists.
///
public List> GetMultipleCellData(string query)
{
var result = new List>();
if (connection == null) return result;
try
{
using var command = new SQLiteCommand(query, connection);
using var reader = command.ExecuteReader();
while (reader.Read())
{
var row = new List();
for (int i = 0; i < reader.FieldCount; i++)
{
row.Add(reader[i]);
}
result.Add(row);
}
}
catch (Exception ex)
{
Debug.WriteLine($"SQLite read error: {ex.Message}");
}
return result;
}
///
/// Executes a parameterized query and returns all rows/columns as a list of lists.
///
public List> GetMultipleCellData(string query, string[,] paramAndValue)
{
var result = new List>();
if (connection == null) return result;
try
{
using var command = new SQLiteCommand(query, connection);
for (int i = 0; i < paramAndValue.GetLength(0); i++)
{
command.Parameters.AddWithValue(paramAndValue[i, 0], paramAndValue[i, 1]);
}
command.Prepare();
using var reader = command.ExecuteReader();
while (reader.Read())
{
var row = new List();
for (int i = 0; i < reader.FieldCount; i++)
{
row.Add(reader[i]);
}
result.Add(row);
}
}
catch (Exception ex)
{
Debug.WriteLine($"SQLite read error: {ex.Message}");
}
return result;
}
///
/// Executes a parameterized query and returns the first cell of the first row.
///
public object? GetSingleCellData(string query, string[,] paramAndValue)
{
if (connection == null) return null;
try
{
using var command = new SQLiteCommand(query, connection);
for (int i = 0; i < paramAndValue.GetLength(0); i++)
{
command.Parameters.AddWithValue(paramAndValue[i, 0], paramAndValue[i, 1]);
}
command.Prepare();
return command.ExecuteScalar();
}
catch (Exception ex)
{
Debug.WriteLine($"SQLite read error: {ex.Message}");
return null;
}
}
public void Dispose()
{
if (disposed) return;
disposed = true;
connection?.Dispose();
connection = null;
}
}
}
================================================
FILE: OpenNetMeter.Old/DatabaseEngine/DatabaseEngine.csproj
================================================
net8.0-windows
enable
true
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/App.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/App.xaml.cs
================================================
using OpenNetMeter.Views;
using System.Windows;
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
using System;
namespace OpenNetMeter
{
///
/// Interaction logic for App.xaml
///
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
//log any unhandled exceptions
AppDomain.CurrentDomain.UnhandledException += (s, ex) =>
{
EventLogger.Error("Unhandled exception", (Exception)ex.ExceptionObject);
};
EventLogger.Info("Application starting");
bool startMinimized = false;
for (int i = 0; i != e.Args.Length; ++i)
{
if (e.Args[i] == "/StartMinimized")
startMinimized = true;
}
MainWindow window = new MainWindow();
if (startMinimized)
window.Exit_Button_Click(null, null);
else
window.Show();
}
private void Application_Exit(object sender, ExitEventArgs e)
{
EventLogger.Info("Application exiting");
SettingsManager.Save();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/AssemblyInfo.cs
================================================
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Models/ApplicationDB.cs
================================================
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using DatabaseEngine;
namespace OpenNetMeter.Models
{
///
/// Provides access to the application's SQLite database for storing network usage data.
/// Uses a singleton connection pattern to prevent database locking issues.
///
/// Thread-safe: All write operations are serialized via a lock.
///
internal class ApplicationDB : IDisposable
{
private static string LogTime() => DateTime.Now.ToString("HH:mm:ss.fff");
//
// THE ER DIAGRAM
// ------------------
//
// |-----------------------| |-----------------------| |-----------------------|
// | Process | | ProcessDate | | Date |
// |-----------------------| |-----------------------| |-----------------------|
// | PK | ID |---| | PK | ID | |---| PK | ID |
// |-------|---------------| | |-------|---------------| | |-------|---------------|
// | | Name | |==+| FK | ProcessID | | | | Year |
// |-------|---------------| |-------|---------------| | |-------|---------------|
// | FK | DateID |+==| | | Month |
// |-------|---------------| |-------|---------------|
// | | DataReceived| | | Day |
// |-------|---------------| |-------|---------------|
// | | DataSent |
// |-------|---------------|
//
//---------- Singleton Connection ------------//
private static Database? sharedDb;
private static readonly object dbLock = new object();
private static int refCount = 0;
//---------- Constants ------------//
public const string UnifiedDBFileName = "OpenNetMeter";
public const int DataStoragePeriodInDays = 60;
//---------- Instance State ------------//
private readonly string adapterName;
private bool disposed = false;
//---------- Constructor / Disposal ------------//
///
/// Creates a database accessor for the specified adapter.
/// The underlying connection is shared across all instances.
///
/// Adapter name (used for filtering data)
/// Additional connection string parameters (optional)
public ApplicationDB(string dBFileName, string[]? extraParams = null)
{
adapterName = dBFileName;
lock (dbLock)
{
if (sharedDb == null)
{
Debug.WriteLine("ApplicationDB: Creating shared database connection");
sharedDb = new Database(Properties.Global.GetFilePath(), UnifiedDBFileName, extraParams);
sharedDb.ConfigureForConcurrency();
}
refCount++;
}
}
public static string GetUnifiedDBFullPath()
{
return Path.Combine(Properties.Global.GetFilePath(), UnifiedDBFileName + ".sqlite");
}
public void Dispose()
{
if (disposed) return;
disposed = true;
lock (dbLock)
{
refCount--;
if (refCount == 0 && sharedDb != null)
{
Debug.WriteLine("ApplicationDB: Closing shared database connection");
sharedDb.Dispose();
sharedDb = null;
}
}
}
//---------- Helper to Access Shared DB ------------//
///
/// Gets the shared database instance. Throws if not initialized.
///
private static Database DB
{
get
{
if (sharedDb == null)
throw new InvalidOperationException("Database not initialized");
return sharedDb;
}
}
//---------- Public Write Operations (Thread-Safe) ------------//
///
/// Pushes process network data to the database.
/// Thread-safe: serialized with other write operations.
///
public void PushToDB(string processName, long totalDataRecv, long totalDataSend)
{
lock (dbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][WPF] PushToDB adapter='{adapterName}' process='{processName}' recv={totalDataRecv} sent={totalDataSend}");
InsertUniqueRow_ProcessTable(processName);
InsertUniqueRow_AdapterTable(adapterName);
long dateID = GetID_DateTable(DateTime.Today);
long processID = GetID_ProcessTable(processName);
long adapterID = GetID_AdapterTable(adapterName);
// If insert fails (row exists), update instead
if (InsertUniqueRow_ProcessDateTable(processID, dateID, adapterID, totalDataRecv, totalDataSend) < 1)
{
UpdateRow_ProcessDateTable(processID, dateID, adapterID, totalDataRecv, totalDataSend);
}
}
}
///
/// Creates all required tables if they don't exist.
/// Thread-safe: serialized with other write operations.
///
public int CreateTable()
{
lock (dbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][WPF] CreateTable adapter='{adapterName}' db='{GetUnifiedDBFullPath()}'");
// If any function returns negative, result will be negative
return CreateAdapterTable() >> 31 |
CreateProcessTable() >> 31 |
CreateDateTable() >> 31 |
CreateProcessDateTable() >> 31;
}
}
///
/// Inserts today's date and removes old data beyond retention period.
/// Thread-safe: serialized with other write operations.
///
public void UpdateDatesInDB()
{
lock (dbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][WPF] UpdateDatesInDB adapter='{adapterName}' today='{DateTime.Today:yyyy-MM-dd}' retentionDays={DataStoragePeriodInDays}");
InsertUniqueRow_DateTable(DateTime.Today);
RemoveOldDate();
RemoveOldProcess();
}
}
///
/// Ensures the adapter exists in the Adapter table.
/// Thread-safe: serialized with other write operations.
///
public int InsertUniqueRow_AdapterTable(string adapter)
{
lock (dbLock)
{
Console.WriteLine($"[{LogTime()}] [SQLite][WPF] InsertUniqueRow_AdapterTable adapter='{adapter}'");
return DB.RunSQLiteNonQuery(
"INSERT OR IGNORE INTO Adapter(Name) VALUES(@Name)",
new string[,] { { "@Name", adapter } });
}
}
//---------- Public Read Operations ------------//
// Reads don't need the lock with WAL mode, but we use it for safety
public List> GetDataSum_ProcessDateTable(DateTime date1, DateTime date2)
{
lock (dbLock)
{
return DB.GetMultipleCellData(
"SELECT p1.Name, SUM(pd1.DataReceived), SUM(pd1.DataSent) " +
"FROM ProcessDate pd1 " +
"JOIN Process p1 ON p1.ID = pd1.ProcessID " +
"WHERE DateID IN " +
"(SELECT ID FROM Date WHERE " +
"(Year * 10000 + Month * 100 + Day) " +
"BETWEEN " +
$"({date1.Year * 10000 + date1.Month * 100 + date1.Day}) " +
"AND " +
$"({date2.Year * 10000 + date2.Month * 100 + date2.Day})) " +
"AND AdapterID = @AdapterID " +
"GROUP BY ProcessID",
new string[,] { { "@AdapterID", GetID_AdapterTable_Internal(adapterName).ToString() } });
}
}
public (long, long) GetDataSumBetweenDates(DateTime startDate, DateTime endDate)
{
DateTime fromDate = startDate.Date;
DateTime toDate = endDate.Date;
if (toDate < fromDate)
{
(fromDate, toDate) = (toDate, fromDate);
}
lock (dbLock)
{
var sum = DB.GetMultipleCellData(
"SELECT SUM(DataReceived), SUM(DataSent) FROM ProcessDate " +
"WHERE DateID IN " +
"(SELECT ID FROM Date " +
"WHERE (Year * 10000 + Month * 100 + Day) " +
"BETWEEN " +
$"({fromDate.Year * 10000 + fromDate.Month * 100 + fromDate.Day}) " +
"AND " +
$"({toDate.Year * 10000 + toDate.Month * 100 + toDate.Day})) " +
"AND AdapterID = @AdapterID",
new string[,] { { "@AdapterID", GetID_AdapterTable_Internal(adapterName).ToString() } });
if (sum.Count == 1 && sum[0].Count == 2)
{
long download = Convert.IsDBNull(sum[0][0]) ? 0 : Convert.ToInt64(sum[0][0]);
long upload = Convert.IsDBNull(sum[0][1]) ? 0 : Convert.ToInt64(sum[0][1]);
return (download, upload);
}
}
return (0, 0);
}
public (long, long) GetTodayDataSum_ProcessDateTable()
{
lock (dbLock)
{
var sum = DB.GetMultipleCellData(
"SELECT SUM(DataReceived), SUM(DataSent) FROM ProcessDate " +
"WHERE DateID IN " +
"(SELECT ID FROM Date " +
"WHERE (Year * 10000 + Month * 100 + day) = (@Year * 10000 + @Month * 100 + @Day)) " +
"AND AdapterID = @AdapterID",
new string[,]
{
{ "@Year", DateTime.Today.Year.ToString() },
{ "@Month", DateTime.Today.Month.ToString() },
{ "@Day", DateTime.Today.Day.ToString() },
{ "@AdapterID", GetID_AdapterTable_Internal(adapterName).ToString() }
});
if (sum.Count == 1 && sum[0].Count == 2)
{
if (!Convert.IsDBNull(sum[0][0]) && !Convert.IsDBNull(sum[0][1]))
return (Convert.ToInt64(sum[0][0]), Convert.ToInt64(sum[0][1]));
}
}
return (0, 0);
}
public long GetID_AdapterTable(string adapter)
{
lock (dbLock)
{
return GetID_AdapterTable_Internal(adapter);
}
}
public List GetAllAdapters()
{
var adapters = new List();
lock (dbLock)
{
var rows = DB.GetMultipleCellData("SELECT Name FROM Adapter ORDER BY Name");
foreach (var row in rows)
{
if (row.Count > 0 && !Convert.IsDBNull(row[0]))
adapters.Add(Convert.ToString(row[0])!);
}
}
return adapters;
}
//---------- Private Table Creation ------------//
// These are called within lock from CreateTable()
private int CreateProcessTable()
{
return DB.RunSQLiteNonQuery(
"CREATE TABLE IF NOT EXISTS Process(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"Name TEXT NOT NULL UNIQUE)");
}
private int CreateDateTable()
{
return DB.RunSQLiteNonQuery(
"CREATE TABLE IF NOT EXISTS Date(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"Year INTEGER NOT NULL, " +
"Month INTEGER NOT NULL, " +
"Day INTEGER NOT NULL, " +
"UNIQUE (Year, Month, Day) ON CONFLICT IGNORE)");
}
private int CreateProcessDateTable()
{
return DB.RunSQLiteNonQuery(
"CREATE TABLE IF NOT EXISTS ProcessDate(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"ProcessID INTEGER NOT NULL, " +
"DateID INTEGER NOT NULL, " +
"AdapterID INTEGER NOT NULL, " +
"DataReceived INTEGER NOT NULL, " +
"DataSent INTEGER NOT NULL, " +
"FOREIGN KEY(ProcessID) REFERENCES Process(ID) ON DELETE CASCADE, " +
"FOREIGN KEY(DateID) REFERENCES Date(ID) ON DELETE CASCADE, " +
"FOREIGN KEY(AdapterID) REFERENCES Adapter(ID) ON DELETE CASCADE, " +
"UNIQUE (ProcessID, DateID, AdapterID) ON CONFLICT IGNORE)");
}
private int CreateAdapterTable()
{
return DB.RunSQLiteNonQuery(
"CREATE TABLE IF NOT EXISTS Adapter(" +
"ID INTEGER PRIMARY KEY NOT NULL, " +
"Name TEXT NOT NULL UNIQUE)");
}
//---------- Private Insert/Update Operations ------------//
// These are called within lock from PushToDB() and UpdateDatesInDB()
private int InsertUniqueRow_ProcessTable(string appName)
{
return DB.RunSQLiteNonQuery(
"INSERT OR IGNORE INTO Process(Name) VALUES(@Name)",
new string[,] { { "@Name", appName } });
}
private int InsertUniqueRow_DateTable(DateTime date)
{
return DB.RunSQLiteNonQuery(
"INSERT OR IGNORE INTO Date(Year, Month, Day) VALUES(@Year, @Month, @Day)",
new string[,]
{
{ "@Year", date.Year.ToString() },
{ "@Month", date.Month.ToString() },
{ "@Day", date.Day.ToString() }
});
}
private void RemoveOldDate()
{
var cutoff = DateTime.Now.AddDays(-DataStoragePeriodInDays);
Console.WriteLine($"[{LogTime()}] [SQLite][WPF] RemoveOldDate cutoff='{cutoff:yyyy-MM-dd}'");
DB.RunSQLiteNonQuery(
"DELETE FROM Date WHERE (Year * 10000 + Month * 100 + Day) < " +
$"({cutoff.Year * 10000 + cutoff.Month * 100 + cutoff.Day})");
}
private void RemoveOldProcess()
{
Console.WriteLine($"[{LogTime()}] [SQLite][WPF] RemoveOldProcess");
DB.RunSQLiteNonQuery(
"DELETE FROM Process WHERE ID NOT IN " +
"(SELECT DISTINCT ProcessID FROM ProcessDate)");
}
private int InsertUniqueRow_ProcessDateTable(long processID, long dateID, long adapterID, long dataReceived, long dataSent)
{
return DB.RunSQLiteNonQuery(
"INSERT OR IGNORE INTO ProcessDate(ProcessID, DateID, AdapterID, DataReceived, DataSent) " +
"VALUES(@ProcessID, @DateID, @AdapterID, @DataReceived, @DataSent)",
new string[,]
{
{ "@ProcessID", processID.ToString() },
{ "@DateID", dateID.ToString() },
{ "@AdapterID", adapterID.ToString() },
{ "@DataReceived", dataReceived.ToString() },
{ "@DataSent", dataSent.ToString() }
});
}
private int UpdateRow_ProcessDateTable(long processID, long dateID, long adapterID, long dataReceived, long dataSent)
{
return DB.RunSQLiteNonQuery(
"UPDATE ProcessDate SET " +
"DataReceived = DataReceived + @DataReceived, " +
"DataSent = DataSent + @DataSent " +
"WHERE ProcessID = @ProcessID AND DateID = @DateID AND AdapterID = @AdapterID",
new string[,]
{
{ "@DataReceived", dataReceived.ToString() },
{ "@DataSent", dataSent.ToString() },
{ "@ProcessID", processID.ToString() },
{ "@DateID", dateID.ToString() },
{ "@AdapterID", adapterID.ToString() }
});
}
//---------- Private ID Lookups ------------//
// Internal versions without lock (caller must hold lock)
private long GetID_DateTable(DateTime time)
{
var result = DB.GetSingleCellData(
"SELECT ID FROM Date WHERE Year = @Year AND Month = @Month AND Day = @Day",
new string[,]
{
{ "@Year", time.Year.ToString() },
{ "@Month", time.Month.ToString() },
{ "@Day", time.Day.ToString() }
});
return Convert.ToInt64(result ?? -1);
}
private long GetID_ProcessTable(string appName)
{
var result = DB.GetSingleCellData(
"SELECT ID FROM Process WHERE Name = @Name",
new string[,] { { "@Name", appName } });
return Convert.ToInt64(result ?? -1);
}
private long GetID_AdapterTable_Internal(string adapter)
{
var result = DB.GetSingleCellData(
"SELECT ID FROM Adapter WHERE Name = @Name",
new string[,] { { "@Name", adapter } });
return Convert.ToInt64(result ?? -1);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Models/MyProcess.cs
================================================
using OpenNetMeter.Utilities;
using System.ComponentModel;
using System.Windows.Media;
namespace OpenNetMeter.Models
{
public class MyProcess_Small
{
public string? Name { get; set; }
public long CurrentDataRecv { get; set; }
public long CurrentDataSend { get; set; }
public ImageSource? Icon { get; set; }
public MyProcess_Small(string nameP, long currentDataRecvP, long currentDataSendP, ImageSource? icon = null)
{
Name = nameP;
CurrentDataRecv = currentDataRecvP;
CurrentDataSend = currentDataSendP;
Icon = icon;
}
}
public class MyProcess_Big : INotifyPropertyChanged
{
private string? name;
public string? Name
{
get { return name; }
set { name = value; OnPropertyChanged("Name"); }
}
private long currentdataRecv;
public long CurrentDataRecv
{
get { return currentdataRecv; }
set
{
if(currentdataRecv != value)
{
currentdataRecv = value;
OnPropertyChanged("CurrentDataRecv");
}
}
}
private long currentdataSend;
public long CurrentDataSend
{
get { return currentdataSend; }
set
{
if(currentdataSend != value)
{
currentdataSend = value;
OnPropertyChanged("CurrentDataSend");
}
}
}
private long totaldataRecv;
public long TotalDataRecv
{
get { return totaldataRecv; }
set
{
if(totaldataRecv != value)
{
totaldataRecv = value;
OnPropertyChanged("TotalDataRecv");
}
}
}
private long totaldataSend;
public long TotalDataSend
{
get { return totaldataSend; }
set
{
if(totaldataSend != value)
{
totaldataSend = value;
OnPropertyChanged("TotalDataSend");
}
}
}
private ImageSource? icon;
public ImageSource? Icon
{
get { return icon; }
set
{
if (icon != value)
{
icon = value;
OnPropertyChanged(nameof(Icon));
}
}
}
public MyProcess_Big(string nameP, long currentDataRecvP, long currentDataSendP, long totalDataRecvP, long totalDataSendP, ImageSource? iconP = null)
{
Name = nameP;
CurrentDataRecv = currentDataRecvP;
CurrentDataSend = currentDataSendP;
TotalDataRecv = totalDataRecvP;
TotalDataSend = totalDataSendP;
Icon = iconP;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Models/NativeMethods.cs
================================================
using System;
using System.Drawing;
using System.Runtime.InteropServices;
namespace OpenNetMeter.Models
{
public static class NativeMethods
{
internal static IntPtr GetWindowByClassName(IntPtr parentHandle, string className) => FindWindowEx(parentHandle, IntPtr.Zero, className, string.Empty);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
public enum GW : uint
{
HWNDFIRST = 0,
HWNDLAST = 1,
HWNDNEXT = 2,
HWNDPREV = 3,
OWNER = 4,
CHILD = 5,
ENABLEDPOPUP = 6
}
[DllImport("user32.dll")]
internal static extern IntPtr GetWindow(IntPtr hwnd, uint cmd);
public enum SWP
{
ASYNCWINDOWPOS = 0x4000,
NOACTIVATE = 0x0010,
NOMOVE = 0x0002,
NOSIZE = 0x0001,
}
[DllImport("user32.dll", EntryPoint = "SetWindowPos")]
internal static extern IntPtr SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int wFlags);
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Models/NetworkProcess.TestHooks.cs
================================================
using System.Net;
namespace OpenNetMeter.Models
{
// Test-only helpers kept internal to avoid exposing implementation details publicly.
public partial class NetworkProcess
{
internal void TestSetLocalIPs(byte[] ipv4, byte[] ipv6)
{
localIPv4 = ipv4;
localIPv6 = ipv6;
}
internal void TestInvokeRecvProcess(IPAddress src, IPAddress dest, int size, string name)
{
ProcessPacket(src, dest, size, name, isRecv: true);
}
internal void TestInvokeSendProcess(IPAddress src, IPAddress dest, int size, string name)
{
ProcessPacket(src, dest, size, name, isRecv: false);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Models/NetworkProcess.cs
================================================
using System;
using System.Net;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
using Microsoft.Diagnostics.Tracing.Parsers;
using Microsoft.Diagnostics.Tracing.Parsers.Kernel;
using Microsoft.Diagnostics.Tracing.Session;
using System.Threading;
using System.Net.Sockets;
using System.Collections.Generic;
using OpenNetMeter.Utilities;
using OpenNetMeter.Properties;
using System.Diagnostics.Eventing.Reader;
using System.Text.RegularExpressions;
namespace OpenNetMeter.Models
{
public partial class NetworkProcess : IDisposable
{
//---------- Constants ------------//
private const int OneSec = 1000;
private const int DebounceDelayMs = 300; // Delay before processing network change events
// Default IP values indicating "not connected"
private static readonly byte[] DefaultIPv4 = { 0, 0, 0, 0 };
private static readonly byte[] DefaultIPv6 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
//---------- Network State ------------//
///
/// Immutable snapshot of current network connection state.
/// Used to detect connection changes without race conditions.
///
private sealed record NetworkSnapshot(
string AdapterName, // Display name, includes SSID for Wi-Fi
string AdapterId, // GUID for the adapter
byte[] IPv4, // Assigned IPv4 address bytes
byte[] IPv6 // Assigned IPv6 address bytes
);
// Lock for thread-safe access to local IP addresses
private readonly object stateLock = new object();
// Currently assigned local IP addresses (used for packet filtering)
private byte[] localIPv4 = DefaultIPv4;
private byte[] localIPv6 = DefaultIPv6;
//---------- Debounce & Synchronization ------------//
// Timer-based debounce: each network change event resets the timer.
// When it finally fires (after DebounceDelayMs of silence), HandleNetworkChange runs.
private Timer? debounceTimer;
private volatile bool isDisposed;
// Lock to ensure only one HandleNetworkChange runs at a time
private readonly object networkChangeLock = new object();
//---------- Periodic Tasks ------------//
// Periodic task for updating network speed display
private PeriodicWork? networkSpeedWork;
// Periodic task for pushing process data to database
private PeriodicWork? dbPushWork;
//---------- ETW Session ------------//
// ETW kernel session for capturing network packets
private TraceEventSession? kernelSession;
private const string KernelSessionName = "OpenNetMeter-Kernel";
// Task running the ETW packet capture loop
public Task? PacketTask;
//---------- Public State ------------//
// Current network adapter name (includes SSID for Wi-Fi connections)
public string AdapterName { get; private set; } = "";
// Current adapter's unique ID (GUID) - used for comparison to detect changes
private string currentAdapterId = "";
public string CurrentAdapterId => currentAdapterId;
///
/// Primary buffer for storing process network data.
/// Alternates with MyProcessesBuffer to allow lock-free reading.
///
public Dictionary MyProcesses { get; } = new();
///
/// Secondary buffer for storing process network data.
/// While GUI reads from MyProcesses, new data goes here (and vice versa).
/// This double-buffering prevents lock contention with the UI thread.
///
public Dictionary MyProcessesBuffer { get; } = new();
///
/// Buffer for data pending database write.
/// Cleared after each successful DB push.
///
public Dictionary PushToDBBuffer { get; } = new();
///
/// Controls which buffer receives incoming packet data.
/// true = write to MyProcessesBuffer, read from MyProcesses
/// false = write to MyProcesses, read from MyProcessesBuffer
/// Toggled by the UI layer when extracting data for display.
///
public bool IsBufferTime { get; set; }
// Running totals for current session (reset on adapter change)
public long CurrentSessionDownloadData;
public long CurrentSessionUploadData;
public long UploadSpeed;
//---------- Properties with Change Notification ------------//
private long downloadSpeed;
public long DownloadSpeed
{
get => downloadSpeed;
set { downloadSpeed = value; OnPropertyChanged(nameof(DownloadSpeed)); }
}
private string isNetworkOnline = "Disconnected";
///
/// Current connection status. Either "Disconnected" or the adapter name.
///
public string IsNetworkOnline
{
get => isNetworkOnline;
private set { isNetworkOnline = value; OnPropertyChanged(nameof(IsNetworkOnline)); }
}
//---------- Initialization ------------//
///
/// Call after subscribing to property handlers in MainWindowVM.
/// Sets up network change monitoring and performs initial connection check.
///
public void Initialize()
{
IsNetworkOnline = "Disconnected";
// Create the debounce timer (initially not running)
debounceTimer = new Timer(_ => HandleNetworkChange(), null, Timeout.Infinite, Timeout.Infinite);
// Subscribe to network address changes (fires on connect/disconnect/IP change)
NetworkChange.NetworkAddressChanged += OnNetworkAddressChanged;
// Check current connection state on startup
HandleNetworkChange();
}
//---------- Network Detection (Snapshot-Based) ------------//
///
/// Queries the system for the current active network connection.
/// Returns an immutable snapshot of the connection, or null if disconnected.
///
/// This replaces the old socket-based GetLocalIP() approach which was unreliable
/// during network transitions (socket connect to 8.8.8.8 would fail transiently).
///
private NetworkSnapshot? GetCurrentNetworkSnapshot()
{
var adapters = NetworkInterface.GetAllNetworkInterfaces();
foreach (var adapter in adapters)
{
// Skip adapters that aren't connected
if (adapter.OperationalStatus != OperationalStatus.Up)
continue;
// Skip loopback (127.0.0.1)
if (adapter.NetworkInterfaceType == NetworkInterfaceType.Loopback)
continue;
var props = adapter.GetIPProperties();
// No gateway = not a real internet connection (e.g., virtual adapters)
if (props.GatewayAddresses.Count == 0)
continue;
byte[]? ipv4 = null;
byte[]? ipv6 = null;
// Extract assigned IP addresses
foreach (var unicast in props.UnicastAddresses)
{
var addr = unicast.Address;
if (addr.AddressFamily == AddressFamily.InterNetwork)
{
ipv4 = addr.GetAddressBytes();
}
else if (addr.AddressFamily == AddressFamily.InterNetworkV6
&& !addr.IsIPv6LinkLocal) // Skip link-local (fe80::) addresses
{
ipv6 = addr.GetAddressBytes();
}
}
// Need at least one usable IP to consider this a valid connection
if (ipv4 == null && ipv6 == null)
continue;
// Build display name (include SSID for Wi-Fi)
var name = adapter.Name;
if (adapter.NetworkInterfaceType == NetworkInterfaceType.Wireless80211)
{
var ssid = GetConnectedSsid(adapter.Id);
if (!string.IsNullOrEmpty(ssid))
name += $"({ssid})";
}
Debug.WriteLine($"{adapter.Name} is up, IP: {(ipv4 != null ? new IPAddress(ipv4) : new IPAddress(ipv6!))}");
return new NetworkSnapshot(
name,
adapter.Id,
ipv4 ?? DefaultIPv4,
ipv6 ?? DefaultIPv6
);
}
return null; // No valid connection found
}
///
/// Gets the SSID of the currently connected Wi-Fi network by reading
/// from the Windows WLAN AutoConfig event log.
///
/// This is more reliable than the WinRT API on Windows 11 which requires
/// location permissions to retrieve SSID.
///
private static string? GetConnectedSsid(string adapterGuid)
{
try
{
// Query for EventID 8001 (successful connection) in WLAN-AutoConfig log
var query = new EventLogQuery(
"Microsoft-Windows-WLAN-AutoConfig/Operational",
PathType.LogName,
"*[System[EventID=8001]]"
)
{
ReverseDirection = true // Get most recent first
};
using var reader = new EventLogReader(query);
if (reader.ReadEvent() is EventRecord evt)
{
var message = evt.FormatDescription();
var match = Regex.Match(message, @"^SSID:\s*(.+)$", RegexOptions.Multiline);
if (match.Success)
return match.Groups[1].Value.Trim();
}
}
catch
{
// Silently fail - SSID is nice-to-have, not critical
}
return null;
}
//---------- Network Change Handling ------------//
///
/// Event handler for NetworkChange.NetworkAddressChanged.
/// Resets the debounce timer so that HandleNetworkChange only runs
/// after DebounceDelayMs of silence (no rapid-fire events).
///
private void OnNetworkAddressChanged(object? sender, EventArgs? e)
{
if (isDisposed)
return;
// Reset the timer countdown. If another event arrives before it fires,
// the timer resets again. No allocations, no CTS, no race conditions.
try
{
debounceTimer?.Change(DebounceDelayMs, Timeout.Infinite);
}
catch (ObjectDisposedException)
{
// Timer was disposed between the isDisposed check and the Change call — harmless.
}
}
///
/// Core network state machine. Compares current network state to tracked state
/// and triggers appropriate actions (connect/disconnect/switch/refresh).
///
/// State transitions:
/// - Disconnected -> Connected: Start monitoring
/// - Connected -> Disconnected: Stop monitoring
/// - Connected -> Different adapter: Stop then start monitoring
/// - Connected -> Same adapter:
/// - IP/name changed: Refresh tracked snapshot (no restart)
/// - No meaningful change: Ignore transient event
///
/// Protected by networkChangeLock to prevent concurrent execution.
///
private void HandleNetworkChange()
{
if (isDisposed)
return;
lock (networkChangeLock)
{
if (isDisposed)
return;
var snapshot = GetCurrentNetworkSnapshot();
if (snapshot == null)
{
// No valid connection found
if (IsNetworkOnline != "Disconnected")
{
Debug.WriteLine("Ash: Connection lost");
EndNetworkProcess();
}
return;
}
// Valid connection found - determine if it's new or changed
if (IsNetworkOnline == "Disconnected")
{
// Was disconnected, now connected
Debug.WriteLine("Ash: Connection established");
ApplySnapshot(snapshot);
StartNetworkProcess();
}
else if (currentAdapterId != snapshot.AdapterId)
{
// Was connected to different adapter (e.g., switched Wi-Fi networks)
// Compare by ID, not name, because SSID retrieval can race with event log
Debug.WriteLine("Ash: Network adapter changed");
EndNetworkProcess();
ApplySnapshot(snapshot);
StartNetworkProcess();
}
else
{
// Same adapter can still receive DHCP/IPv6 address changes or an SSID/name update.
// Refresh tracked snapshot so packet filtering keeps matching local traffic.
bool ipChanged = HasSnapshotAddressChanged(snapshot);
bool nameChanged = !string.Equals(AdapterName, snapshot.AdapterName, StringComparison.Ordinal);
if (ipChanged || nameChanged)
{
Debug.WriteLine("Ash: Adapter IP/name changed on same adapter - refreshing snapshot");
ApplySnapshot(snapshot);
// Keep status text in sync when SSID/name changes without adapter GUID change.
if (nameChanged)
IsNetworkOnline = AdapterName;
}
}
}
}
private bool HasSnapshotAddressChanged(NetworkSnapshot snapshot)
{
lock (stateLock)
{
return !ByteArray.Compare(localIPv4, snapshot.IPv4) ||
!ByteArray.Compare(localIPv6, snapshot.IPv6);
}
}
///
/// Updates tracked state from a network snapshot.
/// Called when establishing a new connection, switching adapters,
/// or refreshing state on same-adapter IP/name changes.
///
private void ApplySnapshot(NetworkSnapshot snapshot)
{
lock (stateLock)
{
localIPv4 = snapshot.IPv4;
localIPv6 = snapshot.IPv6;
}
currentAdapterId = snapshot.AdapterId;
AdapterName = snapshot.AdapterName;
}
//---------- Start/Stop Network Monitoring ------------//
///
/// Starts all network monitoring components:
/// - Database table setup
/// - ETW packet capture
/// - Speed monitoring periodic task
/// - Database push periodic task
///
public void StartNetworkProcess()
{
// Ensure database table exists for this adapter
using (var db = new ApplicationDB(AdapterName))
{
if (db.CreateTable() < 0)
{
EventLogger.Error("Error: Create table");
}
else
{
db.InsertUniqueRow_AdapterTable(AdapterName);
db.UpdateDatesInDB();
}
}
TraceEventSession.GetActiveSession(KernelSessionName)?.Stop();
// Start packet capture and periodic tasks
CaptureNetworkPackets();
StartSpeedMonitoring();
StartDbPush();
// Update connection status (triggers UI update via PropertyChanged)
IsNetworkOnline = AdapterName;
}
///
/// Stops all network monitoring components and resets state.
/// Called on disconnect or before switching to a different adapter.
///
public void EndNetworkProcess()
{
// Stop ETW session first (generates most activity)
StopKernelSession();
// Stop periodic tasks
StopPeriodicWork(ref networkSpeedWork, "Network speed");
StopPeriodicWork(ref dbPushWork, "DB push");
// Clear process data buffers
lock (MyProcesses) MyProcesses.Clear();
lock (MyProcessesBuffer) MyProcessesBuffer.Clear();
// Reset speed counters
CurrentSessionDownloadData = 0;
CurrentSessionUploadData = 0;
UploadSpeed = 0;
DownloadSpeed = 0;
// Reset adapter tracking
currentAdapterId = "";
IsNetworkOnline = "Disconnected";
}
///
/// Stops the ETW kernel session and waits for the capture task to complete.
///
private void StopKernelSession()
{
// Disposing the session causes Source.Process() to return
kernelSession?.Dispose();
kernelSession = null;
var task = PacketTask;
if (task != null)
{
try
{
task.Wait(TimeSpan.FromMilliseconds(OneSec));
}
catch (AggregateException ex)
{
EventLogger.Error("Packet capture stop error", ex);
}
finally
{
if (task.IsCompleted)
task.Dispose();
else
task.ContinueWith(t => t.Dispose(), TaskScheduler.Default);
PacketTask = null;
}
}
}
///
/// Safely stops a periodic work task with error handling.
///
private void StopPeriodicWork(ref PeriodicWork? work, string name)
{
try
{
work?.Stop();
}
catch (Exception ex)
{
EventLogger.Error($"{name} stop error", ex);
}
finally
{
work = null;
}
}
//---------- Periodic Tasks ------------//
///
/// Starts the periodic task that calculates current network speed.
/// Runs every second and computes speed as delta from previous totals.
///
private void StartSpeedMonitoring()
{
long tempDownload = 0;
long tempUpload = 0;
networkSpeedWork = new PeriodicWork("Network speed", TimeSpan.FromSeconds(1));
networkSpeedWork.Start(_ =>
{
// Read current totals (atomic reads)
long currentDown = Interlocked.Read(ref CurrentSessionDownloadData);
long currentUp = Interlocked.Read(ref CurrentSessionUploadData);
// Calculate speed based on user's preferred format
if (SettingsManager.Current.NetworkSpeedFormat == 0)
{
// Bits per second
DownloadSpeed = (currentDown - tempDownload) * 8;
UploadSpeed = (currentUp - tempUpload) * 8;
}
else
{
// Bytes per second
DownloadSpeed = currentDown - tempDownload;
UploadSpeed = currentUp - tempUpload;
}
// Store for next iteration's delta calculation
tempDownload = currentDown;
tempUpload = currentUp;
return Task.CompletedTask;
});
}
///
/// Starts the periodic task that pushes accumulated process data to the database.
/// Runs every 5 seconds to batch writes and reduce I/O.
///
private void StartDbPush()
{
dbPushWork = new PeriodicWork("DB push", TimeSpan.FromSeconds(5));
dbPushWork.Start(_ =>
{
#if DEBUG
var sw = Stopwatch.StartNew();
#endif
lock (PushToDBBuffer)
{
if (PushToDBBuffer.Count > 0)
{
using var db = new ApplicationDB(AdapterName);
foreach (var (key, proc) in PushToDBBuffer)
{
if (proc != null)
db.PushToDB(key, proc.CurrentDataRecv, proc.CurrentDataSend);
}
PushToDBBuffer.Clear();
}
}
#if DEBUG
sw.Stop();
Debug.WriteLine($"elapsed time (DBpush): {sw.ElapsedMilliseconds} | time {DateTime.Now:O}");
#endif
return Task.CompletedTask;
});
}
//---------- ETW Packet Capture ------------//
///
/// Starts the ETW (Event Tracing for Windows) kernel session to capture
/// all TCP/IP network packets on the system.
///
/// Uses the NT Kernel Logger session which requires admin privileges.
/// Packets are filtered to only count those matching our local IP.
///
private void CaptureNetworkPackets()
{
PacketTask = Task.Run(() =>
{
if (kernelSession != null) return;
try
{
// Create kernel trace session (requires admin)
kernelSession = new TraceEventSession(KernelSessionName);
kernelSession.EnableKernelProvider(KernelTraceEventParser.Keywords.NetworkTCPIP);
// Subscribe to all TCP/UDP send/receive events
// All events funnel through ProcessPacket for unified handling
var kernel = kernelSession.Source.Kernel;
// Receive events (download)
kernel.TcpIpRecv += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
kernel.TcpIpRecvIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
kernel.UdpIpRecv += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
kernel.UdpIpRecvIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: true);
// Send events (upload)
kernel.TcpIpSend += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
kernel.TcpIpSendIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
kernel.UdpIpSend += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
kernel.UdpIpSendIPV6 += data => ProcessPacket(data.saddr, data.daddr, data.size, data.ProcessName, isRecv: false);
// Blocks until session is disposed
kernelSession.Source.Process();
}
catch (Exception ex)
{
EventLogger.Error("Packet capture loop error", ex);
}
});
}
///
/// Processes a single network packet captured by ETW.
/// Filters packets to only count those involving our local IP,
/// then records to the appropriate buffer based on direction.
///
/// Source IP address
/// Destination IP address
/// Packet size in bytes
/// Name of the process that sent/received the packet
/// True for download, false for upload
private void ProcessPacket(IPAddress src, IPAddress dest, int size, string processName, bool isRecv)
{
// Get current local IP for this address family
byte[] localIp;
lock (stateLock)
{
localIp = src.AddressFamily == AddressFamily.InterNetwork ? localIPv4 : localIPv6;
}
// Cache address bytes to avoid repeated allocations
var srcBytes = src.GetAddressBytes();
var destBytes = dest.GetAddressBytes();
// Check if packet involves our local IP
bool isSrc = ByteArray.Compare(srcBytes, localIp);
bool isDest = ByteArray.Compare(destBytes, localIp);
// XOR: exactly one should match (either we sent it or received it)
// If both match (loopback) or neither match (other adapter), skip
if (!(isSrc ^ isDest))
return;
// Apply network type filter (private/public/both)
if (!ShouldProcessByNetworkType(isSrc, src, dest))
return;
// Record the packet to appropriate counter and buffer
if (isRecv)
RecordRecv(processName, size);
else
RecordSend(processName, size);
}
///
/// Checks if a packet should be counted based on the user's network type filter setting.
///
/// True if local IP is the source (upload), false if destination (download)
/// Source IP
/// Destination IP
/// True if packet should be counted
private bool ShouldProcessByNetworkType(bool isLocalSrc, IPAddress src, IPAddress dest)
{
// Remote IP is the one that isn't ours
var remoteIp = isLocalSrc ? dest : src;
return SettingsManager.Current.NetworkType switch
{
0 => IsPrivateIP(remoteIp), // Private only (LAN traffic)
1 => !IsPrivateIP(remoteIp), // Public only (internet traffic)
2 => true, // Both
_ => false
};
}
///
/// Records a received (download) packet.
///
private void RecordRecv(string name, int size)
{
// Thread-safe increment of running total
Interlocked.Add(ref CurrentSessionDownloadData, size);
RecordToBuffer(name, size, isRecv: true);
}
///
/// Records a sent (upload) packet.
///
private void RecordSend(string name, int size)
{
// Thread-safe increment of running total
Interlocked.Add(ref CurrentSessionUploadData, size);
RecordToBuffer(name, size, isRecv: false);
}
///
/// Records packet data to the active process buffer.
/// Uses double-buffering to minimize lock contention with UI reads.
///
private void RecordToBuffer(string name, int size, bool isRecv)
{
// Empty process name means kernel/system traffic
if (string.IsNullOrEmpty(name))
name = "System";
// Select buffer based on current phase
var dict = IsBufferTime ? MyProcessesBuffer : MyProcesses;
lock (dict)
{
// Get or create process entry (single lookup)
if (!dict.TryGetValue(name, out var proc))
{
proc = new MyProcess_Small(name, 0, 0);
dict[name] = proc;
}
// Accumulate data
if (isRecv)
proc!.CurrentDataRecv += size;
else
proc!.CurrentDataSend += size;
}
}
//---------- IP Classification ------------//
///
/// Determines if an IP address is in a private (non-routable) range.
/// Used for filtering traffic by network type (LAN vs internet).
///
/// Private ranges:
/// - IPv4: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16
/// - IPv6: Link-local (fe80::), site-local, unique-local (fc00::/7)
///
private bool IsPrivateIP(IPAddress ip)
{
// Handle IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)
if (ip.IsIPv4MappedToIPv6)
ip = ip.MapToIPv4();
// Loopback is always private
if (IPAddress.IsLoopback(ip))
return true;
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
var bytes = ip.GetAddressBytes();
return bytes[0] == 10 || // 10.0.0.0/8 (Class A)
(bytes[0] == 172 && bytes[1] >= 16 && bytes[1] <= 31) || // 172.16.0.0/12 (Class B)
(bytes[0] == 192 && bytes[1] == 168) || // 192.168.0.0/16 (Class C)
(bytes[0] == 169 && bytes[1] == 254); // 169.254.0.0/16 (link-local/APIPA)
}
if (ip.AddressFamily == AddressFamily.InterNetworkV6)
{
return ip.IsIPv6LinkLocal || // fe80::/10
#if NET6_0_OR_GREATER
ip.IsIPv6UniqueLocal || // fc00::/7 (only available in .NET 6+)
#endif
ip.IsIPv6SiteLocal; // fec0::/10 (deprecated but still checked)
}
return false;
}
//---------- Property Changed ------------//
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
//---------- Dispose ------------//
///
/// Cleans up all resources: unsubscribes from events, stops monitoring,
/// and flushes any pending data to the database.
///
public void Dispose()
{
isDisposed = true;
// Unsubscribe from network change events
NetworkChange.NetworkAddressChanged -= OnNetworkAddressChanged;
// Dispose the debounce timer (prevents any pending callback from firing)
debounceTimer?.Dispose();
debounceTimer = null;
// Stop monitoring if active
if (IsNetworkOnline != "Disconnected")
EndNetworkProcess();
// Flush any remaining buffered data to database
lock (PushToDBBuffer)
{
if (PushToDBBuffer.Count > 0)
{
using var db = new ApplicationDB(AdapterName);
foreach (var (key, proc) in PushToDBBuffer)
{
if (proc != null)
db.PushToDB(key, proc.CurrentDataRecv, proc.CurrentDataSend);
}
PushToDBBuffer.Clear();
}
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Models/SpeedGraph.cs
================================================
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace OpenNetMeter.Models
{
public class MyLine : INotifyPropertyChanged
{
private Point from;
public Point From
{
get { return from; }
set
{
from = value;
OnPropertyChanged("From");
}
}
private Point to;
public Point To
{
get { return to; }
set
{
to = value;
OnPropertyChanged("To");
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
public class SpeedGraph
{
// ---------- Graph geometries ------------//
public List XLines { get; set; }
public List YLines { get; set; }
public List Borders { get; set; }
public ObservableCollection DownloadLines { get; private set; }
public ObservableCollection UploadLines { get; private set; }
public List DownloadPoints { get; private set; }
public List UploadPoints { get; private set; }
// ----------- Graph Labels ----------------//
public List Xlabels { get; private set; }
public double Xstart { get; set; }
public List Ylabels { get; private set; }
private Size maxYtextSize;
// ----------- Other Graph info ----------- //
public double GraphWidth { get; set; }
public double GraphHeight { get; set; }
public int GridXCount;
public int GridYCount;
public int XaxisResolution { get; set; }
public bool resumeDraw { get; set; }
public bool firstDrawAfterResume { get; set; }
public SpeedGraph(int XlineCount, int YlineCount)
{
GridXCount = XlineCount;
GridYCount = YlineCount;
DownloadPoints = new List();
UploadPoints = new List();
DownloadLines = new ObservableCollection();
UploadLines = new ObservableCollection();
Xlabels = new List();
Ylabels = new List();
XLines = new List();
YLines = new List();
Borders = new List();
firstDrawAfterResume = false;
}
public void Init()
{
XaxisResolution = (GridYCount - 1) * 10;
bool useBytes = SettingsManager.Current.NetworkSpeedFormat != 0;
string sampleLabel = DataSizeSuffix.InStr(512 * 1024 * 1024, 1, useBytes, SpeedMagnitude.Auto);
maxYtextSize = UIMeasure.Shape(new TextBlock { Text = sampleLabel, FontSize = 11, Padding = new Thickness(0) });
maxYtextSize.Width += 2.0;
Xstart = maxYtextSize.Width;
// populate the lists
for (int i = 0; i < XaxisResolution; i++)
{
DownloadLines.Add(new MyLine { From = new Point(0, 0), To = new Point(0, 0) });
UploadLines.Add(new MyLine { From = new Point(0, 0), To = new Point(0, 0) });
DownloadPoints.Add(new MyLine { From = new Point(0, 0), To = new Point(0, 0) });
UploadPoints.Add(new MyLine { From = new Point(0, 0), To = new Point(0, 0) });
}
// Add X labels and Y lines
for (int i = 0; i < GridYCount; i++)
{
Xlabels.Add(
new TextBlock
{
Text = i < (GridYCount - 1) ? (i * 10).ToString() : "seconds",
FontSize = 11,
Padding = new Thickness(0)
});
if (i > 0 && i < GridYCount - 1)
{
YLines.Add(new MyLine());
}
}
// Add Y labels and X lines
long temp = 1;
for (int i = 0; i < GridXCount; i++)
{
if (i == 0 || i == GridXCount - 1)
{
Ylabels.Add(new TextBlock
{
Text = "",
FontSize = 11,
Padding = new Thickness(0, 0, 0, 0)
});
}
else
{
if (i % 2 == 0)
temp *= 2;
else
temp *= 512;
Ylabels.Add(new TextBlock
{
Text = DataSizeSuffix.InStr(temp, 1, useBytes, SpeedMagnitude.Auto),
FontSize = 11,
Padding = new Thickness(0, 0, 0, 0)
});
XLines.Add(new MyLine());
}
}
// Add borders
for (int i = 0; i < 4; i++)
{
Borders.Add(new MyLine());
}
}
private int drawPointCount;
public void DrawPoints(long downloadSpeed, long uploadSpeed)
{
// reset the graph after it completes a full run
if (drawPointCount >= XaxisResolution)
{
//shift xaxis label
App.Current.Dispatcher.Invoke(() =>
{
for (int i = 0; i < Xlabels.Count / 2; i++)
{
string temp = Xlabels[i].Text;
Xlabels[i].Text = Xlabels[i + Xlabels.Count / 2].Text;
Xlabels[i + Xlabels.Count / 2].Text = temp;
}
});
drawPointCount = XaxisResolution / 2;
for (int i = 0; i < DownloadPoints.Count; i++)
{
if (i < XaxisResolution / 2)
{
DownloadPoints[i].From = new Point(DownloadPoints[XaxisResolution / 2 + i].From.X - XaxisResolution / 2, DownloadPoints[XaxisResolution / 2 + i].From.Y);
DownloadPoints[i].To = new Point(DownloadPoints[XaxisResolution / 2 + i].To.X - XaxisResolution / 2, DownloadPoints[XaxisResolution / 2 + i].To.Y);
}
else
{
DownloadPoints[i].From = new Point(0, 0);
DownloadPoints[i].To = new Point(0, 0);
}
}
for (int i = 0; i < UploadPoints.Count; i++)
{
if (i < XaxisResolution / 2)
{
UploadPoints[i].From = new Point(UploadPoints[XaxisResolution / 2 + i].From.X - XaxisResolution / 2, UploadPoints[XaxisResolution / 2 + i].From.Y);
UploadPoints[i].To = new Point(UploadPoints[XaxisResolution / 2 + i].To.X - XaxisResolution / 2, UploadPoints[XaxisResolution / 2 + i].To.Y);
}
else
{
UploadPoints[i].From = new Point(0, 0);
UploadPoints[i].To = new Point(0, 0);
}
}
if (resumeDraw)
{
//reset the chart
for (int i = 0; i < DownloadLines.Count; i++)
{
DownloadLines[i].From = new Point(Xstart + DownloadPoints[i].From.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(DownloadPoints[i].From.Y, GraphHeight));
DownloadLines[i].To = new Point(Xstart + DownloadPoints[i].To.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(DownloadPoints[i].To.Y, GraphHeight));
}
for (int i = 0; i < UploadLines.Count; i++)
{
UploadLines[i].From = new Point(Xstart + UploadPoints[i].From.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(UploadPoints[i].From.Y, GraphHeight));
UploadLines[i].To = new Point(Xstart + UploadPoints[i].To.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(UploadPoints[i].To.Y, GraphHeight));
}
}
}
if (drawPointCount == 0)
{
DownloadPoints[drawPointCount].From = new Point(0, 0);
DownloadPoints[drawPointCount].To = new Point(1, downloadSpeed);
UploadPoints[drawPointCount].From = new Point(0, 0);
UploadPoints[drawPointCount].To = new Point(1, uploadSpeed);
}
else
{
DownloadPoints[drawPointCount].From = new Point(drawPointCount, DownloadPoints[drawPointCount - 1].To.Y);
DownloadPoints[drawPointCount].To = new Point((drawPointCount + 1), downloadSpeed);
UploadPoints[drawPointCount].From = new Point(drawPointCount, UploadPoints[drawPointCount - 1].To.Y);
UploadPoints[drawPointCount].To = new Point((drawPointCount + 1), uploadSpeed);
}
if (resumeDraw)
{
if (firstDrawAfterResume && drawPointCount > 0)
{
DownloadLines[drawPointCount - 1].From = new Point(Xstart + DownloadPoints[drawPointCount - 1].From.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(DownloadPoints[drawPointCount - 1].From.Y, GraphHeight));
DownloadLines[drawPointCount - 1].To = new Point(Xstart + DownloadPoints[drawPointCount - 1].To.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(DownloadPoints[drawPointCount - 1].To.Y, GraphHeight));
UploadLines[drawPointCount - 1].From = new Point(Xstart + UploadPoints[drawPointCount - 1].From.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(UploadPoints[drawPointCount - 1].From.Y, GraphHeight));
UploadLines[drawPointCount - 1].To = new Point(Xstart + UploadPoints[drawPointCount - 1].To.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(UploadPoints[drawPointCount - 1].To.Y, GraphHeight));
}
firstDrawAfterResume = false;
DownloadLines[drawPointCount].From = new Point(Xstart + DownloadPoints[drawPointCount].From.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(DownloadPoints[drawPointCount].From.Y, GraphHeight));
DownloadLines[drawPointCount].To = new Point(Xstart + DownloadPoints[drawPointCount].To.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(DownloadPoints[drawPointCount].To.Y, GraphHeight));
UploadLines[drawPointCount].From = new Point(Xstart + UploadPoints[drawPointCount].From.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(UploadPoints[drawPointCount].From.Y, GraphHeight));
UploadLines[drawPointCount].To = new Point(Xstart + UploadPoints[drawPointCount].To.X * (GraphWidth / (double)XaxisResolution), ConvToGraphCoords(UploadPoints[drawPointCount].To.Y, GraphHeight));
}
drawPointCount++;
}
public void DrawClear()
{
drawPointCount = 0;
for (int i = 0; i < DownloadPoints.Count; i++)
{
DownloadLines[i].From = new Point(Xstart, ConvToGraphCoords(0, GraphHeight));
DownloadLines[i].To = new Point(Xstart, ConvToGraphCoords(0, GraphHeight));
UploadLines[i].From = new Point(Xstart, ConvToGraphCoords(0, GraphHeight));
UploadLines[i].To = new Point(Xstart, ConvToGraphCoords(0, GraphHeight));
DownloadPoints[i].From = new Point(0, 0);
DownloadPoints[i].To = new Point(0, 0);
UploadPoints[i].From = new Point(0, 0);
UploadPoints[i].To = new Point(0, 0);
}
App.Current.Dispatcher.Invoke(() =>
{
for (int i = 0; i < (Xlabels.Count - 1); i++)
{
Xlabels[i].Text = (i * 10).ToString();
}
});
}
public void ChangeYLabel()
{
App.Current.Dispatcher.Invoke(() =>
{
long temp = 1;
for (int i = 0; i < GridXCount; i++)
{
if (i == 0 || i == GridXCount - 1)
{
Ylabels[i].Text = "";
}
else
{
if (i % 2 == 0)
temp *= 2;
else
temp *= 512;
bool useBytes = SettingsManager.Current.NetworkSpeedFormat != 0;
Ylabels[i].Text = DataSizeSuffix.InStr(temp, 1, useBytes, SpeedMagnitude.Auto);
}
}
});
}
public double ConvToGraphCoords(double value, double height)
{
if (value > Math.Pow(1024, 2))
{
return (height) * ((1.0 / 3.0) - (value) / (1024.0 * 1024.0 * 1024.0 * 3.0));
}
else if (value > Math.Pow(1024, 1))
{
return (height) * ((2.0 / 3.0) - (value) / (1024.0 * 1024.0 * 3.0));
}
else
{
return (height) * ((3.0 / 3.0) - (value) / (1024.0 * 3.0));
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/OpenNetMeter.Old.csproj
================================================
WinExe
net8.0-windows10.0.19041.0
true
true
true
..\..\Resources\AppIcon.ico
OpenNetMeter
$(ProductVersion)
$(ProductName)
app.manifest
AnyCPU
enable
True
True
Resources.resx
True
True
Settings.settings
PublicResXFileCodeGenerator
Resources.Designer.cs
SettingsSingleFileGenerator
Settings.Designer.cs
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Properties/AppSettings.cs
================================================
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
namespace OpenNetMeter.Properties
{
public class AppSettings : INotifyPropertyChanged
{
private bool darkMode;
public bool DarkMode { get => darkMode; set { if (darkMode != value) { darkMode = value; OnPropertyChanged("DarkMode"); } } }
private bool startWithWin;
public bool StartWithWin { get => startWithWin; set { if (startWithWin != value) { startWithWin = value; OnPropertyChanged("StartWithWin"); } } }
private bool minimizeOnStart = true;
public bool MinimizeOnStart { get => minimizeOnStart; set { if (minimizeOnStart != value) { minimizeOnStart = value; OnPropertyChanged("MinimizeOnStart"); } } }
private int networkType = 2;
public int NetworkType { get => networkType; set { if (networkType != value) { networkType = value; OnPropertyChanged("NetworkType"); } } }
private Point winPos;
public Point WinPos { get => winPos; set { if (winPos != value) { winPos = value; OnPropertyChanged("WinPos"); } } }
private Size winSize;
public Size WinSize { get => winSize; set { if (winSize != value) { winSize = value; OnPropertyChanged("WinSize"); } } }
private bool launchFirstTime = true;
public bool LaunchFirstTime { get => launchFirstTime; set { if (launchFirstTime != value) { launchFirstTime = value; OnPropertyChanged("LaunchFirstTime"); } } }
private int launchPage;
public int LaunchPage { get => launchPage; set { if (launchPage != value) { launchPage = value; OnPropertyChanged("LaunchPage"); } } }
private Point miniWidgetPos;
public Point MiniWidgetPos { get => miniWidgetPos; set { if (miniWidgetPos != value) { miniWidgetPos = value; OnPropertyChanged("MiniWidgetPos"); } } }
private bool miniWidgetVisibility;
public bool MiniWidgetVisibility { get => miniWidgetVisibility; set { if (miniWidgetVisibility != value) { miniWidgetVisibility = value; OnPropertyChanged("MiniWidgetVisibility"); } } }
private bool miniWidgetPinned;
public bool MiniWidgetPinned { get => miniWidgetPinned; set { if (miniWidgetPinned != value) { miniWidgetPinned = value; OnPropertyChanged("MiniWidgetPinned"); } } }
private int networkSpeedFormat;
public int NetworkSpeedFormat { get => networkSpeedFormat; set { if (networkSpeedFormat != value) { networkSpeedFormat = value; OnPropertyChanged("NetworkSpeedFormat"); } } }
private int networkSpeedMagnitude;
public int NetworkSpeedMagnitude { get => networkSpeedMagnitude; set { if (networkSpeedMagnitude != value) { networkSpeedMagnitude = value; OnPropertyChanged("NetworkSpeedMagnitude"); } } }
private int miniWidgetTransparentSlider;
public int MiniWidgetTransparentSlider { get => miniWidgetTransparentSlider; set { if (miniWidgetTransparentSlider != value) { miniWidgetTransparentSlider = value; OnPropertyChanged("MiniWidgetTransparentSlider"); } } }
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Properties/Global.cs
================================================
using System;
using System.IO;
using System.Reflection;
namespace OpenNetMeter.Properties
{
internal class Global
{
public const string AppName = "OpenNetMeter";
public static string GetFilePath()
{
string path = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
path = Path.Combine(path, AppName);
Directory.CreateDirectory(path);
return path;
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Properties/InternalsVisibleTo.cs
================================================
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("OpenNetMeter.Tests")]
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Properties/Resources.Designer.cs
================================================
//------------------------------------------------------------------------------
//
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//
//------------------------------------------------------------------------------
namespace OpenNetMeter.Properties {
using System;
///
/// A strongly-typed resource class, for looking up localized strings, etc.
///
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
///
/// Returns the cached ResourceManager instance used by this class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("OpenNetMeter.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
///
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
///
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
///
public static System.Drawing.Icon AppIcon {
get {
object obj = ResourceManager.GetObject("AppIcon", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
///
/// Looks up a localized resource of type System.Drawing.Bitmap.
///
public static System.Drawing.Bitmap x48 {
get {
object obj = ResourceManager.GetObject("x48", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
///
/// Looks up a localized resource of type System.Drawing.Bitmap.
///
public static System.Drawing.Bitmap x64 {
get {
object obj = ResourceManager.GetObject("x64", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Properties/Resources.resx
================================================
text/microsoft-resx
2.0
System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
..\..\..\Resources\AppIcon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
..\..\..\Resources\x48.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
..\..\..\Resources\x64.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Properties/SettingsManager.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using Newtonsoft.Json;
using OpenNetMeter.Utilities;
namespace OpenNetMeter.Properties
{
internal static class SettingsManager
{
private static readonly string _filePath;
// Create the one and only instance of our settings.
// The UI will bind to this instance and it will never be replaced.
public static AppSettings Current { get; } = new AppSettings();
static SettingsManager()
{
// Set the path to settings.json in the same directory as the .exe
_filePath = Path.Combine(Global.GetFilePath(), "settings.json");
Load();
}
public static void Load()
{
// If the file doesn't exist, we just keep the default values
// that 'Current' was created with.
if (!File.Exists(_filePath))
return;
try
{
var json = File.ReadAllText(_filePath);
// Instead of creating a new object,
// this updates the properties of the EXISTING 'Current' object.
JsonConvert.PopulateObject(json, Current);
}
catch (Exception ex)
{
// Log an error if the json file is corrupt, for example.
EventLogger.Error("Error loading settings", ex);
}
}
public static void Save()
{
var json = JsonConvert.SerializeObject(Current, Formatting.Indented);
File.WriteAllText(_filePath, json);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/BaseCommand.cs
================================================
using System;
using System.Windows.Input;
namespace OpenNetMeter.Utilities
{
internal class BaseCommand : ICommand
{
private Action action;
private bool canExecute;
public BaseCommand(Action action, bool canExecute)
{
this.action = action;
this.canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return canExecute;
}
public event EventHandler? CanExecuteChanged
{
add { } // to silence warning
remove { } // to silence warning
}
public void Execute(object? parameter)
{
action(parameter);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/ByteArray.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OpenNetMeter.Utilities
{
internal class ByteArray
{
public static bool Compare(ReadOnlySpan a1, ReadOnlySpan a2)
{
return a1.SequenceEqual(a2);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/DataSizeSuffix.cs
================================================
using System;
namespace OpenNetMeter.Utilities
{
public enum SpeedMagnitude
{
Auto = 0,
Kilo = 1,
Mega = 2,
Giga = 3
}
internal static class DataSizeSuffix
{
//Bytes = false will give the suffix in bits. Size passed to this should aready be multiplied by 8 for bits
internal static string InStr(long value, int decimalPlaces = 1, bool bytes = true, SpeedMagnitude magnitude = SpeedMagnitude.Auto)
{
(decimal adjustedSize, int mag) = GetAdjustedSize(value, decimalPlaces, magnitude);
if (bytes)
return decimal.Round(adjustedSize, 2).ToString() + InBytes(mag);
else
return decimal.Round(adjustedSize, 2).ToString() + InBits(mag);
}
internal static (double, int) InInt(long value, int decimalPlaces = 1, SpeedMagnitude magnitude = SpeedMagnitude.Auto)
{
(decimal adjustedSize, int mag) = GetAdjustedSize(value, decimalPlaces, magnitude);
return ((double)decimal.Round(adjustedSize, 2), mag);
}
internal static SpeedMagnitude NormalizeMagnitude(int magnitude)
{
return Enum.IsDefined(typeof(SpeedMagnitude), magnitude) ? (SpeedMagnitude)magnitude : SpeedMagnitude.Auto;
}
private static (decimal adjustedSize, int mag) GetAdjustedSize(long value, int decimalPlaces, SpeedMagnitude magnitude)
{
int mag;
decimal adjustedSize;
if (magnitude == SpeedMagnitude.Auto)
{
// mag is 0 for bytes, 1 for KB, 2 for MB, etc.
mag = value > 0 ? (int)Math.Log(value, 1024) : 0;
if (mag < 0)
mag = 0;
// 1L << (mag * 10) == 2 ^ (10 * mag)
adjustedSize = (decimal)value / (1L << mag * 10);
if (Math.Round(adjustedSize, decimalPlaces) >= 1000)
{
mag += 1;
adjustedSize /= 1024;
}
}
else
{
mag = (int)magnitude;
adjustedSize = (decimal)value / (1L << mag * 10);
}
return (adjustedSize, mag);
}
private static string InBytes(int value)
{
return value == 6 ? "EB" : value == 5 ? "PB" : value == 4 ? "TB" : value == 3 ? "GB" : value == 2 ? "MB" : value == 1 ? "KB" : value == 0 ? "B" : "Error";
}
private static string InBits(int value)
{
return value == 6 ? "Eb" : value == 5 ? "Pb" : value == 4 ? "Tb" : value == 3 ? "Gb" : value == 2 ? "Mb" : value == 1 ? "Kb" : value == 0 ? "b" : "Error";
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/EventLogger.cs
================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
namespace OpenNetMeter.Utilities
{
public static class EventLogger
{
private const string EventSourceName = "OpenNetMeter";
private const int MaxEventLogMessageLength = 31839;
public static void Info(string message, int eventId = 1000, short category = 0)
{
WriteEntrySafe(message, EventLogEntryType.Information, eventId, category);
}
public static void Warn(string message, int eventId = 2000, short category = 0)
{
WriteEntrySafe(message, EventLogEntryType.Warning, eventId, category);
}
public static void Error(
string message,
int eventId = 3000,
short category = 0,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
string logMessage = AddCallerContext(message, memberName, filePath, lineNumber);
WriteEntrySafe(logMessage, EventLogEntryType.Error, eventId, category);
}
public static void Error(
Exception ex,
int eventId = 3001,
short category = 0,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
Error("Unhandled exception", ex, eventId, category, memberName, filePath, lineNumber);
}
public static void Error(
string message,
Exception ex,
int eventId = 3001,
short category = 0,
[CallerMemberName] string memberName = "",
[CallerFilePath] string filePath = "",
[CallerLineNumber] int lineNumber = 0)
{
string exceptionText = ex is AggregateException aggregateEx
? aggregateEx.Flatten().ToString()
: ex.ToString();
string errorMessage = $"{message}{Environment.NewLine}{exceptionText}";
string logMessage = AddCallerContext(errorMessage, memberName, filePath, lineNumber);
WriteEntrySafe(logMessage, EventLogEntryType.Error, eventId, category);
}
private static string AddCallerContext(string message, string memberName, string filePath, int lineNumber)
{
string fileName = string.IsNullOrWhiteSpace(filePath) ? "unknown" : Path.GetFileName(filePath);
return $"[{fileName}:{lineNumber} in {memberName}] {message}";
}
private static void WriteEntrySafe(string message, EventLogEntryType entryType, int eventId, short category)
{
string safeMessage = Truncate(message);
Debug.WriteLine(safeMessage);
try
{
EventLog.WriteEntry(EventSourceName, safeMessage, entryType, eventId, category);
}
catch (Exception writeEx)
{
Debug.WriteLine($"[EventLogger fallback] Failed to write to Windows Event Log: {writeEx}");
}
}
private static string Truncate(string message)
{
if (message.Length <= MaxEventLogMessageLength)
return message;
const string suffix = "... [truncated]";
int maxLength = MaxEventLogMessageLength - suffix.Length;
return message[..Math.Max(0, maxLength)] + suffix;
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/IconToImgSource.cs
================================================
using System.Drawing;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace OpenNetMeter.Utilities
{
public static class IconToImgSource
{
public static ImageSource ToImageSource(this Icon icon)
{
ImageSource imageSource = Imaging.CreateBitmapSourceFromHIcon(
icon.Handle,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions());
return imageSource;
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/JsonHelper.cs
================================================
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace OpenNetMeter.Utilities
{
public static class JsonHelper
{
///
/// Serializes any C# object into a pretty-printed (indented) JSON string.
///
/// The object to serialize.
/// Optional custom JSON settings.
/// An indented JSON string representation of the object.
public static string PrettyPrint(object obj, JsonSerializerSettings? settings = null)
{
if (obj == null)
{
return "null"; // Standard JSON representation for null
}
try
{
// Use default settings if none are provided
var defaultSettings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
// Add other common settings here if you like, e.g.:
// ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
// NullValueHandling = NullValueHandling.Ignore
};
return JsonConvert.SerializeObject(obj, settings ?? defaultSettings);
}
catch (JsonException ex)
{
// Handle serialization errors (e.g., self-referencing loops)
return $"Error serializing object: {ex.Message}";
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/PeriodicWork.cs
================================================
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace OpenNetMeter.Utilities
{
internal sealed class PeriodicWork : IDisposable, IAsyncDisposable
{
private readonly string name;
private readonly TimeSpan interval;
private readonly SemaphoreSlim stopLock = new SemaphoreSlim(1, 1);
private Task? runTask;
private PeriodicTimer? timer;
private CancellationTokenSource? cts;
public PeriodicWork(string name, TimeSpan interval)
{
this.name = name;
this.interval = interval;
}
public void Start(Func onTick)
{
if (runTask != null)
return; // already running
cts = new CancellationTokenSource();
timer = new PeriodicTimer(interval);
runTask = RunAsync(onTick, cts.Token, timer);
}
private async Task RunAsync(Func onTick, CancellationToken token, PeriodicTimer timer)
{
try
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
try
{
await onTick(token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
EventLogger.Error($"{name} tick error", ex);
}
}
}
catch (OperationCanceledException)
{
Debug.WriteLine($"{name} task cancelled");
}
}
public async Task StopAsync()
{
await stopLock.WaitAsync().ConfigureAwait(false);
try
{
if (runTask == null)
return;
cts?.Cancel();
try
{
await runTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// expected on cancellation
}
}
finally
{
timer?.Dispose();
cts?.Dispose();
runTask = null;
timer = null;
cts = null;
stopLock.Release();
}
}
public void Stop()
{
StopAsync().GetAwaiter().GetResult();
}
public void Dispose()
{
Stop();
}
public ValueTask DisposeAsync()
{
return new ValueTask(StopAsync());
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/ProcessIconCache.cs
================================================
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Media;
namespace OpenNetMeter.Utilities
{
public static class ProcessIconCache
{
private static readonly ConcurrentDictionary IconCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly ImageSource? DefaultIcon = CreateDefaultIcon();
public static ImageSource? GetIcon(string processName)
{
if (string.IsNullOrWhiteSpace(processName))
return DefaultIcon;
return IconCache.GetOrAdd(processName, FetchIcon);
}
private static ImageSource? FetchIcon(string processName)
{
try
{
string nameWithoutExtension = Path.GetFileNameWithoutExtension(processName);
Process[] processes = Process.GetProcessesByName(nameWithoutExtension);
try
{
Process? process = processes.FirstOrDefault();
string? path = process?.MainModule?.FileName;
if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
{
using Icon? icon = Icon.ExtractAssociatedIcon(path);
if (icon != null)
{
ImageSource image = IconToImgSource.ToImageSource(icon);
if (image.CanFreeze)
image.Freeze();
return image;
}
}
}
finally
{
foreach (Process proc in processes)
{
proc.Dispose();
}
}
}
catch (Win32Exception winError)
{
EventLogger.Error("Failed to fetch process icon (Win32 error)", winError);
}
catch (SystemException sysExError)
{
EventLogger.Error("Failed to fetch process icon", sysExError);
}
return DefaultIcon;
}
private static ImageSource? CreateDefaultIcon()
{
try
{
using Icon icon = (Icon)SystemIcons.Application.Clone();
ImageSource image = IconToImgSource.ToImageSource(icon);
if (image.CanFreeze)
image.Freeze();
return image;
}
catch
{
return null;
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/UIMeasure.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace OpenNetMeter.Utilities
{
public static class UIMeasure
{
public static Size Shape(TextBlock tb)
{
// Measured Size is bounded to be less than maxSize
Size maxSize = new Size(
double.PositiveInfinity,
double.PositiveInfinity);
tb.Measure(maxSize);
return tb.DesiredSize;
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/UpdateChecker.cs
================================================
using Newtonsoft.Json.Linq;
using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace OpenNetMeter.Utilities
{
public class UpdateChecker
{
private const string owner = "Ashfaaq18";
private const string repo = "OpenNetMeter";
public static async Task<(Version? latestVersion, string? downloadUrl)> CheckForUpdates()
{
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("User-Agent", "OpenNetMeter");
var response = await client.GetAsync($"https://api.github.com/repos/{owner}/{repo}/releases/latest");
if (response.IsSuccessStatusCode)
{
var jsonString = await response.Content.ReadAsStringAsync();
var jsonObject = JObject.Parse(jsonString);
string prettyJson = JsonHelper.PrettyPrint(jsonObject);
Debug.WriteLine(prettyJson);
var latestVersion = jsonObject["tag_name"]?.ToString();
if (latestVersion == null) { return (null, null); }
var assets = jsonObject["assets"] as JArray;
var firstAsset = assets?.FirstOrDefault();
var downloadUrl = firstAsset?["browser_download_url"]?.ToString();
return (new Version(latestVersion.Substring(1)), downloadUrl);
}
}
return (null, null);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/WindowsNetworkCaptureService.cs
================================================
using System;
using System.ComponentModel;
using OpenNetMeter.Models;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Utilities
{
public sealed class WindowsNetworkCaptureService : INetworkCaptureService
{
private NetworkProcess? networkProcess;
private readonly object syncLock = new object();
private bool disposed;
public event EventHandler? NetworkChanged;
public event EventHandler? TrafficObserved;
public void Start()
{
lock (syncLock)
{
ThrowIfDisposed();
if (networkProcess != null)
return;
networkProcess = new NetworkProcess();
networkProcess.PropertyChanged += NetworkProcess_PropertyChanged;
networkProcess.Initialize();
}
}
public void Stop()
{
lock (syncLock)
{
if (networkProcess == null)
return;
networkProcess.PropertyChanged -= NetworkProcess_PropertyChanged;
networkProcess.Dispose();
networkProcess = null;
}
}
public void Dispose()
{
lock (syncLock)
{
if (disposed)
return;
Stop();
disposed = true;
}
}
private void NetworkProcess_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (networkProcess == null)
return;
switch (e.PropertyName)
{
case nameof(NetworkProcess.IsNetworkOnline):
NetworkChanged?.Invoke(
this,
new NetworkSnapshotChangedEventArgs(
networkProcess.AdapterName,
networkProcess.CurrentAdapterId));
break;
case nameof(NetworkProcess.DownloadSpeed):
if (networkProcess.DownloadSpeed > 0)
{
TrafficObserved?.Invoke(
this,
new NetworkTrafficEventArgs("Aggregate", networkProcess.DownloadSpeed, isReceive: true));
}
if (networkProcess.UploadSpeed > 0)
{
TrafficObserved?.Invoke(
this,
new NetworkTrafficEventArgs("Aggregate", networkProcess.UploadSpeed, isReceive: false));
}
break;
}
}
private void ThrowIfDisposed()
{
if (disposed)
throw new ObjectDisposedException(nameof(WindowsNetworkCaptureService));
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/WindowsProcessIconService.cs
================================================
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Utilities
{
public sealed class WindowsProcessIconService : IProcessIconService
{
public object? GetProcessIcon(string processName)
{
return ProcessIconCache.GetIcon(processName);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/WindowsStartupRegistrationService.cs
================================================
using System;
using System.IO;
using OpenNetMeter.PlatformAbstractions;
using TaskScheduler = Microsoft.Win32.TaskScheduler;
namespace OpenNetMeter.Utilities
{
public sealed class WindowsStartupRegistrationService : IStartupRegistrationService
{
private const string TaskFolder = "OpenNetMeter";
private const string TaskName = "OpenNetMeterLogon";
public bool IsEnabled()
{
try
{
TaskScheduler.TaskFolder folder = TaskScheduler.TaskService.Instance.RootFolder.SubFolders[TaskFolder];
return folder.Tasks.Exists(TaskName);
}
catch
{
return false;
}
}
public void SetEnabled(bool enabled, bool startMinimized)
{
try
{
TaskScheduler.TaskFolder folder = TaskScheduler.TaskService.Instance.RootFolder.SubFolders[TaskFolder];
if (!enabled)
{
for (int i = 0; i < folder.Tasks.Count; i++)
{
folder.DeleteTask(folder.Tasks[i].Name);
}
TaskScheduler.TaskService.Instance.RootFolder.DeleteFolder(TaskFolder);
return;
}
}
catch (Exception ex)
{
EventLogger.Error("Error while updating startup task registration", ex);
}
if (enabled)
{
try
{
TaskScheduler.TaskService.Instance.RootFolder.CreateFolder(TaskFolder);
CreateTask(startMinimized);
}
catch (Exception ex)
{
EventLogger.Error("Error creating startup task folder/definition", ex);
}
}
}
private static void CreateTask(bool startMinimized)
{
try
{
TaskScheduler.TaskDefinition td = TaskScheduler.TaskService.Instance.NewTask();
td.RegistrationInfo.Description = "Run OpenNetMeter on system log on";
td.Principal.RunLevel = TaskScheduler.TaskRunLevel.Highest;
td.Principal.LogonType = TaskScheduler.TaskLogonType.InteractiveToken;
td.Settings.DisallowStartIfOnBatteries = false;
td.Settings.StopIfGoingOnBatteries = false;
td.Settings.Compatibility = TaskScheduler.TaskCompatibility.V2_3;
TaskScheduler.LogonTrigger logonTrigger = new TaskScheduler.LogonTrigger
{
Enabled = true,
UserId = null
};
td.Triggers.Add(logonTrigger);
TaskScheduler.ExecAction action = new TaskScheduler.ExecAction
{
Path = Path.Combine(AppContext.BaseDirectory, "OpenNetMeter.exe")
};
if (startMinimized)
action.Arguments = "/StartMinimized";
td.Actions.Add(action);
TaskScheduler.TaskService.Instance.RootFolder.SubFolders[TaskFolder].RegisterTaskDefinition(TaskName, td);
}
catch (Exception ex)
{
EventLogger.Error("Error creating startup task", ex);
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Utilities/WpfUiDispatcher.cs
================================================
using System;
using System.Windows.Threading;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Utilities
{
public sealed class WpfUiDispatcher : IUiDispatcher
{
private readonly Dispatcher dispatcher;
public WpfUiDispatcher(Dispatcher? dispatcher = null)
{
this.dispatcher = dispatcher ?? Dispatcher.CurrentDispatcher;
}
public bool CheckAccess() => dispatcher.CheckAccess();
public void Post(Action action)
{
dispatcher.Invoke(action);
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/ViewModels/DataUsageHistoryVM.cs
================================================
using OpenNetMeter.Models;
using OpenNetMeter.PlatformAbstractions;
using OpenNetMeter.Utilities;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Windows.Input;
using System.Windows.Media;
namespace OpenNetMeter.ViewModels
{
public class DataUsageHistoryVM : INotifyPropertyChanged
{
public DateTime DateMax { get; private set; }
public DateTime DateMin { get; private set; }
public DateTime DateStart { get; set; }
public DateTime DateEnd { get; set; }
private string? selectedProfile;
public string? SelectedProfile
{
get { return selectedProfile; }
set
{
if(value != selectedProfile)
{
selectedProfile = value;
OnPropertyChanged("SelectedProfile");
}
}
}
public ObservableCollection? Profiles { get; set; }
public ObservableCollection MyProcesses { get; set; }
private long totalDownloadData;
public long TotalDownloadData
{
get { return totalDownloadData; }
set
{
if (value != totalDownloadData)
{
totalDownloadData = value;
OnPropertyChanged("TotalDownloadData");
}
}
}
private long totalUploadData;
public long TotalUploadData
{
get { return totalUploadData; }
set
{
if (value != totalUploadData)
{
totalUploadData = value;
OnPropertyChanged("TotalUploadData");
}
}
}
private readonly IUiDispatcher uiDispatcher;
private readonly IProcessIconService processIconService;
public ICommand FilterBtn { get; set; }
public DataUsageHistoryVM()
: this(new WpfUiDispatcher(App.Current?.Dispatcher), new WindowsProcessIconService())
{
}
public DataUsageHistoryVM(IUiDispatcher uiDispatcher, IProcessIconService processIconService)
{
this.uiDispatcher = uiDispatcher;
this.processIconService = processIconService;
UpdateDates();
TotalDownloadData = 0;
TotalUploadData = 0;
PropertyChanged += DataUsageHistoryVM_PropertyChanged;
Profiles = new ObservableCollection();
MyProcesses = new ObservableCollection();;
//set button command
FilterBtn = new BaseCommand(Filter, true);
// initial load
GetAllDBFiles();
}
public void UpdateDates()
{
DateStart = DateTime.Today;
DateEnd = DateTime.Today;
DateMax = DateTime.Today;
DateMin = DateTime.Today.AddDays(-1 * ApplicationDB.DataStoragePeriodInDays);
}
private void DataUsageHistoryVM_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "SelectedProfile":
MyProcesses.Clear();
TotalDownloadData = 0;
TotalUploadData = 0;
break;
default:
break;
}
}
private void Filter(object? obj)
{
MyProcesses.Clear();
TotalDownloadData = 0;
TotalUploadData = 0;
Debug.WriteLine($"Filter {DateStart.ToString("d")} | {DateEnd.ToString("d")}");
if(SelectedProfile != null)
{
using (ApplicationDB dB = new ApplicationDB(SelectedProfile, new string[] { "Read Only=True"}))
{
List> dataStats = dB.GetDataSum_ProcessDateTable(DateStart, DateEnd);
for(int i = 0; i< dataStats.Count; i++)
{
if(dataStats[i].Count == 3)
{
if(!Convert.IsDBNull(dataStats[i][0]) && !Convert.IsDBNull(dataStats[i][1]) && !Convert.IsDBNull(dataStats[i][2]))
{
string processName = Convert.ToString(dataStats[i][0])!;
ImageSource? icon = processIconService.GetProcessIcon(processName) as ImageSource;
MyProcesses.Add(new MyProcess_Small(processName, Convert.ToInt64(dataStats[i][1]), Convert.ToInt64(dataStats[i][2]), icon));
TotalDownloadData += Convert.ToInt64(dataStats[i][1]);
TotalUploadData += Convert.ToInt64(dataStats[i][2]);
}
//Debug.WriteLine($"processID: {dataStats[i][0]}, dataRecieved: {dataStats[i][1]}, dataSent: {dataStats[i][2]}");
}
}
}
}
}
public void GetAllDBFiles()
{
// Ensure collection updates occur on the UI thread
if (!uiDispatcher.CheckAccess())
{
uiDispatcher.Post(GetAllDBFiles);
return;
}
Profiles?.Clear();
// Ensure DB exists and read adapters from it
using (ApplicationDB dB = new ApplicationDB(string.Empty))
{
dB.CreateTable();
var adapters = dB.GetAllAdapters();
foreach (var a in adapters)
{
Profiles?.Add(a);
}
}
if (Profiles?.Count > 0)
SelectedProfile = Profiles?[0];
}
public void DeleteAllDBFiles()
{
try
{
string path = ApplicationDB.GetUnifiedDBFullPath();
if (File.Exists(path))
File.Delete(path);
Profiles?.Clear();
SelectedProfile = null;
}
catch (IOException ex)
{
EventLogger.Error("Failed to delete usage database file", ex);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/ViewModels/DataUsageSummaryVM.cs
================================================
using System;
using OpenNetMeter.Models;
using System.ComponentModel;
using System.Collections.Concurrent;
namespace OpenNetMeter.ViewModels
{
public class DataUsageSummaryVM : INotifyPropertyChanged
{
private long todayDownloadData;
public long TodayDownloadData
{
get { return todayDownloadData; }
set
{
todayDownloadData = value;
OnPropertyChanged("TodayDownloadData");
}
}
private long todayUploadData;
public long TodayUploadData
{
get { return todayUploadData; }
set
{
todayUploadData = value;
OnPropertyChanged("TodayUploadData");
}
}
private DateTime dateMin;
public DateTime DateMin
{
get { return dateMin; }
private set
{
if (dateMin != value)
{
dateMin = value;
OnPropertyChanged("DateMin");
}
}
}
private DateTime dateMax;
public DateTime DateMax
{
get { return dateMax; }
private set
{
if (dateMax != value)
{
dateMax = value;
OnPropertyChanged("DateMax");
}
}
}
private DateTime sinceDate;
public DateTime SinceDate
{
get { return sinceDate; }
set
{
DateTime newDate = value.Date;
if (newDate > DateMax)
newDate = DateMax;
else if (newDate < DateMin)
newDate = DateMin;
if (sinceDate != newDate)
{
sinceDate = newDate;
OnPropertyChanged("SinceDate");
}
}
}
private long currentSessionDownloadData;
public long CurrentSessionDownloadData
{
get { return currentSessionDownloadData; }
set
{
currentSessionDownloadData = value;
OnPropertyChanged("CurrentSessionDownloadData");
}
}
private long currentSessionUploadData;
public long CurrentSessionUploadData
{
get { return currentSessionUploadData; }
set
{
currentSessionUploadData = value;
OnPropertyChanged("CurrentSessionUploadData");
}
}
public ObservableConcurrentDictionary MyProcesses { get; set; }
public SpeedGraph Graph { get; set; }
public DataUsageSummaryVM()
{
TodayDownloadData = 0;
TodayUploadData = 0;
CurrentSessionDownloadData = 0;
CurrentSessionUploadData = 0;
MyProcesses = new ObservableConcurrentDictionary();
Graph = new SpeedGraph(7, 7);
Graph.Init();
RefreshDateBounds();
SinceDate = DateTime.Today;
}
public void RefreshDateBounds()
{
DateMax = DateTime.Today;
DateMin = DateTime.Today.AddDays(-1 * ApplicationDB.DataStoragePeriodInDays);
if (sinceDate == default)
sinceDate = DateMax;
if (SinceDate > DateMax)
{
SinceDate = DateMax;
}
else if (SinceDate < DateMin)
{
SinceDate = DateMin;
}
}
//------property changers---------------//
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/ViewModels/MainWindowVM.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input;
using OpenNetMeter.Models;
using System.Linq;
using OpenNetMeter.Utilities;
using OpenNetMeter.Properties;
using OpenNetMeter.PlatformAbstractions;
using System.Windows.Media;
namespace OpenNetMeter.ViewModels
{
public class MainWindowVM : INotifyPropertyChanged, IDisposable
{
private readonly DataUsageSummaryVM dusvm;
private readonly DataUsageHistoryVM duhvm;
private readonly MiniWidgetVM mwvm;
private readonly IProcessIconService processIconService;
public SettingsVM svm;
private readonly NetworkProcess netProc;
public ICommand SwitchTabCommand { get; set; }
private int tabBtnToggle;
public int TabBtnToggle
{
get { return tabBtnToggle; }
set { tabBtnToggle = value; OnPropertyChanged("TabBtnToggle"); }
}
private object? selectedViewModel;
public object? SelectedViewModel
{
get { return selectedViewModel; }
set { selectedViewModel = value; OnPropertyChanged("SelectedViewModel"); }
}
public long downloadSpeed;
public long DownloadSpeed
{
get { return downloadSpeed; }
set { downloadSpeed = value; OnPropertyChanged("DownloadSpeed"); }
}
public long uploadSpeed;
public long UploadSpeed
{
get { return uploadSpeed; }
set { uploadSpeed = value; OnPropertyChanged("UploadSpeed"); }
}
private string networkStatus;
public string NetworkStatus
{
get { return networkStatus; }
set { networkStatus = value; OnPropertyChanged("NetworkStatus"); }
}
private DateTime date1;
private DateTime date2;
private long initSinceDateTotalDownloadData = 0;
private long initSinceDateTotalUploadData = 0;
private long sinceDateSessionDownloadBaseline = 0;
private long sinceDateSessionUploadBaseline = 0;
private enum TabPage
{
Summary,
History,
Settings
}
public MainWindowVM(MiniWidgetVM mw_DataContext, ConfirmationDialogVM cd_DataContext) //runs once during app init
: this(mw_DataContext, cd_DataContext, new WindowsProcessIconService())
{
}
public MainWindowVM(MiniWidgetVM mw_DataContext, ConfirmationDialogVM cd_DataContext, IProcessIconService processIconService) //runs once during app init
{
this.processIconService = processIconService;
DownloadSpeed = 0;
UploadSpeed = 0;
date1 = DateTime.Now;
date2 = DateTime.Now;
networkStatus = "";
mwvm = mw_DataContext;
svm = new SettingsVM(mw_DataContext, cd_DataContext);
svm.PropertyChanged += Svm_PropertyChanged;
dusvm = new DataUsageSummaryVM();
duhvm = new DataUsageHistoryVM();
netProc = new NetworkProcess();
netProc.PropertyChanged += NetProc_PropertyChanged;
netProc.Initialize(); //have to call this after subscribing to property changer
dusvm.PropertyChanged += Dusvm_PropertyChanged;
// Populate adapters list from the unified DB
duhvm.GetAllDBFiles();
//intial startup page
TabBtnToggle = SettingsManager.Current.LaunchPage;
switch (TabBtnToggle)
{
case ((int)TabPage.Summary):
SelectedViewModel = dusvm;
break;
case ((int)TabPage.History):
SelectedViewModel = duhvm;
break;
case ((int)TabPage.Settings):
SelectedViewModel = svm;
break;
default:
SelectedViewModel = dusvm;
TabBtnToggle = ((int)TabPage.Summary);
break;
}
//assign basecommand
SwitchTabCommand = new BaseCommand(SwitchTab, true);
//get initial data usage details from the database
RefreshSummaryBaseline();
UpdateSummaryTab();
}
private void Svm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "DeleteAllFiles":
if (svm.DeleteAllFiles)
{
if (netProc.IsNetworkOnline != "Disconnected")
{
netProc.EndNetworkProcess();
duhvm.DeleteAllDBFiles();
netProc.StartNetworkProcess();
}
else
{
duhvm.DeleteAllDBFiles();
}
svm.DeleteAllFiles = false;
}
break;
case "NetworkSpeedFormat":
dusvm.Graph.ChangeYLabel();
dusvm.Graph.DrawClear();
break;
default:
break;
}
}
private void UpdateMainWinSpeed()
{
DownloadSpeed = netProc.DownloadSpeed;
UploadSpeed = netProc.UploadSpeed;
}
private void UpdateMiniWidgetValues()
{
mwvm.DownloadSpeed = DownloadSpeed;
mwvm.CurrentSessionDownloadData = netProc.CurrentSessionDownloadData;
mwvm.UploadSpeed = UploadSpeed;
mwvm.CurrentSessionUploadData = netProc.CurrentSessionUploadData;
}
private void RefreshSummaryBaseline()
{
using (ApplicationDB dB = new ApplicationDB(netProc.AdapterName))
{
(long, long) totals = dB.GetDataSumBetweenDates(dusvm.SinceDate, DateTime.Today);
initSinceDateTotalDownloadData = totals.Item1;
initSinceDateTotalUploadData = totals.Item2;
}
sinceDateSessionDownloadBaseline = netProc.CurrentSessionDownloadData;
sinceDateSessionUploadBaseline = netProc.CurrentSessionUploadData;
UpdateTodayTotals();
}
private void UpdateTodayTotals()
{
long sessionDownloadDelta = netProc.CurrentSessionDownloadData - sinceDateSessionDownloadBaseline;
long sessionUploadDelta = netProc.CurrentSessionUploadData - sinceDateSessionUploadBaseline;
if (sessionDownloadDelta < 0)
sessionDownloadDelta = 0;
if (sessionUploadDelta < 0)
sessionUploadDelta = 0;
dusvm.TodayDownloadData = initSinceDateTotalDownloadData + sessionDownloadDelta;
dusvm.TodayUploadData = initSinceDateTotalUploadData + sessionUploadDelta;
}
private void UpdateSummaryTab()
{
//summary tab graph points
dusvm.Graph.DrawPoints(DownloadSpeed, UploadSpeed);
//summary tab session usage variables
dusvm.CurrentSessionDownloadData = netProc.CurrentSessionDownloadData;
dusvm.CurrentSessionUploadData = netProc.CurrentSessionUploadData;
UpdateTodayTotals();
UpdateMyProcessTable();
}
private void UpdateMyProcessTable()
{
if (netProc.MyProcesses != null && netProc.MyProcessesBuffer != null && dusvm.MyProcesses != null && netProc.PushToDBBuffer != null)
{
using (ApplicationDB dB = new ApplicationDB(netProc.AdapterName))
{
if (dB.CreateTable() < 0)
Debug.WriteLine("Error: Create table");
else
{
//when the application stays open during a day transition
if ((date2.Date - date1.Date).Days > 0)
{
dusvm.TodayDownloadData = 0;
dusvm.TodayUploadData = 0;
dB.UpdateDatesInDB();
duhvm.UpdateDates();
dusvm.RefreshDateBounds();
RefreshSummaryBaseline();
date1 = date2;
}
foreach (KeyValuePair app in dusvm.MyProcesses)
{
dusvm.MyProcesses[app.Key].CurrentDataRecv = 0;
dusvm.MyProcesses[app.Key].CurrentDataSend = 0;
}
netProc.IsBufferTime = true;
//this dictionary is locked from being accessible by the other threads like the network data capture Recv()
lock (netProc.MyProcesses)
{
foreach (KeyValuePair app in netProc.MyProcesses) //the contents of this loops remain only for a sec (related to NetworkProcess.cs=>CaptureNetworkSpeed())
{
EnsureProcessEntry(app.Key);
if (app.Value!.CurrentDataRecv == 0 && app.Value!.CurrentDataSend == 0)
{
Debug.WriteLine($"Both zero {app.Key}");
}
dusvm.MyProcesses[app.Key].CurrentDataRecv = app.Value!.CurrentDataRecv;
dusvm.MyProcesses[app.Key].CurrentDataSend = app.Value!.CurrentDataSend;
dusvm.MyProcesses[app.Key].TotalDataRecv += app.Value!.CurrentDataRecv;
dusvm.MyProcesses[app.Key].TotalDataSend += app.Value!.CurrentDataSend;
/*
Debug.WriteLine($"CurrentDataRecv: {dusvm.MyProcesses[app.Key].CurrentDataRecv} , " +
$"CurrentDataSend: {dusvm.MyProcesses[app.Key].CurrentDataSend} , " +
$"TotalDataRecv: {dusvm.MyProcesses[app.Key].TotalDataRecv} , " +
$"TotalDataSend: {dusvm.MyProcesses[app.Key].TotalDataSend} , " );
*/
lock (netProc.PushToDBBuffer)
{
//push data to a buffer which will be pushed to the DB later
netProc.PushToDBBuffer!.TryAdd(app.Key, new MyProcess_Small(app.Key, 0, 0));
netProc.PushToDBBuffer[app.Key]!.CurrentDataRecv += dusvm.MyProcesses[app.Key].CurrentDataRecv;
netProc.PushToDBBuffer[app.Key]!.CurrentDataSend += dusvm.MyProcesses[app.Key].CurrentDataSend;
}
}
netProc.MyProcesses.Clear();
}
netProc.IsBufferTime = false;
lock (netProc.MyProcessesBuffer)
{
foreach (KeyValuePair app in netProc.MyProcessesBuffer) //the contents of this loops remain only for a sec (related to NetworkProcess.cs=>CaptureNetworkSpeed())
{
Debug.WriteLine("BUFFEEERRRRR!!!!!");
EnsureProcessEntry(app.Key);
if (app.Value!.CurrentDataRecv == 0 && app.Value!.CurrentDataSend == 0)
{
Debug.WriteLine($"Both zero {app.Key}");
}
dusvm.MyProcesses[app.Key].CurrentDataRecv += app.Value!.CurrentDataRecv;
dusvm.MyProcesses[app.Key].CurrentDataSend += app.Value!.CurrentDataSend;
dusvm.MyProcesses[app.Key].TotalDataRecv += app.Value!.CurrentDataRecv;
dusvm.MyProcesses[app.Key].TotalDataSend += app.Value!.CurrentDataSend;
lock (netProc.PushToDBBuffer)
{
//push data to a buffer which will be pushed to the DB later
netProc.PushToDBBuffer!.TryAdd(app.Key, new MyProcess_Small(app.Key, 0, 0));
netProc.PushToDBBuffer[app.Key]!.CurrentDataRecv = dusvm.MyProcesses[app.Key].TotalDataRecv;
netProc.PushToDBBuffer[app.Key]!.CurrentDataSend = dusvm.MyProcesses[app.Key].TotalDataSend;
}
}
netProc.MyProcessesBuffer.Clear();
}
}
}
}
}
private void EnsureProcessEntry(string processName)
{
ImageSource? icon = processIconService.GetProcessIcon(processName) as ImageSource;
if (!dusvm.MyProcesses.TryAdd(processName, new MyProcess_Big(processName, 0, 0, 0, 0, icon)))
{
if (dusvm.MyProcesses[processName].Icon == null)
{
dusvm.MyProcesses[processName].Icon = icon;
}
}
}
private void UpdateData()
{
date2 = DateTime.Now;
UpdateMainWinSpeed();
UpdateMiniWidgetValues();
UpdateSummaryTab();
}
private void NetProc_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
switch (e.PropertyName)
{
case "DownloadSpeed":
UpdateData();
break;
case "IsNetworkOnline":
if (netProc.IsNetworkOnline == "Disconnected")
{
NetworkStatus = "Disconnected";
if (dusvm.MyProcesses.Count() > 0)
{
foreach (var row in dusvm.MyProcesses.ToList())
{
dusvm.MyProcesses.Remove(row.Key);
}
}
dusvm.Graph.DrawClear();
dusvm.TodayDownloadData = 0;
dusvm.TodayUploadData = 0;
}
else
{
NetworkStatus = "Connected : " + netProc.IsNetworkOnline;
// Ensure current adapter exists in DB and refresh profiles
using (ApplicationDB dB = new ApplicationDB(netProc.AdapterName))
{
dB.CreateTable();
dB.InsertUniqueRow_AdapterTable(netProc.AdapterName);
}
duhvm.GetAllDBFiles();
}
break;
default:
break;
}
sw.Stop();
// Debug.WriteLine($"elapsed time (NetProc): {sw.ElapsedMilliseconds}");
}
private void Dusvm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DataUsageSummaryVM.SinceDate))
{
RefreshSummaryBaseline();
UpdateSummaryTab();
}
}
private void SwitchTab(object? obj)
{
string? tab = obj as string;
switch (tab)
{
case "summary":
if (TabBtnToggle != ((int)TabPage.Summary))
{
SelectedViewModel = dusvm;
TabBtnToggle = ((int)TabPage.Summary);
SettingsManager.Current.LaunchPage = TabBtnToggle;
SettingsManager.Save();
}
break;
case "history":
if (TabBtnToggle != ((int)TabPage.History))
{
SelectedViewModel = duhvm;
TabBtnToggle = ((int)TabPage.History);
SettingsManager.Current.LaunchPage = TabBtnToggle;
SettingsManager.Save();
}
break;
case "settings":
if (TabBtnToggle != ((int)TabPage.Settings))
{
SelectedViewModel = svm;
TabBtnToggle = ((int)TabPage.Settings);
SettingsManager.Current.LaunchPage = TabBtnToggle;
SettingsManager.Save();
}
break;
default:
break;
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
public void Dispose()
{
dusvm.PropertyChanged -= Dusvm_PropertyChanged;
if (netProc != null)
{
netProc.PropertyChanged -= NetProc_PropertyChanged;
netProc.Dispose();
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/ViewModels/MiniWidgetVM.cs
================================================
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
namespace OpenNetMeter.ViewModels
{
public class MiniWidgetVM : INotifyPropertyChanged
{
private long currentSessionDownloadData;
public long CurrentSessionDownloadData
{
get { return currentSessionDownloadData; }
set
{
currentSessionDownloadData = value;
OnPropertyChanged("CurrentSessionDownloadData");
}
}
private long currentSessionUploadData;
public long CurrentSessionUploadData
{
get { return currentSessionUploadData; }
set
{
currentSessionUploadData = value;
OnPropertyChanged("CurrentSessionUploadData");
}
}
public long downloadSpeed;
public long DownloadSpeed
{
get { return downloadSpeed; }
set
{
downloadSpeed = value;
OnPropertyChanged("DownloadSpeed");
}
}
public long uploadSpeed;
public long UploadSpeed
{
get { return uploadSpeed; }
set
{
uploadSpeed = value;
OnPropertyChanged("UploadSpeed");
}
}
private double width;
public double Width
{
get { return width; }
set
{
width = value;
OnPropertyChanged("Width");
}
}
private double height;
public double Height
{
get { return height; }
set
{
height = value;
OnPropertyChanged("Height");
}
}
public string? backgroundColor;
public string? BackgroundColor
{
get { return backgroundColor; }
set
{
backgroundColor = value;
OnPropertyChanged("BackgroundColor");
}
}
private bool isPinned;
public bool IsPinned
{
get { return isPinned; }
set
{
if (isPinned == value)
{
return;
}
isPinned = value;
SettingsManager.Current.MiniWidgetPinned = value;
SettingsManager.Save();
OnPropertyChanged("IsPinned");
}
}
public MiniWidgetVM()
{
CurrentSessionDownloadData = 0;
CurrentSessionUploadData = 0;
DownloadSpeed = 0;
UploadSpeed = 0;
BackgroundColor = "#ff212121";
int downloadIconSize = 20;
Size size1 = UIMeasure.Shape(new TextBlock { Text = "Total:", FontSize = 12, Padding = new Thickness(0) });
Size size2 = UIMeasure.Shape(new TextBlock { Text = "1024.00Mb", FontSize = 12, Padding = new Thickness(5,0,0,0) });
int widthMargins = 5 + 5; //these are from the miniwidget xaml margins
Width = (size1.Width + size2.Width + widthMargins + downloadIconSize) * 2;
int heightMargins = 2 + 2; //these are from the miniwidget xaml margins
Height = size1.Height * 2 + heightMargins * 2;
isPinned = SettingsManager.Current.MiniWidgetPinned;
OnPropertyChanged("IsPinned");
}
//------property changers---------------//
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/ViewModels/SettingsVM.cs
================================================
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.ViewModels
{
public class SettingsVM : INotifyPropertyChanged
{
private bool setStartWithWin;
public bool SetStartWithWin
{
get { return setStartWithWin; }
set
{
if (setStartWithWin != value)
{
setStartWithWin = value;
OnPropertyChanged("SetStartWithWin");
//set the app settings
SettingsManager.Current.StartWithWin = value;
SettingsManager.Save();
UnlockOptionStartWin = false;
startupRegistrationService.SetEnabled(value, MinimizeOnStart);
UnlockOptionStartWin = true;
if (value)
UnlockMinimizeOnStart = false;
else
UnlockMinimizeOnStart = true;
}
}
}
private bool unlockOptionStartWin;
public bool UnlockOptionStartWin
{
get { return unlockOptionStartWin; }
set
{
if (unlockOptionStartWin != value)
{
unlockOptionStartWin = value;
OnPropertyChanged("UnlockOptionStartWin");
}
}
}
private bool minimizeOnStart;
public bool MinimizeOnStart
{
get { return minimizeOnStart; }
set
{
if (minimizeOnStart != value)
{
minimizeOnStart = value;
SettingsManager.Current.MinimizeOnStart = value;
SettingsManager.Save();
}
}
}
private bool unlockMinimizeOnStart;
public bool UnlockMinimizeOnStart
{
get { return unlockMinimizeOnStart; }
set
{
if (unlockMinimizeOnStart != value)
{
unlockMinimizeOnStart = value;
OnPropertyChanged("UnlockMinimizeOnStart");
}
}
}
//0 == private, 1 == public, 2 == both
private int networkTrafficType;
public int NetworkTrafficType
{
get { return networkTrafficType; }
set
{
if (networkTrafficType != value)
{
networkTrafficType = value;
OnPropertyChanged("NetworkTrafficType");
//set the app settings
SettingsManager.Current.NetworkType = value;
SettingsManager.Save();
}
}
}
private int networkSpeedFormat;
public int NetworkSpeedFormat
{
get { return networkSpeedFormat; }
set
{
if(networkSpeedFormat != value)
{
networkSpeedFormat = value;
SettingsManager.Current.NetworkSpeedFormat = value;
SettingsManager.Save();
OnPropertyChanged("NetworkSpeedFormat");
}
}
}
private int networkSpeedMagnitude;
public int NetworkSpeedMagnitude
{
get { return networkSpeedMagnitude; }
set
{
if (networkSpeedMagnitude != value)
{
networkSpeedMagnitude = value;
SettingsManager.Current.NetworkSpeedMagnitude = value;
SettingsManager.Save();
OnPropertyChanged("NetworkSpeedMagnitude");
}
}
}
private bool darkMode;
public bool DarkMode
{
get { return darkMode; }
set
{
darkMode = value;
OnPropertyChanged("DarkMode");
//trigger the miniwidget's BackgroundColor property.
SetMiniWidgetBackgroundColor(value, MiniWidgetTransparentSlider);
//set the app settings
SettingsManager.Current.DarkMode = value;
SettingsManager.Save();
}
}
private int miniWidgetTransparentSlider;
public int MiniWidgetTransparentSlider
{
get { return miniWidgetTransparentSlider; }
set
{
miniWidgetTransparentSlider = value;
OnPropertyChanged("MiniWidgetTransparentSlider");
//Debug.WriteLine($"MiniWidgetTransparentSlider: {value}");
//trigger the miniwidget's BackgroundColor property.
SetMiniWidgetBackgroundColor(DarkMode, value);
SettingsManager.Current.MiniWidgetTransparentSlider = value;
SettingsManager.Save();
}
}
public bool deleteAllFiles;
public bool DeleteAllFiles
{
get { return deleteAllFiles; }
set
{
if (deleteAllFiles != value)
{
deleteAllFiles = value;
if(value)
OnPropertyChanged("DeleteAllFiles");
}
}
}
public ICommand ResetBtn { get; set; }
public ICommand UpdateCheckBtn { get; set; }
public ICommand DownloadUpdateBtn { get; set; }
private bool miniWidgetVisibility;
public bool MiniWidgetVisibility
{
get { return miniWidgetVisibility; }
set
{
if (miniWidgetVisibility != value)
{
miniWidgetVisibility = value;
OnPropertyChanged("MiniWidgetVisibility");
RequestSetMiniWidgetVisibility?.Invoke(value);
}
}
}
public event Action? RequestSetMiniWidgetVisibility;
public void SyncMiniWidgetVisibility(bool isVisible)
{
if (miniWidgetVisibility == isVisible)
return;
miniWidgetVisibility = isVisible;
OnPropertyChanged("MiniWidgetVisibility");
}
private bool _isUpdateAvailable;
public bool IsUpdateAvailable
{
get { return _isUpdateAvailable; }
set
{
_isUpdateAvailable = value;
OnPropertyChanged("IsUpdateAvailable");
}
}
private string _updateStatusMessage = string.Empty;
public string UpdateStatusMessage
{
get { return _updateStatusMessage; }
set
{
_updateStatusMessage = value;
OnPropertyChanged("UpdateStatusMessage");
}
}
private bool _isCheckingForUpdates;
public bool IsCheckingForUpdates
{
get { return _isCheckingForUpdates; }
set
{
_isCheckingForUpdates = value;
OnPropertyChanged("IsCheckingForUpdates");
}
}
private string DownloadUrl;
private readonly IStartupRegistrationService startupRegistrationService;
private ConfirmationDialogVM? cdvm;
private MiniWidgetVM? mwvm;
public SettingsVM(MiniWidgetVM mw_ref, ConfirmationDialogVM cdvm_ref)
: this(mw_ref, cdvm_ref, new WindowsStartupRegistrationService())
{
}
public SettingsVM(MiniWidgetVM mw_ref, ConfirmationDialogVM cdvm_ref, IStartupRegistrationService startupRegistrationService)
{
this.startupRegistrationService = startupRegistrationService;
mwvm = mw_ref;
cdvm = cdvm_ref;
cdvm.BtnCommand = new BaseCommand(ResetDataYesOrNo, true);
cdvm.DialogMessage = "Warning!!! This will delete all saved profiles.\nDo you still want to continue?";
//start with windows setting
UnlockOptionStartWin = true;
SetStartWithWin = SettingsManager.Current.StartWithWin;
MinimizeOnStart = SettingsManager.Current.MinimizeOnStart;
DarkMode = SettingsManager.Current.DarkMode;
MiniWidgetTransparentSlider = SettingsManager.Current.MiniWidgetTransparentSlider;
MiniWidgetVisibility = SettingsManager.Current.MiniWidgetVisibility;
if (SetStartWithWin)
UnlockMinimizeOnStart = false;
else
UnlockMinimizeOnStart = true;
NetworkTrafficType = SettingsManager.Current.NetworkType;
NetworkSpeedFormat = SettingsManager.Current.NetworkSpeedFormat;
NetworkSpeedMagnitude = SettingsManager.Current.NetworkSpeedMagnitude;
ResetBtn = new BaseCommand(ResetData, true);
UpdateCheckBtn = new BaseCommand(UpdateCheck, true);
DownloadUpdateBtn = new BaseCommand(DownloadUpdate, true);
DownloadUrl = string.Empty;
IsUpdateAvailable = false;
UpdateStatusMessage = "Click here to check for new updates";
IsCheckingForUpdates = false;
DeleteAllFiles = false;
}
private void SetMiniWidgetBackgroundColor(bool darkMode, int transparency)
{
// 00XXXXXX -> 0%
// FFXXXXXX -> 100%
// 00 -> 0 -> 0%, fully transparent
// FF -> (2^8)-1 (256-1) -> 100%, fully opaque
// example, 77XXXXXX -> (64+32+16+4+2+1) = 119
// (119/255) * 100% = 46.67% opaqueness
mwvm!.BackgroundColor = darkMode ? "#" + (((100 - transparency) * 255) / 100).ToString("x2") + "252525" : "#" + (((100 - transparency) * 255) / 100).ToString("x2") + "f1f1f1"; ;
}
private void ResetData(object? obj)
{
if(cdvm != null)
cdvm.IsVisible = UiVisibility.Visible;
}
private async void UpdateCheck(object? obj)
{
IsCheckingForUpdates = true;
UpdateStatusMessage = "Checking for updates...";
string tempMsgStatus = string.Empty;
IsUpdateAvailable = false;
const int minDisplayTimeMs = 2000; // 2 seconds, this is to show a progress bar for the update check, for better ux
var stopwatch = Stopwatch.StartNew();
try
{
(Version? latestVersion, string? downloadUrl) = await UpdateChecker.CheckForUpdates();
if (latestVersion != null && downloadUrl != null)
{
Version? currentVersion = Assembly.GetExecutingAssembly()?.GetName()?.Version;
Debug.WriteLine($"download url: {downloadUrl}, current version: {currentVersion}, latest version: {latestVersion}");
if (currentVersion != null && latestVersion > currentVersion)
{
DownloadUrl = downloadUrl;
tempMsgStatus = $"A new version {latestVersion} is available!";
IsUpdateAvailable = true;
}
else
{
tempMsgStatus = "You have the latest version.";
}
}
else
{
tempMsgStatus = "Error checking for updates.";
}
}
catch (Exception ex)
{
tempMsgStatus = "Error checking for updates.";
EventLogger.Error("Error checking for updates", ex);
}
finally
{
stopwatch.Stop();
int elapsedMs = (int)stopwatch.ElapsedMilliseconds;
int remainingTime = minDisplayTimeMs - elapsedMs;
if (remainingTime > 0)
{
await Task.Delay(remainingTime);
}
IsCheckingForUpdates = false;
UpdateStatusMessage = tempMsgStatus;
}
}
private void DownloadUpdate(object? obj)
{
try
{
var psi = new ProcessStartInfo
{
FileName = DownloadUrl,
UseShellExecute = true
};
Process.Start(psi);
}
catch (Exception ex)
{
EventLogger.Error("Error launching update download URL", ex);
}
}
private void ResetDataYesOrNo(object? obj)
{
if(obj != null)
{
if ((string)obj == "Yes")
DeleteAllFiles = true;
if (cdvm != null)
cdvm.IsVisible = UiVisibility.Hidden;
}
}
//------property changers---------------//
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string propName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/AboutWindow.xaml
================================================
This product is created and maintained by Ashfaaq Riphque.
If you find any problems with the product or any
ideas for improvements, feel free create an issue at,
https://github.com/Ashfaaq18/OpenNetMeter
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/AboutWindow.xaml.cs
================================================
using System.Diagnostics;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Navigation;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for AboutWindow.xaml
///
public partial class AboutWindow : Window
{
private Rect parentWindowRect;
public AboutWindow(Rect parentWindowRect_param)
{
this.Resources.Add("AppVersion", "Version: " + Assembly.GetExecutingAssembly()?.GetName().Version);
InitializeComponent();
parentWindowRect = parentWindowRect_param;
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
this.DragMove();
}
public void CloseWin()
{
Close();
}
private void Exit_Button_Click(object sender, RoutedEventArgs e)
{
this.Visibility = Visibility.Collapsed;
}
private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
ProcessStartInfo psi = new ProcessStartInfo(e.Uri.AbsoluteUri);
psi.UseShellExecute = true;
Process.Start(psi);
e.Handled = true;
}
public void SetParentWindowRect(Rect parentWindowRect_param)
{
parentWindowRect = parentWindowRect_param;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.Left = parentWindowRect.Left + (parentWindowRect.Width / 2) - this.ActualWidth / 2;
this.Top = parentWindowRect.Top + (parentWindowRect.Height / 2) - this.ActualHeight / 2;
}
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (this.Visibility == Visibility.Visible)
{
this.Left = parentWindowRect.Left + (parentWindowRect.Width / 2) - this.ActualWidth / 2;
this.Top = parentWindowRect.Top + (parentWindowRect.Height / 2) - this.ActualHeight / 2;
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ConfirmationDialog.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ConfirmationDialog.xaml.cs
================================================
using OpenNetMeter.ViewModels;
using System.Windows;
using System.Windows.Input;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for ConfirmationDialog.xaml
///
public partial class ConfirmationDialog : Window
{
private Rect parentWindowRect;
public ConfirmationDialog(Rect parentWindowRect_param)
{
InitializeComponent();
DataContext = new ConfirmationDialogVM();
parentWindowRect = parentWindowRect_param;
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
this.DragMove();
}
public void SetParentWindowRect(Rect parentWindowRect_param)
{
parentWindowRect = parentWindowRect_param;
}
private void Exit_Button_Click(object sender, RoutedEventArgs e)
{
this.Visibility = Visibility.Hidden;
}
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if(this.Visibility == Visibility.Visible)
{
this.Left = parentWindowRect.Left + (parentWindowRect.Width / 2) - this.ActualWidth / 2;
this.Top = parentWindowRect.Top + (parentWindowRect.Height / 2) - this.ActualHeight / 2;
}
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.Left = parentWindowRect.Left + (parentWindowRect.Width / 2) - this.ActualWidth / 2;
this.Top = parentWindowRect.Top + (parentWindowRect.Height / 2) - this.ActualHeight / 2;
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/Converters/BitmapToImageConverter.cs
================================================
using System;
using System.Globalization;
using System.IO;
using System.Windows.Data;
using System.Windows.Media.Imaging;
namespace OpenNetMeter.Views
{
class BitmapToImageConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
MemoryStream ms = new MemoryStream();
((System.Drawing.Bitmap)value).Save(ms, System.Drawing.Imaging.ImageFormat.Png);
BitmapImage image = new BitmapImage();
image.BeginInit();
ms.Seek(0, SeekOrigin.Begin);
image.StreamSource = ms;
image.EndInit();
return image;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/Converters/NetSpeedFormatConverter.cs
================================================
using OpenNetMeter.Properties;
using OpenNetMeter.Utilities;
using System;
using System.Globalization;
using System.Windows.Data;
namespace OpenNetMeter.Views
{
public class NetSpeedFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool useBytes = SettingsManager.Current.NetworkSpeedFormat != 0;
SpeedMagnitude magnitude = DataSizeSuffix.NormalizeMagnitude(SettingsManager.Current.NetworkSpeedMagnitude);
return DataSizeSuffix.InStr((long)value, 1, useBytes, magnitude);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/Converters/RadioBoolToIntConverter.cs
================================================
using System;
using System.Globalization;
using System.Windows.Data;
namespace OpenNetMeter.Views
{
public class RadioBoolToIntConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int integer = (int)value;
if (parameter != null)
{
if (integer == int.Parse(parameter.ToString()!))
return true;
else
return false;
}
else
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return parameter;
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/Converters/UiVisibilityToWpfVisibilityConverter.cs
================================================
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using OpenNetMeter.PlatformAbstractions;
namespace OpenNetMeter.Views
{
public class UiVisibilityToWpfVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not UiVisibility uiVisibility)
return Visibility.Hidden;
return uiVisibility switch
{
UiVisibility.Visible => Visibility.Visible,
UiVisibility.Collapsed => Visibility.Collapsed,
_ => Visibility.Hidden,
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is not Visibility visibility)
return UiVisibility.Hidden;
return visibility switch
{
Visibility.Visible => UiVisibility.Visible,
Visibility.Collapsed => UiVisibility.Collapsed,
_ => UiVisibility.Hidden,
};
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/Converters/UnitConverterBytes.cs
================================================
using OpenNetMeter.Utilities;
using System;
using System.Globalization;
using System.Windows.Data;
namespace OpenNetMeter.Views
{
public class UnitConverterBytes : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return DataSizeSuffix.InStr((long)value, 1, true);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/CustomSystemTray.cs
================================================
using System.Drawing;
using System.Windows.Forms;
using OpenNetMeter.Properties;
namespace OpenNetMeter.Views
{
public class MyColorTable : ProfessionalColorTable
{
public override Color MenuItemBorder
{
get
{
if (SettingsManager.Current.DarkMode)
return Color.FromArgb(32, 32, 32);
else
return Color.FromArgb(240, 240, 240);
}
}
public override Color ButtonSelectedHighlight
{
get
{
if (SettingsManager.Current.DarkMode)
return Color.FromArgb(64, 64, 64);
else
return Color.FromArgb(220, 220, 220);
}
}
public override Color ToolStripDropDownBackground
{
get
{
if (SettingsManager.Current.DarkMode)
return Color.FromArgb(32, 32, 32);
else
return Color.FromArgb(240, 240, 240);
}
}
public override Color ImageMarginGradientBegin
{
get
{
if (SettingsManager.Current.DarkMode)
return Color.FromArgb(32, 32, 32);
else
return Color.FromArgb(240, 240, 240);
}
}
public override Color ImageMarginGradientMiddle
{
get
{
if (SettingsManager.Current.DarkMode)
return Color.FromArgb(32, 32, 32);
else
return Color.FromArgb(240, 240, 240);
}
}
public override Color ImageMarginGradientEnd
{
get
{
if (SettingsManager.Current.DarkMode)
return Color.FromArgb(32, 32, 32);
else
return Color.FromArgb(240, 240, 240);
}
}
}
public class CustomSystemTray : ToolStripProfessionalRenderer
{
public CustomSystemTray() : base(new MyColorTable()) { }
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindow.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindow.xaml.cs
================================================
using System.Windows;
using OpenNetMeter.ViewModels;
using System.Windows.Input;
using Forms = System.Windows.Forms;
using System;
using System.Threading.Tasks;
using System.Threading;
using System.Drawing;
using OpenNetMeter.Models;
using System.Windows.Threading;
using System.Diagnostics;
using System.Windows.Controls;
using System.ComponentModel;
using OpenNetMeter.Properties;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for MainWindow.xaml
///
public partial class MainWindow : Window
{
private Mutex? mutex;
public bool IsSingleInstance()
{
bool createdNew;
mutex = new Mutex(true, "{6C4919CA-062E-47E3-85CC-2393D00CBA4A}", out createdNew);
if (!createdNew)
{
//exit app
MessageBox.Show("An instance is already running,\nCheck if it's minimized to the system tray", "OpenNetMeter", MessageBoxButton.OK);
Application.Current.Shutdown();
return false;
}
else
return true;
}
//--------- windows ------------//
private ConfirmationDialog? confDialog;
private AboutWindow? aboutWin;
private MiniWidgetV? miniWidget;
private MainWindowVM? mainWin;
//--------- timers ------------//
private DispatcherTimer resizeTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 0, 200), IsEnabled = false };
private DispatcherTimer relocationTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 0, 200), IsEnabled = false };
//--------- tray icon -----------//
private Forms.NotifyIcon? trayIcon;
private bool balloonShow;
public MainWindow()
{
if (IsSingleInstance())
{
InitializeComponent();
confDialog = new ConfirmationDialog(new System.Windows.Rect(this.Left, this.Top, this.ActualWidth, this.ActualHeight));
aboutWin = new AboutWindow(new System.Windows.Rect(this.Left, this.Top, this.ActualWidth, this.ActualHeight));
miniWidget = new MiniWidgetV(this);
mainWin = new MainWindowVM((MiniWidgetVM)miniWidget.DataContext, (ConfirmationDialogVM)confDialog.DataContext);
DataContext = mainWin;
mainWin.svm.RequestSetMiniWidgetVisibility += SetMiniWidgetVisibility;
//initialize window position and size
AllWinPosAndSizeInit();
//initialize system tray
trayIcon = new Forms.NotifyIcon();
Forms.ContextMenuStrip cm = new Forms.ContextMenuStrip();
balloonShow = false;
trayIcon.Icon = Properties.Resources.AppIcon;
trayIcon.Visible = true;
trayIcon.DoubleClick += Ni_DoubleClick;
trayIcon.MouseClick += Ni_MouseClick;
cm.Items.Add("Reset all window positions", null, ResetWinPos_Click);
cm.Items.Add("Show Mini Widget", null, MiniWidget_Show_Click);
cm.Items.Add(new Forms.ToolStripSeparator());
cm.Items.Add("Open", null, Cm_Open_Click);
cm.Items.Add("Exit", null, Cm_Exit_Click);
trayIcon.ContextMenuStrip = cm;
//------- events ----------//
this.Closing += MainWindow_Closing;
this.Loaded += MainWindow_Loaded;
}
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
if(confDialog != null)
confDialog.Owner = this;
if(aboutWin != null)
aboutWin.Owner = this;
}
private void ResetWinPos_Click(object? sender, EventArgs e)
{
this.Left = SystemParameters.PrimaryScreenWidth/2 - this.Width / 2;
this.Top = SystemParameters.PrimaryScreenHeight/2 - this.Height / 2;
SaveWinPos((int)this.Left, (int)this.Top);
if(miniWidget != null)
{
miniWidget.Left = this.Left + this.Width / 2 - miniWidget.Width / 2;
miniWidget.Top = this.Top + this.Height / 2 - miniWidget.Height / 2;
miniWidget.SaveWinPos((int)miniWidget.Left, (int)miniWidget.Top);
}
}
private void MiniWidget_Show_Click(object? sender, EventArgs e)
{
ShowMiniWidget();
}
public void ShowMiniWidget()
{
if (miniWidget == null)
return;
miniWidget.ShowMiniWidget();
mainWin?.svm.SyncMiniWidgetVisibility(true);
}
public void HideMiniWidget()
{
if (miniWidget == null)
return;
miniWidget.HideMiniWidget();
mainWin?.svm.SyncMiniWidgetVisibility(false);
}
private void SetMiniWidgetVisibility(bool isVisible)
{
if (isVisible)
ShowMiniWidget();
else
HideMiniWidget();
}
// this is for when the user clicks the window exit button through the alt+tab program switcher
private void MainWindow_Closing(object? sender, CancelEventArgs e)
{
e.Cancel = true;
this.Visibility = Visibility.Collapsed;
}
private void Ni_MouseClick(object? sender, Forms.MouseEventArgs e)
{
switch (e.Button)
{
case Forms.MouseButtons.Right:
if(trayIcon != null && trayIcon.ContextMenuStrip != null)
{
if (SettingsManager.Current.DarkMode)
trayIcon.ContextMenuStrip.ForeColor = Color.White;
else
trayIcon.ContextMenuStrip.ForeColor = Color.Black;
trayIcon.ContextMenuStrip.Renderer = new CustomSystemTray();
}
break;
}
}
private void AllWinPosAndSizeInit()
{
if (SettingsManager.Current.LaunchFirstTime)
{
SettingsManager.Current.WinSize = new System.Drawing.Size((int)this.MinWidth, (int)this.MinHeight);
this.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
SettingsManager.Current.WinPos = new System.Drawing.Point((int)this.Left, (int)this.Top);
if(miniWidget != null)
{
miniWidget.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
SettingsManager.Current.MiniWidgetPos = new System.Drawing.Point((int)miniWidget.Left, (int)miniWidget.Top);
}
SettingsManager.Current.LaunchFirstTime = false;
SettingsManager.Save();
}
this.Left = SettingsManager.Current.WinPos.X;
this.Top = SettingsManager.Current.WinPos.Y;
this.Width = SettingsManager.Current.WinSize.Width;
this.Height = SettingsManager.Current.WinSize.Height;
if(miniWidget!= null)
{
miniWidget.Left = SettingsManager.Current.MiniWidgetPos.X;
miniWidget.Top = SettingsManager.Current.MiniWidgetPos.Y;
}
//check if window is out of bounds. This is for, when the user last opened the app in the 2nd monitor and then reopens it with a 1 monitor setup.
bool isInScreen = false;
for (int i = 0; i < System.Windows.Forms.Screen.AllScreens.Length; i++)
{
//extra margin to repoisition the app when its outside the screen and only its borders are intersecting the edge.
int margin = 32;
Rectangle rectA = System.Windows.Forms.Screen.AllScreens[i].WorkingArea;
if (rectA.Left < (this.Left + this.Width - margin) && (rectA.Left + rectA.Width) > this.Left + margin &&
rectA.Top < (this.Top + this.Height - margin) && (rectA.Top + rectA.Height) > this.Top + margin)
{
isInScreen = true;
}
}
//if main window is out of bounds, center it
if (!isInScreen)
{
this.WindowStartupLocation = System.Windows.WindowStartupLocation.CenterScreen;
SettingsManager.Current.WinPos = new System.Drawing.Point((int)this.Left, (int)this.Top);
SettingsManager.Save();
}
resizeTimer.Tick += ResizeTimer_Tick;
relocationTimer.Tick += RelocationTimer_Tick;
}
private void Cm_Open_Click(object? sender, EventArgs e)
{
this.Visibility = Visibility.Visible;
this.Activate();
}
private void Cm_Exit_Click(object? sender, EventArgs e)
{
confDialog?.Close();
miniWidget?.Close();
aboutWin?.Close();
mainWin?.Dispose();
this.Closing -= MainWindow_Closing;
this.Close();
if(trayIcon != null)
trayIcon.Visible = false;
}
private void Ni_DoubleClick(object? sender, EventArgs e)
{
this.Visibility = Visibility.Visible;
this.Activate();
}
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
if (WindowState == WindowState.Maximized)
{
double fullScreenWidth = this.RenderSize.Width;
double fullScreenHeight = this.RenderSize.Height;
WindowState = WindowState.Normal;
this.Left = e.GetPosition(this).X - (this.RenderSize.Width / fullScreenWidth) * e.GetPosition(this).X;
this.Top = e.GetPosition(this).Y - (this.RenderSize.Height / fullScreenHeight) * e.GetPosition(this).Y;
}
DragMove();
}
private void Minimize_Button_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
public void Exit_Button_Click(object? sender, RoutedEventArgs? e)
{
if (!balloonShow && trayIcon != null)
{
trayIcon.ShowBalloonTip(1000, "", "Minimized to system tray", Forms.ToolTipIcon.None);
balloonShow = true;
}
if(aboutWin != null)
aboutWin.Visibility = Visibility.Collapsed;
if(confDialog != null)
confDialog.Visibility = Visibility.Collapsed;
this.Visibility = Visibility.Collapsed;
}
private void About_Button_Click(object sender, RoutedEventArgs e)
{
if(aboutWin != null)
aboutWin.Visibility = Visibility.Visible;
}
//save window size and position at the end of the respective events
private void ResizeTimer_Tick(object? sender, EventArgs e)
{
resizeTimer.IsEnabled = false;
//Do end of resize processing
SettingsManager.Current.WinSize = new System.Drawing.Size((int)this.Width, (int)this.Height);
SettingsManager.Save();
//pass parent window dimensions to confirmation dialog
confDialog?.SetParentWindowRect(new System.Windows.Rect(this.Left, this.Top, this.ActualWidth, this.ActualHeight));
aboutWin?.SetParentWindowRect(new System.Windows.Rect(this.Left, this.Top, this.ActualWidth, this.ActualHeight));
}
private void MyWindow_SizeChanged(object? sender, SizeChangedEventArgs e)
{
resizeTimer.IsEnabled = true;
resizeTimer.Stop();
resizeTimer.Start();
}
private void RelocationTimer_Tick(object? sender, EventArgs e)
{
relocationTimer.IsEnabled = false;
//Do end of relocation processing
SaveWinPos((int)this.Left, (int)this.Top);
}
private void SaveWinPos(int x, int y)
{
SettingsManager.Current.WinPos = new System.Drawing.Point(x, y);
SettingsManager.Save();
//pass parent window dimensions to confirmation and about dialog
confDialog?.SetParentWindowRect(new System.Windows.Rect(this.Left, this.Top, this.ActualWidth, this.ActualHeight));
aboutWin?.SetParentWindowRect(new System.Windows.Rect(this.Left, this.Top, this.ActualWidth, this.ActualHeight));
}
private void MyWindow_LocationChanged(object sender, EventArgs e)
{
relocationTimer.IsEnabled = true;
relocationTimer.Stop();
relocationTimer.Start();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindowTabs/DataUsageHistoryV.xaml
================================================
Usage of
From:
To:
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindowTabs/DataUsageHistoryV.xaml.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for DataUsageHistoryV.xaml
///
public partial class DataUsageHistoryV : UserControl
{
public DataUsageHistoryV()
{
InitializeComponent();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindowTabs/DataUsageSummaryV.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindowTabs/DataUsageSummaryV.xaml.cs
================================================
using System.Diagnostics;
using System.Windows.Media;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Shapes;
using OpenNetMeter.ViewModels;
using System.Windows;
using System.Collections.Generic;
using System;
using System.Windows.Threading;
using System.Threading;
using OpenNetMeter.Models;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for DataUsageSummaryV.xaml
///
public partial class DataUsageSummaryV : UserControl
{
private DispatcherTimer resizeTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 0, 200), IsEnabled = false };
private DataUsageSummaryVM? dusvm;
//private Rectangle GridBorder;
private Size maxYtextSize;
public DataUsageSummaryV()
{
InitializeComponent();
Loaded += delegate
{
dusvm = (DataUsageSummaryVM)this.DataContext;
maxYtextSize = ShapeMeasure(new TextBlock { Text = "0512Mb", FontSize = 11, Padding = new Thickness(0) });
maxYtextSize.Width += 2.0;
dusvm.Graph.Xstart = maxYtextSize.Width;
double GraphHeight = GraphSize.Height;
double GraphWidth = GraphSize.Width;
dusvm.Graph.GraphWidth = GraphWidth;
dusvm.Graph.GraphHeight = GraphHeight;
resizeTimer.Tick += ResizeTimer_Tick;
Graph_SizeChanged(null,null);
};
}
private void ResizeTimer_Tick(object? sender, EventArgs e)
{
resizeTimer.IsEnabled = false;
//Do end of resize processing
if(dusvm != null)
dusvm.Graph.resumeDraw = true;
}
private Size GraphSize => new Size(
Math.Max(0, GraphGrid.ActualWidth - 16 - maxYtextSize.Width),
Math.Max(0, GraphGrid.ActualHeight - maxYtextSize.Height));
public Size ShapeMeasure(TextBlock tb)
{
// Measured Size is bounded to be less than maxSize
Size maxSize = new Size(
double.PositiveInfinity,
double.PositiveInfinity);
tb.Measure(maxSize);
return tb.DesiredSize;
}
//scale the MyGraph coordinates
private void Graph_SizeChanged(object? sender, System.Windows.SizeChangedEventArgs? e)
{
if (dusvm != null)
{
resizeTimer.IsEnabled = true;
resizeTimer.Stop();
resizeTimer.Start();
//Stop drawing MyGraph
dusvm.Graph.resumeDraw = false;
double GraphHeight = GraphSize.Height;
double GraphWidth = GraphSize.Width;
dusvm.Graph.GraphWidth = GraphWidth;
dusvm.Graph.GraphHeight = GraphHeight;
for (int i = 0; i < dusvm.Graph.DownloadLines.Count; i++) //scale the download line
{
dusvm.Graph.DownloadLines[i].From = new Point(dusvm.Graph.Xstart + dusvm.Graph.DownloadPoints[i].From.X * (GraphWidth / dusvm.Graph.XaxisResolution), dusvm.Graph.ConvToGraphCoords(dusvm.Graph.DownloadPoints[i].From.Y, GraphHeight));
dusvm.Graph.DownloadLines[i].To = new Point(dusvm.Graph.Xstart + dusvm.Graph.DownloadPoints[i].To.X * (GraphWidth / dusvm.Graph.XaxisResolution), dusvm.Graph.ConvToGraphCoords(dusvm.Graph.DownloadPoints[i].To.Y, GraphHeight));
}
for (int i = 0; i < dusvm.Graph.UploadLines.Count; i++) //scale the upload line
{
dusvm.Graph.UploadLines[i].From = new Point(dusvm.Graph.Xstart + dusvm.Graph.UploadPoints[i].From.X * (GraphWidth / dusvm.Graph.XaxisResolution), dusvm.Graph.ConvToGraphCoords(dusvm.Graph.UploadPoints[i].From.Y, GraphHeight));
dusvm.Graph.UploadLines[i].To = new Point(dusvm.Graph.Xstart + dusvm.Graph.UploadPoints[i].To.X * (GraphWidth / dusvm.Graph.XaxisResolution), dusvm.Graph.ConvToGraphCoords(dusvm.Graph.UploadPoints[i].To.Y, GraphHeight));
}
//scale the X lines
for (int i = 0; i < dusvm.Graph.GridXCount; i++)
{
Canvas.SetTop(dusvm.Graph.Ylabels[i], ((GraphHeight / (dusvm.Graph.GridXCount - 1)) * (dusvm.Graph.GridXCount - 1 - i)) - maxYtextSize.Height / 2.0);
Size textSize = ShapeMeasure(dusvm.Graph.Ylabels[i]);
Canvas.SetLeft(dusvm.Graph.Ylabels[i], maxYtextSize.Width - textSize.Width - 4.0);
if (i > 0 && i < dusvm.Graph.GridXCount - 1)
{
dusvm.Graph.XLines[i - 1].From = new Point(maxYtextSize.Width, (GraphHeight / (dusvm.Graph.GridXCount - 1)) * i);
dusvm.Graph.XLines[i - 1].To = new Point(GraphWidth + maxYtextSize.Width, (GraphHeight / (dusvm.Graph.GridXCount - 1)) * i);
}
}
//scale the Y lines
for (int i = 0; i < dusvm.Graph.GridYCount; i++)
{
if (i < dusvm.Graph.GridYCount - 1)
{
Canvas.SetTop(dusvm.Graph.Xlabels[i], GraphHeight);
Canvas.SetLeft(dusvm.Graph.Xlabels[i], (GraphWidth / (dusvm.Graph.GridYCount - 1)) * i + maxYtextSize.Width - dusvm.Graph.Xlabels[i].ActualWidth / 2.0);
}
else
{
Canvas.SetTop(dusvm.Graph.Xlabels[i], GraphHeight);
Canvas.SetLeft(dusvm.Graph.Xlabels[i], GraphWidth + maxYtextSize.Width / 4);
}
if (i > 0 && i < dusvm.Graph.GridYCount - 1)
{
dusvm.Graph.YLines[i - 1].From = new Point((GraphWidth / (dusvm.Graph.GridYCount - 1)) * i + maxYtextSize.Width, 0);
dusvm.Graph.YLines[i - 1].To = new Point((GraphWidth / (dusvm.Graph.GridYCount - 1)) * i + maxYtextSize.Width, GraphHeight);
}
}
//scale the border
//Y lines
dusvm.Graph.Borders[0].From = new Point(maxYtextSize.Width, 0);
dusvm.Graph.Borders[0].To = new Point(maxYtextSize.Width, GraphHeight);
dusvm.Graph.Borders[1].From = new Point((GraphWidth / (dusvm.Graph.GridYCount - 1)) * (dusvm.Graph.GridYCount - 1) + maxYtextSize.Width, 0);
dusvm.Graph.Borders[1].To = new Point((GraphWidth / (dusvm.Graph.GridYCount - 1)) * (dusvm.Graph.GridYCount - 1) + maxYtextSize.Width, GraphHeight);
//X lines
dusvm.Graph.Borders[2].From = new Point(maxYtextSize.Width, 0);
dusvm.Graph.Borders[2].To = new Point(GraphWidth + maxYtextSize.Width, 0);
dusvm.Graph.Borders[3].From = new Point(maxYtextSize.Width, GraphHeight);
dusvm.Graph.Borders[3].To = new Point(GraphWidth + maxYtextSize.Width, GraphHeight);
dusvm.Graph.firstDrawAfterResume = true;
}
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindowTabs/SettingsV.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MainWindowTabs/SettingsV.xaml.cs
================================================
using System.Windows.Controls;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for SettingsV.xaml
///
public partial class SettingsV : UserControl
{
public SettingsV()
{
InitializeComponent();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MiniWidgetV.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/MiniWidgetV.xaml.cs
================================================
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
using OpenNetMeter.Models;
using OpenNetMeter.Properties;
using OpenNetMeter.ViewModels;
namespace OpenNetMeter.Views
{
///
/// Interaction logic for MiniWidgetV.xaml
///
public partial class MiniWidgetV : Window
{
private DispatcherTimer fixZorderTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 0, 200), IsEnabled = false };
private DispatcherTimer relocationTimer = new DispatcherTimer { Interval = new TimeSpan(0, 0, 0, 0, 200), IsEnabled = false };
private Window mainWindow;
public MiniWidgetV(Window mainWindow_ref)
{
InitializeComponent();
DataContext = new MiniWidgetVM();
fixZorderTimer.Tick += FixZorderTimer_Tick;
relocationTimer.Tick += RelocationTimer_Tick;
mainWindow = mainWindow_ref;
this.Visibility = Visibility.Visible;
fixZorderTimer.IsEnabled = true;
Loaded += MiniWidgetV_Loaded;
}
private void MiniWidgetV_Loaded(object sender, RoutedEventArgs e)
{
if (!SettingsManager.Current.MiniWidgetVisibility)
{
this.Visibility = Visibility.Hidden;
fixZorderTimer.IsEnabled = false;
}
}
private void FixZorderTimer_Tick(object? sender, EventArgs e)
{
const int HWND_TOPMOST = -1;
//const int HWND_NOTOPMOST = -2;
const string SHELLTRAY = "Shell_traywnd";
WindowInteropHelper thisWin = new WindowInteropHelper(this);
IntPtr shellTray = NativeMethods.GetWindowByClassName(IntPtr.Zero ,SHELLTRAY);
//Reassign owner when explorer.exe restarts
if (thisWin.Owner != shellTray)
{
Debug.WriteLine("set owner again");
thisWin.Owner = shellTray;
}
//check if window is behind the taskbar. If yes, bring it in front without activating it.
for (IntPtr h = thisWin.Handle; h != IntPtr.Zero; h = NativeMethods.GetWindow(h, (uint)NativeMethods.GW.HWNDPREV))
{
if (h == shellTray)
{
Debug.WriteLine("this window is behind Shell_TrayWnd");
NativeMethods.SetWindowPos(thisWin.Handle, (IntPtr)HWND_TOPMOST, 0, 0, 0, 0,
(int)NativeMethods.SWP.ASYNCWINDOWPOS |
(int)NativeMethods.SWP.NOACTIVATE |
(int)NativeMethods.SWP.NOMOVE |
(int)NativeMethods.SWP.NOSIZE);
//NativeMethods.SetWindowPos(thisWin.Handle, (IntPtr)HWND_NOTOPMOST, 0, 0, 0, 0, SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE);
break;
}
}
}
private void Window_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (DataContext is MiniWidgetVM vm && vm.IsPinned)
{
return;
}
this.DragMove();
}
private void MenuItem_Hide_Click(object sender, RoutedEventArgs e)
{
if (mainWindow is MainWindow mw) // hide from miniwidget
mw.HideMiniWidget();
else // hide from settings page
HideMiniWidget();
}
private void MenuItem_Open_Click(object sender, RoutedEventArgs e)
{
if (mainWindow != null)
{
mainWindow.Visibility = Visibility.Visible;
mainWindow.Activate();
}
}
///
/// Save window position when user moves window
///
///
///
private void RelocationTimer_Tick(object? sender, EventArgs e)
{
relocationTimer.IsEnabled = false;
//Do end of relocation processing
SaveWinPos((int)this.Left, (int)this.Top);
}
public void SaveWinPos(int x, int y)
{
SettingsManager.Current.MiniWidgetPos = new System.Drawing.Point(x, y);
SettingsManager.Save();
}
private void Window_LocationChanged(object sender, System.EventArgs e)
{
relocationTimer.IsEnabled = true;
relocationTimer.Stop();
relocationTimer.Start();
}
public void ShowMiniWidget()
{
this.Visibility = Visibility.Visible;
this.Activate();
fixZorderTimer.IsEnabled = true;
SettingsManager.Current.MiniWidgetVisibility = true;
SettingsManager.Save();
}
public void HideMiniWidget()
{
this.Visibility = Visibility.Hidden;
fixZorderTimer.IsEnabled = false;
SettingsManager.Current.MiniWidgetVisibility = false;
SettingsManager.Save();
}
}
}
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/CustomDatePicker.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.Colors.xaml
================================================
White
Black
Gray
LightGray
DarkGray
WhiteSmoke
#FFFFFF20
#F3F4F6
#1F2937
White
#6B7280
#4F5050
#a9abab
#4287f5
#97b9f0
#374151
#666666
#AAAAAA
#1a1a1a
#f0f0f0
White
#f1f1f1
#f8f8f8
#f0f0f0
#F9FAFB
#E5E7EB
#151515
#252525
#1f1f1f
#2a2a2a
#3a3a3a
#202020
#303030
#00A6A0
#F0FDFA
#E6FFFA
#E5E7EB
#ffcab5
#f7bda6
#c97c5d
#c99079
#c0e3da
#afe6d9
#9fd4c7
#7fb6a8
#367061
#2a5248
#458270
#a9d9cc
#b6e3d7
#6da395
#3b695c
#FFA37E
#EF4444
#EF8888
#EFAAAA
#E81123
#4F5050
#616161
#D1D5DB
#333333
#555555
#999999
#9CA3AF
#FF888888
#000000
#dddddd
#0d0d0d
#111827
LightSeaGreen
LightSalmon
#D98868
#6A22C55E
#6AF97316
#01000000
#30000000
#40000000
#5DD3CD
#20B2AA
#FFB899
#FFA07A
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.ComboBox.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.ContextMenu.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.DataGrid.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.MainWindowTabs.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.Styles.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/Theme.SummaryPage.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/Views/ResourceDictionaries/ThemeResources.xaml
================================================
================================================
FILE: OpenNetMeter.Old/OpenNetMeter/app.manifest
================================================
================================================
FILE: OpenNetMeter.PlatformAbstractions/IExternalLinkService.cs
================================================
namespace OpenNetMeter.PlatformAbstractions;
public interface IExternalLinkService
{
void Open(string uri);
}
================================================
FILE: OpenNetMeter.PlatformAbstractions/INetworkCaptureService.cs
================================================
using System;
namespace OpenNetMeter.PlatformAbstractions;
public interface INetworkCaptureService : IDisposable
{
event EventHandler? NetworkChanged;
event EventHandler? TrafficObserved;
void Start();
void Stop();
}
public sealed class NetworkSnapshotChangedEventArgs : EventArgs
{
public NetworkSnapshotChangedEventArgs(string adapterName, string adapterId)
{
AdapterName = adapterName;
AdapterId = adapterId;
}
public string AdapterName { get; }
public string AdapterId { get; }
}
public sealed class NetworkTrafficEventArgs : EventArgs
{
public NetworkTrafficEventArgs(string processName, long bytes, bool isReceive)
{
ProcessName = processName;
Bytes = bytes;
IsReceive = isReceive;
}
public string ProcessName { get; }
public long Bytes { get; }
public bool IsReceive { get; }
}
================================================
FILE: OpenNetMeter.PlatformAbstractions/IProcessIconService.cs
================================================
namespace OpenNetMeter.PlatformAbstractions;
public interface IProcessIconService
{
object? GetProcessIcon(string processName);
}
================================================
FILE: OpenNetMeter.PlatformAbstractions/IStartupRegistrationService.cs
================================================
namespace OpenNetMeter.PlatformAbstractions;
public interface IStartupRegistrationService
{
bool IsEnabled();
void SetEnabled(bool enabled, bool startMinimized);
}
================================================
FILE: OpenNetMeter.PlatformAbstractions/IUiDispatcher.cs
================================================
using System;
namespace OpenNetMeter.PlatformAbstractions;
public interface IUiDispatcher
{
bool CheckAccess();
void Post(Action action);
}
================================================
FILE: OpenNetMeter.PlatformAbstractions/IWindowService.cs
================================================
namespace OpenNetMeter.PlatformAbstractions;
public interface IWindowService
{
void MinimizeMainWindow();
void CloseMainWindow();
void ShowAbout();
}
================================================
FILE: OpenNetMeter.PlatformAbstractions/OpenNetMeter.PlatformAbstractions.csproj
================================================
net8.0
enable
================================================
FILE: OpenNetMeter.PlatformAbstractions/UiVisibility.cs
================================================
namespace OpenNetMeter.PlatformAbstractions;
public enum UiVisibility
{
Hidden,
Visible,
Collapsed,
}
================================================
FILE: OpenNetMeter.Tests/NetworkProcessTests.cs
================================================
using System;
using System.Net;
using OpenNetMeter.Models;
using OpenNetMeter.Properties;
namespace OpenNetMeter.Tests;
public class NetworkProcessTests
{
private static readonly byte[] LocalIPv4 = IPAddress.Parse("192.168.1.50").GetAddressBytes();
private static readonly byte[] EmptyIPv6 = new byte[16];
[Fact]
public void RecvProcess_AccumulatesDownloadPerProcess()
{
using var networkType = new NetworkTypeScope(2);
using var netProc = CreateNetworkProcess();
netProc.TestInvokeRecvProcess(IPAddress.Parse("93.184.216.34"), IPAddress.Parse("192.168.1.50"), 400, "chrome");
netProc.TestInvokeRecvProcess(IPAddress.Parse("93.184.216.34"), IPAddress.Parse("192.168.1.50"), 200, "chrome");
Assert.Equal(600, netProc.CurrentSessionDownloadData);
Assert.True(netProc.MyProcesses!.TryGetValue("chrome", out var process));
Assert.NotNull(process);
Assert.Equal(600, process!.CurrentDataRecv);
Assert.Equal(0, process.CurrentDataSend);
}
[Fact]
public void SendProcess_TracksUploadWhileBuffering()
{
using var networkType = new NetworkTypeScope(2);
using var netProc = CreateNetworkProcess();
netProc.IsBufferTime = true;
netProc.TestInvokeSendProcess(IPAddress.Parse("192.168.1.50"), IPAddress.Parse("203.0.113.10"), 1024, "");
Assert.Equal(1024, netProc.CurrentSessionUploadData);
Assert.Empty(netProc.MyProcesses!);
Assert.True(netProc.MyProcessesBuffer!.TryGetValue("System", out var process));
Assert.NotNull(process);
Assert.Equal(1024, process!.CurrentDataSend);
Assert.Equal(0, process.CurrentDataRecv);
}
[Fact]
public void RecvProcess_PublicOnlyDropsPrivatePeer()
{
using var networkType = new NetworkTypeScope(1);
using var netProc = CreateNetworkProcess();
netProc.TestInvokeRecvProcess(IPAddress.Parse("192.168.0.10"), IPAddress.Parse("192.168.1.50"), 500, "lan-app");
Assert.Equal(0, netProc.CurrentSessionDownloadData);
Assert.Empty(netProc.MyProcesses!);
}
[Fact]
public void RecvProcess_PrivateOnlyAcceptsPrivateTraffic()
{
using var networkType = new NetworkTypeScope(0);
using var netProc = CreateNetworkProcess();
netProc.TestInvokeRecvProcess(IPAddress.Parse("10.0.0.5"), IPAddress.Parse("192.168.1.50"), 300, "lan");
netProc.TestInvokeRecvProcess(IPAddress.Parse("93.184.216.34"), IPAddress.Parse("192.168.1.50"), 200, "lan");
Assert.Equal(300, netProc.CurrentSessionDownloadData);
Assert.True(netProc.MyProcesses!.TryGetValue("lan", out var process));
Assert.NotNull(process);
Assert.Equal(300, process!.CurrentDataRecv);
}
private static NetworkProcess CreateNetworkProcess()
{
var proc = new NetworkProcess();
proc.TestSetLocalIPs(LocalIPv4, EmptyIPv6);
return proc;
}
private sealed class NetworkTypeScope : IDisposable
{
private readonly int originalNetworkType;
public NetworkTypeScope(int networkType)
{
originalNetworkType = SettingsManager.Current.NetworkType;
SettingsManager.Current.NetworkType = networkType;
}
public void Dispose()
{
SettingsManager.Current.NetworkType = originalNetworkType;
}
}
}
================================================
FILE: OpenNetMeter.Tests/OpenNetMeter.Tests.csproj
================================================
net8.0-windows10.0.19041.0
true
enable
enable
false
================================================
FILE: OpenNetMeter.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.2.32516.85
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenNetMeter.Old", "OpenNetMeter.Old\OpenNetMeter\OpenNetMeter.Old.csproj", "{711263C7-44E1-4694-8B30-E65FBD2ECBF2}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F081DC62-1772-4E91-BDCB-9D69FD1ED0A4}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
Directory.Build.props = Directory.Build.props
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
LICENSE = LICENSE
NOTICE = NOTICE
README.md = README.md
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DatabaseEngine", "OpenNetMeter.Old\DatabaseEngine\DatabaseEngine.csproj", "{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}"
EndProject
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "OpenNetMeter-Installer", "Installer\OpenNetMeter-Installer.wixproj", "{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}"
ProjectSection(ProjectDependencies) = postProject
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E} = {46E06019-21CF-47C9-A4F2-EF81BF7FA49E}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNetMeter.Tests", "OpenNetMeter.Tests\OpenNetMeter.Tests.csproj", "{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNetMeter.PlatformAbstractions", "OpenNetMeter.PlatformAbstractions\OpenNetMeter.PlatformAbstractions.csproj", "{840B95F6-EBF8-4637-9B85-44E14A25E96A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNetMeter.Core", "OpenNetMeter.Core\OpenNetMeter.Core.csproj", "{DC67BDB8-6F2B-4285-B787-4C438A925389}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenNetMeter", "OpenNetMeter\OpenNetMeter.csproj", "{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|ARM64.Build.0 = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|x64.ActiveCfg = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|x64.Build.0 = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|x86.ActiveCfg = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Debug|x86.Build.0 = Debug|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|Any CPU.Build.0 = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|ARM64.ActiveCfg = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|ARM64.Build.0 = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|x64.ActiveCfg = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|x64.Build.0 = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|x86.ActiveCfg = Release|Any CPU
{711263C7-44E1-4694-8B30-E65FBD2ECBF2}.Release|x86.Build.0 = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|ARM64.Build.0 = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|x64.ActiveCfg = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|x64.Build.0 = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|x86.ActiveCfg = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Debug|x86.Build.0 = Debug|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|Any CPU.Build.0 = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|ARM64.ActiveCfg = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|ARM64.Build.0 = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|x64.ActiveCfg = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|x64.Build.0 = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|x86.ActiveCfg = Release|Any CPU
{49CF6C08-3735-449D-AB1E-33BF6EDEAE4E}.Release|x86.Build.0 = Release|Any CPU
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|Any CPU.ActiveCfg = Debug|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|Any CPU.Build.0 = Debug|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|ARM64.ActiveCfg = Debug|ARM64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|ARM64.Build.0 = Debug|ARM64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|x64.ActiveCfg = Debug|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|x64.Build.0 = Debug|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|x86.ActiveCfg = Debug|x86
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Debug|x86.Build.0 = Debug|x86
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|Any CPU.ActiveCfg = Release|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|Any CPU.Build.0 = Release|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|ARM64.ActiveCfg = Release|ARM64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|ARM64.Build.0 = Release|ARM64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|x64.ActiveCfg = Release|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|x64.Build.0 = Release|x64
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|x86.ActiveCfg = Release|x86
{99E49CF2-A3E6-42D9-B41C-06E2CFDC77AA}.Release|x86.Build.0 = Release|x86
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|ARM64.Build.0 = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|x64.ActiveCfg = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|x64.Build.0 = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|x86.ActiveCfg = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Debug|x86.Build.0 = Debug|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|Any CPU.Build.0 = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|ARM64.ActiveCfg = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|ARM64.Build.0 = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|x64.ActiveCfg = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|x64.Build.0 = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|x86.ActiveCfg = Release|Any CPU
{88D1EC9C-FD79-49BA-A7D5-CC0AB271A85D}.Release|x86.Build.0 = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|ARM64.Build.0 = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|x64.ActiveCfg = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|x64.Build.0 = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|x86.ActiveCfg = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Debug|x86.Build.0 = Debug|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|Any CPU.Build.0 = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|ARM64.ActiveCfg = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|ARM64.Build.0 = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|x64.ActiveCfg = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|x64.Build.0 = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|x86.ActiveCfg = Release|Any CPU
{840B95F6-EBF8-4637-9B85-44E14A25E96A}.Release|x86.Build.0 = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|ARM64.Build.0 = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|x64.ActiveCfg = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|x64.Build.0 = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|x86.ActiveCfg = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Debug|x86.Build.0 = Debug|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|Any CPU.Build.0 = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|ARM64.ActiveCfg = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|ARM64.Build.0 = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|x64.ActiveCfg = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|x64.Build.0 = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|x86.ActiveCfg = Release|Any CPU
{DC67BDB8-6F2B-4285-B787-4C438A925389}.Release|x86.Build.0 = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|ARM64.Build.0 = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|x64.ActiveCfg = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|x64.Build.0 = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|x86.ActiveCfg = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Debug|x86.Build.0 = Debug|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|Any CPU.Build.0 = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|ARM64.ActiveCfg = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|ARM64.Build.0 = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|x64.ActiveCfg = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|x64.Build.0 = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|x86.ActiveCfg = Release|Any CPU
{46E06019-21CF-47C9-A4F2-EF81BF7FA49E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {09D1D226-B6A2-47FB-9C0B-6D9E7C6313A2}
EndGlobalSection
EndGlobal
================================================
FILE: README.md
================================================
# OpenNetMeter
A simple program to monitor your network/data usage. Made for the average windows user.
## Description
This program provides the following features,
- network speed.
- current connection's session data usage.
- Data usage for today.
- retrieve data usage up to the past 60 days and anywhere in between in detailed format.
- A mini widget to show the network speed (can be placed over the taskbar).
## Installation
1. Download and Install [.NET 8.0 Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0/runtime) desktop apps version for the latest release.
2. Download the latest release .msi from this repository and run it (no more .zip format).
3. This application requires admin privileges.
4. Optional : To add this as a startup program, go to the settings tab and tick the checkbox.
## Uninstallation
1. simply uninstall from the control panel.
2. for versions before, 0.12.1 and below, Simply delete the folder.
## Usage/Examples
### Summary tab
### History tab
### Darkmode
### Network speed mini widget

================================================
FILE: Resources/documentation/README.md
================================================
This directory is meant for any documentation for OpenNetMeter
================================================
FILE: scripts/build-avalonia-msi.ps1
================================================
param(
[ValidateSet("Release", "Debug")]
[string]$Configuration = "Release",
[ValidateSet("win-x64")]
[string]$Runtime = "win-x64"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
Set-Location $repoRoot
$propsPath = Join-Path $repoRoot "Directory.Build.props"
[xml]$propsXml = Get-Content $propsPath
$productVersion = $propsXml.Project.PropertyGroup.ProductVersion
$productName = $propsXml.Project.PropertyGroup.ProductName
if ([string]::IsNullOrWhiteSpace($productVersion)) {
throw "ProductVersion not found in $propsPath"
}
if ([string]::IsNullOrWhiteSpace($productName)) {
throw "ProductName not found in $propsPath"
}
Write-Host "Publishing Avalonia app..."
Write-Host "Cleaning Avalonia project..."
dotnet clean ".\OpenNetMeter\OpenNetMeter.csproj" --configuration $Configuration
Write-Host "Cleaning installer project..."
dotnet clean ".\Installer\OpenNetMeter-Installer.wixproj" --configuration $Configuration
$publishOutput = Join-Path $repoRoot "_rc\avalonia\$Runtime"
if (Test-Path $publishOutput) {
Write-Host "Removing previous publish output: $publishOutput"
Remove-Item $publishOutput -Recurse -Force
}
& ".\scripts\publish-avalonia-rc.ps1" -Runtime $Runtime -Configuration $Configuration
$publishedExe = Join-Path $repoRoot "_rc\avalonia\$Runtime\$productName.exe"
if (-not (Test-Path $publishedExe)) {
throw "Expected published exe not found: $publishedExe"
}
Write-Host "Verified publish output: $publishedExe"
Write-Host "Building WiX installer..."
dotnet build ".\Installer\OpenNetMeter-Installer.wixproj" --configuration $Configuration -m:1
$candidateMsiPaths = @(
(Join-Path $repoRoot "Installer\bin\$Configuration\en-us\$productName-$productVersion.msi"),
(Join-Path $repoRoot "Installer\bin\x64\$Configuration\en-us\$productName-$productVersion.msi")
)
$msiPath = $candidateMsiPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $msiPath) {
throw "MSI build completed but the expected MSI file was not found."
}
Write-Host "MSI ready: $msiPath"
================================================
FILE: scripts/publish-avalonia-rc.ps1
================================================
param(
[ValidateSet("win-x64", "win-arm64", "win-x86", "linux-x64")]
[string]$Runtime = "win-x64",
[ValidateSet("Debug", "Release")]
[string]$Configuration = "Release"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$project = Join-Path $repoRoot "OpenNetMeter\OpenNetMeter.csproj"
$output = Join-Path $repoRoot "_rc\avalonia\$Runtime\"
Write-Host "Publishing Avalonia RC..."
Write-Host "Project: $project"
Write-Host "Runtime: $Runtime"
Write-Host "Configuration: $Configuration"
Write-Host "Output: $output"
dotnet publish $project `
--configuration $Configuration `
--runtime $Runtime `
--self-contained true `
-p:PublishSingleFile=true `
-p:IncludeNativeLibrariesForSelfExtract=true `
--output $output
Write-Host "Done. RC files are in $output"