Работа с COM-объектами .NET Framework
Пытаюсь использовать функцию GetApplicationVolume из примера https://gist.github.com/sverrirs/d099b34b7f72bb4fb386 вот так:
public static float? GetApplicationVolume(int pid)
{
ISimpleAudioVolume volume = GetVolumeObject(pid);
if (volume == null)
return null;
float level;
volume.GetMasterVolume(out level);
Marshal.ReleaseComObject(volume);
return level * 100;
}
private static ISimpleAudioVolume GetVolumeObject(int pid)
{
IMMDeviceEnumerator deviceEnumerator = null;
IAudioSessionEnumerator sessionEnumerator = null;
IAudioSessionManager2 mgr = null;
IMMDevice speakers = null;
try
{
// get the speakers (1st render + multimedia) device
deviceEnumerator = (IMMDeviceEnumerator)(new MMDeviceEnumerator());
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out speakers);
// activate the session manager. we need the enumerator
Guid IID_IAudioSessionManager2 = typeof(IAudioSessionManager2).GUID;
object o;
speakers.Activate(ref IID_IAudioSessionManager2, 0, IntPtr.Zero, out o);
mgr = (IAudioSessionManager2)o;
// enumerate sessions for on this device
mgr.GetSessionEnumerator(out sessionEnumerator);
int count;
sessionEnumerator.GetCount(out count);
// search for an audio session with the required process-id
ISimpleAudioVolume volumeControl = null;
for (int i = 0; i < count; ++i)
{
IAudioSessionControl2 ctl = null;
try
{
sessionEnumerator.GetSession(i, out ctl);
// NOTE: we could also use the app name from ctl.GetDisplayName()
int cpid;
ctl.GetProcessId(out cpid);
if (cpid == pid)
{
volumeControl = ctl as ISimpleAudioVolume;
break;
}
}
finally
{
if (ctl != null) Marshal.ReleaseComObject(ctl);
}
}
return volumeControl;
}
finally
{
if (sessionEnumerator != null) Marshal.ReleaseComObject(sessionEnumerator);
if (mgr != null) Marshal.ReleaseComObject(mgr);
if (speakers != null) Marshal.ReleaseComObject(speakers);
if (deviceEnumerator != null) Marshal.ReleaseComObject(deviceEnumerator);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(AudioManager.GetApplicationVolume(5992).ToString());
}
}
и получаю "Необработанное исключение: System.Runtime.InteropServices.InvalidComObjectException: Объект COM, который был отделен от своего базового RCW, использоваться не может.
При гуглении залез в какие дебри про потоки и совсем запутался. Помогите! C C# и .NET до этого не работал, так что не кидайтесь тапками)
Ответы (1 шт):
Вот смотрите, в C# есть IDisposable объекты. Смысл этого интерфейса в том чтобы помочь разработчику правильно очищать неуправляемые ресурсы даже при возникновении исключений в управляемом коде. Либо отдать контроль своими неуправляемыми ресурсами сборщику мусора, но это уже другая история про финализаторы.
Для того, чтобы гарантированно вызвать какой-то код даже при возникновении исключения, используется конструкция try-finally или конкретно для IDisposable есть ее упрощенный синтаксис using. Почитать больше можно здесь: Использование объектов, реализующих IDisposable.
Так к чему я всё это. COM-объекты, это по сути вызов неуправляемого кода, например написанного на C++, но сделанного так, чтобы с ним можно было взаимодействовать через специальное API операционной системы под названием COM (Component Object Model). Так вот, если у реализующего IDisposable класса надо обязательно вызывать Dispose(), то у COM-объекта надо обязательно вызывать Marshal.ReleaseComObject(), при чём используя ту же самую конструкцию try-finally, чтобы не быть причиной утечек памяти в случае, если возниколо исключение в управляемом коде и выполнение кода дальше не пошло.
try
{
var obj = getComCobject();
// работаем с COM-объектом
}
finally
{
Marshal.ReleaseComObject(obj);
}
И разработчик этого класса AudioManager очень строго следует этим принципам, и он перестарался.
Дело в том, что строки кода в методе GetVolumeObject выполняются (при попадании в нужный PID) в следующем порядке
// найден нужный COM-объект
if (cpid == pid) // true
{
volumeControl = ctl as ISimpleAudioVolume;
break;
}
// возврат результата
return volumeControl;
// и следом срабатывает
finally
{
if (ctl != null) Marshal.ReleaseComObject(ctl);
}
И при возврате в метод, вы пытаетесь обратиться к COM-объекту, для которого получается, что ранее уже был вызван Marshal.ReleaseComObject.
volume.GetMasterVolume(out float level);
Говоря дотнетовским языком, пытаетесь воспользоваться уже ранее уничтоженным объектом.
Что же делать? Самый простой способ - обмануть конструкцию блоке finally
finally
{
if (ctl != null) Marshal.ReleaseComObject(ctl);
}
Вот так
if (cpid == pid)
{
volumeControl = ctl as ISimpleAudioVolume;
ctl = null; // эту строчку нужно добавить в класс
break;
}
В этом случае volumeControl вернется из метода GetVolumeObject в метод GetApplicationVolume в рабочем состоянии.
Я проверил подставив PID одного из своих процессов, для наглядности уменьшил немного громкость приложению, с которого её считывал.
Console.WriteLine(AudioManager.GetApplicationVolume(24240) ?? 0);
Вывод в консоль:
93,61702
Ну и вот это тоже работает
Console.WriteLine(AudioManager.GetMasterVolume());
47