Закинул на red-bee.ru несколько страниц с заметками, которые могут оказаться интересными при разработке различного рода административных утилит. Опубликованы здесь. Материал не претендует на абсолютную истину (не исключено, что какие-то задачи можно было решить и более простым путём).
Архив рубрики: C
Быстрый способ проверить наличие запущенного процесса acad.exe или accoreconsole.exe
То, что предоставлено в данной заметке не является документированным способом и получено на основе анализирования состава мьютексов, создаваемых и используемых AutoCAD в процессе своей работы. Способ применим как к acad.exe, так и к accoreconsole.exe. Проверялся с AutoCAD 2009-2016 x64, а так же с AutoCAD 2008 x86, запущенном в Windows x64. Решение продемонстрировано в двух вариантах: C++ и C#.
В .NET получить процессы по имени можно при помощи статического метода Process.GetProcessesByName("acad"). Однако, если переименовать любой EXE файл в acad.exe, то будучи запущенным, он тоже попадёт в выборку, возврашаемую этим методом, хотя на самом деле это не процесс AutoCAD.
Конечно же, приведённый в этой заметке код не является "серебрянной пулей", т.к. всегда можно программно создать Mutex с указанным в нашем коде именем... Однако, обозначенный мною вариант проверки не удастся обмануть простым переименованием любого EXE файла в acad.exe или accoreconsole.exe, поэтому он, как мне кажется, имеет право на жизнь...
Как это сделать на C++:
/* © Andrey Bushman, 2015
http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html
This is the quick way of checking is any AutoCAD launched (acad.exe
or accoreconsole.exe) or not. I've checked my code for the usual
AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
older AutoCAD versions, and I haven't their vertical products,
therefore I can't check my code for these versions.
Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
which was launched in Windows x64.
NOTE
Visual C++ Redistributable for Visual Studio (your version) must be
installed on the target computers.
*/
#include<Windows.h>
#include<iostream>
#include<exception>
#include<string>
#include<tchar.h>
#include<strsafe.h>
using namespace std;
#define UNEXPECTED_EXCEPTION 1
#define UNKNOWN_ERROR 2
BOOL IsLaunchedAnyAutoCAD();
int wmain(int argc, wchar_t *argv[])
try {
// setlocale(LC_ALL, "Russian");
SetConsoleTitle(TEXT("Is any AutoCAD launched?"));
BOOL result = IsLaunchedAnyAutoCAD();
if (result) {
wcout << L"Any AutoCAD is launched." << endl;
}
else {
wcout << L"Any AutoCAD is not launched." << endl;
}
wcout << L"Press 'x' for exit..." << endl;
wchar_t c;
wcin >> c;
}
catch (exception ex) {
wcerr << ex.what() << endl;
return UNEXPECTED_EXCEPTION;
}
catch(...){
wcerr << L"Unknown error." << endl;
return UNKNOWN_ERROR;
}
BOOL IsLaunchedAnyAutoCAD() {
BOOL result = FALSE;
LPCTSTR anyAcadMutexName = TEXT(
// This works for AutoCAD 2008 and newer (I haven't older
// AutoCAD versions, therefore I can't check it for them).
"Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"
// This is for AutoCAD 2009 and newer
// "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
);
HANDLE hAnyAcadMutex = OpenMutex(READ_CONTROL, FALSE, anyAcadMutexName);
if (NULL != hAnyAcadMutex) {
result = TRUE;
CloseHandle(hAnyAcadMutex);
}
return (result);
}
Как это сделать на C#:
/* © Andrey Bushman, 2015
http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html
This is the quick way of checking is any AutoCAD launched (acad.exe
or accoreconsole.exe) or not. I've checked my code for the usual
AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
older AutoCAD versions, and I haven't their vertical products,
therefore I can't check my code for these versions.
Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
which was launched in Windows x64.
*/
using System;
using System.Threading;
namespace Bushman.Sandbox.AutoCAD
{
class Program
{
static Boolean IsAnyAutoCadLaunched()
{
try
{
Mutex m = Mutex.OpenExisting(
// This works for AutoCAD 2008 and newer (I haven't older
// AutoCAD versions, therefore I can't check it for them).
"Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"
// This is for AutoCAD 2009 and newer
// "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
);
m.Close();
return true;
}
catch
{
return false;
}
}
static void Main(string[] args)
{
String msg = IsAnyAutoCadLaunched() ? "Any AutoCAD is launched." :
"Any AutoCAD is not launched.";
Console.WriteLine(msg);
Console.WriteLine("Press any key for exit...");
Console.ReadKey();
}
}
}
В .NET получить процессы по имени можно при помощи статического метода Process.GetProcessesByName("acad"). Однако, если переименовать любой EXE файл в acad.exe, то будучи запущенным, он тоже попадёт в выборку, возврашаемую этим методом, хотя на самом деле это не процесс AutoCAD.
Конечно же, приведённый в этой заметке код не является "серебрянной пулей", т.к. всегда можно программно создать Mutex с указанным в нашем коде именем... Однако, обозначенный мною вариант проверки не удастся обмануть простым переименованием любого EXE файла в acad.exe или accoreconsole.exe, поэтому он, как мне кажется, имеет право на жизнь...
Как это сделать на C++:
/* © Andrey Bushman, 2015
http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html
This is the quick way of checking is any AutoCAD launched (acad.exe
or accoreconsole.exe) or not. I've checked my code for the usual
AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
older AutoCAD versions, and I haven't their vertical products,
therefore I can't check my code for these versions.
Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
which was launched in Windows x64.
NOTE
Visual C++ Redistributable for Visual Studio (your version) must be
installed on the target computers.
*/
#include<Windows.h>
#include<iostream>
#include<exception>
#include<string>
#include<tchar.h>
#include<strsafe.h>
using namespace std;
#define UNEXPECTED_EXCEPTION 1
#define UNKNOWN_ERROR 2
BOOL IsLaunchedAnyAutoCAD();
int wmain(int argc, wchar_t *argv[])
try {
// setlocale(LC_ALL, "Russian");
SetConsoleTitle(TEXT("Is any AutoCAD launched?"));
BOOL result = IsLaunchedAnyAutoCAD();
if (result) {
wcout << L"Any AutoCAD is launched." << endl;
}
else {
wcout << L"Any AutoCAD is not launched." << endl;
}
wcout << L"Press 'x' for exit..." << endl;
wchar_t c;
wcin >> c;
}
catch (exception ex) {
wcerr << ex.what() << endl;
return UNEXPECTED_EXCEPTION;
}
catch(...){
wcerr << L"Unknown error." << endl;
return UNKNOWN_ERROR;
}
BOOL IsLaunchedAnyAutoCAD() {
BOOL result = FALSE;
LPCTSTR anyAcadMutexName = TEXT(
// This works for AutoCAD 2008 and newer (I haven't older
// AutoCAD versions, therefore I can't check it for them).
"Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"
// This is for AutoCAD 2009 and newer
// "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
);
HANDLE hAnyAcadMutex = OpenMutex(READ_CONTROL, FALSE, anyAcadMutexName);
if (NULL != hAnyAcadMutex) {
result = TRUE;
CloseHandle(hAnyAcadMutex);
}
return (result);
}
Как это сделать на C#:
/* © Andrey Bushman, 2015
http://bushman-andrey.blogspot.ru/2015/11/acadexe-accoreconsoleexe.html
This is the quick way of checking is any AutoCAD launched (acad.exe
or accoreconsole.exe) or not. I've checked my code for the usual
AutoCAD 2009-2016 x64. But I haven't their x86 versions, I haven't
older AutoCAD versions, and I haven't their vertical products,
therefore I can't check my code for these versions.
Additionally, Alexander Rivilis checked this code for AutoCAD 2008 x86
which was launched in Windows x64.
*/
using System;
using System.Threading;
namespace Bushman.Sandbox.AutoCAD
{
class Program
{
static Boolean IsAnyAutoCadLaunched()
{
try
{
Mutex m = Mutex.OpenExisting(
// This works for AutoCAD 2008 and newer (I haven't older
// AutoCAD versions, therefore I can't check it for them).
"Global\\8C84DAD6-9865-400e-A6E3-686A61C16968"
// This is for AutoCAD 2009 and newer
// "Local\\AcadProfileStorage_54519085-6DDA-4070-BB93-3A095D7E1140"
);
m.Close();
return true;
}
catch
{
return false;
}
}
static void Main(string[] args)
{
String msg = IsAnyAutoCadLaunched() ? "Any AutoCAD is launched." :
"Any AutoCAD is not launched.";
Console.WriteLine(msg);
Console.WriteLine("Press any key for exit...");
Console.ReadKey();
}
}
}
Открытие и закрытие консольного окна для GUI-приложения
Работая с GUI приложением иногда бывает удобно в режиме реального времени посмотреть, что оно отправляет себе на консоль (т.е. в потоки stdout и stderr), а порой может возникнуть и желание что-то отправить в поток stdin с клавиатуры. Можно, конечно же, выполнять перенаправление в файлы, но этот вариант не всегда удобен. В данной заметке, на примере AutoCAD, показано, как для GUI-приложения открыть консольное окно, выполнить перенаправление потоков и, после того как консольное окно не будет нужно, закрыть его.
Пример C++ кода, выполняющего открытие консольного окна и перенаправляющие потоки ввода-вывода:
// I open console window for AutoCAD GUI application
BOOL result = AllocConsole();
if (0 == result){
DWORD errCode = GetLastError();
LPTSTR msg = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
acutPrintf(_T("\nAllocConsole Error: %s"), msg);
HeapFree(GetProcessHeap(), 0, msg);
}
else{
acutPrintf(_T("\nAllocConsole: OK."));
SetConsoleTitle(L"AutoCAD console window");
// Disable the close button of the Console window
HWND hwnd = GetConsoleWindow();
HMENU hmenu = GetSystemMenu(hwnd, FALSE);
EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED);
// redirecting of the streams
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
freopen("CONIN$", "r", stdin);
// You will see this messages in the console window
wcout << L"wcout ping..." << endl;
wcerr << L"wcerr ping..." << endl;
cout << "cout ping..." << endl;
cerr << "cerr ping..." << endl;
// check wcin accessibility
wcout << L"Press any char: ";
wchar_t c;
wcin >> c;
wcout << L"You pressed the '" << c << "' char." << endl;
}
Результат выглядит следующим образом:
После того, как консольное окно станет ненужным, его можно закрыть. Однако его нельзя закрывать обычным кликом мышки по кнопке "X" в верхнем правом углу, иначе это приведёт к аварийному завершению работы GUI-приложения (в нашем примере - AutoCAD). Для того, чтобы пользователь случайно не нажал этой кнопки, в обозначенном выше коде мы делаем её недоступной.
Закрытие консольного окна выполняем так же программно:
BOOL result = FreeConsole();
if (0 == result){
DWORD errCode = GetLastError();
LPTSTR msg = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
acutPrintf(_T("\nFreeConsole Error: %s"), msg);
HeapFree(GetProcessHeap(), 0, msg);
}
else{
// redirecting of the streams
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
freopen("CONIN$", "r", stdin);
}
Пример C++ кода, выполняющего открытие консольного окна и перенаправляющие потоки ввода-вывода:
// I open console window for AutoCAD GUI application
BOOL result = AllocConsole();
if (0 == result){
DWORD errCode = GetLastError();
LPTSTR msg = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
acutPrintf(_T("\nAllocConsole Error: %s"), msg);
HeapFree(GetProcessHeap(), 0, msg);
}
else{
acutPrintf(_T("\nAllocConsole: OK."));
SetConsoleTitle(L"AutoCAD console window");
// Disable the close button of the Console window
HWND hwnd = GetConsoleWindow();
HMENU hmenu = GetSystemMenu(hwnd, FALSE);
EnableMenuItem(hmenu, SC_CLOSE, MF_GRAYED);
// redirecting of the streams
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
freopen("CONIN$", "r", stdin);
// You will see this messages in the console window
wcout << L"wcout ping..." << endl;
wcerr << L"wcerr ping..." << endl;
cout << "cout ping..." << endl;
cerr << "cerr ping..." << endl;
// check wcin accessibility
wcout << L"Press any char: ";
wchar_t c;
wcin >> c;
wcout << L"You pressed the '" << c << "' char." << endl;
}
Результат выглядит следующим образом:
После того, как консольное окно станет ненужным, его можно закрыть. Однако его нельзя закрывать обычным кликом мышки по кнопке "X" в верхнем правом углу, иначе это приведёт к аварийному завершению работы GUI-приложения (в нашем примере - AutoCAD). Для того, чтобы пользователь случайно не нажал этой кнопки, в обозначенном выше коде мы делаем её недоступной.
Закрытие консольного окна выполняем так же программно:
BOOL result = FreeConsole();
if (0 == result){
DWORD errCode = GetLastError();
LPTSTR msg = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
| FORMAT_MESSAGE_IGNORE_INSERTS, NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
acutPrintf(_T("\nFreeConsole Error: %s"), msg);
HeapFree(GetProcessHeap(), 0, msg);
}
else{
// redirecting of the streams
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
freopen("CONIN$", "r", stdin);
}
Пример создания именованных объектов ядра ОС в разных пространствах имён
Маленькая шпаргалка-пример на тему совместного использования именованных объектов ядра несколькими процессами. Показан вариант размещения именованных объектов ядра в глобальном, локальном и приватном пространствах имён.
Исходный код примера:
#include <Windows.h>
#include <iostream>
#include <aclapi.h>
#include <exception>
#include <tchar.h>
using namespace std;
void PrintErrorMsg(LPCTSTR prefix);
int main(int argc, TCHAR *argv[])
try {
setlocale(LC_ALL, "Russian");
HANDLE hGlobalMutex = CreateMutex(NULL, FALSE, L"Global\\G_Mutex");
if (NULL == hGlobalMutex) {
PrintErrorMsg(L"Global Mutex: ");
}
else {
// Check the reason of success
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
wcout << L"Named global mutex opened." << endl;
}
else {
wcout << L"Named global mutex created." << endl;
}
}
HANDLE hLocalMutex = CreateMutex(NULL, FALSE, L"Local\\L_Mutex");
if (NULL == hLocalMutex) {
PrintErrorMsg(L"Local Mutex: ");
}
else {
// Check the reason of success
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
wcout << L"Named local mutex opened." << endl;
}
else {
wcout << L"Named local mutex created." << endl;
}
}
LPCTSTR boundaryName = L"BushmanBoundary";
HANDLE hBoundary = CreateBoundaryDescriptor(boundaryName, 0);
HANDLE hNamespase = NULL;
HANDLE hMutex = NULL;
if (NULL == hBoundary) {
PrintErrorMsg(L"CreateBoundaryDescriptor: ");
}
else {
wcout << L"Boundary descriptor created." << endl;
BYTE sid[SECURITY_MAX_SID_SIZE];
ZeroMemory(sid, sizeof(sid));
DWORD cbSID = sizeof(sid);
BOOL result = CreateWellKnownSid(WinWorldSid, NULL, &sid, &cbSID);
if (0 == result) {
PrintErrorMsg(L"CreateWellKnownSid: ");
}
else {
wcout << L"SID for boundary descriptor created." << endl;
result = AddSIDToBoundaryDescriptor(&hBoundary, &sid);
if (0 == result) {
PrintErrorMsg(L"AddSIDToBoundaryDescriptor: ");
}
else {
wcout << L"SID added to boundary descriptor." << endl;
SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(sa);
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = NULL; // use the default value
LPCTSTR ns = L"Local\\BushNamespace";
hNamespase = CreatePrivateNamespace(&sa, hBoundary,
ns);
if (NULL == hNamespase) {
// Check the reason of failure
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
hNamespase = OpenPrivateNamespace(hBoundary, ns);
if (NULL == hNamespase) {
PrintErrorMsg(L"OpenPrivateNamespace: ");
}
else {
wcout << L"Namespace opened." << endl;
}
}
else {
PrintErrorMsg(L"CreateNamespace: ");
}
}
else {
wcout << L"Namespace created." << endl;
}
if (NULL != hNamespase) {
hMutex = CreateMutex(NULL, FALSE,
L"Local\\BushNamespace\\BushMutex");
if (NULL == hMutex) {
PrintErrorMsg(L"CreateNamespaceMutex: ");
}
else {
// Check the reason of success
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
wcout << L"Named mutex in private namespace opened."
<< endl;
}
else {
wcout << L"Named mutex in private namespace created."
<< endl;
}
}
}
}
}
}
wcout << L"Press any char for exit..." << endl;
wchar_t c;
wcin >> c;
if (NULL != hGlobalMutex) {
CloseHandle(hGlobalMutex);
hGlobalMutex = NULL;
}
if (NULL != hLocalMutex) {
CloseHandle(hLocalMutex);
hLocalMutex = NULL;
}
if (NULL == hMutex) {
CloseHandle(hMutex);
hMutex = NULL;
}
if (NULL != hNamespase) {
ClosePrivateNamespace(hNamespase, 0);
hNamespase = NULL;
}
if (NULL != hBoundary) {
DeleteBoundaryDescriptor(hBoundary);
hBoundary = NULL;
}
}
catch (...) {
wcerr << L"Unknown error." << endl;
return 1;
}
void PrintErrorMsg(LPCTSTR prefix) {
DWORD errCode = GetLastError();
LPTSTR msg = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
wcerr << prefix << msg << endl;
HeapFree(GetProcessHeap(), 0, msg);
}
На скрине результат консольного вывода двух разных процессов:
В Process Explorer видим следующую картину (см. выделенное красным):
Смотрим WinObj, запустив её с правами администратора:
Исходный код примера:
#include <Windows.h>
#include <iostream>
#include <aclapi.h>
#include <exception>
#include <tchar.h>
using namespace std;
void PrintErrorMsg(LPCTSTR prefix);
int main(int argc, TCHAR *argv[])
try {
setlocale(LC_ALL, "Russian");
HANDLE hGlobalMutex = CreateMutex(NULL, FALSE, L"Global\\G_Mutex");
if (NULL == hGlobalMutex) {
PrintErrorMsg(L"Global Mutex: ");
}
else {
// Check the reason of success
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
wcout << L"Named global mutex opened." << endl;
}
else {
wcout << L"Named global mutex created." << endl;
}
}
HANDLE hLocalMutex = CreateMutex(NULL, FALSE, L"Local\\L_Mutex");
if (NULL == hLocalMutex) {
PrintErrorMsg(L"Local Mutex: ");
}
else {
// Check the reason of success
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
wcout << L"Named local mutex opened." << endl;
}
else {
wcout << L"Named local mutex created." << endl;
}
}
LPCTSTR boundaryName = L"BushmanBoundary";
HANDLE hBoundary = CreateBoundaryDescriptor(boundaryName, 0);
HANDLE hNamespase = NULL;
HANDLE hMutex = NULL;
if (NULL == hBoundary) {
PrintErrorMsg(L"CreateBoundaryDescriptor: ");
}
else {
wcout << L"Boundary descriptor created." << endl;
BYTE sid[SECURITY_MAX_SID_SIZE];
ZeroMemory(sid, sizeof(sid));
DWORD cbSID = sizeof(sid);
BOOL result = CreateWellKnownSid(WinWorldSid, NULL, &sid, &cbSID);
if (0 == result) {
PrintErrorMsg(L"CreateWellKnownSid: ");
}
else {
wcout << L"SID for boundary descriptor created." << endl;
result = AddSIDToBoundaryDescriptor(&hBoundary, &sid);
if (0 == result) {
PrintErrorMsg(L"AddSIDToBoundaryDescriptor: ");
}
else {
wcout << L"SID added to boundary descriptor." << endl;
SECURITY_ATTRIBUTES sa = { 0 };
sa.nLength = sizeof(sa);
sa.bInheritHandle = FALSE;
sa.lpSecurityDescriptor = NULL; // use the default value
LPCTSTR ns = L"Local\\BushNamespace";
hNamespase = CreatePrivateNamespace(&sa, hBoundary,
ns);
if (NULL == hNamespase) {
// Check the reason of failure
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
hNamespase = OpenPrivateNamespace(hBoundary, ns);
if (NULL == hNamespase) {
PrintErrorMsg(L"OpenPrivateNamespace: ");
}
else {
wcout << L"Namespace opened." << endl;
}
}
else {
PrintErrorMsg(L"CreateNamespace: ");
}
}
else {
wcout << L"Namespace created." << endl;
}
if (NULL != hNamespase) {
hMutex = CreateMutex(NULL, FALSE,
L"Local\\BushNamespace\\BushMutex");
if (NULL == hMutex) {
PrintErrorMsg(L"CreateNamespaceMutex: ");
}
else {
// Check the reason of success
DWORD errCode = GetLastError();
if (ERROR_ALREADY_EXISTS == errCode) {
wcout << L"Named mutex in private namespace opened."
<< endl;
}
else {
wcout << L"Named mutex in private namespace created."
<< endl;
}
}
}
}
}
}
wcout << L"Press any char for exit..." << endl;
wchar_t c;
wcin >> c;
if (NULL != hGlobalMutex) {
CloseHandle(hGlobalMutex);
hGlobalMutex = NULL;
}
if (NULL != hLocalMutex) {
CloseHandle(hLocalMutex);
hLocalMutex = NULL;
}
if (NULL == hMutex) {
CloseHandle(hMutex);
hMutex = NULL;
}
if (NULL != hNamespase) {
ClosePrivateNamespace(hNamespase, 0);
hNamespase = NULL;
}
if (NULL != hBoundary) {
DeleteBoundaryDescriptor(hBoundary);
hBoundary = NULL;
}
}
catch (...) {
wcerr << L"Unknown error." << endl;
return 1;
}
void PrintErrorMsg(LPCTSTR prefix) {
DWORD errCode = GetLastError();
LPTSTR msg = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, errCode, 0, (LPTSTR)&msg, 0, NULL);
wcerr << prefix << msg << endl;
HeapFree(GetProcessHeap(), 0, msg);
}
На скрине результат консольного вывода двух разных процессов:
В Process Explorer видим следующую картину (см. выделенное красным):
Смотрим WinObj, запустив её с правами администратора:
Ещё немного по теме локализованных ресурсов
В дополнение к предыдущей заметке...
Нейтральная локаль, как и все остальные, определяется в ресурсах EXE или DLL и имеет код 0x000. Я думаю, что зачастую имеет смысл определять более обобщённые варианты ресурсов, дабы не дублировать их для каждой конкретной локали, например:
В качестве примера в файле resources.txt определим текстовые ресурсы с таким набором локализаций:
; // ***** resources.txt *****
; // This is the header section.
MessageIdTypedef=DWORD
SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)
FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)
; // The neutral locale.
LanguageNames=(Neutral=0x000:MSG00000)
; // This locale will be used for any RU-based locale.
LanguageNames=(ru=0x019:MSG00019)
; // This locale will be used only for en-US locale, instead
; // of any EN-based locale.
LanguageNames=(en_US=0x409:MSG00409)
; // The following are message definitions.
MessageId=0x1
Severity=Success
Facility=System
SymbolicName=MSG_HELLO
Language=en_US
USA locale only (en-US).
.
Language=ru
Любая RU-основанная локаль (RU).
.
Language=Neutral
Neutral locale.
.
Далее разными способами программно пытаемся получить локализованный вариант нашего текста:
#include <Windows.h>
#include <tchar.h>
#include <iostream>
#include <exception>
#include "resources.h"
using namespace std;
/*
MSDN resources:
FormatMessage function:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx
Message Text Files:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996906%28v=vs.85%29.aspx
Sample Message Text File:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996907%28v=vs.85%29.aspx
Message Compiler (MC.exe):
https://msdn.microsoft.com/en-us/library/windows/desktop/aa385638%28v=vs.85%29.aspx
*/
// Our function uses the WinAPI mechanism for notifying of error
void PrintMessage(DWORD);
int wmain(int argc, LPTSTR argv[])
try {
// For right displaying of Cyrillic chars in the console window
setlocale(LC_ALL, "Russian");
wcout << L"For neutral locale:" << endl;
DWORD nl = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);
PrintMessage(nl);
TCHAR x[LOCALE_NAME_MAX_LENGTH];
DWORD du = GetUserDefaultLCID();
int n = GetUserDefaultLocaleName(x, LOCALE_NAME_MAX_LENGTH);
if (0 != n) {
wcout << L"For user default locale <" << x << L">:" << endl;
PrintMessage(du);
}
DWORD ds = GetSystemDefaultLCID();
n = GetSystemDefaultLocaleName(x, LOCALE_NAME_MAX_LENGTH);
if (0 != n) {
wcout << L"For system default locale <" << x << L">:" << endl;
PrintMessage(ds);
}
// Any RU-based locale (defined in the resources)
wcout << L"For RU locale:" << endl;
DWORD ru = MAKELANGID(LANG_RUSSIAN, SUBLANG_NEUTRAL);
PrintMessage(ru);
// Specific locale (NOT defined in the resources)
// At this case will be used more common locale: RU
wcout << L"For ru-RU locale:" << endl;
DWORD ru_ru = MAKELANGID(LANG_RUSSIAN, SUBLANG_RUSSIAN_RUSSIA);
PrintMessage(ru_ru);
// Any EN-based locale (defined in the resources)
wcout << L"For EN locale:" << endl;
DWORD en = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL);
PrintMessage(en);
// Specific locale (defined in the resources)
wcout << L"For en-US locale:" << endl;
DWORD en_us = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
PrintMessage(en_us);
wcout << L"Press any char for exit..." << endl;
wchar_t c;
wcin >> c;
}
catch (...) {
wcerr << L"Unknown exception." << endl;
return 1;
}
void PrintMessage(DWORD localeId) {
DWORD msgId = MSG_HELLO;
LPTSTR buffer = NULL;
HMODULE hModule = GetModuleHandle(NULL);
DWORD result = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
hModule, msgId, localeId, (LPTSTR)&buffer, 0, NULL);
if (0 == result) {
msgId = GetLastError();
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, msgId, 0, (LPTSTR)&buffer, 0, NULL);
}
wcout << buffer << endl;
HeapFree(GetProcessHeap(), 0, buffer);
}
Результат работы кода выглядит следующим образом:
Нейтральная локаль, как и все остальные, определяется в ресурсах EXE или DLL и имеет код 0x000. Я думаю, что зачастую имеет смысл определять более обобщённые варианты ресурсов, дабы не дублировать их для каждой конкретной локали, например:
- ru вместо ru-RU
- en вместо en-US, en-GB и т.п.
В качестве примера в файле resources.txt определим текстовые ресурсы с таким набором локализаций:
- нейтральная
- ru
- en-US
; // ***** resources.txt *****
; // This is the header section.
MessageIdTypedef=DWORD
SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)
FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)
; // The neutral locale.
LanguageNames=(Neutral=0x000:MSG00000)
; // This locale will be used for any RU-based locale.
LanguageNames=(ru=0x019:MSG00019)
; // This locale will be used only for en-US locale, instead
; // of any EN-based locale.
LanguageNames=(en_US=0x409:MSG00409)
; // The following are message definitions.
MessageId=0x1
Severity=Success
Facility=System
SymbolicName=MSG_HELLO
Language=en_US
USA locale only (en-US).
.
Language=ru
Любая RU-основанная локаль (RU).
.
Language=Neutral
Neutral locale.
.
Далее разными способами программно пытаемся получить локализованный вариант нашего текста:
#include <Windows.h>
#include <tchar.h>
#include <iostream>
#include <exception>
#include "resources.h"
using namespace std;
/*
MSDN resources:
FormatMessage function:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx
Message Text Files:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996906%28v=vs.85%29.aspx
Sample Message Text File:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996907%28v=vs.85%29.aspx
Message Compiler (MC.exe):
https://msdn.microsoft.com/en-us/library/windows/desktop/aa385638%28v=vs.85%29.aspx
*/
// Our function uses the WinAPI mechanism for notifying of error
void PrintMessage(DWORD);
int wmain(int argc, LPTSTR argv[])
try {
// For right displaying of Cyrillic chars in the console window
setlocale(LC_ALL, "Russian");
wcout << L"For neutral locale:" << endl;
DWORD nl = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);
PrintMessage(nl);
TCHAR x[LOCALE_NAME_MAX_LENGTH];
DWORD du = GetUserDefaultLCID();
int n = GetUserDefaultLocaleName(x, LOCALE_NAME_MAX_LENGTH);
if (0 != n) {
wcout << L"For user default locale <" << x << L">:" << endl;
PrintMessage(du);
}
DWORD ds = GetSystemDefaultLCID();
n = GetSystemDefaultLocaleName(x, LOCALE_NAME_MAX_LENGTH);
if (0 != n) {
wcout << L"For system default locale <" << x << L">:" << endl;
PrintMessage(ds);
}
// Any RU-based locale (defined in the resources)
wcout << L"For RU locale:" << endl;
DWORD ru = MAKELANGID(LANG_RUSSIAN, SUBLANG_NEUTRAL);
PrintMessage(ru);
// Specific locale (NOT defined in the resources)
// At this case will be used more common locale: RU
wcout << L"For ru-RU locale:" << endl;
DWORD ru_ru = MAKELANGID(LANG_RUSSIAN, SUBLANG_RUSSIAN_RUSSIA);
PrintMessage(ru_ru);
// Any EN-based locale (defined in the resources)
wcout << L"For EN locale:" << endl;
DWORD en = MAKELANGID(LANG_ENGLISH, SUBLANG_NEUTRAL);
PrintMessage(en);
// Specific locale (defined in the resources)
wcout << L"For en-US locale:" << endl;
DWORD en_us = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
PrintMessage(en_us);
wcout << L"Press any char for exit..." << endl;
wchar_t c;
wcin >> c;
}
catch (...) {
wcerr << L"Unknown exception." << endl;
return 1;
}
void PrintMessage(DWORD localeId) {
DWORD msgId = MSG_HELLO;
LPTSTR buffer = NULL;
HMODULE hModule = GetModuleHandle(NULL);
DWORD result = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS,
hModule, msgId, localeId, (LPTSTR)&buffer, 0, NULL);
if (0 == result) {
msgId = GetLastError();
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, msgId, 0, (LPTSTR)&buffer, 0, NULL);
}
wcout << buffer << endl;
HeapFree(GetProcessHeap(), 0, buffer);
}
Результат работы кода выглядит следующим образом:
Подписаться на изменение DependecyProperty
Часто бывает, что есть некий потомок DependencyObject, у него есть DependencyProperty, а вот события сообщающего о том, что свойство изменилось нет. Я с такой ситуацией столкнулся при попытках отследить изменение актуальной ширины DataGridColumn. Ну и под катом, как это все можно провернуть.
Для примера, я воспользуюсь вот таким классом:
Первое изменение свойства прошло тихонечко, а на второе уже сработал вывод.
Для примера, я воспользуюсь вот таким классом:
class MyClass : DependencyObjectНу и как мы можем узнать об изменении этого свойства? До очень просто, вот так:
{
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int), typeof(MyClass), new PropertyMetadata(0));
}
static void Main(string[] args)Ну и вот так это выглядит:
{
MyClass m = new MyClass();
m.MyProperty = 10; // До подписывания
PropertyDescriptor descriptor = DependencyPropertyDescriptor.FromProperty(MyClass.MyPropertyProperty, typeof(MyClass));
descriptor.AddValueChanged(m, new EventHandler((sender, e) => Console.WriteLine(((MyClass)sender).MyProperty)));
m.MyProperty = 20;
Console.ReadKey();
}
Первое изменение свойства прошло тихонечко, а на второе уже сработал вывод.
Вставка HTML в буфер обмена
При работе с буфером обмена из C# выяснилось, что вставить туда HTML так, чтобы программы типа Word распознавали его не как текст, а именно как HTML не совсем просто, под катом пример.
Собственно, для вставки HTML в буфер, его нужно специальным образом подготовить. Вот таким методом:
rnrn"Собственно, для вставки HTML в буфер, его нужно специальным образом подготовить. Вот таким методом:
public static string PrepareHtmlToClippboard(stringhtml)
{
Encoding enc = Encoding.UTF8;
string begin = "Version:0.9rnStartHTML:{0:000000}rnEndHTML:{1:000000}"
+ "rnStartFragment:{2:000000}rnEndFragment:{3:000000}rn";
string html_begin = "rnrn"
+ "
+ " content="text/html; charset=" + enc.WebName + "">rn"
+ "HTML clipboard rn
+ "";
string html_end = "rn
rnrn"; string begin_sample = String.Format(begin, 0, 0, 0, 0);
intcount_begin = enc.GetByteCount(begin_sample);
intcount_html_begin = enc.GetByteCount(html_begin);
intcount_html = enc.GetByteCount(html);
intcount_html_end = enc.GetByteCount(html_end);
string html_total = String.Format(
begin
, count_begin
, count_begin + count_html_begin + count_html + count_html_end
, count_begin + count_html_begin
, count_begin + count_html_begin + count_html
) + html_begin + html + html_end;
return html_total;
}
Ну и вот так это можно использовать:DataObject dataObj = new DataObject();
dataObj.SetData(DataFormats.Html, PrepareHtmlToClippboard("здесь HTML"));
dataObj.SetData(DataFormats.Text, "здесь текст, если приемник не понимает html");
Clipboard.SetDataObject(dataObj, true);
Собственно все. Пишем монитор pulseaudio для lxpanel
С момента написания статьи об xmonad кое-что поменялось и я перешёл на использование openbox. Поменялись и панели, теперь я использую lxpanel, который разрабатывается для lxde, легковесного рабочего стола, основанного на openbox. Отображаемые lxpanel элементы являются плагинами, динамически загружаемыми библиотеками. Сегодня мы напишем свой плагин для lxpanel.
Так как в стандартной поставке lxpanel виджет для регулировки громкости не очень хорошо (скорее очень нехорошо) отображает текущий уровень громкости, то я решил написать свой плагин, который исправит ситуацию.
Эта статья будет состоять из двух частей. Первая будет посвящена работе с pulseaudio (читается [pʌlsˈɔːdɪəu] пaлсодыо), а вторая -- lxpanel.

На скриншоте выше результат можно разглядеть слева.
Писать мы будем на C. Да, именно на C, так как заголовочный файл lxpanel является ярким представителем того, как можно писать на C несовместимый с C++ код.
Внимание, весь код, приведённый в этой статье распространяется по лицензии GNU GPL версии 3 и выше!
Основным элементом для работы с асинхронным API pulseaudio является контекст, представленный типом pa_context. Контекст представляет собой мост между приложением и демоном pulseaudio. Чтобы контекст работал, ему нужен mainloop. Pulseaudio представляет возможность использовать три варианта реализации основного цикла:
Так как на машине может быть несколько звуковых устройств, то их надо как-то различать. Pulseaudio использует для этого понятие sink (не знаю, как правильно перевести). Sink`и имеют свой идентификатор или индекс, который является порядковым номером, начиная с нуля.
Итак, на основе полученной информации мы уже можем накидать интерфейс, который мы будем использовать для получения информации от pulseaudio.
Фунции pamonInit и pamonClose соответственно инициализируют и закрывают соединение с демоном pulseaudio. Для инициализации соединения в функцию pamonInit передаётся структура PamonInitData содержащая идентификатор sink`а и колбеки для уведомления о смене громкости и об ошибке. Кроме того структура PamonInitData содержит поле userdata, которое будет передано в колбеки.
Назначение функции pamonSetSinkId должно быть ясно без пояснений.
Используя этот интерфейс можно написать консольное приложение, которое будет выводить текущий уровень громкости.
Перейдём к самому интересному, к реализации интерфейса взаимодействия с pulseaudio.
Если произошла ошибка, о ней сообщаем вызовом performError, который, по сути, зовёт колбек, переданный при инициализации.
Структура pa_sink_info содержит всю необходимую нам информацию. Уровень громкости тут содержится не в процентах, а в довольно больших числах. Кроме того, уровень громкости можно получить для каждого канала отдельно. Поле channels содержит количество доступных каналов, а массив values, состоящий из 32-ух целочисленных значений имеет инициализированными ровно channels первых элементов. Здесь я схалтурил и взял первое попавшееся значение. Карту соответствия индексов массива с реальными каналами можно посмотреть в поле channel_map структуры pa_sink_info.
Высчитать текущий уровень громкости в процентах помогает константа PA_VOLUME_NORM, которая содержит значение громкости при 100%, 65536.
На этом наша работа с pulseaudio заканчивается. Осталось только корректно завершить работу.
За бортом остались две функции: pamonSetSinkId и pamonTranslateError. Их назначение понятно, а код прост, поэтому приведу его без комментариев.
выглядит dzen2, запущенный командой
Первое, что нам надо сделать -- это подключить необходимые заголовочные файлы.
Документации по написанию плагинов для lxpanel ещё меньше, чем документации по pulseaudio, а точнее -- ровно одна страничка. Даже зачитав её до дыр, Вы не сможете написать полноценный плагин. Для того, чтобы написать свой, мне пришлось посмотреть в исходинки плагинов, поставляемых с lxpanel. Я скачал исходники из репозитория debian по этой ссылке. Файл src/plugins/dclock.c, содержащий исходники плагина с часами, оказался наиболее компактным и полезным.
Итак, для того, чтобы встроить плагин в панель, мы должны собрать библиотеку и положить её в /usr/lib/lxpanel/plugins/ (или в /usr/lib/x86_64-linux-gnu/lxpanel/plugins/ для мультиархитектурного debian`а). Есть одна особенность, имя файла не должно содержать префикс lib и иметь имя <имя_плагина>.so. В нашем случае файл будет называться lxpamon.so.
Библиотека с плагином должна экспозить экземпляр структуры PluginClass (к сожалению, ссылок мне давать не на что). Но не просто экспозить, а экспозить с определённым именем. Имя должно иметь вид <имя_плагина>_plugin_class. То есть, наш экземпляр будет иметь имя lxpamon_plugin_class.
Оставшиеся поля контролируют жизненный цикл плагина, это следующие процедуры обратного вызова:
Вторым параметром в конструктор передаётся конфиг, который мы читаем в функции lxpamonInitConfig. Сохраняется конфиг, как уже было сказано, функцией lxpamonSaveConfig.
Для того, чтобы пользователь смог задать настройки, мы должны показать диалог при обработке соответствующего события.
Вся проблема в том, что этой функции нет в файле lxpanel/plugin.h. Она объявлена в самой панели, но стандартные плагины используют именно её. Поэтому мы добавим себе её объявление.
Значение INVALID_VOLUME устанавливается при обработке ошибки от pamon. В этом случае наш плагин напишет слово "ERROR".
Функция panel_draw_label_text не последняя из тех, которые забыли положить в файл lxpanel/plugin.h, ещё нам нужен стандартный обработчик клика мыши.
После того, как настройки панели изменены нам нужно перерисовать свой виджет. Именно для этого мы и подписывались на это событие.
Обработчики событий от pulseaudio тривиальны и не нуждаются в комментариях за одним исключением, ошибка может произойти до того, как поле plugin->priv будет присвоено, поэтому его нужно проверить.
Возможно, проект я буду дорабатывать, если кому интересна изначальная версия, она лежит здесь.Так как в стандартной поставке lxpanel виджет для регулировки громкости не очень хорошо (скорее очень нехорошо) отображает текущий уровень громкости, то я решил написать свой плагин, который исправит ситуацию.
Эта статья будет состоять из двух частей. Первая будет посвящена работе с pulseaudio (читается [pʌlsˈɔːdɪəu] пaлсодыо), а вторая -- lxpanel.

На скриншоте выше результат можно разглядеть слева.
Писать мы будем на C. Да, именно на C, так как заголовочный файл lxpanel является ярким представителем того, как можно писать на C несовместимый с C++ код.
Внимание, весь код, приведённый в этой статье распространяется по лицензии GNU GPL версии 3 и выше!
Монитор Pulseaudio
Начиная работать с pulseaudio натыкаешься на, мягко говоря, скудную документацию, основу которой составляет сгенерированная doxygen`ом. Из документации мы узнаём, что у pulseaudio есть два типа API: simple и asynchronous. Беглый взгляд на simple API позволяет понять, что придётся использовать асинхронный API.Основным элементом для работы с асинхронным API pulseaudio является контекст, представленный типом pa_context. Контекст представляет собой мост между приложением и демоном pulseaudio. Чтобы контекст работал, ему нужен mainloop. Pulseaudio представляет возможность использовать три варианта реализации основного цикла:
- Простой цикл в текущем потоке, который создаётся функцией pa_mainloop_new
- Цикл, живущий в отдельном потоке, создаваемый функцией pa_threaded_mainloop_new
- И основной цикл GLib, который создаётся функцией pa_glib_mainloop_new на основе контекста GLib
Так как на машине может быть несколько звуковых устройств, то их надо как-то различать. Pulseaudio использует для этого понятие sink (не знаю, как правильно перевести). Sink`и имеют свой идентификатор или индекс, который является порядковым номером, начиная с нуля.
Итак, на основе полученной информации мы уже можем накидать интерфейс, который мы будем использовать для получения информации от pulseaudio.
#include <stdint.h>
#define PAMON_UNUSED(var) (void)var
typedef struct Pamon Pamon;
typedef enum
{
PAMON_ERROR_CONNECTION_FAILED,
PAMON_ERROR_INVALID_SINK
} PamonError;
typedef void (* PamonVolumeChangedCallback)(uint32_t current_volume_percents, void * userdata);
typedef void (* PamonErrorCallback)(PamonError error, void * userdata);
typedef struct
{
uint32_t sink_id;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
void * userdata;
} PamonInitData;
Pamon * pamonInit(PamonInitData * init_data);
void pamonSetSinkId(Pamon * pamon, uint32_t id);
int pamonStart(Pamon * pamon);
void pamonClose(Pamon * pamon);
const char * pamonTranslateError(PamonError error);
Немного поясню. Тип PamonError используется для сообщения об ошибке. Значение PAMON_ERROR_CONNECTION_FAILED говорит о том, что соединиться с демоном pulseaudio не удалось, а PAMON_ERROR_INVALID_SINK сообщает о неверном идентификаторе sink`а. Функция pamonTranslateError переводит идентификатор ошибки в понятный человеку вид, в строку.Фунции pamonInit и pamonClose соответственно инициализируют и закрывают соединение с демоном pulseaudio. Для инициализации соединения в функцию pamonInit передаётся структура PamonInitData содержащая идентификатор sink`а и колбеки для уведомления о смене громкости и об ошибке. Кроме того структура PamonInitData содержит поле userdata, которое будет передано в колбеки.
Назначение функции pamonSetSinkId должно быть ясно без пояснений.
Используя этот интерфейс можно написать консольное приложение, которое будет выводить текущий уровень громкости.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <glib.h>
#include "pamon.h"
#define APP_NAME "pamon"
#define DEFAULT_FORMAT "%i%%"
typedef struct
{
char * format;
uint32_t sink;
int print_help;
} Options;
static void pamonVolumeChanged(uint32_t current_volume, void * userdata);
static void pamonError(PamonError error, void * userdata);
static void onSignal(int signum);
static void parseOpts(int argc, char ** argv, Options * result);
static void printUsage();
static Pamon * pamon = NULL;
static GMainLoop * main_loop = NULL;
static char * format = NULL;
int main(int argc, char ** argv)
{
struct sigaction action;
memset(&action, 0, sizeof(struct sigaction));
action.sa_handler = onSignal;
sigaction(SIGINT, &action, NULL);
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
{
Options opts;
memset(&opts, 0, sizeof(Options));
parseOpts(argc, argv, &opts);
if(opts.print_help)
{
printUsage();
free(opts.format);
return EXIT_SUCCESS;
}
pamon_init_data.sink_id = opts.sink;
format = opts.format;
}
pamon_init_data.volume_callback = pamonVolumeChanged;
pamon_init_data.error_callback = pamonError;
main_loop = g_main_loop_new(NULL, FALSE);
pamon = pamonInit(&pamon_init_data);
g_main_loop_run(main_loop);
return EXIT_SUCCESS;
}
void onSignal(int signum)
{
pamonClose(pamon);
g_main_loop_quit(main_loop);
g_main_loop_unref(main_loop);
free(format);
printf("\nquit\n");
exit(EXIT_SUCCESS);
}
void pamonVolumeChanged(uint32_t current_volume, void * userdata)
{
PAMON_UNUSED(userdata);
printf(format ? format : DEFAULT_FORMAT, current_volume);
printf("\n");
fflush(stdout);
}
void pamonError(PamonError error, void * userdata)
{
PAMON_UNUSED(userdata);
fprintf(stderr, "Error: %s\n", pamonTranslateError(error));
}
void parseOpts(int argc, char ** argv, Options * result)
{
for(;;)
{
switch(getopt(argc, argv, "f:s:"))
{
case -1:
return;
case '?':
result->print_help = 1;
break;
case 'f':
result->format = (char *)malloc(strlen(optarg) + 1);
strcpy(result->format, optarg);
break;
case 's':
sscanf(optarg, "%i", &result->sink);
break;
default:
break;
}
}
}
void printUsage()
{
printf(
"Usage: %s [-s sink] [-f format]\n"
" sink - pulseaudio sink id\n"
" format - print format. Use %%i to print current volume value. Use %%%% to print the %% mark\n"
"\n"
"Use Ctrl+C to exit program\n",
APP_NAME);
fflush(stdout);
}
Здесь мы получаем из опций приложения идентификатор sink`а и формат сообщения. Создаём основной цикл GLib и запускам pamon, передав функции pamonVolumeChanged и pamonError в качестве колбеков. Программа завершается по нажатию Ctrl+C. Обработчик сигнала SIGINT позволит освободить занятые ресурсы перед выходом, хотя это и не обязательно.Перейдём к самому интересному, к реализации интерфейса взаимодействия с pulseaudio.
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pulse/pulseaudio.h>
#include <pulse/glib-mainloop.h>
#include "pamon.h"
struct Pamon
{
pa_context * pulse_context;
pa_glib_mainloop * pulse_glib_mainloop;
PamonVolumeChangedCallback volume_callback;
PamonErrorCallback error_callback;
int sink_id;
void * userdata;
};
Pamon * pamonInit(PamonInitData * init_data)
{
Pamon * pamon = (Pamon *)malloc(sizeof(Pamon));
memset(pamon, 0, sizeof(Pamon));
if(init_data)
{
pamon->userdata = init_data->userdata;
pamon->volume_callback = init_data->volume_callback;
pamon->error_callback = init_data->error_callback;
pamon->sink_id = init_data->sink_id;
}
pamon->pulse_glib_mainloop = pa_glib_mainloop_new(NULL);
pa_mainloop_api * api = pa_glib_mainloop_get_api(pamon->pulse_glib_mainloop);
pamon->pulse_context = pa_context_new(api, NULL);
pa_context_set_state_callback(pamon->pulse_context,
(pa_context_notify_cb_t)pulseContextStateCallback, pamon);
pa_context_connect(pamon->pulse_context, NULL, 0, NULL);
return pamon;
}
Функция инициализации создаёт и заполняет экземпляр структкры Pamon. Затем создаётся основной цикл из дефолтного контекста GLib и, после получения API основного цикла, инициализируется контекст. pa_mainloop_api служит абстрактным интерфейсом для работы со всеми тремя типами основных циклов. Так как мы используем асинхронные API и соединение с демоном pulseaudio происходит в отдельном потоке, функция pa_context_connect может сообщить о своём результате только в функцию обратного вызова, которую мы и устанавливаем с помощью pa_context_set_state_callback. При установке колбеков мы можем передавать пользовательские данные, которые будут отданы колбеку. Здесь и далее мы будем использовать указатель на структуру Pamon, хранящую всю служебную информацию.void pulseContextStateCallback(pa_context * context, Pamon * pamon)
{
switch(pa_context_get_state(context))
{
case PA_CONTEXT_READY:
pa_context_set_subscribe_callback(context, (pa_context_subscribe_cb_t)pulseContextCallback, pamon);
pa_context_subscribe(context, PA_SUBSCRIPTION_MASK_SINK, NULL, NULL);
performSinkHandler(context, pamon);
break;
case PA_CONTEXT_FAILED:
performError(pamon, PAMON_ERROR_CONNECTION_FAILED);
break;
default:
break;
}
}
Здесь мы подписываемся на события контекста с помощью функции pa_context_subscribe, устнавив колбек вызовом pa_context_set_subscribe_callback. Так как нам интересны только события от sink`ов, то передаём маску PA_SUBSCRIPTION_MASK_SINK для фильтрации того, что нам будет приходить в колбек.Если произошла ошибка, о ней сообщаем вызовом performError, который, по сути, зовёт колбек, переданный при инициализации.
void performError(Pamon * pamon, PamonError error)
{
if(pamon->error_callback)
pamon->error_callback(error, pamon->userdata);
}
Кроме подписки на события, мы форсируем получение значения громкости для изначальной инициализации подписчика. void performSinkHandler(pa_context * context, Pamon * pamon)
{
pa_operation * operation = pa_context_get_sink_info_by_index(context, pamon->sink_id,
(pa_sink_info_cb_t)pulseSinkInfoCallback, pamon);
pa_operation_unref(operation);
}
Как видно, функция performSinkHandler выполняет только одно действие -- просит информацию о sink`е по его идентификатору вызовом функции pa_context_get_sink_info_by_index. Так как информация о ходе выполнения асинхронной операции нам не интересна, то сразу же отпускаем ссылку на экземпляр pa_operation вызвав pa_operation_unref. Итак, информация о sink`е приходит в функцию pulseSinkInfoCallback. void pulseSinkInfoCallback(pa_context * context, const pa_sink_info * info, int eol, Pamon * pamon)
{
if(eol == -1)
{
performError(pamon, PAMON_ERROR_INVALID_SINK);
}
if(info && pamon && pamon->volume_callback)
{
uint32_t volume = info->mute || !info->volume.channels ? 0 : info->volume.values[0];
uint32_t percents = (uint32_t)round((double)(volume * 100) / PA_VOLUME_NORM);
pamon->volume_callback(percents, pamon->userdata);
}
}
Я не нашёл документации о параметре eol, но методом научного тыка выяснил, что при ошибке этот флаг принимает значение -1.Структура pa_sink_info содержит всю необходимую нам информацию. Уровень громкости тут содержится не в процентах, а в довольно больших числах. Кроме того, уровень громкости можно получить для каждого канала отдельно. Поле channels содержит количество доступных каналов, а массив values, состоящий из 32-ух целочисленных значений имеет инициализированными ровно channels первых элементов. Здесь я схалтурил и взял первое попавшееся значение. Карту соответствия индексов массива с реальными каналами можно посмотреть в поле channel_map структуры pa_sink_info.
Высчитать текущий уровень громкости в процентах помогает константа PA_VOLUME_NORM, которая содержит значение громкости при 100%, 65536.
На этом наша работа с pulseaudio заканчивается. Осталось только корректно завершить работу.
void pamonClose(Pamon * pamon)
{
if(!pamon) return;
pa_context_disconnect(pamon->pulse_context);
pa_context_unref(pamon->pulse_context);
pa_glib_mainloop_free(pamon->pulse_glib_mainloop);
free(pamon);
}
Для этого отсоединяемся от демона pulseaudio c помощью вызова pa_context_disconnect, отпускаем ссылку на контекст с помощью pa_context_unref и высвобождаем память, занятую основным циклом используя pa_glib_mainloop_free. Естественно не забываем и о нашей структуре, хранящей служебную информацию. Следует обратить внимание, что для завершения работы с другими типами mainloop`ов нужно выполнять действия, соответствующие выбранному типу.За бортом остались две функции: pamonSetSinkId и pamonTranslateError. Их назначение понятно, а код прост, поэтому приведу его без комментариев.
#define ERSTR_UNKNOWN "Unknown error"
#define ERSTR_INVALID_SINK "Invalid sink"
#define ERSTR_CONNECTION_FAILED "Unable to connect to the pulseaudio daemon"
void pamonSetSinkId(Pamon * pamon, uint32_t id)
{
if(!pamon || pamon->sink_id == id)
return;
pamon->sink_id = id;
performSinkHandler(pamon->pulse_context, pamon);
}
const char * pamonTranslateError(PamonError error)
{
switch(error)
{
case PAMON_ERROR_CONNECTION_FAILED:
return ERSTR_CONNECTION_FAILED;
case PAMON_ERROR_INVALID_SINK:
return ERSTR_INVALID_SINK;
default:
return ERSTR_UNKNOWN;
}
}
lxpanel
Полученное приложение уже можно использовать, например, в conky или dzen2. Таквыглядит dzen2, запущенный командой
$ ./pamon -f " Current volume: %i%%" | dzen2 -bg "#505050" -fg white -ta left
Но наша цель -- встроить монитор в lxpanel, поэтому создаём ещё один файл, lxpamon.c, который будем компилировать в динамическую библиотеку.Первое, что нам надо сделать -- это подключить необходимые заголовочные файлы.
#include <stdlib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <lxpanel/plugin.h>
#include "pamon.h"
Файл lxpanel/plugin.h и есть тот самый файл, который не способен переварить компилятор C++.Документации по написанию плагинов для lxpanel ещё меньше, чем документации по pulseaudio, а точнее -- ровно одна страничка. Даже зачитав её до дыр, Вы не сможете написать полноценный плагин. Для того, чтобы написать свой, мне пришлось посмотреть в исходинки плагинов, поставляемых с lxpanel. Я скачал исходники из репозитория debian по этой ссылке. Файл src/plugins/dclock.c, содержащий исходники плагина с часами, оказался наиболее компактным и полезным.
Итак, для того, чтобы встроить плагин в панель, мы должны собрать библиотеку и положить её в /usr/lib/lxpanel/plugins/ (или в /usr/lib/x86_64-linux-gnu/lxpanel/plugins/ для мультиархитектурного debian`а). Есть одна особенность, имя файла не должно содержать префикс lib и иметь имя <имя_плагина>.so. В нашем случае файл будет называться lxpamon.so.
Библиотека с плагином должна экспозить экземпляр структуры PluginClass (к сожалению, ссылок мне давать не на что). Но не просто экспозить, а экспозить с определённым именем. Имя должно иметь вид <имя_плагина>_plugin_class. То есть, наш экземпляр будет иметь имя lxpamon_plugin_class.
PluginClass lxpamon_plugin_class = {
PLUGINCLASS_VERSIONING,
type: "lxpamon",
name: N_("Pulseaudio volume monitor"),
version: "1.0",
description: N_("Pulseaudio volume monitor plugin for LXPanel"),
one_per_system: FALSE,
expand_available: FALSE,
constructor: lxpamonConstructor,
destructor: lxpamonDestructor,
config: lxpamonConfigure,
save: lxpamonSaveConfig,
panel_configuration_changed: lxpamonPanelConfigurationChanged
};
Структура PluginClass описывает наш плагин. Макрос PLUGINCLASS_VERSIONING заполняет поля для определения версии структуры. Поле type должно содержать имя нашего плагина. Поля version и description вполне понятны. Флаг one_per_system позволяет ограничит количество активных экземпляров плагина одной штукой, а флаг expand_available разрешает растягивать виджет на всю свободную область панели (как tasklist или spacer). Макрос N_ объявлен в GLib и предназначен для интернационализации.Оставшиеся поля контролируют жизненный цикл плагина, это следующие процедуры обратного вызова:
- constructor -- инициализирует плагин;
- destructor -- вызывается перед выгрузкой плагина;
- config -- колбек, который будет зваться для редактирования настроек (например, при нажатии на кнопку Edit в настройках элементов панели);
- save -- зовётся для сохранения настроек в конфиг;
- panel_configuration_changed -- колбек, сообщающий, что конфигурация самой панели изменилась.
typedef struct
{
Pamon * pamon;
GtkWidget * label;
uint32_t current_volume;
int sink;
char * format;
int bold_font;
} PluginData;
И сразу же приведу код конструктора. int lxpamonConstructor(Plugin * plugin, char ** config)
{
GtkWidget * label = gtk_label_new(NULL);
plugin->pwid = gtk_event_box_new();
gtk_container_add(GTK_CONTAINER(plugin->pwid), GTK_WIDGET(label));
g_signal_connect(G_OBJECT(plugin->pwid), "button_press_event",
G_CALLBACK(plugin_button_press_event), plugin);
gtk_widget_show_all(plugin->pwid);
PluginData * plug_data = g_new0(PluginData, 1);
plug_data->label = label;
lxpamonInitConfig(config, plug_data);
plugin->priv = plug_data;
PamonInitData pamon_init_data;
memset(&pamon_init_data, 0, sizeof(PamonInitData));
pamon_init_data.volume_callback = (PamonVolumeChangedCallback)pamonVolumeChanged;
pamon_init_data.error_callback = (PamonErrorCallback)pamonError;
pamon_init_data.userdata = plugin;
pamon_init_data.sink_id = plug_data->sink;
Pamon * pamon = pamonInit(&pamon_init_data);
plug_data->pamon = pamon;
return TRUE;
}
Здесь создаются GTK+ контейнер и label, который кладётся в контейнер. Вообще говоря, Вы может создавать любые виджеты. Осонвное, что Вы должны сделать -- положить указатель на основной виджет в поле pwid структуры Plugin, которая передаётся на вход конструктору. Вообще, указатель на структуру Plugin будет передаваться во все колбеки. Эта структура содержит кое-какие полезные поля, с которыми мы познакомимся по ходу дела. Поле priv предназначено для хранения пользовательских данных, поэтому положим туда указатель на объект структуры PluginData.Вторым параметром в конструктор передаётся конфиг, который мы читаем в функции lxpamonInitConfig. Сохраняется конфиг, как уже было сказано, функцией lxpamonSaveConfig.
#define DEFAULT_FORMAT "%i%%"
#define CONFIG_FORMAT "Format"
#define CONFIG_SINK "Sink"
#define CONFIG_BOLD "BoldFont"
void lxpamonInitConfig(char ** src, PluginData * plug_data)
{
line str;
str.len = 256;
while(lxpanel_get_line(src, &str))
{
if(strcmp(CONFIG_FORMAT, str.t[0]) == 0)
plug_data->format = g_strdup(str.t[1]);
else if(strcmp(CONFIG_SINK, str.t[0]) == 0)
sscanf(str.t[1], "%i", &plug_data->sink);
else if(strcmp(CONFIG_BOLD, str.t[0]) == 0)
plug_data->bold_font = str2num(bool_pair, str.t[1], 0);
}
if(!plug_data->format)
plug_data->format = g_strdup(DEFAULT_FORMAT);
}
void lxpamonSaveConfig(Plugin * plugin, FILE * file)
{
PluginData * plug_data = (PluginData *)plugin->priv;
lxpanel_put_str(file, CONFIG_FORMAT, plug_data->format);
lxpanel_put_int(file, CONFIG_SINK, plug_data->sink);
lxpanel_put_bool(file, CONFIG_BOLD, plug_data->bold_font);
}
Для записи данных в конфиг, используются макросы lxpanel_put_str, lxpanel_put_bool и lxpanel_put_int, кторые принемают файл, имя параметра и значение. Наш конфиг будет выглядеть примерно так Plugin {
type = lxpamon
Config {
Format=%i%%
Sink=0
BoldFont=1
}
}
Конфиги лежат в ~/.config/lxpanel/default/panels/<имя_панели>. Для последовательного чтения записей из конфига следует использовать функцию lxpanel_get_line. Она записывает в свой второй параметр пару имя-значение из конфига. Как только пары кончатся, функция вернёт ноль. Функция str2num хоть и кажется, что переводит строку в число, но на деле она работает только с предопределёнными соответствиями, которые передаются в первом параметре. Например, bool_pair представлен примерно так { {"0", 0}, {"1", 1 } }.Для того, чтобы пользователь смог задать настройки, мы должны показать диалог при обработке соответствующего события.
void lxpamonConfigure(Plugin * plugin, GtkWindow * parent)
{
PluginData * data = (PluginData *)plugin->priv;
GtkWidget * dlg = create_generic_config_dlg(_(plugin->class->name), GTK_WIDGET(parent),
(GSourceFunc)lxpamonApplyConfiguration, plugin,
_("Pulseaudio sink"), &data->sink, CONF_TYPE_INT,
_("Volume display format"), &data->format, CONF_TYPE_STR,
_("Use %i to print current volume. Use %% to print the % mark."), NULL, CONF_TYPE_TRIM,
_("Bold font"), &data->bold_font, CONF_TYPE_BOOL,
NULL);
gtk_window_present(GTK_WINDOW(dlg));
}
Здесь используется функция create_generic_config_dlg, показывающая стандартный диалог с настройками.Вся проблема в том, что этой функции нет в файле lxpanel/plugin.h. Она объявлена в самой панели, но стандартные плагины используют именно её. Поэтому мы добавим себе её объявление.
extern GtkWidget * create_generic_config_dlg(const char * title, GtkWidget * parent,
GSourceFunc apply_func, Plugin * plugin, const char * name, ...);
Эта функция принимает родителя, заголовок и колбек, который будет вызван для применения настроек. В списке неопределённых параметров функция принимает тройки "имя параметра", "указатель на значение" и "тип значения". Завершается список параметров NULL`ом. Макрос _ объявлен в GLib и нужен для интернационализации. При принятии параметров мы устанавливаем новый sink для монитора и перерисовываем виджет void lxpamonApplyConfiguration(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonSetSinkId(plug_data->pamon, plug_data->sink);
lxpamonDrawVolume(plugin);
}
static void lxpamonDrawVolume(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
char * format = plug_data->format ? plug_data->format : "%%i";
char * buf = (char *)malloc(strlen(format) + 4);
if(INVALID_VOLUME == plug_data->current_volume)
strcpy(buf, "ERROR");
else
sprintf(buf, plug_data->format, plug_data->current_volume);
panel_draw_label_text(plugin->panel, plug_data->label, buf, plug_data->bold_font, TRUE);
free(buf);
}
При отрисовке мы снова используем функцию, которая не объявлена в lxpanel/plugin.h. extern void panel_draw_label_text(Panel * p, GtkWidget * label, char * text,
gboolean bold, gboolean custom_color);
Функция panel_draw_label_text рисует текст на лейбле цветом, заданным в конфигурации панели. Структура Plugin содержит поле panel, у которого в свою очередь есть поля, хранящие цвета панели, в том числе цвет шрифта. Но есть и флаг usefontcolor, который установлен в 0, а при попытке использовать значение цвета получается что-то красное. Функция panel_draw_label_text также позволяет рисовать полужирным шрифтом, чем мы тоже пользуемся.Значение INVALID_VOLUME устанавливается при обработке ошибки от pamon. В этом случае наш плагин напишет слово "ERROR".
Функция panel_draw_label_text не последняя из тех, которые забыли положить в файл lxpanel/plugin.h, ещё нам нужен стандартный обработчик клика мыши.
extern gboolean plugin_button_press_event(GtkWidget * widget, GdkEventButton * event, Plugin * plugin);
Эту функцию мы повесели на "button_press_event" в конструкторе. Эта функция добавляет в контекстное меню пункты для редактирования настроек и удаления виджета с панели.После того, как настройки панели изменены нам нужно перерисовать свой виджет. Именно для этого мы и подписывались на это событие.
void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}
Обработчики событий от pulseaudio тривиальны и не нуждаются в комментариях за одним исключением, ошибка может произойти до того, как поле plugin->priv будет присвоено, поэтому его нужно проверить.
void lxpamonPanelConfigurationChanged(Plugin * plugin)
{
lxpamonDrawVolume(plugin);
}
void pamonVolumeChanged(uint32_t current_volume, Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
plug_data->current_volume = current_volume;
lxpamonDrawVolume(plugin);
}
void pamonError(PamonError error, Plugin * plugin)
{
fprintf(stderr, "Pulseaudio monitor error: %s\n", pamonTranslateError(error));
PluginData * plug_data = (PluginData *)plugin->priv;
if(plug_data)
{
plug_data->current_volume = INVALID_VOLUME;
lxpamonDrawVolume(plugin);
}
}
Последнее, что осталось сделать -- освободить ресурсы при выгрузке плагина. Здесь это гораздо важнее, чем в консольном приложении, так как панель может продолжать работать ещё долгое время и утекшая память не вернётся системе. void lxpamonDestructor(Plugin * plugin)
{
PluginData * plug_data = (PluginData *)plugin->priv;
pamonClose(plug_data->pamon);
g_free(plug_data->format);
g_free(plug_data);
}
Поле plugin->pwid осовобождать не нужно, lxpanel это сделает за Вас.P.S.
Исходники проекта лежат на github.git clone https://github.com/brainstream/lxpamon.git
Получившийся результат можно посмотреть на видео ниже