Skip to content

Commit 5982dc3

Browse files
authored
Merge pull request #262 from nyu-mlab/fix-gui
Fix Device API Thread and Labeling Packet Bug
2 parents 8560dde + b34d514 commit 5982dc3

File tree

7 files changed

+62
-30
lines changed

7 files changed

+62
-30
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
# IoT Inspector 3
22

3-
Simply run `./start.bash`. It will take care of all the dependencies.
3+
Simply run `./start.bash` for Linux/Mac and `start.bat` for Windows. It will take care of all the dependencies.
44

5-
If the underlying Inspector core library is updated, please run the following first:
5+
If the underlying dependencies is updated, please run the following first:
66

77
```bash
88
uv cache clean
9-
uv lock --upgrade-package libinspector
9+
uv lock
1010
uv sync
1111
```
1212

13+
# User guide
14+
Please review the [User Guide](https://github.com/nyu-mlab/iot-inspector-client/wiki) for instructions how to run IoT Inspector.
1315

1416
# Developer Guide
1517

1618
If you are developing IoT Inspector, please read this section.
1719

1820
## Database Schema
1921

20-
When presenting network stats, IoT Inspector reads from an internal SQLite database.
22+
When presenting network stats, IoT Inspector reads from an internal SQLite database.
23+
To see how the packet collector and database is implemented, look at the [IoT Inspector Core package](https://github.com/nyu-mlab/inspector-core-library).
2124

2225
You should always read from the database using the following approach:
2326

2427
```python
25-
import libinspector
28+
import libinspector.global_state
2629
db_conn, rwlock = libinspector.global_state.db_conn_and_lock
2730
with rwlock:
2831
db_conn.execute("SELECT * FROM devices")
@@ -38,15 +41,15 @@ CREATE TABLE devices (
3841
is_gateway INTEGER DEFAULT 0,
3942
updated_ts INTEGER DEFAULT 0,
4043
metadata_json TEXT DEFAULT '{}'
41-
)
44+
);
4245

4346
CREATE TABLE hostnames (
4447
ip_address TEXT PRIMARY KEY,
4548
hostname TEXT NOT NULL,
4649
updated_ts INTEGER DEFAULT 0,
4750
data_source TEXT NOT NULL,
4851
metadata_json TEXT DEFAULT '{}'
49-
)
52+
);
5053

5154
CREATE TABLE network_flows (
5255
timestamp INTEGER,
@@ -69,5 +72,5 @@ CREATE TABLE network_flows (
6972
src_port, dest_port,
7073
protocol
7174
)
72-
)
75+
);
7376
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "iot-inspector"
77
dynamic = ["version"]
88
description = "IoT Inspector Client for analyzing IoT device firmware"
99
readme = "README.md"
10-
requires-python = ">=3.13"
10+
requires-python = "==3.13.*"
1111
dependencies = [
1212
"libinspector==1.0.10",
1313
"streamlit>=1.50.0",

src/libinspector/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
import threading
44
import json
55
import functools
6-
import libinspector.global_state
7-
from libinspector.privacy import is_ad_tracked
86
import pandas as pd
97
from typing import Any
108
import streamlit as st
119
import logging
1210
import re
11+
import libinspector.global_state
12+
from libinspector.privacy import is_ad_tracked
1313

1414
config_file_name = 'config.json'
1515
config_lock = threading.Lock()

src/libinspector/device_detail_page.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,6 @@ def _send_packets_callback(mac_address: str):
9797
api_url = f"https://{remote_host}:{remote_port}{api_path}"
9898

9999
# NOTE: We avoid st.write here, save the info to session_state instead
100-
101100
response = requests.post(
102101
api_url,
103102
json={
@@ -117,15 +116,35 @@ def _send_packets_callback(mac_address: str):
117116
st.session_state['api_message'] = f"success| {len(pending_packet_list)} Labeled packets successfully sent to the server."
118117
else:
119118
st.session_state[
120-
'api_message'] = f"error|Failed to send labeled packets. Server status: {response.status_code}."
119+
'api_message'] = (f"error|Failed to send labeled packets. Server status: {response.status_code}. "
120+
f"{len(pending_packet_list)} Packets were not sent.")
121121
else:
122122
st.session_state['api_message'] = "error|No packets were captured for labeling."
123-
124123
except requests.RequestException as e:
125124
st.session_state['api_message'] = f"error|An error occurred during API transmission: {e}"
125+
finally:
126+
pending_packet_list.clear()
126127
# st.rerun() will occur after this, showing the results.
127128

128129

130+
def update_device_inspected_status(mac_address: str):
131+
"""
132+
Manually update to inspected status so that all the packets can be collected for the MAC Address.
133+
Args:
134+
mac_address (str): The MAC address of the device to update.
135+
"""
136+
db_conn, rwlock = libinspector.global_state.db_conn_and_lock
137+
with rwlock:
138+
sql = """
139+
UPDATE devices
140+
SET is_inspected = ?
141+
WHERE mac_address = ?
142+
"""
143+
db_conn.execute(sql, (1, mac_address))
144+
device_inspected_config_key = f'device_is_inspected_{mac_address}'
145+
common.config_set(device_inspected_config_key, True)
146+
147+
129148
def label_activity_workflow(mac_address: str):
130149
"""
131150
Manages the interactive, state-driven workflow for labeling network activity in Streamlit.
@@ -145,7 +164,7 @@ def label_activity_workflow(mac_address: str):
145164
5. Control Buttons: Displays 'Start' and 'Labeling Complete' buttons with dynamic 'disabled' states to enforce the correct sequence.
146165
6. Countdown: Executes a 5-second blocking countdown in the main thread after 'Start' is clicked and before packet collection begins.
147166
7. Packet Collection: Starts packet capture by setting a global callback function (save_labeled_activity_packets) for the specified mac_address.
148-
8. Status Display: Uses an st.empty() placeholder and the api_message state variable to display feedback immediately beneath the control buttons for superior UX.
167+
8. Status Display: Uses a st.empty() placeholder and the api_message state variable to display feedback immediately beneath the control buttons for superior UX.
149168
9. Final Summary: Upon session completion, displays the recorded activity label, device name, and duration.
150169
151170
Args:
@@ -185,6 +204,7 @@ def label_activity_workflow(mac_address: str):
185204
disabled=session_active or st.session_state['end_time'] is not None,
186205
help="Click to start labeling an activity for this device. This will reset any previous labeling state."
187206
):
207+
update_device_inspected_status(mac_address)
188208
reset_labeling_state()
189209
# Keep labeling_in_progress=True until the very end, controlled by config_get/set
190210
common.config_set('labeling_in_progress', True)

src/libinspector/device_list_page.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,34 +5,25 @@
55
import common
66
import logging
77
import functools
8-
import threading
98
import os
109
import requests
1110

12-
1311
logger = logging.getLogger("client")
1412

13+
1514
def show():
1615
"""
1716
Creating a page that shows what devices have been discovered so far
1817
"""
1918
toast_obj = st.toast('Discovering devices...')
2019
show_list(toast_obj)
2120

22-
@functools.lru_cache(maxsize=1)
23-
def start_inspector_once():
24-
"""Initialize the Inspector core only once."""
25-
with st.spinner("Starting API Thread..."):
26-
threading.Thread(
27-
target=worker_thread,
28-
daemon=True,
29-
)
30-
3121

3222
def worker_thread():
3323
"""
3424
A worker thread to periodically clear the cache of call_predict_api.
3525
"""
26+
logger.info("[Device ID API] Starting worker thread to periodically call the API for each device.")
3627
while True:
3728
time.sleep(15)
3829
db_conn, rwlock = libinspector.global_state.db_conn_and_lock
@@ -46,6 +37,7 @@ def worker_thread():
4637
with rwlock:
4738
for device_dict in db_conn.execute(sql):
4839
device_list.append(dict(device_dict))
40+
logger.info("[Device ID API] 15 seconds passed, will start calling API for each device if needed.")
4941

5042
# Getting inputs and calling API
5143
for device_dict in device_list:
@@ -54,6 +46,12 @@ def worker_thread():
5446
try:
5547
api_output = call_predict_api(dhcp_hostname, oui_vendor, remote_hostnames, device_dict['mac_address'])
5648
common.config_set(f'device_details@{device_dict["mac_address"]}', api_output)
49+
if "Vendor" in api_output:
50+
# Update name based on API
51+
custom_name_key = f"device_custom_name_{device_dict['mac_address']}"
52+
custom_name = api_output["Vendor"]
53+
if api_output["Vendor"] != "":
54+
common.config_set(custom_name_key, custom_name)
5755
except RuntimeError:
5856
continue
5957

@@ -111,8 +109,8 @@ def call_predict_api(dhcp_hostname: str, oui_vendor: str, remote_hostnames: str,
111109
raise RuntimeError(
112110
"Fewer than two string fields in data are non-empty; refusing to call API. Wait until IoT Inspector collects more data.")
113111

114-
# TODO: Used for debugging, risk is minimal since this is client-facing code. Should have some flag for production.
115-
logger.info("[Device ID API] Calling API with data: %s", json.dumps(data, indent=4))
112+
if common.config_get("debug", default=False):
113+
logger.info("[Device ID API] Calling API with data: %s", json.dumps(data, indent=4))
116114

117115
try:
118116
response = requests.post(url, headers=headers, json=data, timeout=10)

src/libinspector/page_manager.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import functools
1111
import common
1212
import libinspector.core
13+
import threading
1314

1415

1516
def get_page(title, material_icon, show_page_func):
@@ -69,7 +70,17 @@ def initialize_page():
6970
def start_inspector_once():
7071
"""Initialize the Inspector core only once."""
7172
with st.spinner("Starting Inspector Core Library..."):
73+
# Just in case someone closes labeling window without finishing
74+
# Same with the general warning
75+
common.config_set("suppress_warning", False)
76+
common.config_set("labeling_in_progress", False)
7277
libinspector.core.start_threads()
78+
api_thread = threading.Thread(
79+
name="Device API Thread",
80+
target=device_list_page.worker_thread,
81+
daemon=True,
82+
)
83+
api_thread.start()
7384

7485

7586
device_list_page_obj = get_page(

uv.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)