Skip to content

#2651 - Add thread-safe FSUIPC integration#2655

Open
ghingres wants to merge 1 commit intoMobiFlight:mainfrom
ghingres:2651/fix-issue-with-fsuipc-integration
Open

#2651 - Add thread-safe FSUIPC integration#2655
ghingres wants to merge 1 commit intoMobiFlight:mainfrom
ghingres:2651/fix-issue-with-fsuipc-integration

Conversation

@ghingres
Copy link

Fix bug #2651 (FSUIPC Integration is not thread safe) by changing the following

  1. Use thread safe ConcurrentDictionary in place of Dictionary
  2. Use a Lock to avoid FSUIPCConnection.Process() and setValue() running in parallel threads
  3. Set ActionAtNextProcess to be OffsetAction.Write on any setValue() to avoid any Autosense issues (as advised by Peter of FSUIPC).

Fix bug MobiFlight#2651 FSUIPC Integration is not thread safe by changing the following

a) Use thread safe ConcurrentDictionary in place of Dictionary
b) Use a Lock to avoid FSUIPCConnection.Process() and setValue() are running in parallel threads
c) Set ActionAtNextProcess to be OffsetAction.Write on any setValue() flow to avoid any Autosense issues (as advised by Peter of FSUIPC).
@ghingres ghingres requested a review from DocMoebiuz as a code owner January 23, 2026 17:08
@DocMoebiuz
Copy link
Collaborator

Thanks for the PR. I will have to take some time to properly think about this.
This code is very old and it has a lot of repetitive parts and is not so greatly structured. Also we have to think about the FSUIPC Helper Class which is used by the FSUIPCInputAction.

@DocMoebiuz
Copy link
Collaborator

I will let Copilot start with a review and see what it thinks, but I would really try to avoid copy/past repetition.

Copy link
Collaborator

@DocMoebiuz DocMoebiuz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR needs unit tests.

This class requires refactoring. Too many DRY violations.

Also check FSUIPCHelper.

Comment on lines +19 to +28
ConcurrentDictionary<Int32, Offset<Byte>> __cacheByte = new ConcurrentDictionary<Int32, Offset<Byte>>();
ConcurrentDictionary<Int32, Offset<Int16>> __cacheShort = new ConcurrentDictionary<Int32, Offset<Int16>>();
//ConcurrentDictionary<Int32, Offset<UInt16>> __cacheUShort = new ConcurrentDictionary<Int32, Offset<UInt16>>();
ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>();
//ConcurrentDictionary<Int32, Offset<UInt32>> __cacheUInt = new ConcurrentDictionary<Int32, Offset<UInt32>>();
ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>();
ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>();
//ConcurrentDictionary<Int32, Offset<UInt64>> __cacheULong = new ConcurrentDictionary<Int32, Offset<UInt64>>();
ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>();
ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good idea.

Comment on lines +137 to +140
lock (fsuipc_lock)
{
FSUIPCConnection.Process();
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would introduce a method ProcessThreadSafe that wraps this call to avoid repetition of

lock (fsuipc_lock)
{
    FSUIPCConnection.Process();
}

and just call "ProcessThreadSafe()"

this applies to all subsequent calls.

Comment on lines +429 to +446
lock (fsuipc_lock)
{
__cacheByte[offset] = new Offset<Byte>(offset);
_offsetsRegistered = true;
}
if (!__cacheByte.ContainsKey(offset))
{
__cacheByte[offset] = new Offset<Byte>(offset);
_offsetsRegistered = true;
}

__cacheByte[offset].Value = value;
((Offset<Byte>)__cacheByte[offset]).ActionAtNextProcess = OffsetAction.Write;
__cacheByte[offset].Value = value;

//long retval = this.getValue(offset, 1);
//if (retval != value)
//{
// Log.Instance.log($"Recovered SetOffset() failure: Byte offset {offset}, value was {retval} after requesting {value}", LogSeverity.Warn);
// __cacheByte[offset].Value = value;
//}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ConcurrentDictionary this is not necessary. ConcurrentDictionary is already thread-safe.
We have to check which methods are the correct ones to add additional keys to the Dictionary

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DocMoebiuz - The original problem (as demonstrated by the commented out code) was occuring when I used ConcurrentDictionary. Even though threadsafe, the method believed it's setvalue was loaded when in fact a parallel FSUIPCConnection.Process() had overidden it's value ( The commented out code would fire and recover the situation). This was the testcase for proving where the locks were required. The setValue method and the FSUIPCConnection.Process() must not be operating in parallel. Peter confirmed the FSUIPCConnection.Process() is a synchonous process.

@DocMoebiuz
Copy link
Collaborator

I totally missed the

Set ActionAtNextProcess to be OffsetAction.Write on any setValue() to avoid any Autosense issues (as advised by Peter of FSUIPC).

... that's interesing, I was not aware of that. Does that mean that it ONLY performs writes that are pending for the next process?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request aims to fix bug #2651 by adding thread-safety to the FSUIPC integration layer. The changes address race conditions that could cause values to be lost when multiple threads simultaneously read and write FSUIPC offsets.

Changes:

  • Converted Dictionary collections to ConcurrentDictionary for thread-safe storage of FSUIPC offset objects
  • Added locks around all FSUIPCConnection.Process() and FSUIPCConnection.SendControlToFS() calls to prevent concurrent FSUIPC operations
  • Set ActionAtNextProcess = OffsetAction.Write in all setOffset methods to ensure values are written on next Process call, as recommended by FSUIPC author

Comment on lines +462 to +467
//long retval = this.getValue(offset, 2);
//if (retval != value)
//{
// Log.Instance.log($"Recovered SetOffset() failure: Short offset {offset}, value was {retval} after requesting {value}", LogSeverity.Warn);
// __cacheShort[offset].Value = value;
//}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar commented-out debugging code. Either remove it or add a comment explaining its purpose.

Copilot uses AI. Check for mistakes.
Comment on lines +475 to +479
if (!__cacheInt.ContainsKey(offset))
{
__cacheInt[offset] = new Offset<Int32>(offset, writeOnly);
_offsetsRegistered = true;
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar thread safety issue. Use TryAdd method instead: __cacheInt.TryAdd(offset, new Offset<Int32>(offset, writeOnly)).

Copilot uses AI. Check for mistakes.
Comment on lines +519 to +523
if (!__cacheDouble.ContainsKey(offset))
{
__cacheDouble[offset] = new Offset<double>(offset);
_offsetsRegistered = true;
}
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar thread safety issue. Use TryAdd method instead: __cacheDouble.TryAdd(offset, new Offset<double>(offset)).

Copilot uses AI. Check for mistakes.
}

__cacheShort[offset].Value = value;
((Offset<Int16>)__cacheShort[offset]).ActionAtNextProcess = OffsetAction.Write;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the Byte case, the cast to Offset is unnecessary. Consider removing it.

Suggested change
((Offset<Int16>)__cacheShort[offset]).ActionAtNextProcess = OffsetAction.Write;
__cacheShort[offset].ActionAtNextProcess = OffsetAction.Write;

Copilot uses AI. Check for mistakes.
_offsetsRegistered = true;
}

((Offset<double>)__cacheDouble[offset]).ActionAtNextProcess = OffsetAction.Write;
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to other cases, the cast to Offset is unnecessary. Consider removing it.

Copilot uses AI. Check for mistakes.
ConcurrentDictionary<Int32, Offset<Byte>> __cacheByte = new ConcurrentDictionary<Int32, Offset<Byte>>();
ConcurrentDictionary<Int32, Offset<Int16>> __cacheShort = new ConcurrentDictionary<Int32, Offset<Int16>>();
//ConcurrentDictionary<Int32, Offset<UInt16>> __cacheUShort = new ConcurrentDictionary<Int32, Offset<UInt16>>();
ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field '__cacheInt' can be 'readonly'.

Suggested change
ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>();
private readonly ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>();

Copilot uses AI. Check for mistakes.
//ConcurrentDictionary<Int32, Offset<UInt16>> __cacheUShort = new ConcurrentDictionary<Int32, Offset<UInt16>>();
ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>();
//ConcurrentDictionary<Int32, Offset<UInt32>> __cacheUInt = new ConcurrentDictionary<Int32, Offset<UInt32>>();
ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field '__cacheFloat' can be 'readonly'.

Suggested change
ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>();
readonly ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>();

Copilot uses AI. Check for mistakes.
ConcurrentDictionary<Int32, Offset<Int32>> __cacheInt = new ConcurrentDictionary<Int32, Offset<Int32>>();
//ConcurrentDictionary<Int32, Offset<UInt32>> __cacheUInt = new ConcurrentDictionary<Int32, Offset<UInt32>>();
ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>();
ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field '__cacheLong' can be 'readonly'.

Suggested change
ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>();
readonly ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>();

Copilot uses AI. Check for mistakes.
ConcurrentDictionary<Int32, Offset<Single>> __cacheFloat = new ConcurrentDictionary<Int32, Offset<Single>>();
ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>();
//ConcurrentDictionary<Int32, Offset<UInt64>> __cacheULong = new ConcurrentDictionary<Int32, Offset<UInt64>>();
ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field '__cacheDouble' can be 'readonly'.

Suggested change
ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>();
private readonly ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>();

Copilot uses AI. Check for mistakes.
ConcurrentDictionary<Int32, Offset<Int64>> __cacheLong = new ConcurrentDictionary<Int32, Offset<Int64>>();
//ConcurrentDictionary<Int32, Offset<UInt64>> __cacheULong = new ConcurrentDictionary<Int32, Offset<UInt64>>();
ConcurrentDictionary<Int32, Offset<Double>> __cacheDouble = new ConcurrentDictionary<Int32, Offset<Double>>();
ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>();
Copy link

Copilot AI Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Field '__cacheString' can be 'readonly'.

Suggested change
ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>();
readonly ConcurrentDictionary<Int32, Offset<String>> __cacheString = new ConcurrentDictionary<Int32, Offset<String>>();

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DocMoebiuz - The FSUIPC Client DLL action (This property can be used to force this offset to be read or written on the next process) is documented at http://fsuipc.paulhenty.com/Reference/html/classFSUIPC_1_1Offset.html. Paul suggested different group names for read v write Offsets however this would require Mobiflight to segregate usage of FSUIPC offsets. The work-around was to set the Action at next process.

I'm new to modifying code within open source projects. Is there any documentation/advice on how to perform CoPilot code reviews and also why refactoring is considered a better way ? Usually the first action of anyone is a source code compare to understand changes and any refactoring would result in a mass of such notifications ! Perhaps refactoring should be a 2nd step process after initial code supply ?

This PR has been tested in my environment, the commented out debug(s) assisted in proving the issue and that locking resolved it before being commented out. I can add additional comments as to their purpose but again it helps on any future fixes what standards any developers should be adhering to.

Thanks again for all your support

Gary

@DocMoebiuz
Copy link
Collaborator

@ghingres will you be able to check the comments and copilot feedback and implement them?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants