Compare commits

..

356 Commits

Author SHA1 Message Date
Srivats P
318fe124bf Suggest disabling intelligent import if pcap diff 2024-02-02 17:02:04 +05:30
Srivats P
b3ac5f8d5d Add support for RARP 2024-01-19 15:53:00 +05:30
Srivats P
f185ceb206 Stop thread if still running in destructor
Threads addressed in this commit:
 - Transmitter (txThread, txStats)
 - Capturer
 - EmulXcvr
2023-11-24 13:12:35 +05:30
Srivats P
cf84060277 Start/stop the txStats thread along with txThread
This will avoid having an always running thread unnecessarily
2023-11-23 17:02:58 +05:30
Srivats P
469e0b054e Simplify .pro file by removing some duplication 2023-11-16 11:53:52 +05:30
Srivats P
23ee0e6f00 Extract libostfilegui out of libostfile
libostfile now has no GUI dependencies
2023-11-16 11:53:52 +05:30
Srivats P
f3dccb9484 Move spinBoxDelegate back to ostprotogui
This was mistakenly moved to ostfile when it was extracted from
ostprotogui
2023-11-16 11:53:52 +05:30
Srivats P
8f89c577ef Extract file format UI code into separate files 2023-11-16 11:53:52 +05:30
Srivats P
71435869cf Rename pcapfileformatoptions to pcapoptionsdialog 2023-11-16 11:53:52 +05:30
Srivats P
f779b0138b Extract pcapfileformatoptions into separate files 2023-11-16 11:53:52 +05:30
Srivats P
8c58d8610f Prep to move PcapOptions UI into separate files
This commit only creates a copy of pcapfileformat.* files for better
history tracking. The actual code move will happen in next commit.
2023-11-16 11:53:52 +05:30
Srivats P
c285e91d4a Extract libostfile from libprotogui
All .pro file changes only

libostfile still has GUI dependency due to PCAP import options UI.
Need to see if we can remove that - maybe extract into a separate
libostfilegui.
2023-11-16 11:53:52 +05:30
Srivats P
f5350663cd Prepare to extract libostfile from libostprotogui
This commit only copies ostprotogui as ostfile to track changes. Actual
changes will be made in subsequent commits
2023-11-16 11:53:52 +05:30
Srivats P
483f7fb4c5 Fix empty port stats when viewing rsvd ports only
This was a regression introduced in 1.3.0 as part of the port stats UX
improvement. The 'user' row was changed from the first (0) to the last,
but the proxy model continued to use a hard-coded value of 0 to check
for user to determine whether to show a port column or not.
2023-11-08 12:55:39 +05:30
Srivats P
d067019959 Bump to next dev version 1.4.0-dev 2023-11-08 10:44:51 +05:30
Srivats P
eb9d5eaf1a Bump version to 1.3.0 2023-10-19 10:50:25 +05:30
Srivats P
e677dc7d16 Fix div by 0 crash in stream timing stats (jitter) 2023-10-14 12:05:21 +05:30
Srivats P
ebe6aef62d Fix icon-only delegate
The delegate was still rendering display text. But we didn't realize
because the only place where this delegate was used was returning a
QVariant() for DisplayText role earlier. Once we started returning a
valid QString (for copy-paste purposes), this bug showed up.

This didn't work because we further delegate the actual painting to
the base QStyledItemDelegate which re-initializes option.features
from the item index values, so removing HasDisplay does not work.

The fix is to override the displayText() to return a null string.
2023-10-13 12:16:03 +05:30
Srivats P
1e8486991d Include state in port stats text copy to clipboard
The view uses an icon-only delegate for state to suppress the text
display
2023-10-11 13:07:02 +05:30
Srivats P
fff61d7773 Add assert version checks for upcoming 1.3.0 2023-10-11 12:57:25 +05:30
Srivats P
eb3331f74d Update copyright years in About 2023-10-11 12:57:05 +05:30
Srivats P
f3f553d149 Override RxStats filter if specified in drone.ini
This was added to work around any libpcap wrong BPF generation like #354 or
to workaround issues like #355 (stream stats with ICMPv6)
2023-10-03 11:41:57 +05:30
Srivats P
49cb7c40e0 Check and warn about stream stats for ICMP streams
Stream stats are not supportedd for ICMP packets
2023-10-03 11:39:46 +05:30
Srivats P
5581118e2f Work around faulty linux rx stats capture filter
libpcap has a bug on Linux which leads to incorrect BPF generation for our
capture filter. This commit changes the filter expression on Linux such that
correct BPF is generated.

Fixes #354
2023-10-03 10:39:03 +05:30
Srivats P
3bcd31a1ea Use IPv4AddrValidator for IPv4 protocol config UI 2023-07-17 16:10:07 +05:30
Srivats P
268fad0690 Include headers also when copying stream stats
This is for plain text copy format.

This also replaces any newlines in the header text with a space to maintain
formatting.
2023-06-26 18:08:49 +05:30
Srivats P
28b308ce6c Try and fit all stream stats column within window
* Resize columns to content (instead of using default width)
 * Use Kpps/Mpps for Pkt Rate with 3 decimal
 * Use 3 decimal places for bit-rates (is more logical because units change
   every 1000 anyway)
 * Use 2 decimal places for time interval

This just improves the chances of all columns fitting - but is not guaranteed
2023-06-26 17:03:47 +05:30
Srivats P
bfda96a888 Add jitter to Stream Stats in the GUI
These are the GUI side changes to the jitter server side changes committed
earlier.
2023-06-26 14:38:28 +05:30
Srivats P
a2734647b6 Fix stream timing clear for all GUIDs for a port
AbstractPort uses a value of UINT_MAX to signify 'all guids', but
StreamTiming uses 'invalid guid' value which are NOT the same value. But
UINT_MAX is guaranteed to be greater than invalid GUID - so instead of
equality we check for greater than comparison.
2023-06-26 13:03:25 +05:30
Srivats P
5eb2ad1979 Calculate per stream jitter using latency values
We calculate "average jitter".

The changes include only the server side, not the client/GUI side which will
be done in a future commit.
2023-06-26 13:01:11 +05:30
Srivats P
1fa84ec644 Use pkt-set delay instead of list's in intlvd mode
Since we now create an explicit packet set even for interleaved mode, we can
calculate and use the packet set delay instead of using the list loop delay.

With this change both sequential and interleaved mode do not use list loop
delay at all, but we still retain the code for that - in case we need to use
it later for some reason.
2023-06-22 11:45:22 +05:30
Srivats P
c044880f1a Fix inaccurate low tx rates (~1000pps) in seq mode
Creation of explicit packet sets had both a packet set delay and a list loop
delay set leading to lower than configured rate at tx. Earlier implicit
packet set always had set delay as 0 and only list loop delay was used.

Fix is to always set list loop delay to 0 in sequential mode as packet set
will have the correct delay set due to explicit packet sets.
2023-06-22 10:47:13 +05:30
Srivats P
2941c7ec22 Fix crash for top speed tx with ttag in seq mode
Crash was due to loopDelay being 0 in case of topSpeed. For ttags, we
recalculatue loopDelay based on max line rate and avg pkt size.

The problem doesn't exist in interleaved mode because minGap and duration
are non-zero in the top speed tx case.
2023-06-20 11:59:56 +05:30
Srivats P
ee45aaf0eb Change StreamTiming::TxRxKey format
The format now matches the ttag list format input in recordTxTime() making
that API a little more optimized.
2023-06-17 12:14:18 +05:30
Srivats P
5ec7010c51 Fix contention for port streamStats across threads
Tx/Rx stream stats related threads no longer have a direct reference to the
port's stream stats - instead they have their own copy that they keep and
return (in a reset-on-read fashion) when asked for. Each copy also has it's
own lock to prevent contention for read/update/clear.

PcapPort now fetches Tx(Transmitter) and Rx(Poller) stats on demand and
updates the port's stream stats - under protection of a lock.
2023-06-17 11:31:48 +05:30
Srivats P
bef0f1d162 Make stream timing recordTx/Rx APIs inline 2023-06-15 12:56:53 +05:30
Srivats P
598e6bf243 Merge changes made in base code for turbo/latency 2023-06-15 12:32:20 +05:30
Srivats P
63ed64a9d2 Fix extra whitespace in latency feature added code 2023-05-26 11:31:52 +05:30
Srivats P
8ef9da062d Comment out L4Cksum code from pcap tx thread
We are not rewriting L4Cksum for ttag packets at the moment. See
comment in packetsequence.h

Commenting out this code doesn't seem to observably improve tx
performance though.

The latency code seems to reduce stream stats max tx performance
by around 3 to 5%. Recovering this may be done separately as part
of overall performance optimzation of Tx code.
2023-05-26 11:31:52 +05:30
Srivats P
7fee90313e Don't use avgDelay in interleaved mode
The interleaved mode's single packet set MUST always have 0 delay
for accurate rate.

Before latency code, the interleaved packet set was added implicitly
and had 0 delay. Latency code added explicit packet set and used
avgDelay for the set. The avgDelay was needed for the original algo
used to determine when to send ttag packets. That algo is no longer
used (ttag markers are now explicitly configured by AbstractPort).

Turbo still needs avgDelay for other use, but changes will be made in
Turbo code for that.
2023-05-26 11:31:52 +05:30
Srivats P
bafdd948f8 Make win32 specific changes for per-stream latency
There are 2 changes -
1. Encode txPort in ttag packets and use it at TxTtagStats and
RxPcapStats to identify Tx and Rx packets respectively
2. Don't use pcap_sendqueue_transmit() if stream timing is in use -
since we can't modify TTAG packets inside that API
2023-05-26 11:31:52 +05:30
Srivats P
e138fa0d3d Fix windows build issues 2023-05-26 11:31:52 +05:30
Srivats P
8583299a1c Compile out timing debug prints
They can be compiled in if required
2023-05-26 11:31:52 +05:30
Srivats P
4ba98cc520 Fix latency timers not getting started/stopped
In a previous commit, we start/stop these timers based on number of
ports tracking stream stats triggered by RPCs. However, timers cannot
be triggered across threads (RPC thread and main thread in this case).

This fix uses a queued connection to post it to the other queue.
2023-05-26 11:31:52 +05:30
Srivats P
649fa03575 Fix tx ttag pcap filter capture filter syntax
The capture filter was not getting compiled earlier
2023-05-26 11:31:52 +05:30
Srivats P
650c098370 Use TtagTimeInterval to determine ttag markers
This is for interlaved mode; sequential mode was already using it
2023-05-26 11:31:52 +05:30
Srivats P
d375736a39 Don't use udiffTimeStamp() with struct timeval
It will fail to build for non-Linux platforms where TimeStamp is NOT timeval
2023-05-26 11:31:52 +05:30
Srivats P
976fc72de8 Retain seq as param for sendQueueTransmit for now
If and when we remove PacketSequence::ttagL4ChecksumOffset we will take a call
if we should revert back to passing seq->sendQueue instead of seq at that time
2023-05-26 11:31:52 +05:30
Srivats P
4426a51d0f Run stream latency timers only when required
Required => At least one port is tracking stream stats

Also changed some optimization FIXMEs as TODOs
2023-05-26 11:31:52 +05:30
Srivats P
3c6632b6a2 Remove comment about trying read-write lock
The stream timingHash is read by getStreamStats() while it is read/write
for processRecords(), the latter is a more frequent operation so there's
no real benefit of using a read-write lock instead of simple mutex.
2023-05-26 11:31:52 +05:30
Srivats P
d3be505a0c Make app QObject parent of StreamTiming singleton 2023-05-26 11:31:52 +05:30
Srivats P
70f1b6c6e6 Add thread name for PcapRxStats
PcapTxTtagStats thread name has also been shortened since names beyond 16
chars are truncated.
2023-05-26 11:31:52 +05:30
Srivats P
d65fea00d5 Change stream timing GC timer to CLOCK_REALTIME 2023-05-26 11:31:52 +05:30
Srivats P
0afc150264 Don't fix incorrect checksum for Ttag packets
See comments in code
2023-05-26 11:31:52 +05:30
Srivats P
6ba942d00f Remove PcapTxTtagStats::handle_ member
PcaptxTtagStats inherits from PcapSession which already includes a protected
handle_ member.

This removal was likely left off when PcapTxTtagStats started inheriting from
PcapSession.
2023-05-26 11:31:52 +05:30
Srivats P
4ee91c1bc2 Rework sequential mode build for new ttag algo
The previous commit changed the algo to determine which packets were Ttag'd,
but changes were done only for interleaved mode.

This commit adds the changes required for sequential mode.
2023-05-26 11:31:52 +05:30
Srivats P
aebb609e37 Change algo to infer next Ttag pkt (interleaved mode)
The algo works for the following cases of interleaved streams -
 * pktListDuration < ttagTimeInterval
 * pktListDuration > ttagTimeInterval
 * some streams have Ttag, some don't
    - first stream has Ttag
    - first stream does NOT have Ttag
 * no streams have Ttag

Changes for sequential mode are pending
2023-05-26 11:31:52 +05:30
Srivats P
7160f724cb Fix infinite loop in building interleaved streams
Incorrect timestamp comparison was leading to infinite loop
2023-05-26 11:31:52 +05:30
Srivats P
a48a11fe02 Add explicit packet set for interleaved streams
Interleaved mode used an implicitly added packet set in both base and Turbo
code. This has been chaned to use an explicit mode to keep things consistent.

Turbo code still has the implicit packet set related code - that needs to be
removed, once the explicit packet set code is validated and tested.
2023-05-26 11:31:52 +05:30
Srivats P
91a6efdeb7 Use qFrom/ToBigEndian instead of ntohs/htons
For consistency with rest of the code
2023-05-26 11:31:52 +05:30
Srivats P
bfdcee2baa Recompute L4 checksum on-the-fly for TTag packets 2023-05-26 11:31:52 +05:30
Srivats P
c2967b663d Calculate L4 checksum offset for TTag packets 2023-05-26 11:31:52 +05:30
Srivats P
70b5e60440 Rename delay as latency in Protobuf/RPC
The GUI uses the term 'latency', so it's better if the API alsos use the same
term instead of 'delay'
2023-05-26 11:31:52 +05:30
Srivats P
5540253e61 Fix StreamTiming TxRxKey
makeKey was incorrect by mistake
2023-05-26 11:31:52 +05:30
Srivats P
8ecbe78ddd GUI changes to display avg latency
At this time we only show per-guid latency aggregated across all ports
2023-05-26 11:31:52 +05:30
Srivats P
3e3b5144aa Process pending before fetching streamTiming delay 2023-05-26 11:31:52 +05:30
Srivats P
596df69519 Fix MacOS build break by removing unused member
PcapTxTtagStats::lastPcapStats_ was unused because debugStats() was moved to
PcapSession, but removing this member var was left out
2023-05-26 11:31:52 +05:30
Srivats P
680f6eb89f Fix streamTiming garbage collection infinite loop 2023-05-26 11:31:52 +05:30
Srivats P
1b18149aaa Delete PcapTxTtagSession::debugStats
debugStats() was moved to base class PcapSession earlier, but removing it
from here was missed out
2023-05-26 11:31:52 +05:30
Srivats P
bf749847e0 Rename PcapRxStats::id_ as PcapRxStats::portId_
Better code clarity
2023-05-26 11:31:52 +05:30
Srivats P
ab713ce043 Integrate StreamTiming with the code
Bugs found during integration were fixed and minor code improvements were made
such as using consts, const params, renaming members etc.
2023-05-26 11:31:52 +05:30
Srivats P
fc2d8408fa Change StreamTiming::timing_ from QList to QHash
Using QList meant we need to know the port count in the constructor - which is
difficult to know because StreamTiming is designed as a singleton
2023-05-26 11:31:52 +05:30
Srivats P
39c8d6f5f3 Add initial cut of StreamTiming class
This singleton class will keep track of Ttag timing across all ports and GUIDs.

A bunch of FIXMEs/TODOs are pending for this class implementation; also this
class has not been hooked up to the rest of the code yet.
2023-05-26 11:31:52 +05:30
Srivats P
219ad576ad Fix another MacOS build break
Break was due to following warnings (being promoted to errors) -
 * id_ private member was unused
 * tv_usec is int on MacOS (but long on Linux)
2023-05-26 11:31:52 +05:30
Srivats P
3060701386 Move debugStats() from PcapRx to base PcapSession
With this change, other classes that use PcapSession as base can also
use debugStats(), if required
2023-05-26 11:31:52 +05:30
Srivats P
47325c38b0 Ignore failures when stopping stream stats tracking
Stop everything irrespective of any failures
2023-05-26 11:31:52 +05:30
Srivats P
21ce331c43 Create a Tx Ttag tracker thread
For now we are just debug printing timestamp with T-TagId and GUID. We
need to store this tuple and compare when we Rx the same - this will be
in a upcoming commit.
2023-05-26 11:31:52 +05:30
Srivats P
fd2fae711b Fix MacOS build break
For some reason udiffTimeStamp is not defined for MacOS. To be investigated
later.
2023-05-26 11:31:52 +05:30
Srivats P
2d998b3708 Add an incrementing tag id to T-Tag packets 2023-05-26 11:31:52 +05:30
Srivats P
f65aed7bb0 Add infra to update L4 checksum for T-Tag packets
Pending
 * Calculate L4 checksum offset instead of hardcoded value
 * Recalculate packet L4 checksum instead of using 0 value
2023-05-26 11:31:52 +05:30
Srivats P
fdf8c77350 Change T-Tag on the fly 2023-05-26 11:31:52 +05:30
Srivats P
429eff123d Don't create implicit packet sets for Tx
As part of Turbo changes, we made changes to create explicit packet
sets, but for the base code we continued creating implicit packet
sets for some cases. With this change we don't create any implicit
packet set.

This change needs to be tested thoroughly for multiple cases.
2023-05-26 11:31:52 +05:30
Srivats P
3c98900092 Reduce vertical whitespace in sendQueueTransmit() 2023-05-26 11:31:52 +05:30
Srivats P
159cd7c0da Add T-Tag placeholder in sign protocol 2023-05-26 11:31:52 +05:30
Srivats P
46b148b62b Reformat TxThread/run {} to use less vertical space 2023-05-26 11:31:52 +05:30
Srivats P
f29d31d38a Update comments about implict packetset 2023-05-26 11:31:52 +05:30
Srivats P
1056b8d170 Fix werror warning about 0 being signed by default 2023-05-26 11:31:52 +05:30
Srivats P
4a4de23d8a Replace Donate button with Github Sponsors button 2023-05-09 18:27:15 +05:30
Srivats P
425e4ef261 Rename port stats names to begin with send/receive
This will hopefully make it quicker and easier for the user to find
the stats item of interest.
2023-03-04 15:31:24 +05:30
Srivats P
d950432bc9 Move port stats hidden rows to the end
This avoids having to recalculate row indices
2023-03-04 10:49:35 +05:30
Srivats P
9849973562 Visually group related port stats
A horizontal line is drawn between groups
2023-03-04 09:57:56 +05:30
Srivats P
4f6749f16d Set alternate colors for port stats row headers
All 'sent' rows including the row header have the alt color, and
the 'recv' rows including the row header has the base color. This
change should make it easier to quickly distinguish sent/recv rows
2023-03-03 17:37:43 +05:30
Srivats P
8dbd01622c Make sent/recv order consistent for port stats
Also remove sent/recv byte rates as we have bit rates and that
should be sufficient
2023-03-03 14:14:08 +05:30
Srivats P
2f3add63d8 ostinato -[hv] should output to stdout 2023-02-24 15:15:59 +05:30
Srivats P
7b7ede351b Fix build break with stream stats fix
The fix was validated with Windows, but not on Linux. This commit fixes
the build break for non-windows platforms.
2023-02-09 15:06:34 +05:30
Srivats P
c70811eaa4 Fix spurious stream stats drops
The problem happens for bidirectional flows. The sequence of events is
as follows when you start Tx on Ports p1, p2 with the current code -

1. Clear stream stats on p1
2. Start tx on p1
3. Clear stream stats on p2
4. Start tx on p2

By the time #3 is executed, it may have already rx packets from p1 which
are being incorrectly cleared, this will cause these number of packets
to show up as dropped instead - incorrectly.

The fix is to change the order like this -

1. Clear stream stats on p1
2. Clear stream stats on p2
3. Start tx on p1
4. Start tx on p2

Unidirectional flows will not see this problem - as long as startTx is
done only on the Tx port and not the Rx port.

This bug is a regression caused due to the code changes introduced for the
stream stats rates feature implemented in 1.2.0
2023-02-08 16:34:03 +05:30
Srivats P
e2369c02bc Fix value for str/bytes field when save as Python
Protobuf string type should be treated as a Python unicode string usable
in both Python 2.x and Python 3.x. Since we are now using unicode strings,
force encoding as utf-8.

Protobuf bytes type should be treated as a Python byte string. Use hex
values in byte literal even for printable characters, for a better UX.

escapeString() is no longer being used, but has been retained in the
code.
2022-12-28 19:30:50 +05:30
Srivats P
c07d9e8691 Fix drone crash at startup on Linux
The crash happens if there is a default route without a gateway in
the main routing table
2022-12-23 13:14:19 +05:30
Srivats P
d44fdf4ae7 Add a prefix to combo protocol fields
For same protocol numbers (e.g. IP 4o4), use Outer/Inner as prefix. For
"similar" protocols like VlanStack or IP 4o6, use the protocol name as
the prefix.
2022-12-20 13:38:16 +05:30
Srivats P
f07cba39d5 Reset port widget current on portgroup disconnect
Failure to do so was causing a crash because port widget was trying to
disconnect signal from a non-existent port (corresponding to the current
index that was not reset) after the portgroup reconnected or another
portgroup came up and a port was selected in the port list.

This bug was a regression caused by the refactoring changes when
portwidget (and streamswidget) was extracted from portswindow.
2022-12-08 17:00:15 +05:30
Srivats P
1e50f9b095 Use errorOccurred() signal in disconnect as well
8d3f0c807f had changed the signal name in connect, but missed out in
using the new name in disconnect()
2022-11-14 17:27:47 +05:30
Srivats P
3cea0244d4 Rename port configuration slot
Adding a parameter with a default value to the existing on_XXX slot
in a previous commit makes auto connect signal-slots by name to flag
signal not found.

So rename the slot name and add an explicit connection.
2022-10-07 12:58:45 +05:30
Srivats P
8d3f0c807f Fix error msg not shown when missing packet.dll
Qt5.6 renamed the QProcess::error() signal to QProcess::errorOccurred().
Use the new signal name.
2022-09-30 20:33:25 +05:30
Srivats P
449a489986 Change version from 1.3.0-alpha to 1.3.0-dev
Dev is a better word choice for current stage
2022-09-27 11:40:22 +05:30
Srivats P
b645e02963 Double click port to open port config dialog
Or select and press Enter using the keyboard

Fixes #344
2022-09-27 11:38:03 +05:30
Srivats P
32ddf223b6 Show port name/description in port stats window
By default the port name (ifXXX for Windows) is shown. If a port has a
user description, that is shown instead of port name.
2022-09-12 15:46:06 +05:30
Srivats P
42091e5221 Allow user to add a port description
If a user description is available, that is shown in the ports window
other wise the system determined description is shown.

Updates #223
2022-09-09 12:25:17 +05:30
Srivats P
82db82d85b Save PortConfigDialog UI using Qt5 designer
No functional changes. Just whitespace and formatting changes.
2022-09-01 14:48:42 +05:30
Srivats P
e9fa8a0c5b Rename qhexedit2 VERSION file to VERSION.txt
On MacOS, clang incorrectly includes the qHexEdit2 VERSION file
instead of it's own - so rename our file
2022-08-31 18:38:21 +05:30
Srivats P
027b0562de Bump version to 1.3.0-alpha
This bump is to avoid compatiblity issues between development on master
and the actual 1.2.x code.
2022-08-31 18:19:42 +05:30
Srivats P
c82cccc5eb Bump version to 1.2.0 2022-08-15 13:06:22 +05:30
Srivats P
94a6423a96 Add a 1.2.0 version updater test 2022-08-15 13:05:41 +05:30
Srivats P
cda08d9afb Bump version to 1.2.0-rc.1 2022-07-29 20:59:16 +05:30
Srivats P
97068b97e3 Use /usr/share/ostinato-controller/themes on Linux
This is because the Ostinato packaging reccomendation is to use
ostinato-agent and ostinato-controller package names. The ostinato
package is a meta package containing the aforementioned sub-packages
2022-07-29 20:54:14 +05:30
Srivats P
06ad12777f Warn if sign proto may be followed by padding
For now, we check and tell the user. The actual fix should be for
sign proto to add padding before it's content, if required

Updates #313
2022-06-30 18:24:48 +05:30
Srivats P
2970a292c6 Fix build break and warnings 2022-06-28 18:32:39 +05:30
Srivats P
701a058c7d Merge commit 'refs/subrepo/ostinato/fetch' into subrepo/ostinato
Update from master repo into turbo
2022-06-27 14:35:22 +05:30
Srivats P
2103f2c5a6 Minor editorial change for cleaning up message 2022-06-27 13:43:48 +05:30
Srivats P
4be8a2969d Create and print activation hash on request
To request, pass option -a on the command-line
2022-06-27 13:43:48 +05:30
Srivats P
46a54fd56b Define PCAP promisc flag only if not defined 2022-06-27 13:43:48 +05:30
Srivats P
d32f89d30b Enable libbpf logging when drone logs are enabled 2022-06-27 13:43:48 +05:30
Srivats P
1b647ade1b Print "Starting..." after processing command line args 2022-06-27 13:43:47 +05:30
Srivats P
789338c8e1 Add startup/shutdown console prints 2022-06-27 13:43:47 +05:30
Srivats P
898b56fc76 Add turbo license validation code
Signed up for and using keygen.sh licensing SAAS
2022-06-27 13:43:47 +05:30
Srivats P
9d42ed12cc Use a invalid GUID instead of 0 as default
If a packet does not have a sign/guid, use an invalid guid instead of 0
as the default, so that these packets get tracked against the invalid
guid which can be excluded while updating the port's stream stats.
2022-06-27 13:43:47 +05:30
Srivats P
c949dc6682 Integrate XdpPort more cleanly into port manager
* No ugly #ifdef TURBO
* Turbo ports need to be specified explicitly in drone.ini
* If a port is not a turbo port, fall back to LinuxPort
* Turbo ports set their description as 'Turbo' which shows up in GUI
* Some XdpPort creation/destruction error checks
2022-06-27 13:43:47 +05:30
Srivats P
eb2ca12f32 Reorg (part 4) - add left out file
ostinato/server/timespecops.h got left out from part 1 commit of
the reorg - because it was a new file and was ignored by a wrong
.gitignore rule
2022-06-27 13:43:46 +05:30
Srivats P
e79b61b189 Use -Werror for all code except protobuf generated
protobuf files will use -Wno-error
2022-06-27 13:43:46 +05:30
Srivats P
741202ca76 Remove compiler warning
Compiler treats literal '0' as int i.e. signed
2022-06-27 13:43:46 +05:30
Srivats P
843733567a Reoganize code using subrepo for Ostinato (part 2)
This commit includes all the new and modified files
2022-06-27 13:43:46 +05:30
Srivats P
6f4ef70dbc Add FUNDING.yml for GitHub sponsors 2022-06-24 15:46:51 +05:30
Srivats P
b296c5fddd Check duration > 0 before calculating stream rates
Avoid divide by zero error and/or inf/nan values
2022-06-22 12:56:07 +05:30
Srivats P
d9cd90a13d Clear stream stats before starting Tx on port
This is required for correct stream rate calculations
2022-06-21 18:53:35 +05:30
Srivats P
fb879d2c72 Return txDuration as 0 for ports with no streams 2022-06-21 18:51:42 +05:30
Srivats P
5e7bf77b0c
Merge pull request #350 from pstavirs/streamrates
Add average rate per GUID in stream statistics
2022-06-20 17:27:24 +05:30
Srivats P
a1d985e44b Show stream stats in GUI sorted by GUID 2022-06-16 18:14:23 +05:30
Srivats P
b8d5d9421f Show Duration 'n rates in stream stats by default
Port Pkt stats are no longer shown by default. To see them and byte
counters, use context menu option - "Show details" (renamed from the
existing "Show Byte counters")
2022-06-16 14:46:18 +05:30
Srivats P
2868806f3f GUI side changes for displaying stream rates
At the moment, the Tx duration for a GUID is taken to be the largest of
all tx duration for that GUID across all ports. In the future, we may
consider changing it to average instead of largest.
2022-06-16 12:30:13 +05:30
Srivats P
ccf5b5ca47 Add a few asserts to debug GUI client crash
The crash seems to happen in the following scenario -
 * Ostinato and drone are both running (with some devices/streams)
 * Kill and restart drone
 * Wait for GUI to reconnect to drone
 * Click on one of the ports in the port list
2022-06-09 20:32:22 +05:30
Srivats P
c03038167c Initiate ARP/NDP resolve during session open
The current implementation won't work for all cases. See the notes added
in the code.

Updates #311
2022-06-09 20:30:42 +05:30
Srivats P
de9018166d Remove extra braces 2022-06-09 15:11:55 +05:30
Srivats P
c0d860b92d Support 0 rate for interleaved streams
Fixes #297
2022-06-09 13:35:05 +05:30
Srivats P
81c975d6d1 Wait 500ms for ARP/NDP to finish in resolve RPC
Fixes #318
2022-06-06 16:30:33 +05:30
Srivats P
1854f1ab9e
Merge pull request #336 from pstavirs/gre
WIP: Add GRE protocol
2022-05-27 13:43:46 +05:30
Srivats P
5a91dc4561 Remove TODOs from GRE implementation files 2022-05-26 17:30:29 +05:30
Srivats P
3d6f39e84d Implement override checksum for GRE 2022-05-25 15:16:33 +05:30
Srivats P
60f61ed947 Add and use UIntEdit for 32-bit GRE Key/Seq fields 2022-05-23 17:04:27 +05:30
Srivats P
26d0c8ab9c Skip not-included fields in variable fields list
Optional protocol fields not included in the packet have bitSize = 0
2022-05-23 13:31:01 +05:30
Srivats P
25a91e52f6 Change GRE optional fields implementation
* frameFieldCount() always includes all frame fields whether or not
included in packet
* if an optional field is not included in packet
    - FieldName is still returned
    - FieldTextValue is returned as "<not-included>"
    - FieldBitWidth is 0
    - all remaining attributes are QVariant()
2022-05-23 13:18:18 +05:30
Srivats P
c8cc7a021f Merge branch 'master' into gre 2022-05-21 12:16:14 +05:30
Srivats P
564687ffe3
Merge pull request #348 from Benni0109/master
Update portstatswindow.ui
2022-04-22 21:05:38 +05:30
Benjamin
9d4aaa4f7b
Update portstatswindow.ui 2022-04-16 19:14:44 +02:00
Srivats P
329469dd6e Calculate port tx duration and send to controller
This commit only includes server side changes plus the .proto changes.

Client side UI changes are still pending.
2022-02-21 22:20:59 +05:30
Srivats P
7374c74b95 Increase default width of port list pane
The list and detail panes were currently set to 1:2, so that the list
pane is 1/3rd the width. But for some reason it is not working like I
expect it to work. Setting it 1:1 looks better.
2022-02-12 19:26:18 +05:30
Srivats P
6f433bb7a1 Move View menu immediately after Edit menu
This seems to be the convention and also recommended by MacOS HIG
2022-02-11 18:34:06 +05:30
Srivats P
2ee19da15c Add Stream and Device menus to main window menu bar
Stream and Device related actions from the File menu has been moved
under these new menus
2022-02-11 18:09:30 +05:30
Srivats P
22b8c405f7 Use palette colors for stream stats
Using harcoded colors leads to unexpected results in dark mode.
2022-02-11 11:37:03 +05:30
Srivats P
2ce7f0c7a0 Disconnect portgroup before removing from list
Disconnecting _during_ remove sometimes causes crash because disconnect
triggers the logs window alert which **can** lead to repainting of
other windows which access port groups which have not been synced after
the removal of one or more portgroups.

Also emit endRemoveRows after each portgroup so that begin/end signals
match.
2022-02-10 20:34:10 +05:30
Srivats P
041f500e65 Avoid crash at GUI start up
The crash can happen before mainWindow is allocated. Don't remember how
that happens, but I have seen it happen!
2022-02-09 21:30:51 +05:30
Srivats P
7dd070fd98 Fix packet dump view bg color when using dark theme
Qt already paints the background before calling the paint event, so we
don't need to repaint it.
2022-02-07 21:44:40 +05:30
Srivats P
ac52844b3f Don't stretch stream config simple protocols widget
Only avoid the vertical stretch by adding a spacer, let horizontal still
stretch
2022-02-05 21:22:32 +05:30
Srivats P
2bf2973a23
Merge pull request #347 from pstavirs/themes
Add Ostinato Themes
2022-02-05 20:51:32 +05:30
Srivats P
341d0c3be8 Install themes as part of 'make install'
Tested only on Windows; pending on MacOS/Linux
2022-01-30 13:13:20 +05:30
Srivats P
6534312968 Look for themes in different locations on Mac/Unix
Not tested yet - since I'm coding this currently on Windows
2022-01-30 12:15:17 +05:30
Srivats P
a089cc1751 Update material themes and add qds themes 2022-01-29 12:24:08 +05:30
Srivats P
b0a81fb231 Change recalc cksum defaults implementation
These are the existing and desired defaults -
 * GUI: recalcCksum = true
 * PCAP import test: recalcCksum = false

With the existing implementation, doing PCAP import in 'raw' mode twice
would lead to recalcCksum being checked in the dialog the second time.
This was because we were always forcing it to be true for the GUI case
without checking for anything.
2022-01-23 21:46:42 +05:30
Srivats P
735e960dcb Use nsec pcap as intermediate when opening non-pcap
When opening a non-PCAP file, we convert it to intermediate PCAP format.
Use nanosecond pcap format instead of standard microsec pcap format for
higher timestamp resolution.

Updates #238
2022-01-19 18:54:46 +05:30
Srivats P
226705f015 Use integer storage and arithmetic for nanosec pcap
quint64 has larger range than double so has better accuracy. However,
for calculating packet rate, use floating-point arithmetic since the
packet rate is a double

Updates #238
2022-01-19 18:46:09 +05:30
Srivats P
45d5e15f23 Save PCAP always in nanosecond PCAP format
Updates #238
2022-01-17 18:49:24 +05:30
Srivats P
d09d83000e Add comment specifying PDML reader needs PCAP file
Updates #238
2022-01-15 18:36:15 +05:30
Srivats P
0f322fb2d8 Read nanosec PCAP files natively in non-pdml mode
In PDML mode, nanosecond support is already present - in fact, only
nanosecond is supported not microseconds.

Updates #238
2022-01-15 16:22:28 +05:30
Srivats P
0587e22de9 Add theme-manager and qt-material themes 2021-12-24 21:36:14 +05:30
Srivats P
5045b7f273 Update preferences.ui as per recent QtDesigner
No changes to the form - .ui file changes only because QtDesigner
generates the .ui file differently
2021-12-19 14:16:23 +05:30
Srivats P
a89400a4e5
Merge pull request #346 from pstavirs/find-replace
Find replace
2021-12-18 12:03:02 +05:30
Srivats P
db226d7069 Fix build break due to typo in #include files 2021-12-16 17:49:01 +05:30
Srivats P
f28e236276 Ensure mandatory fields to enable find-replace OK button 2021-12-15 21:28:52 +05:30
Srivats P
4bfd546f21 Change pcap import recalcCksum default to false
This was changed so that pcap import test produces minimal diffs like
earlier. However, for user, the import options dialog overrules this
default, so that user default remains recalcCksum as true
2021-12-12 11:06:36 +05:30
Srivats P
9208423372 Tweak find-replace UI
* matchAny op correctly hides findValue/findMask widgets
 * reorder widgets in the .ui file as per their actual appearance
 * remove tab order since the above reorder also fixes tab order
 * fix stretch factors so that UI redering doesn't change too much when
 things are checked/unchecked
2021-12-12 10:52:43 +05:30
Srivats P
16b353ae30 Alloc/free Pcap import options dialog every time
The earlier code was trying to reuse the dialog but this was throwing
an unexpected error every time -

    External WM_DESTROY received for QWidgetWindow

This commit fixes this error message
2021-12-11 21:13:15 +05:30
Srivats P
dbdeee2a6f Add option to recalculate cksums during pcap import
Recalculate cksum is now the default.

This behaviour change has been done to facilitate rewriting packet
fields post PCAP import using the new Find & Replace feature. Without
this any change in fields may cause incorrect checksums.

The earlier rationale for retaining the checksums in the PCAP file
during import was to ensure replayed packet was same as the one in the
PCAP file.

User now has a choice with this option.
2021-12-11 19:27:44 +05:30
Srivats P
366022552f Update pcap import options UI as per latest QtDesigner
There are some file format changes - all cosmetic
2021-12-11 18:11:53 +05:30
Srivats P
b8a0de377a Add find-replace preview as a TODO item 2021-12-11 16:18:38 +05:30
Srivats P
8333e5fbf1 Use QList::last instead of constLast for b/w compatibility
constLast was introduced in Qt5.6; Travis CI has older Qt
2021-12-11 16:00:57 +05:30
Srivats P
969e16aaf3 Fix IGMP/MLD group address find/replace
fieldData/setFieldData changed to work with uint in addition to string
2021-12-11 15:58:30 +05:30
Srivats P
4aedc61c8e Use uint64 for find/replace QVariant instead of string 2021-12-11 15:57:02 +05:30
Srivats P
f0c7ef50f7 Rename FieldEdit::setMask as FieldEdit::setMaskMode 2021-12-11 13:40:44 +05:30
Srivats P
975ce8093d Remove find-replace progress bar TODO as it's done 2021-12-11 13:01:31 +05:30
Srivats P
58d4ceb9c7 Ensure setFieldData return value for all protocols
Some protocol fields were not setting 'isOk' before returning it
2021-12-11 12:56:23 +05:30
Srivats P
fb91a094fc Show find-replace progress bar 2021-12-11 12:36:33 +05:30
Srivats P
a7aa93b9e9 Show affected field and stream count post replace 2021-12-09 10:14:47 +05:30
Srivats P
c1610f4c99 Derive field max from bitSize instead of saving it 2021-12-09 09:52:42 +05:30
Srivats P
dc7ac89c30 Verify find/replace mask works for all field types 2021-12-09 08:26:35 +05:30
Srivats P
4c6d8f35d6 Verify and fix Ip6Address type field edit 2021-12-08 21:36:39 +05:30
Srivats P
2eab3daa2f Set placeholder text for field edit types uint/mac 2021-12-06 21:14:38 +05:30
Srivats P
7cf9c91014 Verify and fix Ip4Address type field edit 2021-12-06 20:56:40 +05:30
Srivats P
9385b31bc5 Verify and fix MacAddress type field edit
Use QRegularExpression instead of QRegExp as the latter is deprecated in
Qt6
2021-12-05 12:09:02 +05:30
Srivats P
d3400f0897 Validate find-replace value based on field type
Only kUInt64 type has been verified for this commit.

Others are pending.
2021-12-04 12:17:37 +05:30
Srivats P
b60aad45d1 Implement find-replace logic for fields <= 64 bits 2021-12-03 19:01:32 +05:30
Srivats P
e19083ed3f Fleshed out some of the find-replace code 2021-11-27 22:13:55 +05:30
Srivats P
990c13e67a Add Find & Replace UI with skeletal code 2021-11-26 22:30:45 +05:30
Srivats P
90f855ecdf
Merge pull request #345 from pstavirs/speed
Improve UI/UX of configured traffic rate on a port
2021-11-24 18:27:37 +05:30
Srivats P
a1c165b7b3 Fix MacOS build break 2021-11-17 18:39:14 +05:30
Srivats P
8ea89f2607 Retreive speed, mtu for BSD/MacOS ports 2021-11-15 22:31:11 +05:30
Srivats P
7e16004c7f Retreive link speed and MTU for Linux ports 2021-11-15 21:29:20 +05:30
Srivats P
f731b48676 Allow k/m units as input to port pps/bps
Also,
  * Revert to old value if new input is not valid
  * Refactored auto calc and display of rate to XLocale
2021-11-11 21:38:57 +05:30
Srivats P
910fccbfc6 Change port bit rate display unit automatically
This is for user convenience and easier comprehension
2021-11-10 19:22:49 +05:30
Srivats P
27e853e6e8 Reduce UI width of load % input box 2021-11-09 22:21:12 +05:30
Srivats P
dd7f4a6fd0 Add load % as an input for port rate 2021-11-07 19:05:11 +05:30
Srivats P
925edb8507 Add port speed and MTU properties
This commit only retreives these properties for Windows. Linux and
BSD/MacOS are pending
2021-11-06 21:27:55 +05:30
Srivats P
14993073fe Extract portWidget from streamsWidget 2021-11-06 19:18:48 +05:30
Srivats P
295fc93e7b Prepare to extract portWidget from streamsWidget 2021-11-05 15:32:23 +05:30
Srivats P
0a825a0aa3 Extract streamsWidget code out of portsWindow
All functionality seems to be working, so hopefully no regressions!

It does appear to me that portWidget can be extracted out of
streamsWidget
2021-11-05 15:27:37 +05:30
Srivats P
214c774fc6 Prepare to extract streamsWidget code from portsWindow 2021-11-05 13:02:19 +05:30
Srivats P
b52d1c5be3 Extract streamswidget.ui out of portswindow.ui
Only UI extraction is done. PortsWindow is still a combined class that
will be separated in the subsequent commit(s).

Although this commit builds successfully, but the stream actions don't
work because the stream widget's signals are not connected to port
window's slots
2021-11-05 13:02:19 +05:30
Srivats P
d9d68ad65c Prepare to extract streamsWidget out of portsWindow 2021-11-05 13:02:19 +05:30
Srivats P
d77074da1a Upgrade portswindow.ui to QtDesigner 5.9.4 2021-11-05 13:02:19 +05:30
Srivats P
9789d475a8 Show/hide the Next column in stream list on tx mode change 2021-10-21 21:52:54 +05:30
Srivats P
9c44bb8e12 Update stream view actions after opening a streams file
Fixes #312
2021-09-25 12:41:31 +05:30
Srivats P
5180f57202 Fix comparison of different enum types 2021-09-23 09:34:09 +05:30
Srivats P
a3f0281c30 Fix comparison of difference enum types 2021-09-23 08:31:52 +05:30
Srivats P
d5365a25d4 Add IMIX as a Frame Length mode
Use Simple IMIX as defined in Wikipedia - Frame lengths 64, 594, 1518 in
a 7:4:1 ratio
2021-09-23 08:11:22 +05:30
Srivats P
9f70f29499 Refactor ICMP/IGMP checksums
Added a new CksumTypeIcmpIgmp and implemented it in AbstractProtocol.
2021-09-05 13:04:57 +05:30
Srivats P
c710adb4d8 Remove extra travis-ci URL from README 2021-09-04 18:59:50 +05:30
Srivats P
8a3837e9c2 Fix travis-ci badge URLs 2021-09-04 18:50:41 +05:30
Srivats P
a498dbf21a Fix incorrect UDP/ICMP checksums
For UDP encaps like VxLAN or Geneve, which can contain IP as
payload, the UDP checksum was incorrect because when summing
UDP payload (i.e. IP), we skipped the IPv4 checksum field,
which should not be skipped in this case.

Similar issue, for ICMP with IP as payload, ICMP checksum was
incorrect.

Essentially, any protocol which checksums over its payload and
the payload contains protocols with checksum fields.
2021-09-04 18:16:40 +05:30
Srivats P
96b6424cae Fix comparison between different next what enum types 2021-08-15 19:22:17 +05:30
Srivats P
658d3dff31 Fix comparison between different send unit enum types 2021-08-15 19:01:10 +05:30
Srivats P
1894a633e1 Fix comparison between different ARP mode enum types 2021-08-15 18:27:13 +05:30
Srivats P
226b6cf35c Fix comparison between different length mode enum types 2021-08-15 18:16:09 +05:30
Srivats P
98f2e9fa6c Fix "" in travis command 2021-08-14 20:35:10 +05:30
Srivats P
0d3efd70ee Add Qt5 bin path to travis.yml
homebrew qt5 is keg-only, it doesn't create symlinks, so we either
need to add to $PATH or create symlinks. Let's try the former for now.
2021-08-14 20:27:00 +05:30
Srivats P
5b49a139d3 Use homebrew addon in travis.yml 2021-08-14 20:13:18 +05:30
Srivats P
c49f402e36 Merge branch 'master' into ci-dev 2021-08-14 20:01:59 +05:30
Srivats P
34d8ac2584 Migrate from travis-ci.org to travis-ci.com 2021-08-14 11:35:49 +05:30
Srivats P
1216cfbce6 Include QRegExpValidator in MacEdit to fix build break
This build break is seen only on some platform/compiler combos
2021-08-14 11:31:38 +05:30
Srivats P
e3a3e84721 Upgrade OSX image to Mojave and more recent xcode 2021-04-25 11:51:18 +05:30
Srivats P
e680cadf7f Fix invalid brew uninstall command 2021-04-25 11:21:31 +05:30
Srivats P
8c2458580f Uninstall java from brew to avoid downloading it 2021-04-25 11:03:34 +05:30
Srivats P
a1705e7619 Try to reconnect with drone after disconnect
This used to work earlier, but got broken when the incompatibility check
was added as we didn't want reconnect to be initiated if versions were
incompatible.

This fix will always try to reconnect, except for following cases -
 * Compatibility check is false
 * Bad data received from drone
 * User triggered disconnect

Fixes #333
2021-04-25 10:52:42 +05:30
Srivats P
d2d5db7df4 Remove brew update and try rebuild with Travis CI 2021-04-25 10:50:04 +05:30
Srivats P
df17547c1b Merge branch 'master' into ci-dev 2021-04-25 10:42:51 +05:30
Srivats P
c2c52d1e6d Fix incorrect display of STP Mac addresses
Use MacEdit instead of QLineEdit to fix and simplify. MacEdit is what
the Mac protocol uses.

Fixes #340
2021-04-24 20:30:30 +05:30
Srivats P
5f10014c23
Add code of conduct 2021-01-17 16:47:40 +05:30
Srivats P
48c00537ac
Minor formatting change to CONTRIBUTING 2021-01-17 16:41:23 +05:30
Srivats P
0adc81eb92 Add contributing guidelines
Inform about copyright assignment
2021-01-17 16:32:24 +05:30
Srivats P
73dd198069 Add GRE protocol 2021-01-09 13:15:26 +05:30
Srivats P
bcc21ccded Gracefully handle portgroup disconnect during Apply
Fixes #326
2020-12-27 17:00:28 +05:30
Srivats P
33dbc42ecb Add Ctrl-C cleanup for Drone on Windows 2020-12-27 12:54:57 +05:30
Srivats P
4ae6b564d3 Close existing RPC connections at exit
Fixes #314
2020-12-27 12:52:47 +05:30
Srivats P
c97a359f85 Warn about retreiving stream stats from a transmitting port
Tx stream stats are available only after transmit is finished.

Fixes #334
2020-12-27 11:28:18 +05:30
Srivats P
ea31cf30fb Select previous tab when a stream stats tab is closed
The default behaviour was to select the tab to the right of the
closed tab which is not very useful for us since the right tab
will be the Logs tab.

Fixes #331
2020-12-27 11:16:39 +05:30
Srivats P
631f0982fe Fix wrong stream getting disabled in interleaved mode
When building packets in interleaved mode, we do 2 passes over the
streams.

In the first pass, we build a number of lists of variables for each
**enabled** stream. One of these variables is the pktBuf content.

In the second pass, we use these lists to build the packets. If the
stream is not variable, we just use the packet content built in the
first pass. However, if the stream is variable we call frameValue to get
the packet content, but we index with the wrong value into stream list
 if we have some disabled streams before us.

Fixes #328
2020-11-13 21:48:36 +05:30
Srivats P
640e7029b9 Fix port reservation getting cleared incorrectly
This would happen when other port config properties were modified but
the reserve checkbox itself was not modified.

Fixes #317
2020-10-23 13:21:02 +05:30
Srivats P
43fe3964f9 Fix unknown GUID 4294967295 in stream statistics
4294967295 (0xffffffff) is actually used internally by Ostinato for the
Total or Aggregate stats. When stream stats are fetched from more than
one portgroups, a kAggrGuid entry was added for each portgroup in the
GUID list. However, the GUI only tags the last row with the kAggrGUID as
the "Total" row.

Fix is to ensure we add kAggrGuid only once to _guidList.
2020-10-23 12:42:57 +05:30
Srivats P
a352ff3ed1 Fix frame variable count when varying frame lengths
The frame variable count should be minimum of frame count and frame
length range size.

Unrelated, in case of random payload pattern, reuse
StreamBase::frameCount() which does the same calculation.

Fixes #325
2020-10-22 20:28:26 +05:30
Srivats P
5edf9a75e5 Bump version to 1.1 2020-06-19 08:00:09 +05:30
Srivats P
910e721182 Add qt5svg package in travis 2020-06-13 22:17:40 +05:30
Srivats P
f2635acb09 Hide the SVG label at creation
When required the appropriate pixmap (error/warn) will be set and the
label will be shown.
2020-06-13 21:36:51 +05:30
Srivats P
23d855f201 Add svg module to required QT modules
MacOS seems to need svg module to show SVG icons
2020-06-13 21:35:53 +05:30
Srivats P
8547eb8bb8 Fix pcap import/replay inter packet timing
Changed to decimal precision for improved replay accuracy.

For inter packet time > 1s, the integer precision would set up rate as 0
instead of 0.x - this also gets fixed by this change.
2020-06-11 20:48:57 +05:30
Srivats P
cb9b648c71 Resolve only valid gateways
Not doing so would lead to errors showing up with host devices
2020-06-08 18:42:38 +05:30
Srivats P
1699dba1cf Change raw_input to input in generated python code 2020-06-06 12:30:53 +05:30
Srivats P
be984135ac Fix diff in pcap import of IPv4 due to ip.flags
Some Wireshark/tshark versions have ip.flags as u16 instead of u8. Code
changed to be able to handle both.

Tested with Wireshark 3.2.4 (u16) and 1.14 (u8)
2020-06-04 21:02:09 +05:30
Srivats P
2028d0f25b Start the error/warn animation from center of app
Irrespective of whether logs window is visible or not, show and
animate the icon from the center of the main window; end point is center
of logs window if visible, or the logs window tab icon if not visible.
2020-05-31 21:01:10 +05:30
Srivats P
4487da1ae6 Add URL to why pcap diff from error message 2020-05-18 20:58:39 +05:30
Srivats P
f7ace4c5c2 Name threads for easier debugging 2020-05-15 18:55:03 +05:30
Srivats P
6f7cbae2dd
Merge pull request #306 from quanterium/isnan_error
Add missing include cmath in port.cpp
2020-05-15 17:56:36 +05:30
David Mueller
f14c02b065
Add missing include cmath 2020-05-13 09:28:59 -07:00
Srivats P
1f8bfd2393 Update copyright to 2020 in About 2020-05-13 20:48:37 +05:30
Srivats P
8859a4c152 Tweak the animated error/warn GIF icons
Two frames were deleted from each end of the sequence so that the icon
never goes fully white/blank
2020-05-13 20:39:45 +05:30
Srivats P
1bc2d08fdb Reset RPC input stream after aborting the connection
Without reset, on reconnection, the input stream reads the buffered
data before abort.
2020-05-12 21:56:40 +05:30
Srivats P
75ce626532 Convert RPC channel's static vars to data members
Using static vars meant all connections were using the same static vars!
A bug that has existed since 2009!

This code is ugly, but since it is such a fundamental piece of code for
every operation, no refactoring is being done except to convert the
static vars to data members renaming where required to avoid name
conflicts. The one exception is that `cumLen` which was a per hdr.type
variable is now a single data member across all types. Since for a
single connection messages will be sequential this is expected to work
(fingers crossed!)

For the future we should look at replacing the custom RPC code with GRPC
(issue #305).

Fixes #304
2020-05-12 21:33:54 +05:30
Srivats P
f92161e755 Add XXX note about LogsWindow::isVisible_ 2020-05-10 19:19:32 +05:30
Srivats P
0e72a58dd7 Call attention to the warn/error logs using animation 2020-05-10 18:43:08 +05:30
Srivats P
dccf2042f0 Abort connection if bad data received from drone
Currently we would crash because of qFatal(). The new behaviour is
better. Error is reported in the logs window alongwith a suggestion on
what to check.
2020-05-09 09:50:32 +05:30
Srivats P
1e1e0b0c48 Clear current and selection when restoring default view
This is done for all top level windows - ports, stats, logs

At startup, the local portgroup would automatically become current with
the result that the welcome page would not be visible. Not sure why but
an explicit setFocus of ports window seems to avoid it.
2020-05-07 18:40:31 +05:30
Srivats P
f309ce99cc Set top and bottom docks to equal height at startup 2020-05-06 18:01:44 +05:30
Srivats P
2006674fc1 Print Settings path at startup for debug 2020-05-06 17:52:18 +05:30
Srivats P
ad38a60171 Fix valid count range for variable fields
To allow full range of a field, the "count" of a variable field,
 * Minimum should be 1, not 0
 * Maximum should be max value of field + 1

Fixes #301
2020-05-04 19:15:49 +05:30
Srivats P
bd20ec724f Check if interface has a link layer (mac) address
Fixes #298
2020-05-04 18:55:57 +05:30
Srivats P
0897ae6dad Fix awk not found while importing pcap on Windows
For non-Windows platforms, the default paths for awk, diff, gzip are
fixed at compile time since these are standard utilities for these
platforms. However, for Windows, we ship these utilities along with
Ostinato, so we need to set default path as the application directory
which is known only at run time. This is done as part of
Preferences::initDefaults() and therefore should be called before
setting OstProtoLib external program paths
2020-04-09 18:43:00 +05:30
Srivats P
a1582ad67b Convert pcapng and other formats to pcap for import
Any capture format supported by tshark can now be imported by Ostinato.

Fixes #83
2020-04-07 21:18:34 +05:30
Srivats P
c335f34651 Import PDML text proto as hexdump if non-text chars 2020-04-07 21:15:27 +05:30
Srivats P
5efffb0c4b Ignore http.file_data during PDML import
This field is just a combination of other http fields
2020-04-07 21:14:44 +05:30
Srivats P
79ada3315a Ignore empty value fields during PDML import 2020-04-07 21:10:39 +05:30
Srivats P
c25256bfb5 Fix STP PCAP/PDML import
As originally written it worked only with Eth encap
2020-04-07 21:08:21 +05:30
Srivats P
63e648bbfe Fix IGMPv3 group record aux data
FieldValue, FieldFrameValue, FieldTextValue were all fixed. This used to
work earlier. I think somewhere across Qt versions, QByteArray() and
QString() changed such that it broke. Or more likely I was using those
incorrectly earlier but output was still correct which is no longer
true.

Anyway, it should be ok going forward (hopefully!)
2020-04-03 20:39:14 +05:30
Srivats P
487f3e653d Fix missing packet summary line in pcap import diff
Looks like starting somewhere in Wireshark 2.x, when we give -x to
tshark, we also need to explicitly give -P to include the one-line
summary of the packet. This one-line summary is used in the diff to find
out which packets experienced a diff.
2020-04-03 20:29:18 +05:30
Srivats P
f1a962a6c4 Fix compilation error with importpcap test 2020-04-03 20:27:42 +05:30
Srivats P
8f510635a9 Use UInt128 for IPv6 address modes (inc, dec, rnd)
Fixes #283
2020-03-27 21:15:49 +05:30
Srivats P
acb8222beb Fix IPv6 stream config prefixLength not being saved 2020-03-27 21:15:49 +05:30
Srivats P
f705f7f606 Fix build break 2020-03-23 20:37:47 +05:30
Srivats P
bfc5477807 Store mac in big-endian order in DeviceKey
This was a bug. The intention was to store it in big endian, so that the
sort order for devices would be VLANs+Mac.

Fixes #289
2020-03-23 18:48:36 +05:30
Srivats P
79ac3a2a20 Fix plain text copy to print selected columns
The case being fixed is when non contiguous columns were selected, e.g.
column 3 and 7
2020-03-22 13:26:38 +05:30
Srivats P
6791eb06dd Change port stats to use XTableView to support copy paste
Also fix plain text copy to only include selected column headers if the
selection behaviour is SelectColumns
2020-03-20 22:15:13 +05:30
Srivats P
fae0bdb611 Remove copy support from Logs Model
XTableView now supports plain text copy for all models, so we don't need
this code in Logs Model
2020-03-18 20:56:07 +05:30
Srivats P
e6213a90ac Copy to clipboard in plain text format also
The plain text copy is written generically and should work with any
model that is attached to a XTableView
2020-03-18 20:50:29 +05:30
Srivats P
3b31a4fade Fix crash when updating 'cut' status
Also added a failsafe to workaround at run time if more problems are
detected in the field while updating cut-copy-paste action status
2020-03-17 18:50:20 +05:30
Srivats P
92fc7f140b Use a better heuristic to determine model canCut() 2020-03-17 18:49:12 +05:30
Srivats P
12f81a1dba Enable/Disable cut-copy-paste actions as required
Cut: focus widget has a selection, a 'cut' slot and 'canCut'
Copy: focus widget has a selection and a 'copy' slot
Paste: focus widget has a 'paste' slot and can accept the clipboard item
2020-03-16 22:26:16 +05:30
Srivats P
7227dd734b Fix build break due to missing include file 2020-03-14 21:42:29 +05:30
Srivats P
18d024481f Add cut-copy-paste to Edit menu and context-menu
Actions have been added to context-menu for stream list and device group
list only for now.
2020-03-14 21:13:24 +05:30
Srivats P
4b16c95b2a Add cut-copy-paste support for devicegroups 2020-03-11 18:19:55 +05:30
Srivats P
43f6959dcc Add cut-copy-paste support for streams 2020-03-11 18:19:55 +05:30
Srivats P
fd6f2c2508 Move copy support for Logs Window to Logs Model
The model is the correct place to determine what data gets copied. This
also paves way to support other models where the data to be copied is
not plain text
2020-03-11 18:19:55 +05:30
Srivats P
88ddb97a52 Find Wireshark exe path when given .app path
Also change the default Wireshark path value to match what recent
versions of Wireshark are using

Fixes #288
2020-02-14 18:47:02 +05:30
Srivats P
3889780d79 Allow editing of external program paths in Preferences
GUI file picker can still be used, but is not required.

Updates #288
2020-02-13 18:28:21 +05:30
Srivats P
db66d7c5ea Add support for npcap as winpcap replacement
For now both winpcap and npcap are supported with the latter being
experimentally supported till we get some feedback from users and
confirm that things are all working fine with npcap.

OID for link state has been changed to one that supports both.

To check which is being used, run 'drone -v'.

Fixes #236
2020-02-12 18:14:48 +05:30
Srivats P
018a8490cf Fix stream rate calculation when avg rate changes from 0 to > 0
Fixes #294
2020-01-30 18:06:47 +05:30
Srivats P
176fc7dd15 Redo fix for incorrect vlan parsing of rx emuldev frames
The previous fix in 6977278654 was incorrect and incomplete since it
won't handle the case when emulated devices have PRIO and/or CFI/DEI
set.

This is the correct fix where Prio and CFI/DEI are no longer part of the
device key.
2020-01-26 19:01:37 +05:30
Srivats P
f0dd0c307b Fix device emulation crash due to contention
Device emulation packets are received and processed in a different
thread compared to the main RPC processing thread where the emulated
devices are created/deleted. No packets should be processed while the
latter is in progress otherwise the former may access devices that have
been deleted.
2020-01-26 18:35:14 +05:30
Srivats P
d2ae04c488 Fix session file not saving port stream stats 2020-01-19 12:04:57 +05:30
Srivats P
6977278654 Fix incorrect vlan parsing of rx device emulation frames 2020-01-15 21:54:05 +05:30
Srivats P
64fe90e5b7 Fix jumbo error being reported multiple times
Also found and fixed another related bug where the loop to check pkt
size for trunc/jumbo ran for frameCount instead of
frameSizeVariableCount() - this change will reduce preflight check time
when frameSizeVariableCount is less than frameCount

Fixes #290
2019-12-29 20:38:08 +05:30
Srivats P
44a9eada48 Fix inputMask for IGMPv3 source IP
The old mask 009.009.009.009 does not compute to valid for input 1.2.3.4
and the Qt delegate doesn't accept it with Qt5 (not the case with Qt4)

Although the new mask 000.000.000.000 cannot prevent invalid entries
like 1.2.. or 300.400.500.600, I decided it was better to accept invalid
input (which gets converted to 0.0.0.0) instead of unintentionally not
allowing valid entries.

Fixes #292
2019-12-29 19:53:56 +05:30
Srivats P
3f76c533fd Add donation blurb to README 2019-09-21 12:28:26 +05:30
Srivats P
2966884fc8 Bump to version 1.0 - 11 years 'n 13 releases in the making! 2019-09-07 12:31:53 +05:30
Srivats P
8ea2e91b47 Add Qt widgets lib for PCAP import tests build
QtWidgets is required since we have ported the code to Qt5
2019-09-06 21:18:05 +05:30
Srivats P
518c7b804b Change logo icon to the full logo instead of a quadrant
* Changed Main Window icon - should change icon in taskbar also
* Changed Windows Icon
* Changed MacOS Icon
2019-08-28 21:37:45 +05:30
Srivats P
a88cfe35bb Increase main window height to make room for stats 2019-08-28 18:26:09 +05:30
Srivats P
884990d03c Set mac mode to fixed for pcap imported streams
We need to explicitly set it to fixed 'coz the default has changed from
fixed to resolve.

Fixes #282
2019-08-25 10:48:46 +05:30
Srivats P
c8a4c3eee9 Restore XTreeView functionality
I thought Qt5 had fixed this behaviour 'bug', but apparently not. So we
still need XTreeView - for Qt5 also.
2019-08-24 12:14:50 +05:30
Srivats P
36e0a64f81 Revert "Use persistent model index in devices widget"
This reverts commit 46dd028a33.

The above commit caused a regression - issue #281

As seen in valgrind, crash was caused by portModel reset writing into
a freed block - presumably pertaining to persistent indexes. The block
was freed by the persistent index itself earlier when it was set to
invalid - so the model should not have been accessing that.

The application code seems correct - maybe the Qt code has a bug?
Unlikely but not impossible. For now go back to using QModelIndex
instead of QPersistentModelIndex

Fixes #281
2019-08-24 11:59:41 +05:30
Srivats P
3e46c1b991 Include <typeinfo> header for using typeid 2019-08-18 13:13:39 +05:30
Srivats P
635ca18bce Fix duplicate deletion of host devices
Since host devices are also included in the deviceList_, they are already
being deleted. Deleting them again causes a segfault.
2019-08-18 12:30:41 +05:30
Srivats P
46dd028a33 Use persistent model index in devices widget 2019-08-10 18:55:23 +05:30
Srivats P
64d1525f50 Fix infinite loop when stopping capture etc.
On some platforms and/or some libpcap verisons, libpcap doesn't support a
timeout which makes interactive stop not possible. So we now use a UNIX
signal to break out. Obviously this works only on *nix platforms - which
includes MacOS. For now the problem is not seen on Windows with WinPCAP,
so we should be fine. May need to revisit when we add Npcap support.

Fixes #215, #234
2019-08-10 13:26:04 +05:30
Srivats P
ea69335d29 Check we have interface info before printing it 2019-07-28 10:42:11 +05:30
Srivats P
1db768afba Remove QThreadX since we have ported to Qt5 2019-07-28 10:42:11 +05:30
Srivats P
e123934c73 Suppress deprecated declarations till next release
Qt5.13 has deprecated a bunch of Qt APIs that we are using - postponing
this activity till next release
2019-07-27 11:46:17 +05:30
Srivats P
6974f4016f Replace the deprecated qSort with std::sort 2019-07-26 21:24:25 +05:30
Srivats P
0a2154d31b Re-add about.png icon that was removed by mistake 2019-07-10 21:07:38 +05:30
Srivats P
23bc0c6890 Add 2019 to copyright years in About dialog 2019-07-10 20:39:58 +05:30
Srivats P
6e551017f4 Show all ports in port stats window at startup
If one or more ports were reserved, these were not shown at client
startup
2019-07-04 19:17:22 +05:30
Srivats P
98378b52e6 Fix ClearAllStats with View Reserved Ports Only 2019-07-04 19:00:13 +05:30
Srivats P
5e565167f2 Fix build break due to fprintf in previous commit 2019-07-02 19:14:38 +05:30
Srivats P
f24a6719fa Disable logs by default in release mode
Enable logs if '-d' command-line option is given.

Additional command-line options -
-v : print version
-h : print usage

Drone only:
-p <port-number> : use given port number for the RPC service
2019-07-02 18:25:31 +05:30
Srivats P
3fca24396d Report "Apply" time in logs 2019-07-02 18:24:55 +05:30
Srivats P
c43f7d1769 Typecast to quint64 before passing to qToBigEndian
For 64-bit arch, Qt defines quint64 as unsigned long long (and quint32 as unsigned int), but Protobuf's google::protobuf::uint64 is defined as unsigned long. Now Qt defines qbswap only for the Qt defined 8/16/32/64 integer types aka q[u]intXXX. So qbswap<unsigned long long> and qbswap<unsigned int> is defined but not qbswap<unsigned long>.

mld.cpp was using qToBigEndian (which uses qbswap in turn) with a protobuf uint64 triggering the undefined references on some 64bit platforms.

Fixes #265
2019-06-26 22:42:09 +05:30
Srivats P
913ef5c0ee Add start/stop tx buttons to stream list window 2019-06-19 22:22:01 +05:30
Srivats P
bb079b9508 Use Qt5's prettyProductName to get system info 2019-06-06 21:36:12 +05:30
Srivats P
64d4b38f41 Add protocol error checks to preflight check
Also commented out stream transmit duration check that was causing false
positives
2019-06-05 11:39:09 +05:30
Srivats P
6817d3f870 Resolve neighbors before build as part of "apply" 2019-05-31 18:34:07 +05:30
Srivats P
edc7ed677c Mark sync complete after build finish during apply
Since build is now the last step during "apply", use that to mark sync
complete. Since build will always be called irrespective of whether any
changes were made to devices/streams, we can now send the stream RPCs
only if required.

Additionally log RPC calls only if they are actually made.
2019-05-28 19:02:50 +05:30
Srivats P
7cf323202f Add new build() RPC
Make build an explicit RPC so that clients can call resolveNeighbor
before build
2019-05-28 18:46:58 +05:30
Srivats P
a4bd6212ef Merge branch 'master' of D:/srivatsp/projects/ostinato/master 2019-05-22 18:51:25 +05:30
Srivats P
dbbb7597a4 Add Check for updates to main menu
Results will be displayed in a message box - if we have a new version or
if we are running the latest version.

Update check at startup will show message box only once in 5 days, other
times it will be shown in the status bar. If we are already on latest
version, nothing is shown.
2019-05-22 18:49:51 +05:30
Srivats P
52b522f92a Show new version availability in a message box 2019-05-22 18:49:51 +05:30
Srivats P
13eed5f528
Merge pull request #275 from pstavirs/hostdev
Hostdev
2019-04-11 21:38:42 +05:30
Srivats P
cd9758f165 Merge branch 'master' into hostdev 2019-04-11 18:56:49 +05:30
Srivats P
2cd64061d1 Fix warning - no matching signal
For slot MainWindow::on_actionOpenSession_triggered(QString)
2019-02-26 18:12:18 +05:30
Srivats P
983a155ccf Update Qt icon for Help | About Qt menu item 2019-02-22 21:54:51 +05:30
Srivats P
0d1b4be682 Connect to local portgroup later in the app startup sequence
This allows drone to init itself and wait for connections before we
attempt to connect to it

Fixes #266
2019-02-22 20:34:59 +05:30
Srivats P
00bc01898d Show a progress dialog during Apply
The most time spent during apply is during packet rebuilding.
Unfortunately, there is no feedback from the drone to know the actual
progress, so this is just a busy indicator for now.

I tried to put a jump url to the FAQ on why it takes time, but 'coz the
app cursor is a "wait cursor", user cannot click on any widgets/elements
inside the dialog box - so this won't work.

Iterate on this dialog and what it displays based on user feedback.
2019-02-22 18:58:13 +05:30
Srivats P
adfe1380da Print drone version/revision first thing at startup
Currently these are printed at the end of init when we start waiting for
client connections - this was done so that this info doesn't get lost in
the barrage of init logs. The downside is if there is a crash during
init, we don't get to know the version/revision from the logs. With this
change this info is printed twice - first thing when we start and just
after init when we start waiting for client connections
2019-02-18 18:16:36 +05:30
Srivats P
9934060216 Use default travis macOS/xcode image for build 2018-12-08 13:10:28 +05:30
253 changed files with 17022 additions and 1812 deletions

76
.github/CODE_OF_CONDUCT.md vendored Normal file
View File

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at support@ostinato.org. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

16
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,16 @@
# Contributing Guidelines
Please use a Pull Request to contribute code. Very small fixes (< 10 lines) can provide the diff via an issue instead of a PR.
In either case, you agree to the below legal terms and you indicate your acceptance by explicitly adding a comment to the issue or PR stating -
```
I have read the contributing guidelines (CONTRIBUTING.md) and I hereby assign the copyrights of these
changes to [Srivats P](https://github.com/pstavirs).
```
## Legal
By submitting a Pull Request, you disavow any rights or claims to any changes submitted to the Ostinato project and assign the copyright of those changes to [Srivats P](https://github.com/pstavirs).
If you cannot or do not want to reassign those rights (your employment contract for your employer may not allow this), you should not submit a PR. Open an issue and someone else can do the work.
This is a legal way of saying "_If you submit a PR to us, that code becomes ours_". 99.9% of the time that's what you intend anyways; we hope it doesn't scare you away from contributing.

1
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1 @@
github: pstavirs

View File

@ -1,4 +1,5 @@
language: cpp
osx_image: xcode11.3
os:
- linux
@ -13,17 +14,14 @@ matrix:
- os: osx
compiler: gcc
before_install:
before_script:
- "if [ $TRAVIS_OS_NAME = 'osx' ]; then \
brew update && \
brew install qt5 && \
brew link qt5 --force && \
brew install protobuf && \
ls -lR /usr/local/include/google/protobuf; \
which clang++; \
clang++ -E -x c++ - -v < /dev/null; \
export CPLUS_INCLUDE_PATH=/usr/local/include; \
export LIBRARY_PATH=/usr/local/lib; \
export PATH=/usr/local/opt/qt/bin:$PATH; \
fi"
addons:
@ -31,12 +29,16 @@ addons:
packages:
- qtbase5-dev
- qtscript5-dev
- libqt5svg5-dev
- libpcap-dev
- libprotobuf-dev
- protobuf-compiler
- libnl-3-dev
- libnl-route-3-dev
homebrew:
packages:
- qt5
- protobuf
script:
- QT_SELECT=qt5 qmake -config debug
- make

View File

@ -1,9 +1,23 @@
# Ostinato
[![Build Status](https://travis-ci.org/pstavirs/ostinato.svg?branch=master)](https://travis-ci.org/pstavirs/ostinato)
[![Build Status](https://app.travis-ci.com/pstavirs/ostinato.svg?branch=master)](https://app.travis-ci.com/pstavirs/ostinato)
This is the code repository for the Ostinato network packet crafter and traffic generator
Visit https://ostinato.org for demo video and details
License: GPLv3+ (see [COPYING](https://raw.githubusercontent.com/pstavirs/ostinato/master/COPYING))
Source License: GPLv3+ (see [COPYING](https://raw.githubusercontent.com/pstavirs/ostinato/master/COPYING))
## Author's note
I have been developing and maintaining Ostinato [single-handedly](https://github.com/pstavirs/ostinato/graphs/contributors) for more than 12 years. And during this time I have grudgingly come around to the view that open source cannot survive and thrive without money. Mixing money with open-source is messy, but I don't see a way forward unless we as a community become open to the idea of talking about it and changing our culture so that money is no longer a bad word.
I sell binary licenses on [ostinato.org](https://ostinato.org/downloads) to try and cover the costs of development. Please consider buying those - they are priced low enough that you can afford it or you could just as easily expense them to your organisation.
If you build Ostinato from source and find it useful, please sponsor to keep the lights on and sustain the project.
[![](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=%23fe8e86&style=for-the-badge)](https://github.com/sponsors/pstavirs)
Read the Ostinato [origin story](https://ostinato.org/about).
Srivats P<br/>
Author, Ostinato

View File

@ -94,7 +94,7 @@
<item>
<widget class="QLabel" name="CopyrightLabel" >
<property name="text" >
<string>Copyright (c) 2007-2018 Srivats P.</string>
<string>Copyright (c) 2007-2023 Srivats P.</string>
</property>
<property name="alignment" >
<set>Qt::AlignCenter</set>

86
client/applymsg.h Normal file
View File

@ -0,0 +1,86 @@
/*
Copyright (C) 2019 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _APPLY_MESSAGE_H
#define _APPLY_MESSAGE_H
#include <QDialog>
#include <QMainWindow>
#include <QProgressBar>
#include <QTimer>
extern QMainWindow *mainWindow;
class ApplyMessage: public QDialog
{
public:
ApplyMessage(QWidget *parent = Q_NULLPTR);
public slots:
void show();
virtual void done(int r);
private:
QLabel *help_;
QTimer *helpTimer_;
};
ApplyMessage::ApplyMessage(QWidget *parent)
: QDialog(parent)
{
auto layout = new QVBoxLayout(this);
auto message = new QLabel(tr("Pushing configuration changes to agent "
"and re-building packets ..."), this);
auto progress = new QProgressBar(this);
progress->setRange(0, 0);
progress->setTextVisible(false);
help_ = new QLabel(tr("<b>This may take some time</b>"), this);
help_->setAlignment(Qt::AlignCenter);
layout->addWidget(message);
layout->addWidget(progress);
layout->addWidget(help_);
helpTimer_ = new QTimer(this);
helpTimer_->setSingleShot(true);
helpTimer_->setInterval(4000);
connect(helpTimer_, SIGNAL(timeout()), help_, SLOT(show()));
}
void ApplyMessage::show()
{
help_->hide(); // shown when helpTimer_ expires
QWidget *parent = parentWidget();
if (!parent)
parent = mainWindow;
move(parent->frameGeometry().center() - rect().center());
setModal(true);
QDialog::show();
helpTimer_->start();
}
void ApplyMessage::done(int r)
{
helpTimer_->stop();
QDialog::done(r);
}
#endif

View File

@ -136,6 +136,11 @@ QVariant ArpStatusModel::data(const QModelIndex &index, int role) const
return QVariant();
}
Qt::DropActions ArpStatusModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
void ArpStatusModel::setDeviceIndex(Port *port, int deviceIndex)
{
beginResetModel();

View File

@ -40,6 +40,8 @@ public:
int role = Qt::DisplayRole) const;
QVariant data(const QModelIndex &index, int role) const;
Qt::DropActions supportedDropActions() const;
void setDeviceIndex(Port *port, int deviceIndex);
public slots:

221
client/clipboardhelper.cpp Normal file
View File

@ -0,0 +1,221 @@
/*
Copyright (C) 2020 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "clipboardhelper.h"
#include "xtableview.h"
#include <QAction>
#include <QApplication>
#include <QClipboard>
#include <QIcon>
#include <QItemSelection>
#if 0 // change 0 to 1 for debug
#define xDebug(...) qDebug(__VA_ARGS__)
#else
#define xDebug(...)
#endif
ClipboardHelper::ClipboardHelper(QObject *parent)
: QObject(parent)
{
actionCut_ = new QAction(tr("&Cut"), this);
actionCut_->setObjectName(QStringLiteral("actionCut"));
actionCut_->setIcon(QIcon(QString::fromUtf8(":/icons/cut.png")));
actionCopy_ = new QAction(tr("Cop&y"), this);
actionCopy_->setObjectName(QStringLiteral("actionCopy"));
actionCopy_->setIcon(QIcon(QString::fromUtf8(":/icons/copy.png")));
actionPaste_ = new QAction(tr("&Paste"), this);
actionPaste_->setObjectName(QStringLiteral("actionPaste"));
actionPaste_->setIcon(QIcon(QString::fromUtf8(":/icons/paste.png")));
connect(actionCut_, SIGNAL(triggered()), SLOT(actionTriggered()));
connect(actionCopy_, SIGNAL(triggered()), SLOT(actionTriggered()));
connect(actionPaste_, SIGNAL(triggered()), SLOT(actionTriggered()));
// XXX: failsafe in case updation of cut/copy/status causes issues
// Temporary for 1 release - will be removed after that
if (qEnvironmentVariableIsSet("X-OSTINATO-CCP-STATUS")) {
qWarning("FAILSAFE: Cut-Copy-Paste action status will not be updated");
return;
}
connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)),
SLOT(updateCutCopyStatus(QWidget*, QWidget*)));
connect(qApp, SIGNAL(focusChanged(QWidget*, QWidget*)),
SLOT(updatePasteStatus()));
connect(QGuiApplication::clipboard(), SIGNAL(dataChanged()),
SLOT(updatePasteStatus()));
}
QList<QAction*> ClipboardHelper::actions()
{
QList<QAction*> actionList({actionCut_, actionCopy_, actionPaste_});
return actionList;
}
void ClipboardHelper::actionTriggered()
{
QWidget *focusWidget = qApp->focusWidget();
if (!focusWidget)
return;
// single slot to handle cut/copy/paste - find which action was triggered
QString action = sender()->objectName()
.remove("action").append("()").toLower();
if (focusWidget->metaObject()->indexOfSlot(qPrintable(action)) < 0) {
xDebug("%s slot not found for object %s:%s ",
qPrintable(action),
qPrintable(focusWidget->objectName()),
focusWidget->metaObject()->className());
return;
}
action.remove("()");
QMetaObject::invokeMethod(focusWidget, qPrintable(action),
Qt::DirectConnection);
}
void ClipboardHelper::updateCutCopyStatus(QWidget *old, QWidget *now)
{
xDebug("In %s", __FUNCTION__);
const XTableView *view = dynamic_cast<XTableView*>(old);
if (view) {
disconnect(view->selectionModel(),
SIGNAL(selectionChanged(const QItemSelection&,
const QItemSelection&)),
this,
SLOT(focusWidgetSelectionChanged(const QItemSelection&,
const QItemSelection&)));
disconnect(view->model(), SIGNAL(modelReset()),
this, SLOT(focusWidgetModelReset()));
}
if (!now) {
xDebug("No focus widget to copy from");
actionCut_->setEnabled(false);
actionCopy_->setEnabled(false);
return;
}
const QMetaObject *meta = now->metaObject();
if (meta->indexOfSlot("copy()") < 0) {
xDebug("Focus Widget (%s) doesn't have a copy slot",
qPrintable(now->objectName()));
actionCut_->setEnabled(false);
actionCopy_->setEnabled(false);
return;
}
view = dynamic_cast<XTableView*>(now);
if (view) {
connect(view->selectionModel(),
SIGNAL(selectionChanged(const QItemSelection&,
const QItemSelection&)),
SLOT(focusWidgetSelectionChanged(const QItemSelection&,
const QItemSelection&)));
connect(view->model(), SIGNAL(modelReset()),
this, SLOT(focusWidgetModelReset()));
if (!view->hasSelection()) {
xDebug("%s doesn't have anything selected to copy",
qPrintable(view->objectName()));
actionCut_->setEnabled(false);
actionCopy_->setEnabled(false);
return;
}
xDebug("%s model can cut: %d", qPrintable(view->objectName()),
view->canCut());
actionCut_->setEnabled(view->canCut());
}
xDebug("%s has a selection and copy slot: copy possible",
qPrintable(now->objectName()));
actionCopy_->setEnabled(true);
}
void ClipboardHelper::focusWidgetSelectionChanged(
const QItemSelection &selected, const QItemSelection &/*deselected*/)
{
xDebug("In %s", __FUNCTION__);
// Selection changed in the XTableView that has focus
const XTableView *view = dynamic_cast<XTableView*>(qApp->focusWidget());
xDebug("canCut:%d empty:%d", view->canCut(), selected.indexes().isEmpty());
actionCut_->setEnabled(!selected.indexes().isEmpty()
&& view && view->canCut());
actionCopy_->setEnabled(!selected.indexes().isEmpty());
}
void ClipboardHelper::updatePasteStatus()
{
xDebug("In %s", __FUNCTION__);
QWidget *focusWidget = qApp->focusWidget();
if (!focusWidget) {
xDebug("No focus widget to paste into");
actionPaste_->setEnabled(false);
return;
}
const QMimeData *item = QGuiApplication::clipboard()->mimeData();
if (!item || item->formats().isEmpty()) {
xDebug("Nothing on clipboard to paste");
actionPaste_->setEnabled(false);
return;
}
const QMetaObject *meta = focusWidget->metaObject();
if (meta->indexOfSlot("paste()") < 0) {
xDebug("Focus Widget (%s) doesn't have a paste slot",
qPrintable(focusWidget->objectName()));
actionPaste_->setEnabled(false);
return;
}
const XTableView *view = dynamic_cast<XTableView*>(focusWidget);
if (view && !view->canPaste(item)) {
xDebug("%s cannot accept this item (%s)",
qPrintable(view->objectName()),
qPrintable(item->formats().join("|")));
actionPaste_->setEnabled(false);
return;
}
xDebug("%s can accept this item (%s): paste possible",
qPrintable(focusWidget->objectName()),
qPrintable(item->formats().join("|")));
actionPaste_->setEnabled(true);
}
void ClipboardHelper::focusWidgetModelReset()
{
xDebug("In %s", __FUNCTION__);
QWidget *focusWidget = qApp->focusWidget();
updateCutCopyStatus(focusWidget, focusWidget); // re-eval cut/copy status
}
#undef xDebug

51
client/clipboardhelper.h Normal file
View File

@ -0,0 +1,51 @@
/*
Copyright (C) 2020 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _CLIPBOARD_HELPER_H
#define _CLIPBOARD_HELPER_H
#include <QObject>
#include <QList>
class QAction;
class QItemSelection;
class ClipboardHelper : public QObject
{
Q_OBJECT
public:
ClipboardHelper(QObject *parent=nullptr);
QList<QAction*> actions();
private slots:
void actionTriggered();
void updateCutCopyStatus(QWidget *old, QWidget *now);
void focusWidgetSelectionChanged(const QItemSelection &selected,
const QItemSelection &deselected);
void focusWidgetModelReset();
void updatePasteStatus();
private:
QAction *actionCut_{nullptr};
QAction *actionCopy_{nullptr};
QAction *actionPaste_{nullptr};
};
#endif

View File

@ -209,7 +209,7 @@ void DeviceGroupDialog::updateTotalDeviceCount()
void DeviceGroupDialog::updateIp4Gateway()
{
quint32 net = ip4Address->value() & (~0 << (32 - ip4PrefixLength->value()));
quint32 net = ip4Address->value() & (~0UL << (32 - ip4PrefixLength->value()));
ip4Gateway->setValue(net | 0x01);
}

View File

@ -25,6 +25,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "uint128.h"
#include <QHostAddress>
#include <QMimeData>
const QLatin1String kDeviceGroupsMimeType(
"application/vnd.ostinato.devicegroups");
enum {
kName,
@ -205,6 +209,85 @@ bool DeviceGroupModel::setData(
return false;
}
QStringList DeviceGroupModel::mimeTypes() const
{
return QStringList() << kDeviceGroupsMimeType;
}
QMimeData* DeviceGroupModel::mimeData(const QModelIndexList &indexes) const
{
using ::google::protobuf::uint8;
if (indexes.isEmpty())
return nullptr;
// indexes may include multiple columns for a row - but we are only
// interested in rows 'coz we have a single data for all columns
// XXX: use QMap instead of QSet to keep rows in sorted order
QMap<int, int> rows;
foreach(QModelIndex index, indexes)
rows.insert(index.row(), index.row());
OstProto::DeviceGroupConfigList dgList;
dgList.mutable_port_id()->set_id(port_->id());
foreach(int row, rows) {
OstProto::DeviceGroup *devGrp = dgList.add_device_group();
devGrp->CopyFrom(*port_->deviceGroupByIndex(row));
}
QByteArray data;
data.resize(dgList.ByteSize());
dgList.SerializeWithCachedSizesToArray((uint8*)data.data());
//qDebug("copy %s", dgList.DebugString().c_str());
//TODO: copy DebugString as text/plain?
QMimeData *mimeData = new QMimeData();
mimeData->setData(kDeviceGroupsMimeType, data);
return mimeData; // XXX: caller is expected to take ownership and free!
}
bool DeviceGroupModel::dropMimeData(const QMimeData *data,
Qt::DropAction action, int row, int /*column*/,
const QModelIndex &parent)
{
if (!data)
return false;
if (!data->hasFormat(kDeviceGroupsMimeType))
return false;
if (action != Qt::CopyAction)
return false;
OstProto::DeviceGroupConfigList dgList;
QByteArray ba(data->data(kDeviceGroupsMimeType));
dgList.ParseFromArray((void*)ba.constData(), ba.size());
//qDebug("paste %s", dgList.DebugString().c_str());
if ((row < 0) || (row > rowCount(parent)))
row = rowCount(parent);
// Delete rows that we are going to overwrite
int c = 0, count = dgList.device_group_size();
if (row < rowCount(parent))
removeRows(row, qMin(rowCount() - row, count));
beginInsertRows(parent, row, row+count-1);
for (int i = 0; i < count; i++) {
if (port_->newDeviceGroupAt(row+i, &dgList.device_group(i)))
c++;
}
endInsertRows();
if (c != count) {
qWarning("failed to copy rows in DeviceGroupModel at row %d; "
"requested = %d, actual = %d", row, count, c);
return false;
}
return true;
}
bool DeviceGroupModel::insertRows(
int row,
int count,

View File

@ -42,6 +42,12 @@ public:
QVariant data(const QModelIndex &index, int role) const;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole);
QStringList mimeTypes() const;
QMimeData* mimeData(const QModelIndexList &indexes) const;
bool dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent);
bool insertRows (int row, int count,
const QModelIndex &parent = QModelIndex());
bool removeRows (int row, int count,

View File

@ -242,6 +242,11 @@ QVariant DeviceModel::data(const QModelIndex &index, int role) const
return QVariant();
}
Qt::DropActions DeviceModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
void DeviceModel::setPort(Port *port)
{
beginResetModel();

View File

@ -39,6 +39,8 @@ public:
int role = Qt::DisplayRole) const;
QVariant data(const QModelIndex &index, int role) const;
Qt::DropActions supportedDropActions() const;
void setPort(Port *port);
QAbstractItemModel* detailModel(const QModelIndex &index);

View File

@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "deviceswidget.h"
#include "clipboardhelper.h"
#include "devicegroupdialog.h"
#include "port.h"
#include "portgrouplist.h"
@ -26,6 +27,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include <QHeaderView>
#include <QKeyEvent>
extern ClipboardHelper *clipboardHelper;
DevicesWidget::DevicesWidget(QWidget *parent)
: QWidget(parent), portGroups_(NULL)
{
@ -49,6 +52,14 @@ DevicesWidget::DevicesWidget(QWidget *parent)
// DevicesWidget's actions is an aggegate of all sub-widget's actions
addActions(deviceGroupList->actions());
// Add the clipboard actions to the context menu of deviceGroupList
// but not to DeviceWidget's actions since they are already available
// in the global Edit Menu
QAction *sep = new QAction("Clipboard", this);
sep->setSeparator(true);
deviceGroupList->addAction(sep);
deviceGroupList->addActions(clipboardHelper->actions());
}
void DevicesWidget::setPortGroupList(PortGroupList *portGroups)

View File

@ -158,8 +158,8 @@ To emulate a device, click on Configuration and create a device group</string>
<property name="whatsThis">
<string>IP neighbor cache is empty</string>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>

View File

@ -262,8 +262,10 @@ void DumpView::paintEvent(QPaintEvent* /*event*/)
// FIXME(LOW): unable to set the self widget's font in constructor
painter.setFont(QFont("Courier"));
// set a white background
painter.fillRect(rect(), QBrush(QColor(Qt::white)));
// Qt automatically clears the background before we are called
// QWidget::paintEvent doc:
// When the paint event occurs, the update region has normally
// been erased, so you are painting on the widget's background.
if (model())
{

98
client/fieldedit.cpp Normal file
View File

@ -0,0 +1,98 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "fieldedit.h"
FieldEdit::FieldEdit(QWidget *parent)
: QLineEdit(parent)
{
setType(kUInt64);
}
void FieldEdit::setType(FieldType type)
{
// clear existing contents before changing the validator
clear();
setPlaceholderText("");
type_ = type;
switch (type_) {
case kUInt64:
setValidator(&uint64Validator_);
if (isMaskMode_)
setText("0xFFFFFFFFFFFFFFFF");
break;
case kMacAddress:
setValidator(&macValidator_);
setPlaceholderText("00:00:00:00:00:00");
if (isMaskMode_)
setText("FF:FF:FF:FF:FF:FF");
break;
case kIp4Address:
setValidator(&ip4Validator_);
setPlaceholderText("0.0.0.0");
if (isMaskMode_)
setText("255.255.255.255");
break;
case kIp6Address:
setValidator(&ip6Validator_);
setPlaceholderText("::");
if (isMaskMode_)
setText("FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF");
break;
default:
setValidator(nullptr);
break;
}
}
// Applicable only if type is kUInt64
void FieldEdit::setRange(quint64 min, quint64 max)
{
uint64Validator_.setRange(min, max);
if (type_ == kUInt64) {
setPlaceholderText(QString("%1 - %2").arg(min).arg(max));
if (isMaskMode_)
setText(QString::number(max, 16).toUpper().prepend("0x"));
}
}
void FieldEdit::setMaskMode(bool maskMode)
{
isMaskMode_ = maskMode;
}
QString FieldEdit::text() const
{
QString str = QLineEdit::text();
switch (type_) {
case kMacAddress:
str.remove(QRegularExpression("[:-]"));
str.prepend("0x");
break;
case kIp4Address:
str = QString::number(QHostAddress(str).toIPv4Address());
break;
default:
break;
}
return str;
}

60
client/fieldedit.h Normal file
View File

@ -0,0 +1,60 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _FIELD_EDIT_H
#define _FIELD_EDIT_H
#include "ipv4addressvalidator.h"
#include "ipv6addressvalidator.h"
#include "macaddressvalidator.h"
#include "ulonglongvalidator.h"
#include <QLineEdit>
class FieldEdit: public QLineEdit
{
Q_OBJECT
public:
enum FieldType {
kUInt64,
kMacAddress,
kIp4Address,
kIp6Address
};
FieldEdit(QWidget *parent = 0);
void setType(FieldType type);
void setRange(quint64 min, quint64 max);
void setMaskMode(bool maskMode);
QString text() const;
private:
FieldType type_{kUInt64};
bool isMaskMode_{false};
IPv4AddressValidator ip4Validator_;
IPv6AddressValidator ip6Validator_;
MacAddressValidator macValidator_;
ULongLongValidator uint64Validator_;
};
#endif

230
client/findreplace.cpp Normal file
View File

@ -0,0 +1,230 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "findreplace.h"
#include "abstractprotocol.h"
#include "iputils.h"
#include "mandatoryfieldsgroup.h"
#include "protocolmanager.h"
#include "stream.h"
#include "uint128.h"
#include <QPushButton>
extern ProtocolManager *OstProtocolManager;
// TODO: It might be useful for this dialog to support a "preview"
// of the replacements
FindReplaceDialog::FindReplaceDialog(Action *action, QWidget *parent)
: QDialog(parent), action_(action)
{
setupUi(this);
findMask->setMaskMode(true);
replaceMask->setMaskMode(true);
// Keep things simple and don't use mask(s) (default)
useFindMask->setChecked(false);
useReplaceMask->setChecked(false);
// TODO: remove combo protocols - see note in StreamBase::findReplace
QStringList protocolList = OstProtocolManager->protocolDatabase();
protocolList.sort();
protocol->addItems(protocolList);
// Enable this setting if we have streams selected on input
selectedStreamsOnly->setEnabled(action->selectedStreamsOnly);
// Reset for user input
action->selectedStreamsOnly = false;
QPushButton *ok = buttonBox->button(QDialogButtonBox::Ok);
ok->setText(tr("Replace All"));
ok->setDisabled(true);
mandatoryFields_ = new MandatoryFieldsGroup(this);
mandatoryFields_->add(protocol);
mandatoryFields_->add(field);
mandatoryFields_->add(findValue);
mandatoryFields_->add(findMask);
mandatoryFields_->add(replaceValue);
mandatoryFields_->add(replaceMask);
mandatoryFields_->setSubmitButton(ok);
}
FindReplaceDialog::~FindReplaceDialog()
{
delete mandatoryFields_;
}
void FindReplaceDialog::on_protocol_currentIndexChanged(const QString &name)
{
field->clear();
fieldAttrib_.clear();
Stream stream;
AbstractProtocol *protocol = OstProtocolManager->createProtocol(
name, &stream);
int count = protocol->fieldCount();
for (int i = 0; i < count; i++) {
// XXX: It might be useful to support meta fields too, later!
if (!protocol->fieldFlags(i).testFlag(AbstractProtocol::FrameField))
continue;
int bitSize = protocol->fieldData(i, AbstractProtocol::FieldBitSize)
.toInt();
if (bitSize <= 0) // skip optional fields
continue;
FieldAttrib fieldAttrib;
fieldAttrib.index = i; // fieldIndex
fieldAttrib.bitSize = bitSize;
// field and fieldAttrib_ have same count and order of fields
fieldAttrib_.append(fieldAttrib);
field->addItem(protocol->fieldData(i, AbstractProtocol::FieldName)
.toString());
}
protocolId_ = protocol->protocolNumber();
delete protocol;
}
void FindReplaceDialog::on_field_currentIndexChanged(int index)
{
if (index < 0)
return;
QString fieldName = field->currentText();
FieldAttrib fieldAttrib = fieldAttrib_.at(index);
// Use heuristics to determine field type
if (fieldAttrib.bitSize == 48) {
findMask->setType(FieldEdit::kMacAddress);
findValue->setType(FieldEdit::kMacAddress);
replaceMask->setType(FieldEdit::kMacAddress);
replaceValue->setType(FieldEdit::kMacAddress);
} else if ((fieldAttrib.bitSize == 32)
&& (fieldName.contains(QRegularExpression(
"address|source|destination",
QRegularExpression::CaseInsensitiveOption)))) {
findMask->setType(FieldEdit::kIp4Address);
findValue->setType(FieldEdit::kIp4Address);
replaceMask->setType(FieldEdit::kIp4Address);
replaceValue->setType(FieldEdit::kIp4Address);
} else if ((fieldAttrib.bitSize == 128)
&& (fieldName.contains(QRegularExpression(
"address|source|destination",
QRegularExpression::CaseInsensitiveOption)))) {
findMask->setType(FieldEdit::kIp6Address);
findValue->setType(FieldEdit::kIp6Address);
replaceMask->setType(FieldEdit::kIp6Address);
replaceValue->setType(FieldEdit::kIp6Address);
} else {
quint64 max = quint64(~0) >> (64-fieldAttrib.bitSize);
qDebug("XXXXXX %s bitSize %d max %llx",
qPrintable(field->currentText()),
fieldAttrib.bitSize, max);
findMask->setType(FieldEdit::kUInt64);
findMask->setRange(0, max);
findValue->setType(FieldEdit::kUInt64);
findValue->setRange(0, max);
replaceMask->setType(FieldEdit::kUInt64);
replaceMask->setRange(0, max);
replaceValue->setType(FieldEdit::kUInt64);
replaceValue->setRange(0, max);
}
}
void FindReplaceDialog::on_matchAny_toggled(bool checked)
{
if (checked) {
findValueLabel->setHidden(true);
findValue->setHidden(true);
useFindMask->setHidden(true);
findMask->setHidden(true);
findMaskHint->setHidden(true);
} else {
findValueLabel->setVisible(true);
findValue->setVisible(true);
useFindMask->setVisible(true);
if (useFindMask->isChecked()) {
findMask->setVisible(true);
findMaskHint->setVisible(true);
}
}
}
void FindReplaceDialog::on_buttonBox_accepted()
{
FieldAttrib fieldAttrib = fieldAttrib_.at(field->currentIndex());
action_->protocolField = QString("%1 %2")
.arg(protocol->currentText())
.arg(field->currentText());
action_->protocolNumber = protocolId_;
action_->fieldIndex = fieldAttrib.index;
action_->fieldBitSize = fieldAttrib.bitSize;
if (fieldAttrib.bitSize == 128) { // IPv6 address
if (matchAny->isChecked()) {
action_->findMask.setValue(UInt128(0));
action_->findValue.setValue(UInt128(0));
} else {
action_->findMask.setValue(
useFindMask->isChecked() ?
ipUtils::ip6StringToUInt128(findMask->text()) :
~UInt128(0));
action_->findValue.setValue(
ipUtils::ip6StringToUInt128(findValue->text()));
}
action_->replaceMask.setValue(
useReplaceMask->isChecked() ?
ipUtils::ip6StringToUInt128(replaceMask->text()) :
~UInt128(0));
action_->replaceValue.setValue(
ipUtils::ip6StringToUInt128(replaceValue->text()));
} else { // everything else
if (matchAny->isChecked()) {
action_->findMask.setValue(0);
action_->findValue.setValue(0);
} else {
action_->findMask.setValue(
useFindMask->isChecked() ?
findMask->text().toULongLong(nullptr, 0) :
~quint64(0));
action_->findValue.setValue(
findValue->text().toULongLong(nullptr, 0));
}
action_->replaceMask.setValue(
useReplaceMask->isChecked() ?
replaceMask->text().toULongLong(nullptr, 0) :
~quint64(0));
action_->replaceValue.setValue(QString::number(
replaceValue->text().toULongLong(nullptr, 0)));
}
action_->selectedStreamsOnly = selectedStreamsOnly->isChecked();
}

71
client/findreplace.h Normal file
View File

@ -0,0 +1,71 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _FIND_REPLACE_H
#define _FIND_REPLACE_H
#include "ui_findreplace.h"
class MandatoryFieldsGroup;
class FindReplaceDialog: public QDialog, public Ui::FindReplace
{
Q_OBJECT
public:
struct Action;
FindReplaceDialog(Action *action, QWidget *parent = 0);
~FindReplaceDialog();
private slots:
void on_protocol_currentIndexChanged(const QString &name);
void on_field_currentIndexChanged(int index);
void on_matchAny_toggled(bool checked);
void on_buttonBox_accepted();
private:
struct FieldAttrib;
quint32 protocolId_{0};
Action *action_{nullptr};
QList<FieldAttrib> fieldAttrib_;
MandatoryFieldsGroup *mandatoryFields_{nullptr};
};
struct FindReplaceDialog::Action
{
QString protocolField;
quint32 protocolNumber;
quint32 fieldIndex;
int fieldBitSize;
QVariant findValue;
QVariant findMask;
QVariant replaceValue;
QVariant replaceMask;
bool selectedStreamsOnly; // in-out param
};
struct FindReplaceDialog::FieldAttrib
{
quint32 index;
int bitSize;
};
#endif

265
client/findreplace.ui Normal file
View File

@ -0,0 +1,265 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FindReplace</class>
<widget class="QDialog" name="FindReplace">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>361</width>
<height>309</height>
</rect>
</property>
<property name="windowTitle">
<string>Find &amp; Replace</string>
</property>
<property name="windowIcon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/find.png</normaloff>:/icons/find.png</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="find">
<property name="title">
<string>Find</string>
</property>
<layout class="QGridLayout" name="gridLayout" columnstretch="0,1,0">
<item row="0" column="0">
<widget class="QLabel" name="protocolLabel">
<property name="text">
<string>Protocol</string>
</property>
<property name="buddy">
<cstring>protocol</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="QComboBox" name="protocol"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="fieldLabel">
<property name="text">
<string>Field</string>
</property>
<property name="buddy">
<cstring>field</cstring>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QComboBox" name="field"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="findValueLabel">
<property name="text">
<string>Value</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="2">
<widget class="FieldEdit" name="findValue"/>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="useFindMask">
<property name="text">
<string>Mask</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="FieldEdit" name="findMask"/>
</item>
<item row="3" column="2">
<widget class="QLabel" name="findMaskHint">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;Matches a field only if &lt;span style=&quot;white-space:nowrap&quot;&gt;(FieldValue &amp;amp; FindMask) = FindValue&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="pixmap">
<pixmap resource="ostinato.qrc">:/icons/info.png</pixmap>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QCheckBox" name="matchAny">
<property name="text">
<string>Match any value</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="replace">
<property name="title">
<string>Replace with</string>
</property>
<layout class="QGridLayout" name="gridLayout_2" columnstretch="0,1,0">
<item row="0" column="0">
<widget class="QLabel" name="replaceValueLabel">
<property name="text">
<string>Value</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="2">
<widget class="FieldEdit" name="replaceValue"/>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="useReplaceMask">
<property name="text">
<string>Mask</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="FieldEdit" name="replaceMask"/>
</item>
<item row="1" column="2">
<widget class="QLabel" name="replaceMaskHint">
<property name="toolTip">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;New field value = &lt;span style=&quot;white-space:nowrap&quot;&gt;(OldFieldValue &amp;amp; ~ReplaceMask) | (ReplaceValue &amp;amp; ReplaceMask)&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="pixmap">
<pixmap resource="ostinato.qrc">:/icons/info.png</pixmap>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="selectedStreamsOnly">
<property name="text">
<string>Selected Streams Only</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>FieldEdit</class>
<extends>QLineEdit</extends>
<header>fieldedit.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="ostinato.qrc"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>FindReplace</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>277</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>FindReplace</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>283</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>useFindMask</sender>
<signal>toggled(bool)</signal>
<receiver>findMask</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>60</x>
<y>115</y>
</hint>
<hint type="destinationlabel">
<x>76</x>
<y>119</y>
</hint>
</hints>
</connection>
<connection>
<sender>useReplaceMask</sender>
<signal>toggled(bool)</signal>
<receiver>replaceMask</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>56</x>
<y>228</y>
</hint>
<hint type="destinationlabel">
<x>73</x>
<y>227</y>
</hint>
</hints>
</connection>
<connection>
<sender>useReplaceMask</sender>
<signal>toggled(bool)</signal>
<receiver>replaceMaskHint</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>50</x>
<y>230</y>
</hint>
<hint type="destinationlabel">
<x>333</x>
<y>233</y>
</hint>
</hints>
</connection>
<connection>
<sender>useFindMask</sender>
<signal>toggled(bool)</signal>
<receiver>findMaskHint</receiver>
<slot>setVisible(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>46</x>
<y>116</y>
</hint>
<hint type="destinationlabel">
<x>335</x>
<y>122</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -31,9 +31,13 @@ class IconOnlyDelegate : public QStyledItemDelegate
{
QStyleOptionViewItem opt = option;
opt.decorationPosition = QStyleOptionViewItem::Top;
opt.features &= ~QStyleOptionViewItem::HasDisplay;
QStyledItemDelegate::paint(painter, opt, index);
}
QString displayText(const QVariant&, const QLocale&) const
{
return QString();
}
};
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 16 KiB

BIN
client/icons/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

BIN
client/icons/cut.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

23
client/icons/error.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="31.44mm" height="31.44mm" version="1.1" viewBox="0 0 31.44 31.44" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(-37.525 -210.5)">
<circle cx="53.245" cy="226.22" r="15.72" fill="#e96c59" opacity=".8"/>
<path d="m53.419 212.28c-2.0736-0.0217-4.1616 0.41684-6.0884 1.3285-4.4585 2.1095-7.4154 6.2656-7.9501 11.175-0.50808 4.6643 1.5269 9.4967 5.2044 12.359 2.066 1.6079 4.6873 2.6928 7.1183 2.9462 2.643 0.2755 5.2282-0.17492 7.6011-1.3243 1.9456-0.94242 3.6302-2.2856 4.9044-3.9102 3.2672-4.1658 3.8994-9.8451 1.6275-14.62-1.0934-2.298-2.6916-4.1431-4.8639-5.6154-2.245-1.5216-4.8872-2.3101-7.5533-2.338zm-0.0276 2.2048c2.2445 0.0235 4.4688 0.68733 6.3588 1.9683 1.8288 1.2395 3.1743 2.7928 4.0948 4.7274 1.9126 4.0198 1.3803 8.8011-1.3702 12.308-1.0727 1.3677-2.4908 2.4985-4.1288 3.2919-1.9976 0.96764-4.1741 1.3468-6.3992 1.1148-2.0466-0.21332-4.2533-1.1267-5.9926-2.4803-3.096-2.4094-4.8092-6.4777-4.3814-10.404 0.45017-4.1327 2.9394-7.6316 6.6928-9.4075 1.6221-0.76747 3.38-1.1367 5.1257-1.1184z" fill="#f9bbab" opacity=".8"/>
<g fill="#fff">
<rect x="50.933" y="217.52" width="4.6236" height="12.862" opacity=".8"/>
<rect transform="scale(1,-1)" x="50.933" y="-234.92" width="4.6236" height="3.5749" opacity=".8"/>
<rect x="50.933" y="217.52" width="4.6236" height="12.862" opacity=".8"/>
<rect transform="scale(1,-1)" x="50.933" y="-234.92" width="4.6236" height="3.5749" opacity=".8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
client/icons/find.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

BIN
client/icons/info.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 34 KiB

BIN
client/icons/paste.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

25
client/icons/warn.svg Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg width="221.25mm" height="193.24mm" version="1.1" viewBox="0 0 221.25 193.24" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient879" x1="193.48" x2="101.09" y1="198.47" y2="254.08" gradientTransform="matrix(1.7412 0 0 1.7412 -267.69 -212.02)" gradientUnits="userSpaceOnUse">
<stop stop-color="#c16602" stop-opacity=".99608" offset="0"/>
<stop stop-color="#e8b839" offset="1"/>
</linearGradient>
</defs>
<metadata>
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g transform="translate(97.776 -43.224)">
<path d="m12.851 49.318 104.53 181.06h-209.07z" fill="#fff" opacity=".8" stroke="url(#linearGradient879)" stroke-linecap="round" stroke-linejoin="round" stroke-opacity=".99216" stroke-width="12.188"/>
<path d="m12.851 101.1 62.098 107.56h-124.2z" fill="#f5db61" opacity=".8"/>
<rect x=".23388" y="113.27" width="25.235" height="54.405" fill="#c28832" opacity=".8"/>
<rect transform="scale(1,-1)" x=".23388" y="-196.48" width="25.235" height="14.793" fill="#c28832" opacity=".8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -108,6 +108,11 @@ QVariant LogsModel::data(const QModelIndex &index, int role) const
return QVariant();
}
Qt::DropActions LogsModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
// --------------------------------------------- //
// Slots
// --------------------------------------------- //

View File

@ -44,6 +44,8 @@ public:
int role = Qt::DisplayRole) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
Qt::DropActions supportedDropActions() const;
public slots:
void clear();
void setLogLevel(int level);

View File

@ -26,6 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include <QHeaderView>
#include <QMainWindow>
#include <QMovie>
#include <QPropertyAnimation>
extern QMainWindow *mainWindow;
@ -49,6 +50,14 @@ LogsWindow::LogsWindow(LogsModel *model, QWidget *parent)
warnAnime_ = new QMovie(":/icons/anime_warn.gif", QByteArray(), this);
errorAnime_ = new QMovie(":/icons/anime_error.gif", QByteArray(), this);
alert_ = new QLabel("ALERT!", this,
Qt::FramelessWindowHint|Qt::WindowStaysOnTopHint);
alert_->setScaledContents(true);
alert_->hide();
alertAnime_ = new QPropertyAnimation(alert_, "geometry", this);
alertAnime_->setDuration(2000);
alertAnime_->setEasingCurve(QEasingCurve::InOutExpo);
connect(level, SIGNAL(currentIndexChanged(int)),
model, SLOT(setLogLevel(int)));
connect(clear, SIGNAL(clicked()), model, SLOT(clear()));
@ -75,12 +84,17 @@ LogsWindow::~LogsWindow()
delete logsModelTest_;
}
void LogsWindow::clearCurrentSelection()
{
logs->selectionModel()->clearCurrentIndex();
logs->clearSelection();
}
void LogsWindow::when_visibilityChanged(bool visible)
{
if (visible) {
logs->resizeRowsToContents();
state_ = kInfo;
notify();
setState(kInfo);
}
isVisible_ = visible;
@ -90,29 +104,26 @@ void LogsWindow::when_visibilityChanged(bool visible)
void LogsWindow::when_rowsInserted(const QModelIndex &parent,
int first, int last)
{
if (isVisible_) {
if (isVisible_)
logs->resizeRowsToContents();
return;
}
if (state_ == kError)
return;
State incrementalState = kInfo;
for (int i = first; i <= last; i++) {
// FIXME: use a user-role instead, so we don't need to know column and
// have to compare strings?
QString level = logs->model()->data(logs->model()->index(i, 1, parent))
.toString();
if (level == "Error") {
state_ = kError;
incrementalState = kError;
break; // Highest level - no need to look further
}
else if (level == "Warning") {
state_ = kWarning;
incrementalState = kWarning;
}
}
notify();
alert(incrementalState);
if (incrementalState > state())
setState(incrementalState);
}
void LogsWindow::on_autoScroll_toggled(bool checked)
@ -129,6 +140,19 @@ void LogsWindow::on_autoScroll_toggled(bool checked)
}
}
LogsWindow::State LogsWindow::state()
{
return state_;
}
void LogsWindow::setState(State state)
{
if (isVisible_)
return;
state_ = state;
notify();
}
QLabel* LogsWindow::tabIcon()
{
QList<QTabBar*> tabBars = mainWindow->findChildren<QTabBar*>();
@ -149,6 +173,57 @@ QLabel* LogsWindow::tabIcon()
return nullptr;
}
//! Popup and animate a big icon
void LogsWindow::alert(State state)
{
if (state == kInfo)
return;
// start - center of main window
QRect start;
QWidget *view = mainWindow;
if (!view)
return;
alert_->setParent(view);
alert_->raise();
start.setSize(QSize(256, 256).scaled(view->size()/2, Qt::KeepAspectRatio));
start.moveCenter(QPoint(view->size().width()/2,
view->size().height()/2));
// end - center of logs window if visible, tab icon otherwise
QPoint c;
QLabel *icon = tabIcon();
view = isVisible_ ? dynamic_cast<QWidget*>(this) : mainWindow;
if (icon && !isVisible_) {
c = icon->geometry().center(); // in icon's parent (tabBar) coords
c = icon->mapFromParent(c); // in icon's own coords
c = icon->mapTo(view, c); // in mainWindow's coords
} else {
c = view->geometry().center();
c = view->mapTo(mainWindow, c); // in mainWindow's coords
}
QRect end;
end.moveCenter(c);
switch (state) {
case kError:
alert_->setPixmap(QPixmap(":/icons/error.svg"));
break;
case kWarning:
alert_->setPixmap(QPixmap(":/icons/warn.svg"));
break;
default:
Q_UNREACHABLE();
break;
}
alertAnime_->setStartValue(start);
alertAnime_->setEndValue(end);
alert_->show(); // ensure it's visible before starting animation
alertAnime_->start();
}
//! Show tab icon
void LogsWindow::notify()
{
QString annotation;
@ -158,7 +233,7 @@ void LogsWindow::notify()
warnAnime_->stop();
errorAnime_->stop();
switch (state_) {
switch (state()) {
case kError:
anime = errorAnime_;
annotation = " - Error(s)";

View File

@ -26,6 +26,7 @@ class LogsModel;
class QDockWidget;
class QShowEvent;
class QMovie;
class QPropertyAnimation;
class LogsWindow: public QWidget, private Ui::LogsWindow
{
@ -34,6 +35,9 @@ public:
LogsWindow(LogsModel *model, QWidget *parent = 0);
~LogsWindow();
public slots:
void clearCurrentSelection();
private slots:
void when_visibilityChanged(bool visible);
void when_rowsInserted(const QModelIndex &parent, int first, int last);
@ -43,15 +47,24 @@ private:
enum State {kInfo, kWarning, kError};
QLabel* tabIcon();
State state();
void setState(State state);
void alert(State state);
void notify();
State state_{kInfo};
QDockWidget *parentDock_;
QMovie *warnAnime_{nullptr};
QMovie *errorAnime_{nullptr};
QLabel *alert_{nullptr};
QPropertyAnimation *alertAnime_{nullptr};
QString windowTitle_;
bool isVisible_{false};
bool isVisible_{false}; // see XXX below
QObject *logsModelTest_;
// XXX: We cannot use isVisible() instead of isVisible_ since
// LogsWindow::isVisible() returns true even when the parent LogsDock
// is tabified but not the selected tab
};
#endif

View File

@ -24,6 +24,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "params.h"
#include "preferences.h"
#include "settings.h"
#include "thememanager.h"
#include <QApplication>
#include <QDateTime>
@ -42,6 +43,9 @@ Params appParams;
QSettings *appSettings;
QMainWindow *mainWindow;
void NoMsgHandler(QtMsgType type, const QMessageLogContext &context,
const QString &msg);
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
@ -54,9 +58,16 @@ int main(int argc, char* argv[])
appParams.parseCommandLine(argc, argv);
#ifndef QT_DEBUG // Release mode
if (appParams.optLogsDisabled())
qInstallMessageHandler(NoMsgHandler);
#endif
OstProtocolManager = new ProtocolManager();
OstProtocolWidgetFactory = new ProtocolWidgetFactory();
Preferences::initDefaults();
/* (Portable Mode) If we have a .ini file in the same directory as the
executable, we use that instead of the platform specific location
and format for the settings */
@ -66,6 +77,7 @@ int main(int argc, char* argv[])
appSettings = new QSettings(portableIni, QSettings::IniFormat);
else
appSettings = new QSettings();
qDebug("Settings: %s", qPrintable(appSettings->fileName()));
OstProtoLib::setExternalApplicationPaths(
appSettings->value(kTsharkPathKey, kTsharkPathDefaultValue).toString(),
@ -73,7 +85,8 @@ int main(int argc, char* argv[])
appSettings->value(kDiffPathKey, kDiffPathDefaultValue).toString(),
appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString());
Preferences::initDefaults();
ThemeManager::instance()->setTheme(appSettings->value(kThemeKey).toString());
qsrand(QDateTime::currentDateTime().toTime_t());
mainWindow = new MainWindow;
@ -87,3 +100,14 @@ int main(int argc, char* argv[])
return exitCode;
}
void NoMsgHandler(QtMsgType type, const QMessageLogContext &/*context*/,
const QString &msg)
{
if (type == QtFatalMsg) {
fprintf(stderr, "%s\n", qPrintable(msg));
fflush(stderr);
abort();
}
}

View File

@ -23,6 +23,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "dbgthread.h"
#endif
#include "clipboardhelper.h"
#include "fileformatoptions.h"
#include "jumpurl.h"
#include "logsmodel.h"
#include "logswindow.h"
@ -38,6 +40,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "fileformat.pb.h"
#include <QDate>
#include <QDesktopServices>
#include <QDockWidget>
#include <QFileDialog>
@ -59,6 +62,7 @@ extern const char* revision;
PortGroupList *pgl;
LogsModel *appLogs;
ClipboardHelper *clipboardHelper;
MainWindow::MainWindow(QWidget *parent)
: QMainWindow (parent)
@ -82,8 +86,14 @@ MainWindow::MainWindow(QWidget *parent)
localServer_ = new QProcess(this);
connect(localServer_, SIGNAL(finished(int, QProcess::ExitStatus)),
SLOT(onLocalServerFinished(int, QProcess::ExitStatus)));
#if QT_VERSION >= 0x050600
connect(localServer_, SIGNAL(errorOccurred(QProcess::ProcessError)),
SLOT(onLocalServerError(QProcess::ProcessError)));
#else
connect(localServer_, SIGNAL(error(QProcess::ProcessError)),
SLOT(onLocalServerError(QProcess::ProcessError)));
#endif
localServer_->setProcessChannelMode(QProcess::ForwardedChannels);
localServer_->start(serverApp, QStringList());
QTimer::singleShot(5000, this, SLOT(stopLocalServerMonitor()));
@ -93,6 +103,7 @@ MainWindow::MainWindow(QWidget *parent)
pgl = new PortGroupList;
appLogs = new LogsModel(this);
clipboardHelper = new ClipboardHelper(this);
portsWindow = new PortsWindow(pgl, this);
statsWindow = new PortStatsWindow(pgl, this);
@ -115,7 +126,11 @@ MainWindow::MainWindow(QWidget *parent)
setupUi(this);
menuFile->insertActions(menuFile->actions().at(3), portsWindow->actions());
menuFile->insertActions(menuFile->actions().at(3),
portsWindow->portActions());
menuEdit->addActions(clipboardHelper->actions());
menuStreams->addActions(portsWindow->streamActions());
menuDevices->addActions(portsWindow->deviceActions());
statsDock->setWidget(statsWindow);
addDockWidget(Qt::BottomDockWidgetArea, statsDock);
@ -128,6 +143,13 @@ MainWindow::MainWindow(QWidget *parent)
portsDock->setWidget(portsWindow);
addDockWidget(Qt::TopDockWidgetArea, portsDock);
#if QT_VERSION >= 0x050600
// Set top and bottom docks to equal height
resizeDocks({portsDock, statsDock}, {height()/2, height()/2}, Qt::Vertical);
#endif
portsWindow->setFocus();
// Save the default window geometry and layout ...
defaultGeometry_ = geometry();
defaultLayout_ = saveState(0);
@ -153,10 +175,17 @@ MainWindow::MainWindow(QWidget *parent)
this, SLOT(onNewVersion(QString)));
updater->checkForNewVersion();
// TODO: If session file specified (and valid?), don't add local drone PG
// Add the "Local" Port Group
if (appParams.optLocalDrone()) {
PortGroup *pg = new PortGroup;
pgl->addPortGroup(*pg);
}
if (appParams.argumentCount()) {
QString fileName = appParams.argument(0);
if (QFile::exists(fileName))
on_actionOpenSession_triggered(fileName);
openSession(fileName);
else
QMessageBox::information(NULL, qApp->applicationName(),
QString("File not found: " + fileName));
@ -201,7 +230,7 @@ MainWindow::~MainWindow()
}
}
void MainWindow::on_actionOpenSession_triggered(QString fileName)
void MainWindow::openSession(QString fileName)
{
qDebug("Open Session Action (%s)", qPrintable(fileName));
@ -253,6 +282,11 @@ _exit:
return;
}
void MainWindow::on_actionOpenSession_triggered()
{
openSession();
}
void MainWindow::on_actionSaveSession_triggered()
{
qDebug("Save Session Action");
@ -345,6 +379,10 @@ void MainWindow::on_actionViewRestoreDefaults_triggered()
statsDock->raise();
actionViewShowMyReservedPortsOnly->setChecked(false);
portsWindow->clearCurrentSelection();
statsWindow->clearCurrentSelection();
logsWindow_->clearCurrentSelection();
}
void MainWindow::on_actionHelpOnline_triggered()
@ -357,6 +395,14 @@ void MainWindow::on_actionDonate_triggered()
QDesktopServices::openUrl(QUrl(jumpUrl("donate", "app", "menu")));
}
void MainWindow::on_actionCheckForUpdates_triggered()
{
Updater *updater = new Updater();
connect(updater, SIGNAL(latestVersion(QString)),
this, SLOT(onLatestVersion(QString)));
updater->checkForNewVersion();
}
void MainWindow::on_actionHelpAbout_triggered()
{
QDialog *aboutDialog = new QDialog;
@ -374,8 +420,13 @@ void MainWindow::on_actionHelpAbout_triggered()
void MainWindow::stopLocalServerMonitor()
{
// We are only interested in startup errors
#if QT_VERSION >= 0x050600
disconnect(localServer_, SIGNAL(errorOccurred(QProcess::ProcessError)),
this, SLOT(onLocalServerError(QProcess::ProcessError)));
#else
disconnect(localServer_, SIGNAL(error(QProcess::ProcessError)),
this, SLOT(onLocalServerError(QProcess::ProcessError)));
#endif
disconnect(localServer_, SIGNAL(finished(int, QProcess::ExitStatus)),
this, SLOT(onLocalServerFinished(int, QProcess::ExitStatus)));
}
@ -416,8 +467,8 @@ void MainWindow::reportLocalServerError()
if (localServer_->exitCode() == STATUS_DLL_NOT_FOUND)
errorStr.append(tr("<p>This is most likely because Packet.dll "
"was not found - make sure you have "
"<a href='%1'>WinPcap"
"</a> installed.</p>")
"<a href='%1'>npcap installed and accessible</a>."
"</p>")
.arg(jumpUrl("winpcap")));
#endif
msgBox.setText(errorStr);
@ -435,12 +486,52 @@ void MainWindow::reportLocalServerError()
void MainWindow::onNewVersion(QString newVersion)
{
QLabel *msg = new QLabel(tr("New Ostinato version %1 available. Visit "
"<a href='%2'>ostinato.org</a> to download")
QDate today = QDate::currentDate();
QDate lastChecked = QDate::fromString(
appSettings->value(kLastUpdateCheck).toString(),
Qt::ISODate);
if (lastChecked.daysTo(today) >= 5) {
QMessageBox::information(this, tr("Update check"),
tr("<p><b>Ostinato version %1 is now available</b> (you have %2). "
"See <a href='%3'>change log</a>.</p>"
"<p>Visit <a href='%4'>ostinato.org</a> to download.</p>")
.arg(newVersion)
.arg(version)
.arg(jumpUrl("changelog", "app", "status", "update"))
.arg(jumpUrl("download", "app", "status", "update")));
}
else {
QLabel *msg = new QLabel(tr("New Ostinato version %1 available. Visit "
"<a href='%2'>ostinato.org</a> to download")
.arg(newVersion)
.arg(jumpUrl("download", "app", "status", "update")));
msg->setOpenExternalLinks(true);
statusBar()->addPermanentWidget(msg);
msg->setOpenExternalLinks(true);
statusBar()->addPermanentWidget(msg);
}
appSettings->setValue(kLastUpdateCheck, today.toString(Qt::ISODate));
sender()->deleteLater();
}
void MainWindow::onLatestVersion(QString latestVersion)
{
if (version != latestVersion) {
QMessageBox::information(this, tr("Update check"),
tr("<p><b>Ostinato version %1 is now available</b> (you have %2). "
"See <a href='%3'>change log</a>.</p>"
"<p>Visit <a href='%4'>ostinato.org</a> to download.</p>")
.arg(latestVersion)
.arg(version)
.arg(jumpUrl("changelog", "app", "status", "update"))
.arg(jumpUrl("download", "app", "status", "update")));
}
else {
QMessageBox::information(this, tr("Update check"),
tr("You are already running the latest Ostinato version - %1")
.arg(version));
}
sender()->deleteLater();
}
//! Returns true on success (or user cancel) and false on failure
@ -457,7 +548,7 @@ bool MainWindow::openSession(QString fileName, QString &error)
goto _fail;
}
if ((optDialog = fmt->openOptionsDialog()))
if ((optDialog = FileFormatOptions::openOptionsDialog(fmt)))
{
int ret;
optDialog->setParent(this, Qt::Dialog);

View File

@ -36,6 +36,7 @@ class MainWindow : public QMainWindow, private Ui::MainWindow
Q_OBJECT
private:
void openSession(QString fileName = QString());
bool openSession(QString fileName, QString &error);
bool saveSession(QString fileName, QString fileType, QString &error);
@ -55,12 +56,13 @@ public:
~MainWindow();
public slots:
void on_actionOpenSession_triggered(QString fileName = QString());
void on_actionOpenSession_triggered();
void on_actionSaveSession_triggered();
void on_actionPreferences_triggered();
void on_actionViewRestoreDefaults_triggered();
void on_actionHelpOnline_triggered();
void on_actionDonate_triggered();
void on_actionCheckForUpdates_triggered();
void on_actionHelpAbout_triggered();
private slots:
@ -69,6 +71,7 @@ private slots:
void onLocalServerError(QProcess::ProcessError error);
void reportLocalServerError();
void onNewVersion(QString version);
void onLatestVersion(QString version);
};
#endif

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1024</width>
<height>600</height>
<height>700</height>
</rect>
</property>
<property name="windowTitle" >
@ -25,19 +25,14 @@
<addaction name="actionOpenSession" />
<addaction name="actionSaveSession" />
<addaction name="separator" />
<addaction name="separator" />
<addaction name="actionPreferences" />
<addaction name="actionFileExit" />
</widget>
<widget class="QMenu" name="menuHelp" >
<widget class="QMenu" name="menuEdit" >
<property name="title" >
<string>&amp;Help</string>
<string>&amp;Edit</string>
</property>
<addaction name="actionHelpOnline" />
<addaction name="separator" />
<addaction name="actionDonate" />
<addaction name="separator" />
<addaction name="actionHelpAbout" />
<addaction name="actionAboutQt" />
</widget>
<widget class="QMenu" name="menuView" >
<property name="title" >
@ -46,8 +41,34 @@
<addaction name="actionViewShowMyReservedPortsOnly" />
<addaction name="actionViewRestoreDefaults" />
</widget>
<widget class="QMenu" name="menuStreams" >
<property name="title" >
<string>&amp;Streams</string>
</property>
<addaction name="actionTest" />
</widget>
<widget class="QMenu" name="menuDevices" >
<property name="title" >
<string>&amp;Devices</string>
</property>
</widget>
<widget class="QMenu" name="menuHelp" >
<property name="title" >
<string>&amp;Help</string>
</property>
<addaction name="actionHelpOnline" />
<addaction name="separator" />
<addaction name="actionDonate" />
<addaction name="actionCheckForUpdates" />
<addaction name="separator" />
<addaction name="actionHelpAbout" />
<addaction name="actionAboutQt" />
</widget>
<addaction name="menuFile" />
<addaction name="menuEdit" />
<addaction name="menuView" />
<addaction name="menuStreams" />
<addaction name="menuDevices" />
<addaction name="menuHelp" />
</widget>
<widget class="QStatusBar" name="statusbar" />
@ -122,6 +143,11 @@
<string>Donate</string>
</property>
</action>
<action name="actionCheckForUpdates" >
<property name="text" >
<string>Check for Updates...</string>
</property>
</action>
</widget>
<resources>
<include location="ostinato.qrc" />

View File

@ -0,0 +1,126 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "mandatoryfieldsgroup.h"
// No need for QDateEdit, QSpinBox, etc., since these always return values
#include <QCheckBox>
#include <QComboBox>
#include <QLineEdit>
#include <QPushButton>
#include <QWidget>
void MandatoryFieldsGroup::add(QWidget *widget)
{
if (!widgets_.contains(widget)) {
if (widget->inherits("QCheckBox"))
connect(qobject_cast<QCheckBox*>(widget),
SIGNAL(clicked()),
this, SLOT(changed()));
else if (widget->inherits("QComboBox"))
connect(qobject_cast<QComboBox*>(widget),
SIGNAL(highlighted(int)),
this, SLOT(changed()));
else if (widget->inherits("QLineEdit"))
connect(qobject_cast<QLineEdit*>(widget),
SIGNAL(textChanged(const QString&)),
this, SLOT(changed()));
else {
qWarning("MandatoryFieldsGroup: unsupported class %s",
widget->metaObject()->className());
return;
}
widgets_.append(widget);
changed();
}
}
void MandatoryFieldsGroup::remove(QWidget *widget)
{
widgets_.removeAll(widget);
changed();
}
void MandatoryFieldsGroup::setSubmitButton(QPushButton *button)
{
if (submitButton_ && submitButton_ != button)
submitButton_->setEnabled(true);
submitButton_ = button;
changed();
}
void MandatoryFieldsGroup::changed()
{
if (!submitButton_)
return;
bool enable = true;
for (auto widget : widgets_) {
// Invisible mandatory widgets are treated as non-mandatory
if (!widget->isVisible())
continue;
if (widget->inherits("QCheckBox")) {
// Makes sense only for tristate checkbox
auto checkBox = qobject_cast<const QCheckBox*>(widget);
if (checkBox->checkState() == Qt::PartiallyChecked) {
enable = false;
break;
} else
continue;
}
if (widget->inherits("QComboBox")) {
auto comboBox = qobject_cast<const QComboBox*>(widget);
if (comboBox->currentText().isEmpty()) {
enable = false;
break;
} else
continue;
}
if (widget->inherits("QLineEdit")) {
auto lineEdit = qobject_cast<const QLineEdit*>(widget);
if (lineEdit->text().isEmpty()
|| !lineEdit->hasAcceptableInput()) {
enable = false;
break;
} else
continue;
}
}
submitButton_->setEnabled(enable);
}
void MandatoryFieldsGroup::clear()
{
widgets_.clear();
if (submitButton_) {
submitButton_->setEnabled(true);
submitButton_ = nullptr;
}
}

View File

@ -0,0 +1,59 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _MANDATORY_FIELDS_GROUP_H
#define _MANDATORY_FIELDS_GROUP_H
#include <QObject>
#include <QList>
class QPushButton;
class QWidget;
// Adapted from https://doc.qt.io/archives/qq/qq11-mandatoryfields.html
// and improved
class MandatoryFieldsGroup : public QObject
{
Q_OBJECT
public:
MandatoryFieldsGroup(QObject *parent)
: QObject(parent)
{
}
void add(QWidget *widget);
void remove(QWidget *widget);
void setSubmitButton(QPushButton *button);
public slots:
void clear();
private slots:
void changed();
private:
QList<const QWidget*> widgets_;
QPushButton *submitButton_{nullptr};
};
#endif

View File

@ -140,6 +140,11 @@ QVariant NdpStatusModel::data(const QModelIndex &index, int role) const
return QVariant();
}
Qt::DropActions NdpStatusModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
void NdpStatusModel::setDeviceIndex(Port *port, int deviceIndex)
{
beginResetModel();

View File

@ -40,6 +40,8 @@ public:
int role = Qt::DisplayRole) const;
QVariant data(const QModelIndex &index, int role) const;
Qt::DropActions supportedDropActions() const;
void setDeviceIndex(Port *port, int deviceIndex);
public slots:

View File

@ -3,47 +3,46 @@ CONFIG += qt ver_info
macx: TARGET = Ostinato
win32:RC_FILE = ostinato.rc
macx:ICON = icons/logo.icns
QT += widgets network script xml
QT += widgets network script xml svg
INCLUDEPATH += "../rpc/" "../common/"
OBJDIR = .
win32 {
QMAKE_LFLAGS += -static
CONFIG(debug, debug|release) {
LIBS += -L"../common/debug" -lostprotogui -lostproto
LIBS += -L"../rpc/debug" -lpbrpc
POST_TARGETDEPS += \
"../common/debug/libostprotogui.a" \
"../common/debug/libostproto.a" \
"../rpc/debug/libpbrpc.a"
OBJDIR = debug
} else {
LIBS += -L"../common/release" -lostprotogui -lostproto
LIBS += -L"../rpc/release" -lpbrpc
POST_TARGETDEPS += \
"../common/release/libostprotogui.a" \
"../common/release/libostproto.a" \
"../rpc/release/libpbrpc.a"
OBJDIR = release
}
} else {
LIBS += -L"../common" -lostprotogui -lostproto
LIBS += -L"../rpc" -lpbrpc
POST_TARGETDEPS += \
"../common/libostprotogui.a" \
"../common/libostproto.a" \
"../rpc/libpbrpc.a"
}
LIBS += -L"../common/$$OBJDIR" -lostfile -lostfilegui
LIBS += -L"../common/$$OBJDIR" -lostprotogui -lostproto
LIBS += -L"../rpc/$$OBJDIR" -lpbrpc
POST_TARGETDEPS += \
"../common/$$OBJDIR/libostfilegui.a" \
"../common/$$OBJDIR/libostfile.a" \
"../common/$$OBJDIR/libostprotogui.a" \
"../common/$$OBJDIR/libostproto.a" \
"../rpc/$$OBJDIR/libpbrpc.a"
LIBS += -lprotobuf
LIBS += -L"../extra/qhexedit2/$(OBJECTS_DIR)/" -lqhexedit2
RESOURCES += ostinato.qrc
HEADERS += \
arpstatusmodel.h \
clipboardhelper.h \
devicegroupdialog.h \
devicegroupmodel.h \
devicemodel.h \
deviceswidget.h \
dumpview.h \
fieldedit.h \
hexlineedit.h \
logsmodel.h \
logswindow.h \
findreplace.h \
mainwindow.h \
mandatoryfieldsgroup.h \
ndpstatusmodel.h \
packetmodel.h \
port.h \
@ -56,6 +55,7 @@ HEADERS += \
portstatsproxymodel.h \
portstatswindow.h \
portswindow.h \
portwidget.h \
preferences.h \
settings.h \
streamconfigdialog.h \
@ -64,25 +64,31 @@ HEADERS += \
streamstatsfiltermodel.h \
streamstatsmodel.h \
streamstatswindow.h \
variablefieldswidget.h
streamswidget.h \
variablefieldswidget.h \
xtableview.h
FORMS += \
about.ui \
devicegroupdialog.ui \
deviceswidget.ui \
findreplace.ui \
logswindow.ui \
mainwindow.ui \
portconfigdialog.ui \
portstatsfilter.ui \
portstatswindow.ui \
portswindow.ui \
portwidget.ui \
preferences.ui \
streamconfigdialog.ui \
streamstatswindow.ui \
streamswidget.ui \
variablefieldswidget.ui
SOURCES += \
arpstatusmodel.cpp \
clipboardhelper.cpp \
devicegroupdialog.cpp \
devicegroupmodel.cpp \
devicemodel.cpp \
@ -92,8 +98,11 @@ SOURCES += \
hexlineedit.cpp \
logsmodel.cpp \
logswindow.cpp \
fieldedit.cpp \
findreplace.cpp \
main.cpp \
mainwindow.cpp \
mandatoryfieldsgroup.cpp \
ndpstatusmodel.cpp \
packetmodel.cpp \
params.cpp \
@ -106,18 +115,31 @@ SOURCES += \
portstatsfilterdialog.cpp \
portstatswindow.cpp \
portswindow.cpp \
portwidget.cpp \
preferences.cpp \
streamconfigdialog.cpp \
streamlistdelegate.cpp \
streammodel.cpp \
streamstatsmodel.cpp \
streamstatswindow.cpp \
streamswidget.cpp \
thememanager.cpp \
variablefieldswidget.cpp
THEMES += \
themes/material-dark.qss \
themes/material-dark.rcc \
themes/material-light.qss \
themes/material-light.rcc \
themes/qds-dark.qss \
themes/qds-dark.rcc \
themes/qds-light.qss \
themes/qds-light.rcc \
QMAKE_DISTCLEAN += object_script.*
include(../install.pri)
include(../shared.pri)
include(../version.pri)
include(../options.pri)

View File

@ -1,5 +1,7 @@
<RCC>
<qresource prefix="/">
<file>icons/find.png</file>
<file>icons/info.png</file>
<file>icons/about.png</file>
<file>icons/add.png</file>
<file>icons/anime_error.gif</file>
@ -14,13 +16,16 @@
<file>icons/bullet_red.png</file>
<file>icons/bullet_white.png</file>
<file>icons/bullet_yellow.png</file>
<file>icons/copy.png</file>
<file>icons/control_play.png</file>
<file>icons/control_stop.png</file>
<file>icons/cut.png</file>
<file>icons/delete.png</file>
<file>icons/devicegroup_add.png</file>
<file>icons/devicegroup_delete.png</file>
<file>icons/devicegroup_edit.png</file>
<file>icons/donate.png</file>
<file>icons/error.svg</file>
<file>icons/exit.png</file>
<file>icons/frag_capture.png</file>
<file>icons/frag_exclusive.png</file>
@ -35,6 +40,7 @@
<file>icons/name.png</file>
<file>icons/neighbor_clear.png</file>
<file>icons/neighbor_resolve.png</file>
<file>icons/paste.png</file>
<file>icons/portgroup_add.png</file>
<file>icons/portgroup_connect.png</file>
<file>icons/portgroup_delete.png</file>
@ -53,5 +59,6 @@
<file>icons/stream_edit.png</file>
<file>icons/stream_stats.png</file>
<file>icons/transmit_on.png</file>
<file>icons/warn.svg</file>
</qresource>
</RCC>

View File

@ -243,3 +243,9 @@ QVariant PacketModel::data(const QModelIndex &index, int role) const
return QVariant();
}
Qt::DropActions PacketModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}

View File

@ -42,6 +42,7 @@ public:
QModelIndex index (int row, int col, const QModelIndex & parent = QModelIndex() ) const;
QModelIndex parent(const QModelIndex &index) const;
Qt::DropActions supportedDropActions() const;
private:
typedef union _IndexId
{

View File

@ -21,9 +21,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include <unistd.h>
extern char *version;
extern char *revision;
Params::Params()
{
localDrone_ = true;
logsDisabled_ = true;
}
int Params::parseCommandLine(int argc, char* argv[])
@ -31,14 +35,22 @@ int Params::parseCommandLine(int argc, char* argv[])
int c, n = 0;
opterr = 0;
while ((c = getopt (argc, argv, "c")) != -1) {
while ((c = getopt (argc, argv, "cdhv")) != -1) {
switch (c)
{
case 'c':
localDrone_ = false;
break;
case 'd':
logsDisabled_ = false;
break;
case 'v':
printf("Ostinato %s rev %s\n", version, revision);
exit(0);
case 'h':
default:
qDebug("ignoring unrecognized option (%c)", c);
printf("usage: %s [-cdhv]\n", argv[0]);
exit(1);
}
n++;
}
@ -54,6 +66,11 @@ bool Params::optLocalDrone()
return localDrone_;
}
bool Params::optLogsDisabled()
{
return logsDisabled_;
}
int Params::argumentCount()
{
return args_.size();

View File

@ -28,12 +28,14 @@ public:
int parseCommandLine(int argc, char* argv[]);
bool optLocalDrone();
bool optLogsDisabled();
int argumentCount();
QString argument(int index);
private:
bool localDrone_;
bool logsDisabled_;
QStringList args_;
};

View File

@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "port.h"
#include "emulation.h"
#include "fileformatoptions.h"
#include "streamfileformat.h"
#include <QApplication>
@ -28,6 +29,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include <QVariant>
#include <google/protobuf/descriptor.h>
#include <vector>
#include <cmath>
extern QMainWindow *mainWindow;
@ -87,7 +89,10 @@ void Port::updatePortConfig(OstProto::Port *port)
setAlias(QString("if%1").arg(id()));
if (recalc)
{
recalculateAverageRates();
emit streamListChanged(mPortGroupId, mPortId); // show/hide 'next' col
}
}
void Port::updateStreamOrdinalsFromIndex()
@ -98,7 +103,7 @@ void Port::updateStreamOrdinalsFromIndex()
void Port::reorderStreamsByOrdinals()
{
qSort(mStreams.begin(), mStreams.end(), StreamBase::StreamLessThan);
std::sort(mStreams.begin(), mStreams.end(), StreamBase::StreamLessThan);
}
void Port::setDirty(bool dirty)
@ -187,6 +192,11 @@ void Port::setAveragePacketRate(double packetsPerSec)
Q_ASSERT(false); // Unreachable!!
}
// if old avgPps is 0, new rate will be calculated as nan (infinity)
// because of divide by 0 (old avgPps) above - fix that
if (std::isnan(rate))
rate = packetsPerSec;
qDebug("cur stream pps = %g", s->averagePacketRate());
s->setAveragePacketRate(rate);
@ -261,6 +271,11 @@ void Port::setAverageBitRate(double bitsPerSec)
Q_ASSERT(false); // Unreachable!!
}
// if old avgBps is 0, new rate will be calculated as nan (infinity)
// because of divide by 0 (old avgBps) above - fix that
if (std::isnan(rate))
rate = bitsPerSec/((s->frameLenAvg()+kEthOverhead)*8);
qDebug("cur stream pps = %g", s->averagePacketRate());
s->setAveragePacketRate(rate);
@ -303,6 +318,12 @@ void Port::setAverageBitRate(double bitsPerSec)
emit portRateChanged(mPortGroupId, mPortId);
}
void Port::setAverageLoadRate(double load)
{
Q_ASSERT(d.speed() > 0);
setAverageBitRate(load*d.speed()*1e6);
}
bool Port::newStreamAt(int index, OstProto::Stream const *stream)
{
Stream *s = new Stream;
@ -495,6 +516,11 @@ bool Port::modifiablePortConfig(OstProto::Port &config) const
modCfg.set_user_name(config.user_name());
change = true;
}
if (config.is_tracking_stream_stats() != d.is_tracking_stream_stats()) {
modCfg.set_is_tracking_stream_stats(config.is_tracking_stream_stats());
change = true;
}
if (change) {
modCfg.mutable_port_id()->set_id(id());
@ -579,12 +605,11 @@ bool Port::openStreams(QString fileName, bool append, QString &error)
goto _fail;
}
if ((optDialog = fmt->openOptionsDialog()))
if ((optDialog = FileFormatOptions::openOptionsDialog(fmt)))
{
int ret;
optDialog->setParent(mainWindow, Qt::Dialog);
ret = optDialog->exec();
optDialog->setParent(0, Qt::Dialog);
if (ret == QDialog::Rejected)
goto _user_opt_cancel;
}

View File

@ -88,8 +88,15 @@ public:
{ return d.port_id().id(); }
const QString name() const
{ return QString().fromStdString(d.name()); }
const QString description() const
const QString systemDescription() const
{ return QString().fromStdString(d.description()); }
const QString userDescription() const
{ return QString().fromStdString(d.user_description()); }
const QString description() const
{
return userDescription().isEmpty() ?
systemDescription() : userDescription();
}
const QString notes() const
{ return QString().fromStdString(d.notes()); }
const QString userName() const
@ -102,6 +109,10 @@ public:
{ return d.transmit_mode(); }
bool trackStreamStats() const
{ return d.is_tracking_stream_stats(); }
double speed() const
{ return d.speed(); }
double averageLoadRate() const
{ return d.speed() ? avgBitsPerSec_/(d.speed()*1e6) : 0; }
double averagePacketRate() const
{ return avgPacketsPerSec_; }
double averageBitRate() const
@ -183,6 +194,7 @@ public:
void setAveragePacketRate(double packetsPerSec);
void setAverageBitRate(double bitsPerSec);
void setAverageLoadRate(double loadPercent);
// FIXME(MED): Bad Hack! port should not need an external trigger to
// recalculate - refactor client side domain objects and model objects
void recalculateAverageRates();

View File

@ -32,6 +32,8 @@ PortConfigDialog::PortConfigDialog(
setupUi(this);
description->setPlaceholderText(portConfig_.description().c_str());
description->setText(portConfig_.user_description().c_str());
switch(portConfig_.transmit_mode())
{
case OstProto::kSequentialTransmit:
@ -80,6 +82,8 @@ void PortConfigDialog::accept()
{
OstProto::Port pc;
pc.set_user_description(description->text().toStdString());
if (sequentialStreamsButton->isChecked())
pc.set_transmit_mode(OstProto::kSequentialTransmit);
else if (interleavedStreamsButton->isChecked())
@ -87,6 +91,7 @@ void PortConfigDialog::accept()
else
Q_ASSERT(false); // Unreachable!!!
pc.set_user_name(portConfig_.user_name());
switch (reservedBy_) {
case kSelf:
if (!reserveButton->isChecked())
@ -108,6 +113,11 @@ void PortConfigDialog::accept()
pc.set_is_tracking_stream_stats(streamStatsButton->isChecked());
// Update fields that have changed, clear the rest
if (pc.user_description() != portConfig_.user_description())
portConfig_.set_user_description(pc.user_description());
else
portConfig_.clear_user_description();
if (pc.transmit_mode() != portConfig_.transmit_mode())
portConfig_.set_transmit_mode(pc.transmit_mode());
else

View File

@ -1,37 +1,51 @@
<ui version="4.0" >
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PortConfigDialog</class>
<widget class="QDialog" name="PortConfigDialog" >
<property name="geometry" >
<widget class="QDialog" name="PortConfigDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>244</width>
<height>257</height>
<width>248</width>
<height>292</height>
</rect>
</property>
<property name="windowTitle" >
<property name="windowTitle">
<string>Port Config</string>
</property>
<layout class="QVBoxLayout" >
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox" name="transmitModeBox" >
<property name="title" >
<widget class="QLabel" name="label">
<property name="text">
<string>Description</string>
</property>
<property name="buddy">
<cstring>description</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="description"/>
</item>
<item>
<widget class="QGroupBox" name="transmitModeBox">
<property name="title">
<string>Transmit Mode</string>
</property>
<layout class="QVBoxLayout" >
<layout class="QVBoxLayout">
<item>
<widget class="QRadioButton" name="sequentialStreamsButton" >
<property name="text" >
<widget class="QRadioButton" name="sequentialStreamsButton">
<property name="text">
<string>Sequential Streams</string>
</property>
<property name="checked" >
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="interleavedStreamsButton" >
<property name="text" >
<widget class="QRadioButton" name="interleavedStreamsButton">
<property name="text">
<string>Interleaved Streams</string>
</property>
</widget>
@ -40,21 +54,21 @@
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox" >
<property name="title" >
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Reservation</string>
</property>
<layout class="QVBoxLayout" >
<layout class="QVBoxLayout">
<item>
<widget class="QLabel" name="reservedBy" >
<property name="text" >
<widget class="QLabel" name="reservedBy">
<property name="text">
<string>Reserved by: </string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="reserveButton" >
<property name="text" >
<widget class="QCheckBox" name="reserveButton">
<property name="text">
<string>Reserve</string>
</property>
</widget>
@ -63,25 +77,25 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="exclusiveControlButton" >
<property name="text" >
<widget class="QCheckBox" name="exclusiveControlButton">
<property name="text">
<string>Exclusive Control</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="streamStatsButton" >
<property name="text" >
<widget class="QCheckBox" name="streamStatsButton">
<property name="text">
<string>Stream Statistics</string>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation" >
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<property name="sizeHint" stdset="0">
<size>
<width>226</width>
<height>71</height>
@ -90,17 +104,25 @@
</spacer>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok</set>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>description</tabstop>
<tabstop>sequentialStreamsButton</tabstop>
<tabstop>interleavedStreamsButton</tabstop>
<tabstop>reserveButton</tabstop>
<tabstop>exclusiveControlButton</tabstop>
<tabstop>streamStatsButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
@ -109,11 +131,11 @@
<receiver>PortConfigDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<hint type="sourcelabel">
<x>234</x>
<y>205</y>
</hint>
<hint type="destinationlabel" >
<hint type="destinationlabel">
<x>157</x>
<y>214</y>
</hint>
@ -125,11 +147,11 @@
<receiver>PortConfigDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<hint type="sourcelabel">
<x>234</x>
<y>205</y>
</hint>
<hint type="destinationlabel" >
<hint type="destinationlabel">
<x>243</x>
<y>214</y>
</hint>

View File

@ -206,6 +206,7 @@ void PortGroup::processVersionCompatibility(PbRpcController *controller)
logError(id(), QString("checkVersion failed: %1")
.arg(QString::fromStdString(verCompat->notes())));
compat = kIncompatible;
reconnect = false;
emit portGroupDataChanged(mPortGroupId);
QMessageBox msgBox;
@ -241,7 +242,8 @@ _error_exit:
void PortGroup::on_rpcChannel_disconnected()
{
qDebug("disconnected\n");
qDebug("disconnected %s:%u",
qPrintable(rpcChannel->serverName()), rpcChannel->serverPort());
logError(id(), "PortGroup disconnected");
emit portListAboutToBeChanged(mPortGroupId);
@ -252,6 +254,16 @@ void PortGroup::on_rpcChannel_disconnected()
emit portListChanged(mPortGroupId);
emit portGroupDataChanged(mPortGroupId);
// Disconnected during apply? Restore UI.
if (applyTimer_.isValid()) {
applyTimer_.invalidate();
emit applyFinished();
mainWindow->setEnabled(true);
QApplication::restoreOverrideCursor();
}
isGetStatsPending_ = false;
if (reconnect)
@ -265,11 +277,24 @@ void PortGroup::on_rpcChannel_disconnected()
void PortGroup::on_rpcChannel_error(QAbstractSocket::SocketError socketError)
{
qDebug("%s: error %d", __FUNCTION__, socketError);
qDebug("%s: error %d %s:%u", __FUNCTION__, socketError,
qPrintable(rpcChannel->serverName()), rpcChannel->serverPort());
emit portGroupDataChanged(mPortGroupId);
if (socketError == QAbstractSocket::RemoteHostClosedError)
switch(socketError)
{
case QAbstractSocket::SslInvalidUserDataError: // actually abort()
logWarn(id(), QString("Bad data received from portgroup, "
"aborting connection; "
"who is listening on %1:%2 "
" - is it drone or some other process?")
.arg(rpcChannel->serverName())
.arg(rpcChannel->serverPort()));
reconnect = false;
break;
default:
break;
}
qDebug("%s: state %d", __FUNCTION__, rpcChannel->state());
if ((rpcChannel->state() == QAbstractSocket::UnconnectedState) && reconnect)
@ -502,6 +527,7 @@ void PortGroup::when_configApply(int portIndex)
{
OstProto::StreamIdList *streamIdList;
OstProto::StreamConfigList *streamConfigList;
OstProto::BuildConfig *buildConfig;
OstProto::Ack *ack;
PbRpcController *controller;
@ -513,14 +539,7 @@ void PortGroup::when_configApply(int portIndex)
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
mainWindow->setDisabled(true);
// FIXME: as currently written this code will make unnecessary RPCs
// even if the request contains no data; the fix will need to take
// care to identify when sync is complete
// NOTE: DeviceGroup RPCs are no longer called unnecessarily;
// Stream RPCs need to be fixed similarly
// Also, drone currently updates its packet list at the end of
// modifyStream() implicitly assuming that will be the last API
// called - this will also need to be fixed
applyTimer_.start();
//
// Update/Sync DeviceGroups
@ -530,12 +549,12 @@ void PortGroup::when_configApply(int portIndex)
bool refreshReqd = false;
qDebug("applying 'deleted deviceGroups' ...");
logInfo(id(), mPorts[portIndex]->id(),
QString("Deleting old DeviceGroups"));
deviceGroupIdList = new OstProto::DeviceGroupIdList;
deviceGroupIdList->mutable_port_id()->set_id(mPorts[portIndex]->id());
mPorts[portIndex]->getDeletedDeviceGroupsSinceLastSync(*deviceGroupIdList);
if (deviceGroupIdList->device_group_id_size()) {
logInfo(id(), mPorts[portIndex]->id(),
QString("Deleting old DeviceGroups"));
ack = new OstProto::Ack;
controller = new PbRpcController(deviceGroupIdList, ack);
serviceStub->deleteDeviceGroup(controller, deviceGroupIdList, ack,
@ -547,12 +566,12 @@ void PortGroup::when_configApply(int portIndex)
delete deviceGroupIdList;
qDebug("applying 'new deviceGroups' ...");
logInfo(id(), mPorts[portIndex]->id(),
QString("Creating new DeviceGroups"));
deviceGroupIdList = new OstProto::DeviceGroupIdList;
deviceGroupIdList->mutable_port_id()->set_id(mPorts[portIndex]->id());
mPorts[portIndex]->getNewDeviceGroupsSinceLastSync(*deviceGroupIdList);
if (deviceGroupIdList->device_group_id_size()) {
logInfo(id(), mPorts[portIndex]->id(),
QString("Creating new DeviceGroups"));
ack = new OstProto::Ack;
controller = new PbRpcController(deviceGroupIdList, ack);
serviceStub->addDeviceGroup(controller, deviceGroupIdList, ack,
@ -564,13 +583,13 @@ void PortGroup::when_configApply(int portIndex)
delete deviceGroupIdList;
qDebug("applying 'modified deviceGroups' ...");
logInfo(id(), mPorts[portIndex]->id(),
QString("Modifying changed DeviceGroups"));
deviceGroupConfigList = new OstProto::DeviceGroupConfigList;
deviceGroupConfigList->mutable_port_id()->set_id(mPorts[portIndex]->id());
mPorts[portIndex]->getModifiedDeviceGroupsSinceLastSync(
*deviceGroupConfigList);
if (deviceGroupConfigList->device_group_size()) {
logInfo(id(), mPorts[portIndex]->id(),
QString("Modifying changed DeviceGroups"));
ack = new OstProto::Ack;
controller = new PbRpcController(deviceGroupConfigList, ack);
serviceStub->modifyDeviceGroup(controller, deviceGroupConfigList, ack,
@ -588,43 +607,72 @@ void PortGroup::when_configApply(int portIndex)
// Update/Sync Streams
//
qDebug("applying 'deleted streams' ...");
logInfo(id(), mPorts[portIndex]->id(), QString("Deleting old Streams"));
streamIdList = new OstProto::StreamIdList;
ack = new OstProto::Ack;
controller = new PbRpcController(streamIdList, ack);
streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id());
mPorts[portIndex]->getDeletedStreamsSinceLastSync(*streamIdList);
serviceStub->deleteStream(controller, streamIdList, ack,
NewCallback(this, &PortGroup::processDeleteStreamAck, controller));
if (streamIdList->stream_id_size()) {
logInfo(id(), mPorts[portIndex]->id(), QString("Deleting old Streams"));
ack = new OstProto::Ack;
controller = new PbRpcController(streamIdList, ack);
serviceStub->deleteStream(controller, streamIdList, ack,
NewCallback(this, &PortGroup::processDeleteStreamAck, controller));
}
else
delete streamIdList;
qDebug("applying 'new streams' ...");
logInfo(id(), mPorts[portIndex]->id(), QString("Creating new Streams"));
streamIdList = new OstProto::StreamIdList;
ack = new OstProto::Ack;
controller = new PbRpcController(streamIdList, ack);
streamIdList->mutable_port_id()->set_id(mPorts[portIndex]->id());
mPorts[portIndex]->getNewStreamsSinceLastSync(*streamIdList);
serviceStub->addStream(controller, streamIdList, ack,
NewCallback(this, &PortGroup::processAddStreamAck, controller));
if (streamIdList->stream_id_size()) {
logInfo(id(), mPorts[portIndex]->id(), QString("Creating new Streams"));
ack = new OstProto::Ack;
controller = new PbRpcController(streamIdList, ack);
serviceStub->addStream(controller, streamIdList, ack,
NewCallback(this, &PortGroup::processAddStreamAck, controller));
}
else
delete streamIdList;
qDebug("applying 'modified streams' ...");
logInfo(id(), mPorts[portIndex]->id(),
QString("Modifying changed Streams"));
streamConfigList = new OstProto::StreamConfigList;
ack = new OstProto::Ack;
controller = new PbRpcController(streamConfigList, ack);
streamConfigList->mutable_port_id()->set_id(mPorts[portIndex]->id());
mPorts[portIndex]->getModifiedStreamsSinceLastSync(*streamConfigList);
if (streamConfigList->stream_size()) {
logInfo(id(), mPorts[portIndex]->id(),
QString("Modifying changed Streams"));
ack = new OstProto::Ack;
controller = new PbRpcController(streamConfigList, ack);
serviceStub->modifyStream(controller, streamConfigList, ack,
NewCallback(this, &PortGroup::processModifyStreamAck,
portIndex, controller));
}
else
delete streamConfigList;
serviceStub->modifyStream(controller, streamConfigList, ack,
NewCallback(this, &PortGroup::processModifyStreamAck,
qDebug("resolve neighbors before building ...");
logInfo(id(), mPorts[portIndex]->id(),
QString("Resolving device neighbors"));
OstProto::PortIdList *portIdList = new OstProto::PortIdList;
OstProto::PortId *portId = portIdList->add_port_id();
portId->set_id(mPorts[portIndex]->id());
ack = new OstProto::Ack;
controller = new PbRpcController(portIdList, ack);
serviceStub->resolveDeviceNeighbors(controller, portIdList, ack,
NewCallback(this, &PortGroup::processResolveDeviceNeighborsAck,
controller));
qDebug("finish apply by building ...");
logInfo(id(), mPorts[portIndex]->id(),
QString("Re-building packets"));
buildConfig = new OstProto::BuildConfig;
ack = new OstProto::Ack;
controller = new PbRpcController(buildConfig, ack);
buildConfig->mutable_port_id()->set_id(mPorts[portIndex]->id());
serviceStub->build(controller, buildConfig, ack,
NewCallback(this, &PortGroup::processApplyBuildAck,
portIndex, controller));
}
void PortGroup::processAddDeviceGroupAck(PbRpcController *controller)
@ -752,8 +800,33 @@ void PortGroup::processModifyStreamAck(int portIndex,
OstProto::Ack *ack = static_cast<OstProto::Ack*>(controller->response());
if (controller->Failed())
{
qDebug("%s: rpc failed(%s)", __FUNCTION__,
qPrintable(controller->ErrorString()));
logError(id(), mPorts[portIndex]->id(), controller->ErrorString());
goto _error_exit;
}
if (ack->status())
logError(id(), mPorts[portIndex]->id(),
QString::fromStdString(ack->notes()));
_error_exit:
delete controller;
}
void PortGroup::processApplyBuildAck(int portIndex, PbRpcController *controller)
{
qDebug("In %s", __FUNCTION__);
OstProto::Ack *ack = static_cast<OstProto::Ack*>(controller->response());
qDebug("apply completed");
logInfo(id(), mPorts[portIndex]->id(), QString("All port changes applied"));
logInfo(id(), mPorts[portIndex]->id(),
QString("All port changes applied - in %1s")
.arg(applyTimer_.elapsed()/1e3));
applyTimer_.invalidate();
if (controller->Failed())
{
@ -769,6 +842,7 @@ void PortGroup::processModifyStreamAck(int portIndex,
_error_exit:
mPorts[portIndex]->when_syncComplete();
emit applyFinished();
mainWindow->setEnabled(true);
QApplication::restoreOverrideCursor();
@ -889,18 +963,47 @@ void PortGroup::modifyPort(int portIndex, OstProto::Port portConfig)
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
mainWindow->setDisabled(true);
logInfo(id(), mPorts[portIndex]->id(),
QString("Modifying port configuration"));
OstProto::Port *port = portConfigList->add_port();
port->CopyFrom(portConfig);
port->mutable_port_id()->set_id(mPorts[portIndex]->id());
PbRpcController *controller = new PbRpcController(portConfigList, ack);
serviceStub->modifyPort(controller, portConfigList, ack,
NewCallback(this, &PortGroup::processModifyPortAck, true, controller));
NewCallback(this, &PortGroup::processModifyPortAck, controller));
logInfo(id(), mPorts[portIndex]->id(),
QString("Re-building packets"));
OstProto::BuildConfig *buildConfig = new OstProto::BuildConfig;
ack = new OstProto::Ack;
controller = new PbRpcController(buildConfig, ack);
buildConfig->mutable_port_id()->set_id(mPorts[portIndex]->id());
serviceStub->build(controller, buildConfig, ack,
NewCallback(this, &PortGroup::processModifyPortBuildAck,
true, controller));
}
void PortGroup::processModifyPortAck(bool restoreUi,PbRpcController *controller)
void PortGroup::processModifyPortAck(PbRpcController *controller)
{
qDebug("In %s", __FUNCTION__);
if (controller->Failed())
{
qDebug("%s: rpc failed(%s)", __FUNCTION__,
qPrintable(controller->ErrorString()));
logError(id(), controller->ErrorString());
}
OstProto::Ack *ack = static_cast<OstProto::Ack*>(controller->response());
if (ack->status())
logError(id(), QString::fromStdString(ack->notes()));
delete controller;
}
void PortGroup::processModifyPortBuildAck(bool restoreUi, PbRpcController *controller)
{
qDebug("In %s", __FUNCTION__);
if (controller->Failed())
@ -1008,8 +1111,11 @@ void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller)
// * modify (new) deviceGroups
// * add (new) stream ids
// * modify (new) streams
// * resolve neighbors
// * build packets
// XXX: This assumes getDeviceGroupIdList() was invoked before
// getStreamIdList() - if the order changes this code will break!
// XXX: See resolve/build notes below
// XXX: same name as input param, but shouldn't cause any problem
PbRpcController *controller;
@ -1046,10 +1152,11 @@ void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller)
serviceStub->modifyPort(controller, portConfigList, ack,
NewCallback(this, &PortGroup::processModifyPortAck,
false, controller));
controller));
}
// add/modify deviceGroups
bool resolve = false;
if (newPortContent->device_groups_size())
{
OstProto::DeviceGroupIdList *deviceGroupIdList
@ -1081,6 +1188,7 @@ void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller)
deviceGroupConfigList, ack,
NewCallback(this, &PortGroup::processModifyDeviceGroupAck,
portIndex, controller));
resolve = true;
}
// add/modify streams
@ -1111,8 +1219,37 @@ void PortGroup::processStreamIdList(int portIndex, PbRpcController *controller)
serviceStub->modifyStream(controller, streamConfigList, ack,
NewCallback(this, &PortGroup::processModifyStreamAck,
portIndex, controller));
resolve = true;
}
// XXX: Ideally resolve and build should be called after **all**
// ports and portgroups are configured. As of now, any resolve
// replied to by ports/portgroups configured later in the open
// session sequence will fail.
// However, to do that, we may need to rethink the open session
// implementation - so going with this for now
if (resolve)
{
OstProto::PortIdList *portIdList = new OstProto::PortIdList;
portIdList->add_port_id()->set_id(portId);
OstProto::Ack *ack = new OstProto::Ack;
controller = new PbRpcController(portIdList, ack);
serviceStub->resolveDeviceNeighbors(controller, portIdList, ack,
NewCallback(this,
&PortGroup::processResolveDeviceNeighborsAck,
controller));
resolve = false;
}
// build packets using the new config
OstProto::BuildConfig *buildConfig = new OstProto::BuildConfig;
OstProto::Ack *ack = new OstProto::Ack;
controller = new PbRpcController(buildConfig, ack);
buildConfig->mutable_port_id()->set_id(mPorts[portIndex]->id());
serviceStub->build(controller, buildConfig, ack,
NewCallback(this, &PortGroup::processModifyPortBuildAck,
false, controller));
// delete newPortConfig
atConnectPortConfig_[portIndex] = NULL;
@ -1915,9 +2052,12 @@ bool PortGroup::getStreamStats(QList<uint> *portList)
if (portList == NULL)
guidList->mutable_port_id_list()->CopyFrom(*portIdList_);
else
for (int i = 0; i < portList->size(); i++)
for (int i = 0; i < portList->size(); i++) {
guidList->mutable_port_id_list()->add_port_id()
->set_id(portList->at(i));
if (mPorts.at(i)->isTransmitting())
logWarn(id(), i, "Port is still transmitting - stream stats may be unavailable or incomplete");
}
serviceStub->getStreamStats(controller, guidList, statsList,
NewCallback(this, &PortGroup::processStreamStatsList, controller));

View File

@ -21,6 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#define _PORT_GROUP_H
#include "port.h"
#include <QElapsedTimer>
#include <QHostAddress>
#include <QTcpSocket>
@ -62,6 +63,7 @@ private:
PbRpcChannel *rpcChannel;
PbRpcController *statsController;
bool isGetStatsPending_;
QElapsedTimer applyTimer_;
OstProto::OstService::Stub *serviceStub;
@ -118,6 +120,7 @@ public:
void processAddStreamAck(PbRpcController *controller);
void processDeleteStreamAck(PbRpcController *controller);
void processModifyStreamAck(int portIndex, PbRpcController *controller);
void processApplyBuildAck(int portIndex, PbRpcController *controller);
void processAddDeviceGroupAck(PbRpcController *controller);
void processDeleteDeviceGroupAck(PbRpcController *controller);
@ -127,7 +130,8 @@ public:
void processDeviceNeighbors(int portIndex, PbRpcController *controller);
void modifyPort(int portId, OstProto::Port portConfig);
void processModifyPortAck(bool restoreUi, PbRpcController *controller);
void processModifyPortAck(PbRpcController *controller);
void processModifyPortBuildAck(bool restoreUi, PbRpcController *controller);
void processUpdatedPortConfig(PbRpcController *controller);
void getStreamIdList();
@ -171,6 +175,7 @@ public:
void processStreamStatsList(PbRpcController *controller);
signals:
void applyFinished();
void portGroupDataChanged(int portGroupId, int portId = 0xFFFF);
void portListAboutToBeChanged(quint32 portGroupId);
void portListChanged(quint32 portGroupId);

View File

@ -43,12 +43,6 @@ PortGroupList::PortGroupList()
deviceGroupModelTester_ = new ModelTest(getDeviceGroupModel());
deviceModelTester_ = new ModelTest(getDeviceModel());
#endif
// Add the "Local" Port Group
if (appParams.optLocalDrone()) {
PortGroup *pg = new PortGroup;
addPortGroup(*pg);
}
}
PortGroupList::~PortGroupList()
@ -74,6 +68,7 @@ bool PortGroupList::isPort(const QModelIndex& index)
PortGroup& PortGroupList::portGroup(const QModelIndex& index)
{
Q_ASSERT(index.isValid());
Q_ASSERT(mPortGroupListModel.isPortGroup(index));
return *(mPortGroups[index.row()]);
@ -81,6 +76,8 @@ PortGroup& PortGroupList::portGroup(const QModelIndex& index)
Port& PortGroupList::port(const QModelIndex& index)
{
Q_ASSERT(index.isValid());
Q_ASSERT(index.parent().isValid());
Q_ASSERT(mPortGroupListModel.isPort(index));
return (*mPortGroups.at(index.parent().row())->mPorts[index.row()]);
}
@ -103,6 +100,9 @@ void PortGroupList::addPortGroup(PortGroup &portGroup)
connect(&portGroup, SIGNAL(portListChanged(quint32)),
&mPortStatsModel, SLOT(when_portListChanged()));
connect(&portGroup, SIGNAL(portGroupDataChanged(int, int)),
&mPortStatsModel, SLOT(when_portGroupDataChanged(int, int)));
connect(&portGroup, SIGNAL(statsChanged(quint32)),
&mPortStatsModel, SLOT(when_portGroup_stats_update(quint32)));
@ -116,8 +116,10 @@ void PortGroupList::addPortGroup(PortGroup &portGroup)
void PortGroupList::removePortGroup(PortGroup &portGroup)
{
mPortGroupListModel.portGroupAboutToBeRemoved(&portGroup);
// Disconnect before removing from list
portGroup.disconnectFromHost();
mPortGroupListModel.portGroupAboutToBeRemoved(&portGroup);
PortGroup* pg = mPortGroups.takeAt(mPortGroups.indexOf(&portGroup));
qDebug("after takeAt()");
mPortGroupListModel.portGroupRemoved();
@ -134,11 +136,12 @@ void PortGroupList::removeAllPortGroups()
do {
PortGroup *pg = mPortGroups.at(0);
pg->disconnectFromHost();
mPortGroupListModel.portGroupAboutToBeRemoved(pg);
mPortGroups.removeFirst();
delete pg;
mPortGroupListModel.portGroupRemoved();
} while (!mPortGroups.isEmpty());
mPortGroupListModel.portGroupRemoved();
mPortGroupListModel.when_portListChanged();
mPortStatsModel.when_portListChanged();

View File

@ -200,6 +200,11 @@ QVariant PortModel::headerData(int /*section*/, Qt::Orientation orientation, int
return QString("Name");
}
Qt::DropActions PortModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
QModelIndex PortModel::index (int row, int col,
const QModelIndex & parent) const
{

View File

@ -45,6 +45,8 @@ public:
const QModelIndex &parent = QModelIndex()) const;
QModelIndex parent(const QModelIndex &index) const;
Qt::DropActions supportedDropActions() const;
bool isPortGroup(const QModelIndex& index);
bool isPort(const QModelIndex& index);
quint32 portGroupId(const QModelIndex& index);

View File

@ -49,7 +49,8 @@ QList<uint> PortStatsFilterDialog::getItemList(bool* ok,
{
QStandardItem *item;
item = new QStandardItem(model->headerData(i, orientation).toString());
item = new QStandardItem(model->headerData(i, orientation)
.toString().replace('\n', ' '));
item->setData(i, kLogicalIndex);
item->setData(initial.indexOf(i), kVisualIndex);
item->setFlags(Qt::ItemIsSelectable
@ -87,7 +88,7 @@ void PortStatsFilterDialog::on_tbSelectIn_clicked()
foreach(QModelIndex idx, lvUnselected->selectionModel()->selectedIndexes())
rows.append(idx.row());
qSort(rows.begin(), rows.end(), qGreater<int>());
std::sort(rows.begin(), rows.end(), qGreater<int>());
QModelIndex idx = lvSelected->selectionModel()->currentIndex();
int insertAt = idx.isValid() ? idx.row() : mSelected.rowCount();
@ -105,7 +106,7 @@ void PortStatsFilterDialog::on_tbSelectOut_clicked()
foreach(QModelIndex idx, lvSelected->selectionModel()->selectedIndexes())
rows.append(idx.row());
qSort(rows.begin(), rows.end(), qGreater<int>());
std::sort(rows.begin(), rows.end(), qGreater<int>());
foreach(int row, rows)
{

View File

@ -20,7 +20,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "portstatsmodel.h"
#include "portgrouplist.h"
#include <QApplication>
#include <QPainter>
#include <QPalette>
#include <QPixmapCache>
#include <QTimer>
@ -135,7 +137,10 @@ QVariant PortStatsModel::data(const QModelIndex &index, int role) const
// States
case e_COMBO_STATE:
return QVariant();
return QString("Link %1%2%3")
.arg(LinkStateName.at(stats.state().link_state()))
.arg(stats.state().is_transmit_on() ? ";Tx On" : "")
.arg(stats.state().is_capture_on() ? ";Cap On" : "");
// Statistics
case e_STAT_FRAMES_RCVD:
@ -156,11 +161,13 @@ QVariant PortStatsModel::data(const QModelIndex &index, int role) const
case e_STAT_BYTES_SENT:
return QString("%L1").arg(quint64(stats.tx_bytes()));
#if 0
case e_STAT_BYTE_SEND_RATE:
return QString("%L1").arg(quint64(stats.tx_bps()));
case e_STAT_BYTE_RECV_RATE:
return QString("%L1").arg(quint64(stats.rx_bps()));
#endif
case e_STAT_BIT_SEND_RATE:
return QString("%L1").arg(quint64(
@ -270,6 +277,14 @@ QVariant PortStatsModel::headerData(int section, Qt::Orientation orientation, in
return QVariant();
}
if ((role == Qt::BackgroundRole) && (orientation == Qt::Vertical)
&& qApp->styleSheet().isEmpty())
{
QPalette palette = QApplication::palette();
return section & 0x1 ?
palette.alternateBase() : palette.base();
}
if (role != Qt::DisplayRole)
return QVariant();
@ -280,16 +295,23 @@ QVariant PortStatsModel::headerData(int section, Qt::Orientation orientation, in
if (numPorts.isEmpty() || section >= numPorts.last())
return QVariant();
getDomainIndexes(index(0, section), portGroupIdx, portIdx);
PortGroup *portGroup = pgl->mPortGroups.at(portGroupIdx);
Port *port = portGroup->mPorts.at(portIdx);
portName = QString("Port %1-%2")
.arg(pgl->mPortGroups.at(portGroupIdx)->id())
.arg(pgl->mPortGroups.at(portGroupIdx)->mPorts.at(portIdx)->id());
.arg(portGroup->id())
.arg(port->id());
if (portGroupIdx < (uint) pgl->mPortGroups.size()
&& portIdx < (uint) pgl->mPortGroups.at(portGroupIdx)->mPorts.size())
&& portIdx < (uint) portGroup->mPorts.size())
{
if (!pgl->mPortGroups.at(portGroupIdx)->mPorts[portIdx]->notes()
.isEmpty())
if (!port->notes().isEmpty())
portName += " *";
portName += "\n";
portName += port->userDescription().isEmpty() ?
port->userAlias() : port->userDescription();
}
return portName;
}
@ -297,6 +319,11 @@ QVariant PortStatsModel::headerData(int section, Qt::Orientation orientation, in
return PortStatName.at(section);
}
Qt::DropActions PortStatsModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
void PortStatsModel::portListFromIndex(QModelIndexList indices,
QList<PortGroupAndPortList> &portList)
{
@ -358,6 +385,16 @@ void PortStatsModel::when_portListChanged()
endResetModel();
}
void PortStatsModel::when_portGroupDataChanged(int /*portGroupId*/, int /*portId*/)
{
if (!columnCount())
return;
// Port (user) description may have changed - update column headers
// TODO: update only the changed ports, not all
emit headerDataChanged(Qt::Horizontal, 0, columnCount()-1);
}
// FIXME: unused? if used, the index calculation row/column needs to be swapped
#if 0
void PortStatsModel::on_portStatsUpdate(int port, void* /*stats*/)

View File

@ -26,15 +26,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
class QTimer;
typedef enum {
// Info
e_INFO_START = 0,
e_INFO_USER = e_INFO_START,
e_INFO_END = e_INFO_USER,
// State
e_STATE_START,
e_STATE_START = 0,
e_COMBO_STATE = e_STATE_START,
@ -43,14 +36,16 @@ typedef enum {
// Statistics
e_STATISTICS_START,
e_STAT_FRAMES_RCVD = e_STATISTICS_START,
e_STAT_FRAMES_SENT,
e_STAT_FRAMES_SENT = e_STATISTICS_START,
e_STAT_FRAMES_RCVD,
e_STAT_BYTES_SENT,
e_STAT_BYTES_RCVD,
e_STAT_FRAME_SEND_RATE,
e_STAT_FRAME_RECV_RATE,
e_STAT_BYTES_RCVD,
e_STAT_BYTES_SENT,
#if 0
e_STAT_BYTE_SEND_RATE,
e_STAT_BYTE_RECV_RATE,
#endif
e_STAT_BIT_SEND_RATE,
e_STAT_BIT_RECV_RATE,
#if 0
@ -69,24 +64,34 @@ typedef enum {
e_STATISTICS_END = e_STAT_RX_FRAME_ERRORS,
// Info
e_INFO_START,
// XXX: keep hidden rows at end to avoid having to recalculate rows
e_INFO_USER = e_INFO_START,
e_INFO_END = e_INFO_USER,
e_STAT_MAX
} PortStat;
static QStringList PortStatName = (QStringList()
<< "User"
static const QStringList PortStatName = (QStringList()
<< "Status"
<< "Frames Received"
<< "Frames Sent"
<< "Frame Send Rate (fps)"
<< "Frame Receive Rate (fps)"
<< "Bytes Received"
<< "Bytes Sent"
<< "Byte Send Rate (Bps)"
<< "Byte Receive Rate (Bps)"
<< "Bit Send Rate (bps)"
<< "Bit Receive Rate (bps)"
<< "Sent Frames"
<< "Received Frames"
<< "Sent Bytes"
<< "Received Bytes"
<< "Send Frame Rate (fps)"
<< "Receive Frame Rate (fps)"
#if 0
<< "Send Byte Rate (Bps)"
<< "Receive Byte Rate (Bps)"
#endif
<< "Send Bit Rate (bps)"
<< "Receive Bit Rate (bps)"
#if 0
<< "Frames Received (NIC)"
<< "Frames Sent (NIC)"
@ -98,6 +103,8 @@ static QStringList PortStatName = (QStringList()
<< "Receive Errors"
<< "Receive Fifo Errors"
<< "Receive Frame Errors"
<< "User"
);
static QStringList LinkStateName = (QStringList()
@ -123,6 +130,8 @@ class PortStatsModel : public QAbstractTableModel
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const;
Qt::DropActions supportedDropActions() const;
class PortGroupAndPortList {
public:
uint portGroupId;
@ -134,6 +143,7 @@ class PortStatsModel : public QAbstractTableModel
public slots:
void when_portListChanged();
//void on_portStatsUpdate(int port, void*stats);
void when_portGroupDataChanged(int portGroupId, int portId);
void when_portGroup_stats_update(quint32 portGroupId);
private slots:

View File

@ -26,16 +26,17 @@ class PortStatsProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
PortStatsProxyModel(QObject *parent = 0)
: QSortFilterProxyModel(parent)
PortStatsProxyModel(int userRow, QObject *parent = 0)
: QSortFilterProxyModel(parent), userRow_(userRow)
{
setFilterRegExp(QRegExp(".*"));
}
protected:
bool filterAcceptsColumn(int sourceColumn,
const QModelIndex &sourceParent) const
{
QModelIndex index = sourceModel()->index(0, sourceColumn, sourceParent);
QModelIndex index = sourceModel()->index(userRow_, sourceColumn,sourceParent);
QString user = sourceModel()->data(index).toString();
return filterRegExp().exactMatch(user) ? true : false;
@ -43,9 +44,10 @@ protected:
bool filterAcceptsRow(int sourceRow,
const QModelIndex &/*sourceParent*/) const
{
// Hide row 0 - username (needed only by this filter class)
return (sourceRow > 0) ? true : false;
return sourceRow == userRow_ ? false : true;
}
private:
int userRow_;
};
#endif

View File

@ -24,6 +24,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "portstatsfilterdialog.h"
#include "portstatsmodel.h"
#include "portstatsproxymodel.h"
#include "rowborderdelegate.h"
#include "streamstatsmodel.h"
#include "streamstatswindow.h"
#include "settings.h"
@ -42,7 +43,8 @@ PortStatsWindow::PortStatsWindow(PortGroupList *pgl, QWidget *parent)
this->pgl = pgl;
model = pgl->getPortStatsModel();
proxyStatsModel = new PortStatsProxyModel(this);
// Hide 'user' row
proxyStatsModel = new PortStatsProxyModel(e_INFO_USER, this);
if (proxyStatsModel) {
proxyStatsModel->setSourceModel(model);
tvPortStats->setModel(proxyStatsModel);
@ -55,6 +57,17 @@ PortStatsWindow::PortStatsWindow(PortGroupList *pgl, QWidget *parent)
tvPortStats->verticalHeader()->setDefaultSectionSize(
tvPortStats->verticalHeader()->minimumSectionSize());
// XXX: Set Delegates for port stats view
// RowBorderDelegate: Group related stats using a horizontal line
// IconOnlyDelegate : For status, show only icons not icons+text
tvPortStats->setItemDelegate(
new RowBorderDelegate(
QSet<int>({
e_STAT_FRAMES_SENT,
e_STAT_FRAME_SEND_RATE,
e_STAT_RX_DROPS}),
this));
statusDelegate = new IconOnlyDelegate(this);
#if 0
// XXX: Ideally we should use this, but it doesn't work because in
@ -68,9 +81,7 @@ PortStatsWindow::PortStatsWindow(PortGroupList *pgl, QWidget *parent)
statusDelegate);
#else
// ... so we use this hard-coded hack
tvPortStats->setItemDelegateForRow(
proxyStatsModel ? e_COMBO_STATE-1 : e_COMBO_STATE,
statusDelegate);
tvPortStats->setItemDelegateForRow(e_COMBO_STATE, statusDelegate);
#endif
connect(tvPortStats->selectionModel(),
@ -89,6 +100,12 @@ PortStatsWindow::~PortStatsWindow()
/* ------------- SLOTS (public) -------------- */
void PortStatsWindow::clearCurrentSelection()
{
tvPortStats->selectionModel()->clearCurrentIndex();
tvPortStats->clearSelection();
}
void PortStatsWindow::showMyReservedPortsOnly(bool enabled)
{
if (!proxyStatsModel)
@ -289,6 +306,11 @@ void PortStatsWindow::on_tbClearAll_clicked()
}
}
if (proxyStatsModel) {
for(QModelIndex &index : shownColumns)
index = proxyStatsModel->mapToSource(index);
}
// Get ports corresponding to the shown columns
model->portListFromIndex(shownColumns, portList);
@ -320,9 +342,24 @@ void PortStatsWindow::on_tbGetStreamStats_clicked()
QDockWidget *statsDock = mainWindow->findChild<QDockWidget*>(
"statsDock");
mainWindow->addDockWidget(Qt::BottomDockWidgetArea, dock);
// Add stream stats tab to the immediate right of port-stats ...
mainWindow->tabifyDockWidget(statsDock, dock);
mainWindow->splitDockWidget(statsDock, dock, Qt::Horizontal);
// ... make it the currently visible tab ...
dock->show();
dock->raise();
// ... and set tab remove behaviour
// XXX: unfortunately, there's no direct way to get the TabBar
QList<QTabBar*> tabBars = mainWindow->findChildren<QTabBar*>();
foreach(QTabBar* tabBar, tabBars) {
if (tabBar->tabText(tabBar->currentIndex())
== dock->widget()->windowTitle())
tabBar->setSelectionBehaviorOnRemove(
QTabBar::SelectPreviousTab);
}
}
// Get stream stats for selected ports, portgroup by portgroup

View File

@ -38,6 +38,7 @@ public:
~PortStatsWindow();
public slots:
void clearCurrentSelection();
void showMyReservedPortsOnly(bool enabled);
private slots:

View File

@ -165,7 +165,7 @@
<string>Stop Capture</string>
</property>
<property name="statusTip">
<string>End capture on selecteed port(s)</string>
<string>End capture on selected port(s)</string>
</property>
<property name="text">
<string>Stop</string>
@ -279,7 +279,7 @@
</widget>
</item>
<item>
<widget class="QTableView" name="tvPortStats">
<widget class="XTableView" name="tvPortStats">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectColumns</enum>
</property>
@ -287,6 +287,14 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>XTableView</class>
<extends>QTableView</extends>
<header>xtableview.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="ostinato.qrc"/>
</resources>

View File

@ -19,22 +19,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "portswindow.h"
#include "applymsg.h"
#include "deviceswidget.h"
#include "portconfigdialog.h"
#include "settings.h"
#include "streamconfigdialog.h"
#include "streamfileformat.h"
#include "streamlistdelegate.h"
#include "fileformat.pb.h"
#include "portconfigdialog.h"
#include "portgrouplist.h"
#include "portwidget.h"
#include "settings.h"
#include "streamswidget.h"
#include "xqlocale.h"
#include <QFileInfo>
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QMainWindow>
#include <QMessageBox>
#include <QProgressDialog>
#include <QSortFilterProxyModel>
extern QMainWindow *mainWindow;
@ -42,25 +39,18 @@ extern QMainWindow *mainWindow;
PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
: QWidget(parent), proxyPortModel(NULL)
{
QAction *sep;
delegate = new StreamListDelegate;
proxyPortModel = new QSortFilterProxyModel(this);
//slm = new StreamListModel();
//plm = new PortGroupList();
plm = pgl;
setupUi(this);
applyMsg_ = new ApplyMessage();
portWidget->setPortGroupList(plm);
streamsWidget->setPortGroupList(plm);
devicesWidget->setPortGroupList(plm);
tvPortList->header()->hide();
tvStreamList->setItemDelegate(delegate);
tvStreamList->verticalHeader()->setDefaultSectionSize(
tvStreamList->verticalHeader()->minimumSectionSize());
// Populate PortList Context Menu Actions
tvPortList->addAction(actionNew_Port_Group);
tvPortList->addAction(actionDelete_Port_Group);
@ -70,33 +60,18 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
tvPortList->addAction(actionExclusive_Control);
tvPortList->addAction(actionPort_Configuration);
// Populate StreamList Context Menu Actions
tvStreamList->addAction(actionNew_Stream);
tvStreamList->addAction(actionEdit_Stream);
tvStreamList->addAction(actionDuplicate_Stream);
tvStreamList->addAction(actionDelete_Stream);
sep = new QAction(this);
sep->setSeparator(true);
tvStreamList->addAction(sep);
tvStreamList->addAction(actionOpen_Streams);
tvStreamList->addAction(actionSave_Streams);
// PortList, StreamList, DeviceWidget actions combined
// make this window's actions
addActions(tvPortList->actions());
sep = new QAction(this);
QAction *sep = new QAction(this);
sep->setSeparator(true);
addAction(sep);
addActions(tvStreamList->actions());
addActions(streamsWidget->actions());
sep = new QAction(this);
sep->setSeparator(true);
addAction(sep);
addActions(devicesWidget->actions());
tvStreamList->setModel(plm->getStreamModel());
// XXX: It would be ideal if we only needed to do the below to
// get the proxy model to do its magic. However, the QModelIndex
// used by the source model and the proxy model are different
@ -122,57 +97,35 @@ PortsWindow::PortsWindow(PortGroupList *pgl, QWidget *parent)
connect(plm->getPortModel(), SIGNAL(modelReset()),
SLOT(when_portModel_reset()));
connect( tvPortList->selectionModel(),
connect(actionPort_Configuration, SIGNAL(triggered()),
SLOT(when_actionPort_Configuration_triggered()));
connect(tvPortList, SIGNAL(activated(const QModelIndex&)),
SLOT(when_actionPort_Configuration_triggered(const QModelIndex&)));
connect(tvPortList->selectionModel(),
SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
this, SLOT(when_portView_currentChanged(const QModelIndex&,
const QModelIndex&)));
connect(this,
SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)),
portWidget,
SLOT(setCurrentPortIndex(const QModelIndex&, const QModelIndex&)));
connect(this,
SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)),
streamsWidget, SLOT(setCurrentPortIndex(const QModelIndex&)));
connect(this,
SIGNAL(currentPortChanged(const QModelIndex&, const QModelIndex&)),
devicesWidget, SLOT(setCurrentPortIndex(const QModelIndex&)));
connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)),
SLOT(updateStreamViewActions()));
connect(plm->getStreamModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)),
SLOT(updateStreamViewActions()));
connect(tvStreamList->selectionModel(),
SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
SLOT(updateStreamViewActions()));
connect(tvStreamList->selectionModel(),
SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
SLOT(updateStreamViewActions()));
tvStreamList->resizeColumnToContents(StreamModel::StreamIcon);
tvStreamList->resizeColumnToContents(StreamModel::StreamStatus);
// Initially we don't have any ports/streams/devices
// - so send signal triggers
when_portView_currentChanged(QModelIndex(), QModelIndex());
updateStreamViewActions();
connect(plm->getStreamModel(),
SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
this, SLOT(streamModelDataChanged()));
connect(plm->getStreamModel(),
SIGNAL(modelReset()),
this, SLOT(streamModelDataChanged()));
}
void PortsWindow::streamModelDataChanged()
{
QModelIndex current = tvPortList->currentIndex();
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
if (plm->isPort(current))
plm->port(current).recalculateAverageRates();
}
PortsWindow::~PortsWindow()
{
delete delegate;
delete proxyPortModel;
delete applyMsg_;
}
int PortsWindow::portGroupCount()
@ -285,6 +238,27 @@ bool PortsWindow::saveSession(
return true;
}
QList<QAction*> PortsWindow::portActions()
{
return tvPortList->actions();
}
QList<QAction*> PortsWindow::streamActions()
{
return streamsWidget->actions();
}
QList<QAction*> PortsWindow::deviceActions()
{
return devicesWidget->actions();
}
void PortsWindow::clearCurrentSelection()
{
tvPortList->selectionModel()->clearCurrentIndex();
tvPortList->clearSelection();
}
void PortsWindow::showMyReservedPortsOnly(bool enabled)
{
if (!proxyPortModel)
@ -302,30 +276,6 @@ void PortsWindow::showMyReservedPortsOnly(bool enabled)
proxyPortModel->setFilterRegExp(QRegExp(""));
}
void PortsWindow::on_tvStreamList_activated(const QModelIndex & index)
{
if (!index.isValid())
{
qDebug("%s: invalid index", __FUNCTION__);
return;
}
qDebug("stream list activated\n");
Port &curPort = plm->port(proxyPortModel ?
proxyPortModel->mapToSource(tvPortList->currentIndex()) :
tvPortList->currentIndex());
QList<Stream*> streams;
streams.append(curPort.mutableStreamByIndex(index.row(), false));
StreamConfigDialog scd(streams, curPort, this);
if (scd.exec() == QDialog::Accepted) {
curPort.recalculateAverageRates();
curPort.setLocalConfigChanged(true);
}
}
void PortsWindow::when_portView_currentChanged(const QModelIndex& currentIndex,
const QModelIndex& previousIndex)
{
@ -337,16 +287,12 @@ void PortsWindow::when_portView_currentChanged(const QModelIndex& currentIndex,
previous = proxyPortModel->mapToSource(previous);
}
plm->getStreamModel()->setCurrentPortIndex(current);
updatePortViewActions(currentIndex);
updateStreamViewActions();
qDebug("In %s", __FUNCTION__);
if (previous.isValid() && plm->isPort(previous))
{
disconnect(&(plm->port(previous)), SIGNAL(portRateChanged(int, int)),
this, SLOT(updatePortRates()));
disconnect(&(plm->port(previous)),
SIGNAL(localConfigChanged(int, int, bool)),
this,
@ -367,9 +313,6 @@ void PortsWindow::when_portView_currentChanged(const QModelIndex& currentIndex,
else if (plm->isPort(current))
{
swDetail->setCurrentIndex(2); // port detail page
updatePortRates();
connect(&(plm->port(current)), SIGNAL(portRateChanged(int, int)),
SLOT(updatePortRates()));
connect(&(plm->port(current)),
SIGNAL(localConfigChanged(int, int, bool)),
SLOT(updateApplyHint(int, int, bool)));
@ -377,8 +320,8 @@ void PortsWindow::when_portView_currentChanged(const QModelIndex& currentIndex,
updateApplyHint(plm->port(current).portGroupId(),
plm->port(current).id(), true);
else if (plm->port(current).numStreams())
applyHint->setText("Use the Statistics window to transmit "
"packets");
applyHint->setText("Click <img src=':/icons/control_play'/> "
"to transmit packets");
else
applyHint->setText("");
}
@ -427,102 +370,6 @@ void PortsWindow::when_portModel_reset()
when_portView_currentChanged(QModelIndex(), tvPortList->currentIndex());
}
void PortsWindow::on_averagePacketsPerSec_editingFinished()
{
QModelIndex current = tvPortList->currentIndex();
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
Q_ASSERT(plm->isPort(current));
bool isOk;
double pps = XLocale().toDouble(averagePacketsPerSec->text(), &isOk);
plm->port(current).setAveragePacketRate(pps);
}
void PortsWindow::on_averageBitsPerSec_editingFinished()
{
QModelIndex current = tvPortList->currentIndex();
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
Q_ASSERT(plm->isPort(current));
bool isOk;
double bps = XLocale().toDouble(averageBitsPerSec->text(), &isOk);
plm->port(current).setAverageBitRate(bps);
}
void PortsWindow::updatePortRates()
{
QModelIndex current = tvPortList->currentIndex();
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
if (!current.isValid())
return;
if (!plm->isPort(current))
return;
averagePacketsPerSec->setText(QString("%L1")
.arg(plm->port(current).averagePacketRate(), 0, 'f', 4));
averageBitsPerSec->setText(QString("%L1")
.arg(plm->port(current).averageBitRate(), 0, 'f', 0));
}
void PortsWindow::updateStreamViewActions()
{
QModelIndex current = tvPortList->currentIndex();
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
// For some reason hasSelection() returns true even if selection size is 0
// so additional check for size introduced
if (tvStreamList->selectionModel()->hasSelection() &&
(tvStreamList->selectionModel()->selection().size() > 0))
{
qDebug("Has selection %d",
tvStreamList->selectionModel()->selection().size());
// If more than one non-contiguous ranges selected,
// disable "New" and "Edit"
if (tvStreamList->selectionModel()->selection().size() > 1)
{
actionNew_Stream->setDisabled(true);
actionEdit_Stream->setDisabled(true);
}
else
{
actionNew_Stream->setEnabled(true);
actionEdit_Stream->setEnabled(true);
}
// Duplicate/Delete are always enabled as long as we have a selection
actionDuplicate_Stream->setEnabled(true);
actionDelete_Stream->setEnabled(true);
}
else
{
qDebug("No selection");
if (plm->isPort(current))
actionNew_Stream->setEnabled(true);
else
actionNew_Stream->setDisabled(true);
actionEdit_Stream->setDisabled(true);
actionDuplicate_Stream->setDisabled(true);
actionDelete_Stream->setDisabled(true);
}
actionOpen_Streams->setEnabled(plm->isPort(current));
actionSave_Streams->setEnabled(tvStreamList->model()->rowCount() > 0);
}
void PortsWindow::updateApplyHint(int /*portGroupId*/, int /*portId*/,
bool configChanged)
{
@ -530,9 +377,12 @@ void PortsWindow::updateApplyHint(int /*portGroupId*/, int /*portId*/,
applyHint->setText("Configuration has changed - "
"<font color='red'><b>click Apply</b></font> "
"to activate the changes");
else if (plm->getStreamModel()->rowCount() > 0)
applyHint->setText("Configuration activated - "
"click <img src=':/icons/control_play'/> "
"to transmit packets");
else
applyHint->setText("Configuration activated. Use the Statistics "
"window to transmit packets");
applyHint->setText("Configuration activated");
}
void PortsWindow::updatePortViewActions(const QModelIndex& currentIndex)
@ -647,6 +497,11 @@ void PortsWindow::on_pbApply_clicked()
goto _exit;
}
disconnect(applyMsg_);
connect(&(plm->portGroup(curPortGroup)), SIGNAL(applyFinished()),
applyMsg_, SLOT(hide()));
applyMsg_->show();
// FIXME(HI): shd this be a signal?
//portGroup.when_configApply(port);
// FIXME(MED): mixing port id and index!!!
@ -747,9 +602,11 @@ void PortsWindow::on_actionExclusive_Control_triggered(bool checked)
}
}
void PortsWindow::on_actionPort_Configuration_triggered()
void PortsWindow::when_actionPort_Configuration_triggered(
const QModelIndex &portIndex)
{
QModelIndex current = tvPortList->selectionModel()->currentIndex();
QModelIndex current = portIndex.isValid() ?
portIndex : tvPortList->selectionModel()->currentIndex();
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
@ -764,6 +621,8 @@ void PortsWindow::on_actionPort_Configuration_triggered()
// TODO: extend Port::protoDataCopyInto() to accept an optional param
// which says copy only modifiable fields
//plm->port(current).protoDataCopyInto(&config);
config.set_description(port.systemDescription().toStdString());
config.set_user_description(port.userDescription().toStdString());
config.set_transmit_mode(port.transmitMode());
config.set_is_tracking_stream_stats(port.trackStreamStats());
config.set_is_exclusive_control(port.hasExclusiveControl());
@ -774,258 +633,3 @@ void PortsWindow::on_actionPort_Configuration_triggered()
if (dialog.exec() == QDialog::Accepted)
plm->portGroup(current.parent()).modifyPort(current.row(), config);
}
void PortsWindow::on_actionNew_Stream_triggered()
{
qDebug("New Stream Action");
QItemSelectionModel* selectionModel = tvStreamList->selectionModel();
if (selectionModel->selection().size() > 1) {
qDebug("%s: Unexpected selection size %d, can't add", __FUNCTION__,
selectionModel->selection().size());
return;
}
// In case nothing is selected, insert 1 row at the end
StreamModel *streamModel = plm->getStreamModel();
int row = streamModel->rowCount(), count = 1;
// In case we have a single range selected; insert as many rows as
// in the singe selected range before the top of the selected range
if (selectionModel->selection().size() == 1)
{
row = selectionModel->selection().at(0).top();
count = selectionModel->selection().at(0).height();
}
Port &curPort = plm->port(proxyPortModel ?
proxyPortModel->mapToSource(tvPortList->currentIndex()) :
tvPortList->currentIndex());
QList<Stream*> streams;
for (int i = 0; i < count; i++)
streams.append(new Stream);
StreamConfigDialog scd(streams, curPort, this);
scd.setWindowTitle(tr("Add Stream"));
if (scd.exec() == QDialog::Accepted)
streamModel->insert(row, streams);
}
void PortsWindow::on_actionEdit_Stream_triggered()
{
qDebug("Edit Stream Action");
QItemSelectionModel* streamModel = tvStreamList->selectionModel();
if (!streamModel->hasSelection())
return;
Port &curPort = plm->port(proxyPortModel ?
proxyPortModel->mapToSource(tvPortList->currentIndex()) :
tvPortList->currentIndex());
QList<Stream*> streams;
foreach(QModelIndex index, streamModel->selectedRows())
streams.append(curPort.mutableStreamByIndex(index.row(), false));
StreamConfigDialog scd(streams, curPort, this);
if (scd.exec() == QDialog::Accepted) {
curPort.recalculateAverageRates();
curPort.setLocalConfigChanged(true);
}
}
void PortsWindow::on_actionDuplicate_Stream_triggered()
{
QItemSelectionModel* model = tvStreamList->selectionModel();
QModelIndex current = tvPortList->selectionModel()->currentIndex();
qDebug("Duplicate Stream Action");
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
if (model->hasSelection())
{
bool isOk;
int count = QInputDialog::getInt(this, "Duplicate Streams",
"Count", 1, 1, 9999, 1, &isOk);
if (!isOk)
return;
QList<int> list;
foreach(QModelIndex index, model->selectedRows())
list.append(index.row());
plm->port(current).duplicateStreams(list, count);
}
else
qDebug("No selection");
}
void PortsWindow::on_actionDelete_Stream_triggered()
{
qDebug("Delete Stream Action");
QModelIndex index;
if (tvStreamList->selectionModel()->hasSelection())
{
qDebug("SelectedIndexes %d",
tvStreamList->selectionModel()->selectedRows().size());
while(tvStreamList->selectionModel()->selectedRows().size())
{
index = tvStreamList->selectionModel()->selectedRows().at(0);
plm->getStreamModel()->removeRows(index.row(), 1);
}
}
else
qDebug("No selection");
}
void PortsWindow::on_actionOpen_Streams_triggered()
{
qDebug("Open Streams Action");
QStringList fileTypes = StreamFileFormat::supportedFileTypes(
StreamFileFormat::kOpenFile);
QString fileType;
QModelIndex current = tvPortList->selectionModel()->currentIndex();
static QString dirName;
QString fileName;
QString errorStr;
bool append = true;
bool ret;
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
Q_ASSERT(plm->isPort(current));
if (fileTypes.size())
fileType = fileTypes.at(0);
fileName = QFileDialog::getOpenFileName(this, tr("Open Streams"),
dirName, fileTypes.join(";;"), &fileType);
if (fileName.isEmpty())
goto _exit;
if (tvStreamList->model()->rowCount())
{
QMessageBox msgBox(QMessageBox::Question, qApp->applicationName(),
tr("Append to existing streams? Or overwrite?"),
QMessageBox::NoButton, this);
QPushButton *appendBtn = msgBox.addButton(tr("Append"),
QMessageBox::ActionRole);
QPushButton *overwriteBtn = msgBox.addButton(tr("Overwrite"),
QMessageBox::ActionRole);
QPushButton *cancelBtn = msgBox.addButton(QMessageBox::Cancel);
msgBox.exec();
if (msgBox.clickedButton() == cancelBtn)
goto _exit;
else if (msgBox.clickedButton() == appendBtn)
append = true;
else if (msgBox.clickedButton() == overwriteBtn)
append = false;
else
Q_ASSERT(false);
}
ret = plm->port(current).openStreams(fileName, append, errorStr);
if (!ret || !errorStr.isEmpty())
{
QMessageBox msgBox(this);
QStringList str = errorStr.split("\n\n\n\n");
msgBox.setIcon(ret ? QMessageBox::Warning : QMessageBox::Critical);
msgBox.setWindowTitle(qApp->applicationName());
msgBox.setText(str.at(0));
if (str.size() > 1)
msgBox.setDetailedText(str.at(1));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.exec();
}
dirName = QFileInfo(fileName).absolutePath();
_exit:
return;
}
void PortsWindow::on_actionSave_Streams_triggered()
{
qDebug("Save Streams Action");
QModelIndex current = tvPortList->selectionModel()->currentIndex();
static QString fileName;
QStringList fileTypes = StreamFileFormat::supportedFileTypes(
StreamFileFormat::kSaveFile);
QString fileType;
QString errorStr;
QFileDialog::Options options;
if (proxyPortModel)
current = proxyPortModel->mapToSource(current);
// On Mac OS with Native Dialog, getSaveFileName() ignores fileType
// which we need
#if defined(Q_OS_MAC)
options |= QFileDialog::DontUseNativeDialog;
#endif
if (fileTypes.size())
fileType = fileTypes.at(0);
Q_ASSERT(plm->isPort(current));
_retry:
fileName = QFileDialog::getSaveFileName(this, tr("Save Streams"),
fileName, fileTypes.join(";;"), &fileType, options);
if (fileName.isEmpty())
goto _exit;
if (QFileInfo(fileName).suffix().isEmpty()) {
QString fileExt = fileType.section(QRegExp("[\\*\\)]"), 1, 1);
qDebug("Adding extension '%s' to '%s'",
qPrintable(fileExt), qPrintable(fileName));
fileName.append(fileExt);
if (QFileInfo(fileName).exists()) {
if (QMessageBox::warning(this, tr("Overwrite File?"),
QString("The file \"%1\" already exists.\n\n"
"Do you wish to overwrite it?")
.arg(QFileInfo(fileName).fileName()),
QMessageBox::Yes|QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
goto _retry;
}
}
fileType = fileType.remove(QRegExp("\\(.*\\)")).trimmed();
if (!fileType.startsWith("Ostinato")
&& !fileType.startsWith("Python"))
{
if (QMessageBox::warning(this, tr("Ostinato"),
QString("You have chosen to save in %1 format. All stream "
"attributes may not be saved in this format.\n\n"
"It is recommended to save in native Ostinato format.\n\n"
"Continue to save in %2 format?").arg(fileType).arg(fileType),
QMessageBox::Yes|QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
goto _retry;
}
// TODO: all or selected?
if (!plm->port(current).saveStreams(fileName, fileType, errorStr))
QMessageBox::critical(this, qApp->applicationName(), errorStr);
else if (!errorStr.isEmpty())
QMessageBox::warning(this, qApp->applicationName(), errorStr);
fileName = QFileInfo(fileName).absolutePath();
_exit:
return;
}

View File

@ -20,12 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#ifndef _PORTS_WINDOW_H
#define _PORTS_WINDOW_H
#include <QWidget>
#include <QAbstractItemModel>
#include "ui_portswindow.h"
#include "portgrouplist.h"
#include <QWidget>
class ApplyMessage;
class PortGroupList;
class QAbstractItemDelegate;
class QProgressDialog;
class QSortFilterProxyModel;
@ -37,9 +37,6 @@ class PortsWindow : public QWidget, private Ui::PortsWindow
{
Q_OBJECT
//QAbstractItemModel *slm; // stream list model
PortGroupList *plm;
public:
PortsWindow(PortGroupList *pgl, QWidget *parent = 0);
~PortsWindow();
@ -53,27 +50,22 @@ public:
QString &error,
QProgressDialog *progress = NULL);
QList<QAction*> portActions();
QList<QAction*> streamActions();
QList<QAction*> deviceActions();
signals:
void currentPortChanged(const QModelIndex &current,
const QModelIndex &previous);
private:
QString lastNewPortGroup;
QAbstractItemDelegate *delegate;
QSortFilterProxyModel *proxyPortModel;
public slots:
void clearCurrentSelection();
void showMyReservedPortsOnly(bool enabled);
private slots:
void updateApplyHint(int portGroupId, int portId, bool configChanged);
void updatePortViewActions(const QModelIndex& currentIndex);
void updateStreamViewActions();
void on_averagePacketsPerSec_editingFinished();
void on_averageBitsPerSec_editingFinished();
void updatePortRates();
void on_tvStreamList_activated(const QModelIndex & index);
void when_portView_currentChanged(const QModelIndex& currentIndex,
const QModelIndex& previousIndex);
void when_portModel_dataChanged(const QModelIndex& topLeft,
@ -88,17 +80,14 @@ private slots:
void on_actionDisconnect_Port_Group_triggered();
void on_actionExclusive_Control_triggered(bool checked);
void on_actionPort_Configuration_triggered();
void when_actionPort_Configuration_triggered(
const QModelIndex &portIndex = QModelIndex());
void on_actionNew_Stream_triggered();
void on_actionEdit_Stream_triggered();
void on_actionDuplicate_Stream_triggered();
void on_actionDelete_Stream_triggered();
void on_actionOpen_Streams_triggered();
void on_actionSave_Streams_triggered();
void streamModelDataChanged();
private:
PortGroupList *plm;
QString lastNewPortGroup;
QSortFilterProxyModel *proxyPortModel;
ApplyMessage *applyMsg_;
};
#endif

View File

@ -39,7 +39,7 @@
<widget class="QStackedWidget" name="swDetail">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>2</horstretch>
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
@ -51,7 +51,7 @@
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;p&gt;&lt;b&gt;Welcome to Ostinato&lt;/b&gt;&lt;/p&gt;
<string>&lt;p&gt;&lt;b&gt;Welcome to Ostinato!&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;The port list on the left contains all the ports on which you can transmit packets.&lt;/p&gt;
&lt;p&gt;Ports belong to a port group. Make sure the Port Group has a &lt;img src=&quot;:/icons/bullet_green.png&quot;/&gt; next to it, then double click the port group to show or hide the ports in the port group.&lt;/p&gt;
&lt;p&gt;To generate packets, you need to create and configure packet streams. A stream is a sequence of one or more packets.&lt;/p&gt;
@ -126,7 +126,16 @@
</widget>
<widget class="QWidget" name="portDetail">
<layout class="QVBoxLayout">
<property name="margin">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
@ -138,7 +147,16 @@
<enum>QFrame::Raised</enum>
</property>
<layout class="QHBoxLayout">
<property name="margin">
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>3</number>
</property>
<property name="rightMargin">
<number>3</number>
</property>
<property name="bottomMargin">
<number>3</number>
</property>
<item>
@ -195,80 +213,10 @@
</attribute>
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QRadioButton" name="radioButton">
<property name="text">
<string>Avg pps</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="averagePacketsPerSec"/>
</item>
<item>
<widget class="QRadioButton" name="radioButton_2">
<property name="text">
<string>Avg bps</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="averageBitsPerSec">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
<widget class="PortWidget" name="portWidget" native="true"/>
</item>
<item>
<widget class="XTableView" name="tvStreamList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
</property>
<property name="whatsThis">
<string>This is the stream list for the selected port
A stream is a sequence of one or more packets
Right-click to create a stream</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
<widget class="StreamsWidget" name="streamsWidget" native="true"/>
</item>
</layout>
</widget>
@ -326,33 +274,6 @@ Right-click to create a stream</string>
<string>Disconnect Port Group</string>
</property>
</action>
<action name="actionNew_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_add.png</normaloff>:/icons/stream_add.png</iconset>
</property>
<property name="text">
<string>New Stream</string>
</property>
</action>
<action name="actionDelete_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_delete.png</normaloff>:/icons/stream_delete.png</iconset>
</property>
<property name="text">
<string>Delete Stream</string>
</property>
</action>
<action name="actionEdit_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_edit.png</normaloff>:/icons/stream_edit.png</iconset>
</property>
<property name="text">
<string>Edit Stream</string>
</property>
</action>
<action name="actionExclusive_Control">
<property name="checkable">
<bool>true</bool>
@ -361,30 +282,11 @@ Right-click to create a stream</string>
<string>Exclusive Port Control (EXPERIMENTAL)</string>
</property>
</action>
<action name="actionOpen_Streams">
<property name="text">
<string>Open Streams ...</string>
</property>
</action>
<action name="actionSave_Streams">
<property name="text">
<string>Save Streams ...</string>
</property>
</action>
<action name="actionPort_Configuration">
<property name="text">
<string>Port Configuration ...</string>
</property>
</action>
<action name="actionDuplicate_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_duplicate.png</normaloff>:/icons/stream_duplicate.png</iconset>
</property>
<property name="text">
<string>Duplicate Stream</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
@ -399,46 +301,20 @@ Right-click to create a stream</string>
<header>xtreeview.h</header>
</customwidget>
<customwidget>
<class>XTableView</class>
<extends>QTableView</extends>
<header>xtableview.h</header>
<class>StreamsWidget</class>
<extends>QWidget</extends>
<header>streamswidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PortWidget</class>
<extends>QWidget</extends>
<header>portwidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="ostinato.qrc"/>
</resources>
<connections>
<connection>
<sender>radioButton</sender>
<signal>toggled(bool)</signal>
<receiver>averagePacketsPerSec</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>326</x>
<y>80</y>
</hint>
<hint type="destinationlabel">
<x>454</x>
<y>79</y>
</hint>
</hints>
</connection>
<connection>
<sender>radioButton_2</sender>
<signal>toggled(bool)</signal>
<receiver>averageBitsPerSec</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>523</x>
<y>80</y>
</hint>
<hint type="destinationlabel">
<x>651</x>
<y>88</y>
</hint>
</hints>
</connection>
</connections>
<connections/>
</ui>

186
client/portwidget.cpp Normal file
View File

@ -0,0 +1,186 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "portwidget.h"
#include "portgrouplist.h"
#include "xqlocale.h"
#include <cfloat>
PortWidget::PortWidget(QWidget *parent)
: QWidget(parent)
{
setupUi(this);
}
void PortWidget::setPortGroupList(PortGroupList *portGroups)
{
plm = portGroups;
connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)),
SLOT(updatePortActions()));
connect(plm->getStreamModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)),
SLOT(updatePortActions()));
connect(plm->getStreamModel(), SIGNAL(modelReset()),
SLOT(updatePortActions()));
updatePortActions();
}
PortWidget::~PortWidget()
{
}
void PortWidget::setCurrentPortIndex(const QModelIndex &currentIndex,
const QModelIndex &previousIndex)
{
if (!plm)
return;
qDebug("In %s", __PRETTY_FUNCTION__);
// XXX: We assume indices corresponds to sourceModel, not proxyModel
// - caller/sender should ensure this
// Disconnect previous port
if (plm->isPort(previousIndex))
disconnect(&(plm->port(previousIndex)),
SIGNAL(portRateChanged(int, int)),
this, SLOT(updatePortRates()));
if (!plm->isPort(currentIndex)) {
currentPortIndex_ = QModelIndex(); // set to invalid
return;
}
currentPortIndex_ = currentIndex;
// Connect current port
connect(&(plm->port(currentPortIndex_)),
SIGNAL(portRateChanged(int, int)),
this, SLOT(updatePortRates()));
double speed = plm->port(currentPortIndex_).speed();
portSpeed->setText(QString("Max %L1 Mbps").arg(speed));
rbLoad->setVisible(speed > 0);
averageLoadPercent->setVisible(speed > 0);
speedSep->setVisible(speed > 0);
portSpeed->setVisible(speed > 0);
updatePortRates();
updatePortActions();
}
void PortWidget::on_startTx_clicked()
{
Q_ASSERT(plm->isPort(currentPortIndex_));
QModelIndex curPortGroup = plm->getPortModel()->parent(currentPortIndex_);
Q_ASSERT(curPortGroup.isValid());
Q_ASSERT(plm->isPortGroup(curPortGroup));
QList<uint> portList({plm->port(currentPortIndex_).id()});
plm->portGroup(curPortGroup).startTx(&portList);
}
void PortWidget::on_stopTx_clicked()
{
Q_ASSERT(plm->isPort(currentPortIndex_));
QModelIndex curPortGroup = plm->getPortModel()->parent(currentPortIndex_);
Q_ASSERT(curPortGroup.isValid());
Q_ASSERT(plm->isPortGroup(curPortGroup));
QList<uint> portList({plm->port(currentPortIndex_).id()});
plm->portGroup(curPortGroup).stopTx(&portList);
}
void PortWidget::on_averageLoadPercent_editingFinished()
{
Q_ASSERT(plm->isPort(currentPortIndex_));
plm->port(currentPortIndex_).setAverageLoadRate(
averageLoadPercent->value()/100);
}
void PortWidget::on_averagePacketsPerSec_editingFinished()
{
Q_ASSERT(plm->isPort(currentPortIndex_));
bool isOk;
double pps = XLocale().toPacketsPerSecond(averagePacketsPerSec->text(),
&isOk);
if (isOk)
plm->port(currentPortIndex_).setAveragePacketRate(pps);
else
updatePortRates();
}
void PortWidget::on_averageBitsPerSec_editingFinished()
{
Q_ASSERT(plm->isPort(currentPortIndex_));
bool isOk;
double bps = XLocale().toBitsPerSecond(averageBitsPerSec->text(), &isOk);
if (isOk)
plm->port(currentPortIndex_).setAverageBitRate(bps);
else
updatePortRates();
}
void PortWidget::updatePortRates()
{
if (!currentPortIndex_.isValid())
return;
if (!plm->isPort(currentPortIndex_))
return;
// XXX: pps/bps input widget is a LineEdit and not a SpinBox
// because we want users to be able to enter values in various
// units e.g. "1.5 Mbps", "1000K", "50" (bps) etc.
// XXX: It's a considered decision NOT to show frame rate in
// higher units of Kpps and Mpps as most users may not be
// familiar with those and also we want frame rate to have a
// high resolution for input e.g. if user enters 1,488,095.2381
// it should NOT be shown as 1.4881 Mpps
averagePacketsPerSec->setText(QString("%L1 pps")
.arg(plm->port(currentPortIndex_).averagePacketRate(), 0, 'f', 4));
averageBitsPerSec->setText(XLocale().toBitRateString(
plm->port(currentPortIndex_).averageBitRate()));
averageLoadPercent->setValue(
plm->port(currentPortIndex_).averageLoadRate()*100);
}
void PortWidget::updatePortActions()
{
if (!plm->isPort(currentPortIndex_))
return;
startTx->setEnabled(plm->port(currentPortIndex_).numStreams() > 0);
stopTx->setEnabled(plm->port(currentPortIndex_).numStreams() > 0);
}

61
client/portwidget.h Normal file
View File

@ -0,0 +1,61 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _PORT_WIDGET_H
#define _PORT_WIDGET_H
#include "ui_portwidget.h"
#include <QModelIndex>
#include <QWidget>
class PortGroupList;
class PortWidget : public QWidget, private Ui::PortWidget
{
Q_OBJECT
public:
PortWidget(QWidget *parent = 0);
~PortWidget();
void setPortGroupList(PortGroupList *portGroups);
public slots:
void setCurrentPortIndex(const QModelIndex &currentIndex,
const QModelIndex &previousIndex);
private slots:
void on_startTx_clicked();
void on_stopTx_clicked();
void on_averageLoadPercent_editingFinished();
void on_averagePacketsPerSec_editingFinished();
void on_averageBitsPerSec_editingFinished();
void updatePortActions();
void updatePortRates();
private:
PortGroupList *plm{nullptr}; // FIXME: rename to portGroups_?
QModelIndex currentPortIndex_;
};
#endif

218
client/portwidget.ui Normal file
View File

@ -0,0 +1,218 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PortWidget</class>
<widget class="QWidget" name="PortWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>806</width>
<height>73</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QToolButton" name="startTx">
<property name="toolTip">
<string>Start Transmit</string>
</property>
<property name="statusTip">
<string>Start transmit on selected port</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/control_play.png</normaloff>:/icons/control_play.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="stopTx">
<property name="toolTip">
<string>Stop Transmit</string>
</property>
<property name="statusTip">
<string>Stop transmit on selected port</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/control_stop.png</normaloff>:/icons/control_stop.png</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Line" name="rateSep">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioButton">
<property name="text">
<string>Frame Rate</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="averagePacketsPerSec"/>
</item>
<item>
<widget class="QRadioButton" name="radioButton_2">
<property name="text">
<string>Bit Rate</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="averageBitsPerSec">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Bit rate on the line including overhead such as Preamble, IPG, FCS etc.</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rbLoad">
<property name="text">
<string>Load</string>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="averageLoadPercent">
<property name="enabled">
<bool>false</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::NoButtons</enum>
</property>
<property name="suffix">
<string>%</string>
</property>
<property name="decimals">
<number>4</number>
</property>
<property name="maximum">
<double>999.999900000000025</double>
</property>
</widget>
</item>
<item>
<widget class="Line" name="speedSep">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="portSpeed">
<property name="toolTip">
<string>Port Speed</string>
</property>
<property name="statusTip">
<string/>
</property>
<property name="text">
<string>Max speed</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="ostinato.qrc"/>
</resources>
<connections>
<connection>
<sender>radioButton</sender>
<signal>toggled(bool)</signal>
<receiver>averagePacketsPerSec</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>450</x>
<y>44</y>
</hint>
<hint type="destinationlabel">
<x>593</x>
<y>45</y>
</hint>
</hints>
</connection>
<connection>
<sender>radioButton_2</sender>
<signal>toggled(bool)</signal>
<receiver>averageBitsPerSec</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>661</x>
<y>44</y>
</hint>
<hint type="destinationlabel">
<x>804</x>
<y>45</y>
</hint>
</hints>
</connection>
<connection>
<sender>rbLoad</sender>
<signal>toggled(bool)</signal>
<receiver>averageLoadPercent</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>281</x>
<y>43</y>
</hint>
<hint type="destinationlabel">
<x>308</x>
<y>45</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -21,9 +21,11 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "../common/ostprotolib.h"
#include "settings.h"
#include "thememanager.h"
#include <QFileDialog>
#include <QtGlobal>
#include <QXmlStreamReader>
#if defined(Q_OS_WIN32)
QString kGzipPathDefaultValue;
@ -39,6 +41,7 @@ Preferences::Preferences()
setupUi(this);
// Program paths
wiresharkPathEdit->setText(appSettings->value(kWiresharkPathKey,
kWiresharkPathDefaultValue).toString());
tsharkPathEdit->setText(appSettings->value(kTsharkPathKey,
@ -50,6 +53,10 @@ Preferences::Preferences()
awkPathEdit->setText(appSettings->value(kAwkPathKey,
kAwkPathDefaultValue).toString());
// Theme
theme->addItems(ThemeManager::instance()->themeList());
theme->setCurrentText(appSettings->value(kThemeKey).toString());
// TODO(only if required): kUserKey
}
@ -77,6 +84,7 @@ void Preferences::initDefaults()
void Preferences::accept()
{
// Program paths
appSettings->setValue(kWiresharkPathKey, wiresharkPathEdit->text());
appSettings->setValue(kTsharkPathKey, tsharkPathEdit->text());
appSettings->setValue(kGzipPathKey, gzipPathEdit->text());
@ -89,6 +97,9 @@ void Preferences::accept()
appSettings->value(kDiffPathKey, kDiffPathDefaultValue).toString(),
appSettings->value(kAwkPathKey, kAwkPathDefaultValue).toString());
// Theme
ThemeManager::instance()->setTheme(theme->currentText());
QDialog::accept();
}
@ -98,6 +109,31 @@ void Preferences::on_wiresharkPathButton_clicked()
path = QFileDialog::getOpenFileName(0, "Locate Wireshark",
wiresharkPathEdit->text());
#ifdef Q_OS_MAC
// Find executable inside app bundle using Info.plist
if (!path.isEmpty() && path.endsWith(".app")) {
QFile plist(path+"/Contents/Info.plist");
plist.open(QIODevice::ReadOnly);
QXmlStreamReader xml(&plist);
while (!xml.atEnd()) {
xml.readNext();
if (xml.isStartElement()
&& (xml.name() == "key")
&& (xml.readElementText() == "CFBundleExecutable")) {
xml.readNext(); // </key>
xml.readNext(); // <string>
if (xml.isStartElement() && (xml.name() == "string"))
path = path+"/Contents/MacOs/"+xml.readElementText();
break;
}
if (xml.hasError())
qDebug("%lld:%lld Error reading Info.plist: %s",
xml.lineNumber(), xml.columnNumber(),
qPrintable(xml.errorString()));
}
}
#endif
if (!path.isEmpty())
wiresharkPathEdit->setText(path);

View File

@ -1,7 +1,8 @@
<ui version="4.0" >
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Preferences</class>
<widget class="QDialog" name="Preferences" >
<property name="geometry" >
<widget class="QDialog" name="Preferences">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
@ -9,148 +10,149 @@
<height>220</height>
</rect>
</property>
<property name="windowTitle" >
<property name="windowTitle">
<string>Preferences</string>
</property>
<property name="windowIcon" >
<iconset resource="ostinato.qrc" >:/icons/preferences.png</iconset>
<property name="windowIcon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/preferences.png</normaloff>:/icons/preferences.png</iconset>
</property>
<layout class="QVBoxLayout" >
<layout class="QVBoxLayout">
<item>
<widget class="QFrame" name="frame" >
<property name="frameShape" >
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="frameShadow" >
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<layout class="QGridLayout" >
<item row="0" column="0" >
<widget class="QLabel" name="label" >
<property name="text" >
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>'wireshark' Path</string>
</property>
<property name="buddy" >
<property name="buddy">
<cstring>wiresharkPathEdit</cstring>
</property>
</widget>
</item>
<item row="0" column="1" >
<widget class="QLineEdit" name="wiresharkPathEdit" >
<property name="enabled" >
<bool>false</bool>
<item row="0" column="1">
<widget class="QLineEdit" name="wiresharkPathEdit">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2" >
<widget class="QToolButton" name="wiresharkPathButton" >
<property name="text" >
<item row="0" column="2">
<widget class="QToolButton" name="wiresharkPathButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="1" column="0" >
<widget class="QLabel" name="label_2" >
<property name="text" >
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>'tshark' Path</string>
</property>
<property name="buddy" >
<property name="buddy">
<cstring>tsharkPathEdit</cstring>
</property>
</widget>
</item>
<item row="1" column="1" >
<widget class="QLineEdit" name="tsharkPathEdit" >
<property name="enabled" >
<bool>false</bool>
<item row="1" column="1">
<widget class="QLineEdit" name="tsharkPathEdit">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="2" >
<widget class="QToolButton" name="tsharkPathButton" >
<property name="text" >
<item row="1" column="2">
<widget class="QToolButton" name="tsharkPathButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="2" column="0" >
<widget class="QLabel" name="label_5" >
<property name="text" >
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>'gzip' Path</string>
</property>
<property name="buddy" >
<property name="buddy">
<cstring>diffPathEdit</cstring>
</property>
</widget>
</item>
<item row="2" column="1" >
<widget class="QLineEdit" name="gzipPathEdit" >
<property name="enabled" >
<bool>false</bool>
<item row="2" column="1">
<widget class="QLineEdit" name="gzipPathEdit">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="2" >
<widget class="QToolButton" name="gzipPathButton" >
<property name="text" >
<item row="2" column="2">
<widget class="QToolButton" name="gzipPathButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="3" column="0" >
<widget class="QLabel" name="label_3" >
<property name="text" >
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>'diff' Path</string>
</property>
<property name="buddy" >
<property name="buddy">
<cstring>diffPathEdit</cstring>
</property>
</widget>
</item>
<item row="3" column="1" >
<widget class="QLineEdit" name="diffPathEdit" >
<property name="enabled" >
<bool>false</bool>
<item row="3" column="1">
<widget class="QLineEdit" name="diffPathEdit">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="2" >
<widget class="QToolButton" name="diffPathButton" >
<property name="text" >
<item row="3" column="2">
<widget class="QToolButton" name="diffPathButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="4" column="0" >
<widget class="QLabel" name="label_4" >
<property name="text" >
<item row="4" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>'awk' Path</string>
</property>
<property name="buddy" >
<property name="buddy">
<cstring>awkPathEdit</cstring>
</property>
</widget>
</item>
<item row="4" column="1" >
<widget class="QLineEdit" name="awkPathEdit" >
<property name="enabled" >
<bool>false</bool>
<item row="4" column="1">
<widget class="QLineEdit" name="awkPathEdit">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="2" >
<widget class="QToolButton" name="awkPathButton" >
<property name="text" >
<item row="4" column="2">
<widget class="QToolButton" name="awkPathButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="5" column="1" >
<item row="5" column="1">
<spacer>
<property name="orientation" >
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" >
<property name="sizeHint" stdset="0">
<size>
<width>21</width>
<height>61</height>
@ -162,12 +164,26 @@
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox" >
<property name="orientation" >
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Theme (Experimental)</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="theme"/>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons" >
<set>QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok</set>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
@ -187,7 +203,7 @@
<tabstop>buttonBox</tabstop>
</tabstops>
<resources>
<include location="ostinato.qrc" />
<include location="ostinato.qrc"/>
</resources>
<connections>
<connection>
@ -196,11 +212,11 @@
<receiver>Preferences</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel" >
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel" >
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
@ -212,11 +228,11 @@
<receiver>Preferences</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel" >
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel" >
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>

View File

@ -0,0 +1,50 @@
/*
Copyright (C) 2023 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _ROW_BORDER_DELEGATE
#define _ROW_BORDER_DELEGATE
#include <QStyledItemDelegate>
#include <QSet>
class RowBorderDelegate : public QStyledItemDelegate
{
public:
RowBorderDelegate(QSet<int> rows, QObject *parent = nullptr)
: QStyledItemDelegate(parent), rows_(rows)
{
}
private:
void paint(QPainter *painter, const QStyleOptionViewItem &option,
const QModelIndex &index) const
{
QStyledItemDelegate::paint(painter, option, index);
if (rows_.contains(index.row())) {
const QRect rect(option.rect);
painter->drawLine(rect.topLeft(), rect.topRight());
}
}
QSet<int> rows_;
};
#endif

View File

@ -31,7 +31,7 @@ const QString kWiresharkPathDefaultValue(
"C:/Program Files/Wireshark/wireshark.exe");
#elif defined(Q_OS_MAC)
const QString kWiresharkPathDefaultValue(
"/Applications/Wireshark.app/Contents/Resources/bin/wireshark");
"/Applications/Wireshark.app/Contents/MacOS/Wireshark");
#else
const QString kWiresharkPathDefaultValue("/usr/bin/wireshark");
#endif
@ -74,6 +74,8 @@ const QString kAwkPathDefaultValue("/usr/bin/awk");
const QString kAwkPathDefaultValue("/usr/bin/awk");
#endif
const QString kThemeKey("Theme");
const QString kUserKey("User");
extern QString kUserDefaultValue;
@ -82,6 +84,7 @@ extern QString kUserDefaultValue;
//
const QString kApplicationWindowGeometryKey("LastUse/ApplicationWindowGeometry");
const QString kApplicationWindowLayout("LastUse/ApplicationWindowLayout");
const QString kLastUpdateCheck("LastUse/UpdateCheck");
#endif

View File

@ -432,6 +432,12 @@ void StreamConfigDialog::on_cmbPktLenMode_currentIndexChanged(QString mode)
lePktLenMin->setEnabled(true);
lePktLenMax->setEnabled(true);
}
else if (mode == "IMIX")
{
lePktLen->setDisabled(true);
lePktLenMin->setDisabled(true);
lePktLenMax->setDisabled(true);
}
else
{
qWarning("Unhandled/Unknown PktLenMode = %s", qPrintable(mode));
@ -1165,12 +1171,7 @@ void StreamConfigDialog::on_lePacketsPerSec_textChanged(const QString &text)
{
bool isOk;
Stream *pStream = mpStream;
uint frameLen;
if (pStream->lenMode() == Stream::e_fl_fixed)
frameLen = pStream->frameLen();
else
frameLen = (pStream->frameLenMin() + pStream->frameLenMax())/2;
uint frameLen = pStream->frameLenAvg();
if (rbSendPackets->isChecked())
{
@ -1189,13 +1190,9 @@ void StreamConfigDialog::on_leBurstsPerSec_textChanged(const QString &text)
bool isOk;
Stream *pStream = mpStream;
uint burstSize = lePacketsPerBurst->text().toULong(&isOk);
uint frameLen;
uint frameLen = pStream->frameLenAvg();
qDebug("start of %s(%s)", __FUNCTION__, qPrintable(text));
if (pStream->lenMode() == Stream::e_fl_fixed)
frameLen = pStream->frameLen();
else
frameLen = (pStream->frameLenMin() + pStream->frameLenMax())/2;
if (rbSendBursts->isChecked())
{
@ -1215,12 +1212,7 @@ void StreamConfigDialog::on_leBitsPerSec_textEdited(const QString &text)
bool isOk;
Stream *pStream = mpStream;
uint burstSize = lePacketsPerBurst->text().toULong(&isOk);
uint frameLen;
if (pStream->lenMode() == Stream::e_fl_fixed)
frameLen = pStream->frameLen();
else
frameLen = (pStream->frameLenMin() + pStream->frameLenMax())/2;
uint frameLen = pStream->frameLenAvg();
if (rbSendPackets->isChecked())
{

View File

@ -108,6 +108,11 @@ QLineEdit:enabled[inputMask = &quot;HH HH HH HH HH HH; &quot;] { background-colo
<string>Random</string>
</property>
</item>
<item>
<property name="text">
<string>IMIX</string>
</property>
</item>
</widget>
</item>
<item row="0" column="1">
@ -670,6 +675,19 @@ QLineEdit:enabled[inputMask = &quot;HH HH HH HH HH HH; &quot;] { background-colo
</layout>
</widget>
</item>
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">

View File

@ -22,6 +22,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "portgrouplist.h"
#include "qicon.h"
#include <QMimeData>
const QLatin1String kStreamsMimeType("application/vnd.ostinato.streams");
StreamModel::StreamModel(PortGroupList *p, QObject *parent)
: QAbstractTableModel(parent)
{
@ -223,6 +227,77 @@ QVariant StreamModel::headerData(int section, Qt::Orientation orientation, int r
return QVariant();
}
QStringList StreamModel::mimeTypes() const
{
return QStringList() << kStreamsMimeType;
}
QMimeData* StreamModel::mimeData(const QModelIndexList &indexes) const
{
using ::google::protobuf::uint8;
if (indexes.isEmpty())
return nullptr;
// indexes may include multiple columns for a row - but we are only
// interested in rows 'coz we have a single data for all columns
// XXX: use QMap instead of QSet to keep rows in sorted order
QMap<int, int> rows;
foreach(QModelIndex index, indexes)
rows.insert(index.row(), index.row());
OstProto::StreamConfigList streams;
streams.mutable_port_id()->set_id(mCurrentPort->id());
foreach(int row, rows) {
OstProto::Stream *stream = streams.add_stream();
mCurrentPort->streamByIndex(row)->protoDataCopyInto(*stream);
}
QByteArray data;
data.resize(streams.ByteSize());
streams.SerializeWithCachedSizesToArray((uint8*)data.data());
//qDebug("copy %s", streams.DebugString().c_str());
//TODO: copy DebugString as text/plain?
QMimeData *mimeData = new QMimeData();
mimeData->setData(kStreamsMimeType, data);
return mimeData; // XXX: caller is expected to take ownership and free!
}
bool StreamModel::dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int /*column*/, const QModelIndex &parent)
{
if (!data)
return false;
if (!data->hasFormat(kStreamsMimeType))
return false;
if (action != Qt::CopyAction)
return false;
OstProto::StreamConfigList streamsData;
QByteArray ba(data->data(kStreamsMimeType));
streamsData.ParseFromArray((void*)ba.constData(), ba.size());
//qDebug("paste %s", streamsData.DebugString().c_str());
QList<Stream*> streams;
for (int i = 0; i < streamsData.stream_size(); i++) {
Stream *stream = new Stream;
stream->protoDataCopyFrom(streamsData.stream(i));
streams.append(stream);
}
if ((row < 0) || (row > rowCount(parent)))
row = rowCount(parent);
// Delete rows that we are going to overwrite
if (row < rowCount(parent))
removeRows(row, qMin(rowCount() - row, streams.size()));
return insert(row, streams); // callee will free streams after insert
}
/*!
* Inserts streams before the given row
*
@ -286,15 +361,13 @@ void StreamModel::setCurrentPortIndex(const QModelIndex &current)
else
{
qDebug("change to valid port");
// Disconnect any existing connection to avoid duplication
// Qt 4.6 has Qt::UniqueConnection, but we want to remain compatible
// with earlier Qt versions
if (mCurrentPort)
{
disconnect(mCurrentPort, SIGNAL(streamListChanged(int, int)),
this, SLOT(when_mCurrentPort_streamListChanged(int, int)));
}
quint16 pg = current.internalId() >> 16;
// TODO: make mCurrentPort a smart weak pointer
mCurrentPort = pgl->mPortGroups[pgl->indexOfPortGroup(pg)]->mPorts[current.row()];
connect(mCurrentPort, SIGNAL(streamListChanged(int, int)),
this, SLOT(when_mCurrentPort_streamListChanged(int, int)));

View File

@ -39,12 +39,19 @@ class StreamModel : public QAbstractTableModel
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex &parent = QModelIndex()) const;
Qt::ItemFlags flags(const QModelIndex &index) const;
QVariant data(const QModelIndex &index, int role) const;
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole);
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const;
QStringList mimeTypes() const;
QMimeData* mimeData(const QModelIndexList &indexes) const;
bool dropMimeData(const QMimeData *data, Qt::DropAction action,
int row, int column, const QModelIndex &parent);
bool insert(int row, QList<Stream*> &streams);
bool insertRows (int row, int count,
const QModelIndex & parent = QModelIndex());

View File

@ -20,8 +20,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>
#include "streamstatsmodel.h"
#include "protocol.pb.h"
#include "xqlocale.h"
#include <QApplication>
#include <QBrush>
#include <QFont>
#include <QPalette>
// XXX: Keep the enum in sync with it's string
enum {
@ -42,12 +46,26 @@ enum {
kAggrTxPkts,
kAggrRxPkts,
kAggrPktLoss,
kTxDuration,
kAvgTxFrameRate,
kAvgRxFrameRate,
kAvgTxBitRate,
kAvgRxBitRate,
kAvgLatency,
kAvgJitter,
kMaxAggrStreamStats
};
static QStringList aggrStatTitles = QStringList()
<< "Total\nTx Pkts"
<< "Total\nRx Pkts"
<< "Total\nPkt Loss";
<< "Total\nPkt Loss"
<< "Duration\n(secs)"
<< "Avg\nTx PktRate"
<< "Avg\nRx PktRate"
<< "Avg\nTx BitRate"
<< "Avg\nRx BitRate"
<< "Avg\nLatency"
<< "Avg\nJitter";
static const uint kAggrGuid = 0xffffffff;
@ -103,20 +121,35 @@ QVariant StreamStatsModel::data(const QModelIndex &index, int role) const
return Qt::AlignRight;
int portColumn = index.column() - kMaxAggrStreamStats;
if (role == Qt::BackgroundRole) {
if (portColumn < 0)
return QBrush(QColor("lavender")); // Aggregate Column
if (index.row() == (guidList_.size() - 1))
return QBrush(QColor("burlywood")); // Aggregate Row
else if ((portColumn/kMaxStreamStats) & 1)
return QBrush(QColor("beige")); // Color alternate Ports
// Stylesheets typically don't use or set palette colors, so if
// using one, don't use palette colors
if ((role == Qt::BackgroundRole) && qApp->styleSheet().isEmpty()) {
QPalette palette = QApplication::palette();
if (index.row() == (guidList_.size() - 1)) // Aggregate Row
return palette.dark();
if (portColumn < 0) // Aggregate Column
return palette.alternateBase();
if ((portColumn/kMaxStreamStats) & 1) // Color alternate Ports
return palette.alternateBase();
}
Guid guid = guidList_.at(index.row());
if (role == Qt::ForegroundRole) {
if ((role == Qt::ForegroundRole && qApp->styleSheet().isEmpty())) {
QPalette palette = QApplication::palette();
if ((index.column() == kAggrPktLoss)
&& aggrGuidStats_.value(guid).pktLoss)
return QBrush(QColor("firebrick"));
return palette.link();
if (index.row() == (guidList_.size() - 1)) // Aggregate Row
return palette.brightText();
}
if (role == Qt::FontRole ) {
if (index.row() == (guidList_.size() - 1)) { // Aggregate Row
QFont font;
font.setBold(true);
return font;
}
}
if (role != Qt::DisplayRole)
@ -131,6 +164,42 @@ QVariant StreamStatsModel::data(const QModelIndex &index, int role) const
return QString("%L1").arg(aggrGuidStats_.value(guid).txPkts);
case kAggrPktLoss:
return QString("%L1").arg(aggrGuidStats_.value(guid).pktLoss);
case kTxDuration:
return QString("%L1").arg(aggrGuidStats_.value(guid).txDuration);
case kAvgTxFrameRate:
return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") :
XLocale().toPktRateString(
aggrGuidStats_.value(guid).txPkts
/ aggrGuidStats_.value(guid).txDuration);
case kAvgRxFrameRate:
return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") :
XLocale().toPktRateString(
aggrGuidStats_.value(guid).rxPkts
/ aggrGuidStats_.value(guid).txDuration);
case kAvgTxBitRate:
return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") :
XLocale().toBitRateString(
(aggrGuidStats_.value(guid).txBytes
+ 24 * aggrGuidStats_.value(guid).txPkts) * 8
/ aggrGuidStats_.value(guid).txDuration);
case kAvgRxBitRate:
return aggrGuidStats_.value(guid).txDuration <= 0 ? QString("-") :
XLocale().toBitRateString(
(aggrGuidStats_.value(guid).rxBytes
+ 24 * aggrGuidStats_.value(guid).rxPkts) * 8
/ aggrGuidStats_.value(guid).txDuration);
case kAvgLatency:
return aggrGuidStats_.value(guid).latencyCount <= 0
|| aggrGuidStats_.value(guid).latencySum <= 0 ? QString("-") :
XLocale().toTimeIntervalString(
aggrGuidStats_.value(guid).latencySum
/ aggrGuidStats_.value(guid).latencyCount);
case kAvgJitter:
return aggrGuidStats_.value(guid).latencyCount <= 0
|| aggrGuidStats_.value(guid).latencySum <= 0 ? QString("-") :
XLocale().toTimeIntervalString(
aggrGuidStats_.value(guid).jitterSum
/ aggrGuidStats_.value(guid).latencyCount);
default:
break;
};
@ -156,6 +225,11 @@ QVariant StreamStatsModel::data(const QModelIndex &index, int role) const
return QVariant();
}
Qt::DropActions StreamStatsModel::supportedDropActions() const
{
return Qt::IgnoreAction; // read-only model, doesn't accept any data
}
// --------------------------------------------- //
// Slots
// --------------------------------------------- //
@ -200,6 +274,8 @@ void StreamStatsModel::appendStreamStatsList(
ss.txPkts = s.tx_pkts();
ss.rxBytes = s.rx_bytes();
ss.txBytes = s.tx_bytes();
ss.rxLatency = s.latency();
ss.rxJitter = s.jitter();
aggrPort.rxPkts += ss.rxPkts;
aggrPort.txPkts += ss.txPkts;
@ -209,10 +285,28 @@ void StreamStatsModel::appendStreamStatsList(
aggrGuid.rxPkts += ss.rxPkts;
aggrGuid.txPkts += ss.txPkts;
aggrGuid.pktLoss += ss.txPkts - ss.rxPkts;
aggrGuid.rxBytes += ss.rxBytes;
aggrGuid.txBytes += ss.txBytes;
if (s.tx_duration() > aggrGuid.txDuration)
aggrGuid.txDuration = s.tx_duration(); // XXX: use largest or avg?
if (ss.rxLatency) {
aggrGuid.latencySum += ss.rxLatency;
aggrGuid.jitterSum += ss.rxJitter;
aggrGuid.latencyCount++;
}
aggrAggr.rxPkts += ss.rxPkts;
aggrAggr.txPkts += ss.txPkts;
aggrAggr.pktLoss += ss.txPkts - ss.rxPkts;
aggrAggr.rxBytes += ss.rxBytes;
aggrAggr.txBytes += ss.txBytes;
if (aggrGuid.txDuration > aggrAggr.txDuration)
aggrAggr.txDuration = aggrGuid.txDuration;
if (ss.rxLatency) {
aggrAggr.latencySum += ss.rxLatency;
aggrAggr.jitterSum += ss.rxJitter;
aggrAggr.latencyCount++;
}
if (!portList_.contains(pgp))
portList_.append(pgp);
@ -220,9 +314,11 @@ void StreamStatsModel::appendStreamStatsList(
guidList_.append(guid);
}
if (guidList_.size())
if (guidList_.size() && !guidList_.contains(kAggrGuid))
guidList_.append(kAggrGuid);
std::sort(guidList_.begin(), guidList_.end());
#if QT_VERSION >= 0x040600
endResetModel();
#else

View File

@ -43,6 +43,8 @@ public:
int role = Qt::DisplayRole) const;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
Qt::DropActions supportedDropActions() const;
public slots:
void clearStats();
void appendStreamStatsList(quint32 portGroupId,
@ -55,11 +57,19 @@ private:
quint64 txPkts;
quint64 rxBytes;
quint64 txBytes;
quint64 rxLatency;
quint64 rxJitter;
};
struct AggrGuidStats {
quint64 rxPkts;
quint64 txPkts;
quint64 rxBytes;
quint64 txBytes;
qint64 pktLoss;
double txDuration;
quint64 latencySum;
quint64 jitterSum;
uint latencyCount;
};
QList<Guid> guidList_;
QList<PortGroupPort> portList_;

View File

@ -31,7 +31,7 @@ StreamStatsWindow::StreamStatsWindow(QAbstractItemModel *model, QWidget *parent)
: QWidget(parent)
{
setupUi(this);
streamStats->addAction(actionShowByteCounters);
streamStats->addAction(actionShowDetails);
if (id)
setWindowTitle(windowTitle() + QString("(%1)").arg(id));
@ -39,13 +39,17 @@ StreamStatsWindow::StreamStatsWindow(QAbstractItemModel *model, QWidget *parent)
count++;
filterModel_ = new StreamStatsFilterModel(this);
filterModel_->setFilterRegExp(QRegExp(".*Pkt.*"));
filterModel_->setFilterRegExp(QRegExp(kDefaultFilter_));
filterModel_->setSourceModel(model);
streamStats->setModel(filterModel_);
streamStats->verticalHeader()->setHighlightSections(false);
streamStats->verticalHeader()->setDefaultSectionSize(
streamStats->verticalHeader()->minimumSectionSize());
// Fit all columns in window whenever data changes
connect(model, &QAbstractItemModel::modelReset,
[=]() { streamStats->resizeColumnsToContents(); });
}
StreamStatsWindow::~StreamStatsWindow()
@ -56,10 +60,12 @@ StreamStatsWindow::~StreamStatsWindow()
id = 0;
}
void StreamStatsWindow::on_actionShowByteCounters_triggered(bool checked)
void StreamStatsWindow::on_actionShowDetails_triggered(bool checked)
{
if (checked)
filterModel_->setFilterRegExp(QRegExp(".*"));
else
filterModel_->setFilterRegExp(QRegExp(".*Pkt.*"));
filterModel_->setFilterRegExp(QRegExp(kDefaultFilter_));
streamStats->resizeColumnsToContents();
}

View File

@ -33,9 +33,10 @@ public:
~StreamStatsWindow();
private slots:
void on_actionShowByteCounters_triggered(bool checked);
void on_actionShowDetails_triggered(bool checked);
private:
QString kDefaultFilter_{"^(?!Port).*"};
QSortFilterProxyModel *filterModel_;
};

View File

@ -19,6 +19,9 @@
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="whatsThis">
<string>Oops! We don't seem to have any stream statistics for the requested port(s)
@ -27,12 +30,12 @@ Wait a little bit to see if they appear, otherwise verify your stream stats conf
</widget>
</item>
</layout>
<action name="actionShowByteCounters">
<action name="actionShowDetails">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>Show Byte Counters</string>
<string>Show Details</string>
</property>
</action>
</widget>

519
client/streamswidget.cpp Normal file
View File

@ -0,0 +1,519 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "streamswidget.h"
#include "clipboardhelper.h"
#include "findreplace.h"
#include "portgrouplist.h"
#include "streamconfigdialog.h"
#include "streamfileformat.h"
#include "streamlistdelegate.h"
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QMessageBox>
extern ClipboardHelper *clipboardHelper;
StreamsWidget::StreamsWidget(QWidget *parent)
: QWidget(parent)
{
setupUi(this);
delegate = new StreamListDelegate;
tvStreamList->setItemDelegate(delegate);
tvStreamList->verticalHeader()->setDefaultSectionSize(
tvStreamList->verticalHeader()->minimumSectionSize());
// Populate StreamList Context Menu Actions
tvStreamList->addAction(actionNew_Stream);
tvStreamList->addAction(actionEdit_Stream);
tvStreamList->addAction(actionDuplicate_Stream);
tvStreamList->addAction(actionDelete_Stream);
QAction *sep2 = new QAction(this);
sep2->setSeparator(true);
tvStreamList->addAction(sep2);
tvStreamList->addAction(actionFind_Replace);
QAction *sep3 = new QAction(this);
sep3->setSeparator(true);
tvStreamList->addAction(sep3);
tvStreamList->addAction(actionOpen_Streams);
tvStreamList->addAction(actionSave_Streams);
// StreamWidget's actions is an aggegate of all sub-widget's actions
addActions(tvStreamList->actions());
// Add the clipboard actions to the context menu of streamList
// but not to StreamsWidget's actions since they are already available
// in the global Edit Menu
QAction *sep4 = new QAction("Clipboard", this);
sep4->setSeparator(true);
tvStreamList->insertAction(sep2, sep4);
tvStreamList->insertActions(sep2, clipboardHelper->actions());
}
void StreamsWidget::setPortGroupList(PortGroupList *portGroups)
{
plm = portGroups;
tvStreamList->setModel(plm->getStreamModel());
connect(plm->getStreamModel(), SIGNAL(rowsInserted(QModelIndex, int, int)),
SLOT(updateStreamViewActions()));
connect(plm->getStreamModel(), SIGNAL(rowsRemoved(QModelIndex, int, int)),
SLOT(updateStreamViewActions()));
connect(tvStreamList->selectionModel(),
SIGNAL(currentChanged(const QModelIndex&, const QModelIndex&)),
SLOT(updateStreamViewActions()));
connect(tvStreamList->selectionModel(),
SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)),
SLOT(updateStreamViewActions()));
tvStreamList->resizeColumnToContents(StreamModel::StreamIcon);
tvStreamList->resizeColumnToContents(StreamModel::StreamStatus);
updateStreamViewActions();
connect(plm->getStreamModel(),
SIGNAL(dataChanged(const QModelIndex&, const QModelIndex&)),
this, SLOT(streamModelDataChanged()));
connect(plm->getStreamModel(),
SIGNAL(modelReset()),
this, SLOT(streamModelDataChanged()));
}
void StreamsWidget::streamModelDataChanged()
{
if (plm->isPort(currentPortIndex_))
plm->port(currentPortIndex_).recalculateAverageRates();
}
StreamsWidget::~StreamsWidget()
{
delete delegate;
}
void StreamsWidget::on_tvStreamList_activated(const QModelIndex & index)
{
if (!index.isValid())
{
qDebug("%s: invalid index", __FUNCTION__);
return;
}
qDebug("stream list activated\n");
Q_ASSERT(plm->isPort(currentPortIndex_));
Port &curPort = plm->port(currentPortIndex_);
QList<Stream*> streams;
streams.append(curPort.mutableStreamByIndex(index.row(), false));
StreamConfigDialog scd(streams, curPort, this);
if (scd.exec() == QDialog::Accepted) {
curPort.recalculateAverageRates();
curPort.setLocalConfigChanged(true);
}
}
void StreamsWidget::setCurrentPortIndex(const QModelIndex &portIndex)
{
if (!plm)
return;
// XXX: We assume portIndex corresponds to sourceModel, not proxyModel;
// caller should ensure this
qDebug("In %s", __PRETTY_FUNCTION__);
currentPortIndex_ = portIndex;
plm->getStreamModel()->setCurrentPortIndex(portIndex);
updateStreamViewActions();
}
void StreamsWidget::updateStreamViewActions()
{
// For some reason hasSelection() returns true even if selection size is 0
// so additional check for size introduced
if (tvStreamList->selectionModel()->hasSelection() &&
(tvStreamList->selectionModel()->selection().size() > 0))
{
qDebug("Has selection %d",
tvStreamList->selectionModel()->selection().size());
// If more than one non-contiguous ranges selected,
// disable "New" and "Edit"
if (tvStreamList->selectionModel()->selection().size() > 1)
{
actionNew_Stream->setDisabled(true);
actionEdit_Stream->setDisabled(true);
}
else
{
actionNew_Stream->setEnabled(true);
actionEdit_Stream->setEnabled(true);
}
// Duplicate/Delete are always enabled as long as we have a selection
actionDuplicate_Stream->setEnabled(true);
actionDelete_Stream->setEnabled(true);
}
else
{
qDebug("No selection");
if (plm->isPort(currentPortIndex_))
actionNew_Stream->setEnabled(true);
else
actionNew_Stream->setDisabled(true);
actionEdit_Stream->setDisabled(true);
actionDuplicate_Stream->setDisabled(true);
actionDelete_Stream->setDisabled(true);
}
actionFind_Replace->setEnabled(tvStreamList->model()->rowCount() > 0);
actionOpen_Streams->setEnabled(plm->isPort(currentPortIndex_));
actionSave_Streams->setEnabled(tvStreamList->model()->rowCount() > 0);
}
void StreamsWidget::on_actionNew_Stream_triggered()
{
qDebug("New Stream Action");
QItemSelectionModel* selectionModel = tvStreamList->selectionModel();
if (selectionModel->selection().size() > 1) {
qDebug("%s: Unexpected selection size %d, can't add", __FUNCTION__,
selectionModel->selection().size());
return;
}
// In case nothing is selected, insert 1 row at the end
StreamModel *streamModel = plm->getStreamModel();
int row = streamModel->rowCount(), count = 1;
// In case we have a single range selected; insert as many rows as
// in the singe selected range before the top of the selected range
if (selectionModel->selection().size() == 1)
{
row = selectionModel->selection().at(0).top();
count = selectionModel->selection().at(0).height();
}
Q_ASSERT(plm->isPort(currentPortIndex_));
Port &curPort = plm->port(currentPortIndex_);
QList<Stream*> streams;
for (int i = 0; i < count; i++)
streams.append(new Stream);
StreamConfigDialog scd(streams, curPort, this);
scd.setWindowTitle(tr("Add Stream"));
if (scd.exec() == QDialog::Accepted)
streamModel->insert(row, streams);
}
void StreamsWidget::on_actionEdit_Stream_triggered()
{
qDebug("Edit Stream Action");
QItemSelectionModel* streamModel = tvStreamList->selectionModel();
if (!streamModel->hasSelection())
return;
Q_ASSERT(plm->isPort(currentPortIndex_));
Port &curPort = plm->port(currentPortIndex_);
QList<Stream*> streams;
foreach(QModelIndex index, streamModel->selectedRows())
streams.append(curPort.mutableStreamByIndex(index.row(), false));
StreamConfigDialog scd(streams, curPort, this);
if (scd.exec() == QDialog::Accepted) {
curPort.recalculateAverageRates();
curPort.setLocalConfigChanged(true);
}
}
void StreamsWidget::on_actionDuplicate_Stream_triggered()
{
QItemSelectionModel* model = tvStreamList->selectionModel();
qDebug("Duplicate Stream Action");
Q_ASSERT(plm->isPort(currentPortIndex_));
if (model->hasSelection())
{
bool isOk;
int count = QInputDialog::getInt(this, "Duplicate Streams",
"Count", 1, 1, 9999, 1, &isOk);
if (!isOk)
return;
QList<int> list;
foreach(QModelIndex index, model->selectedRows())
list.append(index.row());
plm->port(currentPortIndex_).duplicateStreams(list, count);
}
else
qDebug("No selection");
}
void StreamsWidget::on_actionDelete_Stream_triggered()
{
qDebug("Delete Stream Action");
QModelIndex index;
if (tvStreamList->selectionModel()->hasSelection())
{
qDebug("SelectedIndexes %d",
tvStreamList->selectionModel()->selectedRows().size());
while(tvStreamList->selectionModel()->selectedRows().size())
{
index = tvStreamList->selectionModel()->selectedRows().at(0);
plm->getStreamModel()->removeRows(index.row(), 1);
}
}
else
qDebug("No selection");
}
void StreamsWidget::on_actionFind_Replace_triggered()
{
qDebug("Find & Replace Action");
Q_ASSERT(plm->isPort(currentPortIndex_));
QItemSelectionModel* selectionModel = tvStreamList->selectionModel();
FindReplaceDialog::Action action;
action.selectedStreamsOnly = selectionModel->selection().size() > 0 ?
true : false;
FindReplaceDialog findReplace(&action, this);
if (findReplace.exec() == QDialog::Accepted) {
QProgressDialog progress(this);
progress.setLabelText(tr("Replacing %1 ...").arg(action.protocolField));
progress.setWindowModality(Qt::WindowModal);
int c, fc = 0, sc = 0; // replace counts
Port &port = plm->port(currentPortIndex_);
if (action.selectedStreamsOnly) {
QModelIndexList selected = selectionModel->selectedRows();
int count = selected.size();
progress.setMaximum(count);
for (int i = 0; i < count; i++) {
QModelIndex index = selected.at(i);
Stream *stream = port.mutableStreamByIndex(index.row(), false);
c = stream->protocolFieldReplace(action.protocolNumber,
action.fieldIndex,
action.fieldBitSize,
action.findValue,
action.findMask,
action.replaceValue,
action.replaceMask);
if (c) {
fc += c;
sc++;
}
progress.setValue(i+1);
if (progress.wasCanceled())
break;
}
} else {
int count = tvStreamList->model()->rowCount();
progress.setMaximum(count);
for (int i = 0; i < count; i++) {
Stream *stream = port.mutableStreamByIndex(i, false);
c = stream->protocolFieldReplace(action.protocolNumber,
action.fieldIndex,
action.fieldBitSize,
action.findValue,
action.findMask,
action.replaceValue,
action.replaceMask);
if (c) {
fc += c;
sc++;
}
progress.setValue(i+1);
if (progress.wasCanceled())
break;
}
}
if (fc)
port.setLocalConfigChanged(true);
QMessageBox::information(this, tr("Find & Replace"),
tr("%1 fields replaced in %2 streams").arg(fc).arg(sc));
}
}
void StreamsWidget::on_actionOpen_Streams_triggered()
{
qDebug("Open Streams Action");
QStringList fileTypes = StreamFileFormat::supportedFileTypes(
StreamFileFormat::kOpenFile);
QString fileType;
static QString dirName;
QString fileName;
QString errorStr;
bool append = true;
bool ret;
Q_ASSERT(plm->isPort(currentPortIndex_));
if (fileTypes.size())
fileType = fileTypes.at(0);
fileName = QFileDialog::getOpenFileName(this, tr("Open Streams"),
dirName, fileTypes.join(";;"), &fileType);
if (fileName.isEmpty())
goto _exit;
if (tvStreamList->model()->rowCount())
{
QMessageBox msgBox(QMessageBox::Question, qApp->applicationName(),
tr("Append to existing streams? Or overwrite?"),
QMessageBox::NoButton, this);
QPushButton *appendBtn = msgBox.addButton(tr("Append"),
QMessageBox::ActionRole);
QPushButton *overwriteBtn = msgBox.addButton(tr("Overwrite"),
QMessageBox::ActionRole);
QPushButton *cancelBtn = msgBox.addButton(QMessageBox::Cancel);
msgBox.exec();
if (msgBox.clickedButton() == cancelBtn)
goto _exit;
else if (msgBox.clickedButton() == appendBtn)
append = true;
else if (msgBox.clickedButton() == overwriteBtn)
append = false;
else
Q_ASSERT(false);
}
ret = plm->port(currentPortIndex_).openStreams(fileName, append, errorStr);
if (!ret || !errorStr.isEmpty())
{
QMessageBox msgBox(this);
QStringList str = errorStr.split("\n\n\n\n");
msgBox.setIcon(ret ? QMessageBox::Warning : QMessageBox::Critical);
msgBox.setWindowTitle(qApp->applicationName());
msgBox.setText(str.at(0));
if (str.size() > 1)
msgBox.setDetailedText(str.at(1));
msgBox.setStandardButtons(QMessageBox::Ok);
msgBox.exec();
}
dirName = QFileInfo(fileName).absolutePath();
updateStreamViewActions();
_exit:
return;
}
void StreamsWidget::on_actionSave_Streams_triggered()
{
qDebug("Save Streams Action");
static QString fileName;
QStringList fileTypes = StreamFileFormat::supportedFileTypes(
StreamFileFormat::kSaveFile);
QString fileType;
QString errorStr;
QFileDialog::Options options;
// On Mac OS with Native Dialog, getSaveFileName() ignores fileType
// which we need
#if defined(Q_OS_MAC)
options |= QFileDialog::DontUseNativeDialog;
#endif
if (fileTypes.size())
fileType = fileTypes.at(0);
Q_ASSERT(plm->isPort(currentPortIndex_));
_retry:
fileName = QFileDialog::getSaveFileName(this, tr("Save Streams"),
fileName, fileTypes.join(";;"), &fileType, options);
if (fileName.isEmpty())
goto _exit;
if (QFileInfo(fileName).suffix().isEmpty()) {
QString fileExt = fileType.section(QRegExp("[\\*\\)]"), 1, 1);
qDebug("Adding extension '%s' to '%s'",
qPrintable(fileExt), qPrintable(fileName));
fileName.append(fileExt);
if (QFileInfo(fileName).exists()) {
if (QMessageBox::warning(this, tr("Overwrite File?"),
QString("The file \"%1\" already exists.\n\n"
"Do you wish to overwrite it?")
.arg(QFileInfo(fileName).fileName()),
QMessageBox::Yes|QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
goto _retry;
}
}
fileType = fileType.remove(QRegExp("\\(.*\\)")).trimmed();
if (!fileType.startsWith("Ostinato")
&& !fileType.startsWith("Python"))
{
if (QMessageBox::warning(this, tr("Ostinato"),
QString("You have chosen to save in %1 format. All stream "
"attributes may not be saved in this format.\n\n"
"It is recommended to save in native Ostinato format.\n\n"
"Continue to save in %2 format?").arg(fileType).arg(fileType),
QMessageBox::Yes|QMessageBox::No,
QMessageBox::No) != QMessageBox::Yes)
goto _retry;
}
// TODO: all or selected?
if (!plm->port(currentPortIndex_).saveStreams(fileName, fileType, errorStr))
QMessageBox::critical(this, qApp->applicationName(), errorStr);
else if (!errorStr.isEmpty())
QMessageBox::warning(this, qApp->applicationName(), errorStr);
fileName = QFileInfo(fileName).absolutePath();
_exit:
return;
}

67
client/streamswidget.h Normal file
View File

@ -0,0 +1,67 @@
/*
Copyright (C) 2010 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _STREAMS_WIDGET_H
#define _STREAMS_WIDGET_H
#include "ui_streamswidget.h"
#include <QWidget>
class PortGroupList;
class QAbstractItemDelegate;
class StreamsWidget : public QWidget, private Ui::StreamsWidget
{
Q_OBJECT
public:
StreamsWidget(QWidget *parent = 0);
~StreamsWidget();
void setPortGroupList(PortGroupList *portGroups);
public slots:
void setCurrentPortIndex(const QModelIndex &portIndex);
private slots:
void updateStreamViewActions();
void on_tvStreamList_activated(const QModelIndex & index);
void on_actionNew_Stream_triggered();
void on_actionEdit_Stream_triggered();
void on_actionDuplicate_Stream_triggered();
void on_actionDelete_Stream_triggered();
void on_actionFind_Replace_triggered();
void on_actionOpen_Streams_triggered();
void on_actionSave_Streams_triggered();
void streamModelDataChanged();
private:
PortGroupList *plm{nullptr}; // FIXME: rename to portGroups_?
QModelIndex currentPortIndex_;
QAbstractItemDelegate *delegate;
};
#endif

132
client/streamswidget.ui Normal file
View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>StreamsWidget</class>
<widget class="QWidget" name="StreamsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>602</width>
<height>364</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>9</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="XTableView" name="tvStreamList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="contextMenuPolicy">
<enum>Qt::ActionsContextMenu</enum>
</property>
<property name="whatsThis">
<string>This is the stream list for the selected port
A stream is a sequence of one or more packets
Right-click to create a stream</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
<action name="actionNew_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_add.png</normaloff>:/icons/stream_add.png</iconset>
</property>
<property name="text">
<string>New Stream</string>
</property>
</action>
<action name="actionDelete_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_delete.png</normaloff>:/icons/stream_delete.png</iconset>
</property>
<property name="text">
<string>Delete Stream</string>
</property>
</action>
<action name="actionEdit_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_edit.png</normaloff>:/icons/stream_edit.png</iconset>
</property>
<property name="text">
<string>Edit Stream</string>
</property>
</action>
<action name="actionOpen_Streams">
<property name="text">
<string>Open Streams ...</string>
</property>
</action>
<action name="actionSave_Streams">
<property name="text">
<string>Save Streams ...</string>
</property>
</action>
<action name="actionDuplicate_Stream">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/stream_duplicate.png</normaloff>:/icons/stream_duplicate.png</iconset>
</property>
<property name="text">
<string>Duplicate Stream</string>
</property>
</action>
<action name="actionFind_Replace">
<property name="icon">
<iconset resource="ostinato.qrc">
<normaloff>:/icons/find.png</normaloff>:/icons/find.png</iconset>
</property>
<property name="text">
<string>Find &amp;&amp; Replace</string>
</property>
<property name="toolTip">
<string>Find &amp; Replace protocol field values</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>XTableView</class>
<extends>QTableView</extends>
<header>xtableview.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="ostinato.qrc"/>
</resources>
<connections/>
</ui>

130
client/thememanager.cpp Normal file
View File

@ -0,0 +1,130 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#include "thememanager.h"
#include "settings.h"
#include <QApplication>
#include <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QRegularExpression>
#include <QResource>
ThemeManager *ThemeManager::instance_{nullptr};
ThemeManager::ThemeManager()
{
themeDir_ = QCoreApplication::applicationDirPath() + "/themes/";
#if defined(Q_OS_MAC)
/*
* Executable and Theme directory location inside app bundle -
* Ostinato.app/Contents/MacOS/
* Ostinato.app/Contents/SharedSupport/themes/
*/
themeDir_.replace("/MacOS/", "/SharedSupport/");
#elif defined(Q_OS_UNIX)
/*
* Possible (but not comprehensive) locations for Ostinato executable
* and theme directory locations
*
* non-install-dir/
* non-install-dir/themes/
*
* /usr/[local]/bin/
* /usr/[local]/share/ostinato-controller/themes/
*
* /opt/ostinato/bin/
* /opt/ostinato/share/themes/
*/
if (themeDir_.contains(QRegularExpression("^/usr/.*/bin/")))
themeDir_.replace("/bin/", "/share/ostinato-controller/");
else if (themeDir_.contains(QRegularExpression("^/opt/.*/bin/")))
themeDir_.replace("/bin/", "/share/");
#endif
qDebug("Themes directory: %s", qPrintable(themeDir_));
}
QStringList ThemeManager::themeList()
{
QDir themeDir(themeDir_);
themeDir.setFilter(QDir::Files);
themeDir.setNameFilters(QStringList() << "*.qss");
themeDir.setSorting(QDir::Name);
QStringList themes = themeDir.entryList();
for (QString& theme : themes)
theme.remove(".qss");
themes.prepend("default");
return themes;
}
void ThemeManager::setTheme(QString theme)
{
// Remove current theme, if we have one
QString oldTheme = appSettings->value(kThemeKey).toString();
if (!oldTheme.isEmpty()) {
// Remove stylesheet first so that there are
// no references to resources when unregistering 'em
qApp->setStyleSheet("");
QString rccFile = themeDir_ + oldTheme + ".rcc";
if (QResource::unregisterResource(rccFile)) {
qDebug("Unable to unregister theme rccFile %s",
qPrintable(rccFile));
}
appSettings->setValue(kThemeKey, QVariant());
}
if (theme.isEmpty() || (theme == "default"))
return;
// Apply new theme
QFile qssFile(themeDir_ + theme + ".qss");
if (!qssFile.open(QFile::ReadOnly)) {
qDebug("Unable to open theme qssFile %s",
qPrintable(qssFile.fileName()));
return;
}
// Register theme resource before applying theme style sheet
QString rccFile = themeDir_ + theme + ".rcc";
if (!QResource::registerResource(rccFile))
qDebug("Unable to register theme rccFile %s", qPrintable(rccFile));
#if 0 // FIXME: debug only
QDirIterator it(":", QDirIterator::Subdirectories);
while (it.hasNext()) {
qDebug() << it.next();
}
#endif
QString styleSheet { qssFile.readAll() };
qApp->setStyleSheet(styleSheet);
appSettings->setValue(kThemeKey, theme);
}
ThemeManager* ThemeManager::instance()
{
if (!instance_)
instance_ = new ThemeManager();
return instance_;
}

42
client/thememanager.h Normal file
View File

@ -0,0 +1,42 @@
/*
Copyright (C) 2021 Srivats P.
This file is part of "Ostinato"
This is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
#ifndef _THEME_MANAGER_H
#define _THEME_MANAGER_H
#include <QStringList>
class ThemeManager
{
public:
ThemeManager();
QStringList themeList();
void setTheme(QString theme);
static ThemeManager* instance();
private:
QString themeDir_;
static ThemeManager *instance_;
};
#endif

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More