Преобразование массива байтов во float в JavaScript

Если вам требует в JavaScript преобразовать массив из 4-х байтов в число с плавающей точкой единичной точности (float), то можно воспользоваться следующей функцией:

1
2
3
4
5
6
7
8
function bytesToFloat(bytes) {
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer, 0 , 4); // для ClearScript нужен конструктор со всеми параметрами
for (let i = 0; i < bytes.length; i += 1) {
view.setUint8(i, bytes[i]);
}
return view.getFloat32(0);
}

Обращу внимание на конструкцию вида:

1
const view = new DataView(buffer, 0 , 4);

В теории, при инициализации объекта класса DataView через конструктор должен создаваться view того же размера, что и buffer. Но на практике применения конструктора DataView в ClearScript (см. предыдущий пост с объяснениями что такое ClearScript) выяснилось, что требуется использовать перегрузку конструктора DataView с указанием всех параметров, иначе ClearScript порождает ошибку выполнения с сообщением о том, что view создаётся меньшего размера, чем требуется для buffer.

ClearScript. Добавь скрипты в своё .NET приложение

Были времена, когда обработку скриптов в .NET реализовывали проекты IronPython и IronRuby. IronRuby уже умер, IronPython ещё жив, но надо признать, что не набрал популярности несмотря на солидный возраст. Если вам требуется использовать сценарии в своём .NET приложении обратите внимание на ClearScript.

ClearScript - это библиотека, которая с лёгкостью добавит сценарии на JavaScript (через V8 и JScript) или VBScript в ваше .NET приложение. ClearScript поддерживает несколько видов движков: Google’s V8, Microsoft’s JScript и VBScript. Посредством ClearScript можно запускать JavaScript сценарии как в старом CommonJS, так и в новой ES6 стандарте, причем сценарии будут работать как в Windows, Linux, так и macOS. Количество поддержанных фишек для разных движков несколько различается, но V8 содержит их в максимальном количестве. ClearScript доступен в виде NuGet пакетов для соответствующих платформ.

Для работы с ClearScript требуется подключить следующие пространства имён:

1
2
3
using Microsoft.ClearScript;
using Microsoft.ClearScript.JavaScript;
using Microsoft.ClearScript.V8;

Создать и инициализировать движок скриптов (если не требуется возможность отладки, то V8ScriptEngineFlags.EnableDebugging убрать, оставив просто new() ):

1
2
3
4
5
6
7
8
private static V8ScriptEngine Engine => new(V8ScriptEngineFlags.EnableDebugging)
{
DocumentSettings =
{
AccessFlags = DocumentAccessFlags.EnableFileLoading,
SearchPath = Path.GetDirectoryName(typeof(MyTests).Assembly.Location)
}
};

Запуск вычислений в сценарии на JS приводится ниже:

1
2
3
4
5
var result = Engine.Evaluate(
new DocumentInfo {Category = ModuleCategory.Standard},
$"{_jsModuleContent} {PdpFunctionName}({JsonSerializer.Serialize(initialDevice)})"
);
var resultDevice = JsonSerializer.Deserialize<Device>(result.ToString());

Category = ModuleCategory.Standard означает, что текст сценария написан на ES6 (const, let, import, etc). Category = ModuleCategory.CommonJS - старый формат JS, понимающий только var и require. Из примера видно, что текстовый аргумент представляет из себя содержимое сценария и вызов JS функции с передачей ей в качестве аргумента сериализованного объекта JSON. В качестве результата возвращается экземпляр класса Object, который требуется десериализовать к нужному классу с помощью JsonSerializer.Deserialize. Таким образом, с помощью загрузки и исполнения JavaScript сценария можно динамически управлять выполняющимися инструкциями функции PdpFunctionName!

Удивительно, что можно подгружать классы .NET в движок ClearScript (в том числе даты, дженерики и LINQ) и использовать их для вычислений:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
using (var engine = new V8ScriptEngine()) // создание движка
{
engine.AddHostType("Console", typeof(Console)); // прокинуть тип в движок
engine.Execute("Console.WriteLine('{0} is an interesting number.', Math.PI)"); // 3.14159265358979 is an interesting number.

engine.AddHostObject("random", new Random()); // прокинуть объект в движок
engine.Execute("Console.WriteLine(random.NextDouble())"); // 0.715555223503874

engine.AddHostObject("lib", new HostTypeCollection("mscorlib", "System.Core")); // прокидываем целую сборку
engine.Execute("Console.WriteLine(lib.System.DateTime.Now)"); // 5/11/2017 12:15:32 PM

// создаем хост-объект прямо из скрипта
engine.Execute(@"
birthday = new lib.System.DateTime(2007, 5, 22);
Console.WriteLine(birthday.ToLongDateString());
"); // Tuesday, May 22, 2007

// используем дженерик класс словаря прямо из скрипта
engine.Execute(@"
Dictionary = lib.System.Collections.Generic.Dictionary;
dict = new Dictionary(lib.System.String, lib.System.Int32);
dict.Add('foo', 123);
");

engine.AddHostObject("host", new HostFunctions()); // вызываем хост-метод с out параметром
engine.Execute(@"
intVar = host.newVar(lib.System.Int32);
found = dict.TryGetValue('foo', intVar.out);
Console.WriteLine('{0} {1}', found, intVar);
"); // True 123

// создаем и перебираем хост массив
engine.Execute(@"
numbers = host.newArr(lib.System.Int32, 20);
for (var i = 0; i < numbers.Length; i++) { numbers[i] = i; }
Console.WriteLine(lib.System.String.Join(', ', numbers));
"); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19

// создаем в сценарии делегат
engine.Execute(@"
Filter = lib.System.Func(lib.System.Int32, lib.System.Boolean);
oddFilter = new Filter(function(value) {
return (value & 1) ? true : false;
});
");

// используем LINQ из скрипта
engine.Execute(@"
oddNumbers = numbers.Where(oddFilter);
Console.WriteLine(lib.System.String.Join(', ', oddNumbers));
"); // 1, 3, 5, 7, 9, 11, 13, 15, 17, 19

// используем динамический хост объект
engine.Execute(@"
expando = new lib.System.Dynamic.ExpandoObject();
expando.foo = 123;
expando.bar = 'qux';
delete expando.foo;
");

engine.Execute("function print(x) { Console.WriteLine(x); }"); // создаем и затем вызываем функцию из скрипта
engine.Script.print(DateTime.Now.DayOfWeek); // Thursday

engine.Execute("person = { name: 'Fred', age: 5 }"); // создадим из скрипта объект
Console.WriteLine(engine.Script.person.name); // Fred

engine.Execute("values = new Int32Array([1, 2, 3, 4, 5])"); // создадим в скрипте типизированный массив
var values = (ITypedArray<int>)engine.Script.values; // считаем и преобразуем из Object значения массива
Console.WriteLine(string.Join(", ", values.ToArray())); // 1, 2, 3, 4, 5
}

Отличный ClearScript Tutorial

Особенности потоко-безопасного использования класса HttpClient при отправке Http Headers

Разрабатывая Windows-службу, работающую с МФУ и принтерами по HTTP, я использовал класс HttpClient. Данный класс является потоко-безопасным и не требует создания многочисленных экземпляров: достаточно один раз создать HttpClient, чтобы он обсужил все требующиеся запросы (в том числе многопоточные):

1
private static readonly HttpClient Client = new HttpClient();

или если не требуется, чтобы использовались Cookies:

1
private static readonly HttpClient Client = new HttpClient(new HttpClientHandler { UseCookies = false });

Данный подход избавит от проблемной утилизации (Dispose) экземпляров HttpClient и потенциального исчерпания свободных портов в системе (port exhausting), связанного с проблемной утилизацией.

Всё бы хорошо, но мною была обнаружена проблема разделяемого общего состояния эксземпляра класса HttpClient, когда мне потребовалось кроме сообщений ещё и отправлять Http Headers с аутентификацией и служебной информацией.

Оказалось, что мною используемый подход:

1
2
3
4
5
6
7
Client.DefaultRequestHeaders.Clear(); // doesn't work for concurrency
Client.DefaultRequestHeaders.Add("Authorization", token); // doesn't work for concurrency
var response = await Client.GetAsync(requestUri);
response.EnsureSuccessStatusCode();

var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<UserProfile>(result);

некореектно работает в многопоточной среде, т.к. имеет разделяемое общее состояние хэдеров Http Headers.

Исследование проблемы натолкнуло меня на отличную статью с решением данной проблемы: Concurrency with HttpClient

Решение заключается в отказе от внутреннего разделяемого общего состояния HttpClient’а, а именно от Default Header. Следует дописать метод расширения, который делает то же самое, что и вышеобозначенный код, но без разделяемого общего состояния:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static class HttpClientExtensions
{
public static async Task<HttpResponseMessage> GetAsyncExt(
this HttpClient @this,
string url,
Action<HttpRequestHeaders> beforeRequest)
{
var request = new HttpRequestMessage(HttpMethod.Get, url);
beforeRequest(request.Headers);
return await @this.SendAsync(request);
}

public static async Task<HttpResponseMessage> PostAsyncExt(
this HttpClient @this,
string url,
HttpContent content,
Action<HttpRequestHeaders> beforeRequest)
{
var request = new HttpRequestMessage(HttpMethod.Post, url);
beforeRequest(request.Headers);
request.Content = content;
return await @this.SendAsync(request);
}
}

Теперь вызовы из кода HttpClient с передачей хэдеров Http Headers выглядят так и являются потоко-безопасными:

1
2
3
4
5
6
7
8
var response = await Client.GetAsyncExt(requestUri, h =>
{
h.Add("Authorization", token);
});
response.EnsureSuccessStatusCode();

var result = await response.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<UserProfile>(result);

Простой SNMP сервер на C#

Простой SNMP сервер непрерывно слушает порт (161 для симулятора МФУ) и проверяет присутствие пришедшей по UDP команды OID в словаре dictionary. В качестве ключа dictionary содержит OID команду, а в качестве значения - ответ МФУ на неё. Словарь dictionary формируется при инициализации приложения из appSettings.json. При наличии ключа отправляется ответ отправителю запроса.

Обратите внимание на метод string ConvertBytesToStringOid(byte[] oidBytes), преобразующий пришедшие байты OID команды в текстовое значение к которому мы привыкли (вида 1.3.6.1.2.1.43.5.1.1.17.1). Именно оно используется для поиска в словаре dictionary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public static class SnmpServer
{
public static void StartListening(Dictionary<string, string> dictionary, int port)
{
while (true)
{
var receiver = new UdpClient(port); // UdpClient для получения данных
IPEndPoint remoteIp = null; // адрес входящего подключения

try
{
while (true)
{
var data = receiver.Receive(ref remoteIp); // получаем данные
int requestIdBytesLength = data[16];
var requestIdBytes = new byte[requestIdBytesLength];
Buffer.BlockCopy(data, 17, requestIdBytes, 0, requestIdBytesLength);
int inputOidLength = data[32];
var inputOidBytes = new byte[inputOidLength];
Buffer.BlockCopy(data, 33, inputOidBytes, 0, inputOidLength);
var inputOid = ConvertBytesToStringOid(inputOidBytes);
if (dictionary.TryGetValue(inputOid, out var oidValue))
{
var sender = new UdpClient(remoteIp.Address.ToString(), remoteIp.Port);
var oidValueBytes = Encoding.ASCII.GetBytes(oidValue);
var response = GenerateOidBytesResponse(
requestIdBytes,
inputOidBytes,
oidValueBytes
);
sender.Send(response, response.Length); // отправка
sender.Close();
sender.Dispose();
}
}
}
catch (Exception)
{
receiver.Close();
}
}
}

private static byte[] GenerateOidBytesResponse(
byte[] requestIdBytes,
byte[] inputOidBytes,
byte[] oidValueBytes
)
{
var result1 = new List<byte>(oidValueBytes);
result1.Insert(0, (byte)oidValueBytes.Length);
result1.Insert(0, 0x04);
var result2 = new List<byte>(inputOidBytes);
result2.AddRange(result1);
result2.Insert(0, (byte)inputOidBytes.Length);
result2.Insert(0, 0x06);
result2.Insert(0, (byte)result2.Count);
result2.Insert(0, 0x30);
result2.Insert(0, (byte)result2.Count);
result2.Insert(0, 0x30);

Magic210(result2);
Magic210(result2);

var result3 = new List<byte>(requestIdBytes);
result3.AddRange(result2);
result3.Insert(0, (byte)requestIdBytes.Length);
result3.Insert(0, 0x02);
result3.Insert(0, (byte)result3.Count);
result3.Insert(0, 0xa2);

CommunityPublic(result3);
Magic210(result3);

result3.Insert(0, (byte)result3.Count);
result3.Insert(0, 0x30);

return result3.ToArray();
}

private static void CommunityPublic(List<byte> list)
{
list.Insert(0, 0x63);
list.Insert(0, 0x69);
list.Insert(0, 0x6c);
list.Insert(0, 0x62);
list.Insert(0, 0x75);
list.Insert(0, 0x70);
list.Insert(0, 0x06);
list.Insert(0, 0x04);
}

private static void Magic210(List<byte> list)
{
list.Insert(0, 0x00);
list.Insert(0, 0x01);
list.Insert(0, 0x02);
}

private static string ConvertBytesToStringOid(byte[] oidBytes)
{
var builder = new StringBuilder();

var result = new List<uint> { (uint)(oidBytes[0] / 40), (uint)(oidBytes[0] % 40) };

uint buffer = 0;
for (var i = 1; i < oidBytes.Length; i++)
{
if ((oidBytes[i] & 0x80) == 0)
{
result.Add(oidBytes[i] + (buffer << 7));
buffer = 0;
}
else
{
buffer <<= 7;
buffer += (uint)(oidBytes[i] & 0x7F);
}
}

for (var i = 0; i < result.Count; i++)
{
builder.Append(result[i]);
if (i != result.Count - 1) builder.Append('.');
}

return builder.ToString();
}
}

Структура SNMP ответа get-response

Давайте взглянем на структуру SNMP ответа get-response с помощью Wireshark:

Пакет начинается с байта 0x30 и содержит в нашем примере всего 59 байтов. Второй байт 0x39 показывает длину последующего массива отправляемых байтов (0x3916 = 5710). Пятый байт 0x00 показывает номер версии version-1, за которым следует указатель community и байт его длины 0x06 (6 байт). Значение community начинается в нашем случае с 8-го байта и идёт до 13 байта включительно (0x70 0x75 0x62 0x6C 0x69 0x63). Дальше следуют байты данных самого get-response: открывающий байт 0xA2, длина последующих байтов 0x2C (44 байта), открывающий байт идентификатора запроса 0x02 и длина request-id 0x04. Значение request-id начинается с 18 байта и идёт в нашем случае до 21 байта (0x0B 0x5E 0xA0 0x42), что составляет значение 190750786. Величина request-id соответствует величине request-id пришедшего в SNMP запросе get-request. Далее 24-м байтом идёт error-status, 27-м байтом идёт error-index, а 28-м байтом идёт открывающий байт 0x30 и длина последующих байтов 0x1E (30 байтов). 30-м байтом идёт открывающий байт 0x30 и длина последующих байтов 0x1С (28 байтов). Затем опять повторяется OID, пришедший в запросе get-request, а именно байт длины 0x0B (11 байтов) и их значение: 0x2B 0x06 0x01 0x02 0x01 0x2B 0x05 0x01 0x01 0x11 0x01, что составляет значение 1.3.6.1.2.1.43.5.1.1.17.1. Далее 45-м байтом идёт байт 0x04, что означает что далее последует значение в OctetString и 46-м байтом следует длина последующего значения 0x0D (13 байтов): 0x41 0x37 0x39 0x38 0x30 0x32 0x37 0x35 0x34 0x31 0x32 0x34 0x36, что является серийным номером МФУ А798027541246.

Структура SNMP запроса get-request

Давайте взглянем на структуру SNMP запроса get-request с помощью Wireshark:

Пакет начинается с байта 0x30 и содержит в нашем примере всего 46 байтов. Второй байт 0x2C показывает длину последующего массива пришедших байтов (0x2C16 = 4410). Пятый байт 0x00 показывает номер версии version-1, за которым следует указатель community и байт его длины 0x06 (6 байт). Значение community начинается в нашем случае с 8-го байта и идёт до 13 байта включительно (0x70 0x75 0x62 0x6C 0x69 0x63). Дальше следуют байты данных самого get-request: открывающий байт 0xA0, длина последующих байтов 0x1F (31 байт), открывающий байт идентификатора запроса 0x02 и длина request-id 0x04. Значение request-id начинается с 18 байта и идёт в нашем случае до 21 байта (0x0B 0x5E 0xA0 0x42), что составляет значение 190750786. Аналогичная величина request-id должна присутствовать в SNMP ответе get-response. Далее 24-м байтом идёт error-status, 27-м байтом идёт error-index, а 33-м байтом идёт длина переданного OID 0x0B (11 байтов): 0x2B 0x06 0x01 0x02 0x01 0x2B 0x05 0x01 0x01 0x11 0x01, что составляет значение 1.3.6.1.2.1.43.5.1.1.17.1. Именно на это значение OID отвечает устройство в ответ. Алгоритм того, как одиннадцать вышеперечисленных байтов превращаются в вышеуказанный OID можно найти в статье Простой SNMP сервер на C#. SNMP пакет завершается байтами 0x05 0x00.

О книге "Программист-прагматик" Дэвида Томаса и Эндрю Ханта

Традиционно книга Дэвида Томаса и Эндрю Ханта “Программист-прагматик. Ваш путь к мастерству” (David Thomas, Andrew Hunt - The Pragmatic Programmer) относится к разряду MUST HAVE/READ и не спроста. Колоссальный опыт авторов с великолепным стилем изложения материала (советы, задачи и упражнения) делают данную книгу столько важной в карьере разработчика. Не могу сказать, что прочёл книгу на одном дыхании. Некоторые темы давались труднее других и требовали времени на переваривание. Могу рекомендовать данную книгу как новичкам, так и опытным программистам - все смогут найти полезное в ней по своему уровню.

Также хотел отметить, что авторы не просто сконцентрировались на коде, но и на многих аспектах жизни разработчика, включая нравственные вопросы: кто будет пользоваться данным кодом и приближает ли ваш код наш мир к тому образу будущего, который бы вы хотели видеть? (У вас же есть такой образ, правда?)

Хотел бы вкратце резюмировать советы авторов, которые раскрываются примерами в основном содержимом книги:

  1. Заботьтесь о своем ремесле
  2. Думайте о своей работе
  3. У вас есть свобода выбора
  4. Предлагайте варианты разрешения затруднений, а не оправдания и извинения
  5. Нельзя жить с разбитыми окнами
  6. Будьте катализатором перемен
  7. Не забывайте об общей картине
  8. Включайте в требования к системе вопрос о её качестве
  9. Регулярно инвестируйте в свой багаж знаний
  10. Критически анализируйте то, что вы читаете и слышите
  11. Родной язык - это просто ещё один язык программирования
  12. Важно не только то, что вы говорите, но и как вы это говорите
  13. Создавая документацию, не фиксируйте её навечно
  14. Удачное проектное решение легче изменить, чем неудачное
  15. DRY - Don’t Repeat Yourself (не повторяйся)
  16. Упрощайте повторное использование исходного кода
  17. Исключайте взаимное влияние несвязанных компонентов системы
  18. Окончательных решений не существует
  19. Остерегайтесь увлекаться новомодными веяниями
  20. Пользуйтесь методом трассирующих пуль для отыскания целей
  21. Создавайте прототипы для обучения
  22. Программируйте близко к предметной области
  23. Выполняйте оценку, чтобы исключить неожиданности
  24. Повторно уточняйте график выполнения работ по мере написания кода
  25. Храните свои знания в виде простого текста
  26. Используйте всю мощь командных оболочек
  27. Стремитесь свободно владеть редактором
  28. Всегда пользуйтесь системой контроля версий
  29. Устраните затруднение, а не вините в нём других
  30. Не паникуй!
  31. Вылавливающий ошибку тест должен предшествовать её исправлению
  32. Внимательно читайте сообщения об ошибке, каким бы отвратительным оно ни было
  33. Системный вызов select работает нормально
  34. Не предполагайте, а доказывайте
  35. Изучите язык манипулирования текстом
  36. Написать идеальную программу нельзя
  37. Проектируйте по контракту
  38. Пользуйтесь досрочным аварийным завершением программы
  39. Пользуйтесь утверждениями, чтобы предотвратить невозможное
  40. Завершайте то, что начали
  41. Действуйте локально
  42. Всегда предпринимайте небольшие шаги
  43. Избегайте предсказания будущего
  44. Развязанный код проще изменить
  45. Указывай, а не спрашивай
  46. Не связывайте вызовы методов в цепочку
  47. Избегайте глобальных данных
  48. Если данные настолько важны, чтобы быть глобальными, заключите их в оболочку API
  49. Программирование имеет отношение к коду, а программы - к данным
  50. Не накапливайте состояние, а передавайте его по кругу
  51. Не платите налог на наследование
  52. Выражать полиморфизм предпочтительнее с помощью интерфейсов
  53. Делегируйте полномочия службам: отношение СОДЕРЖИТ превосходит отношение ЯВЛЯЕТСЯ
  54. Пользуйтесь миксинами для совместного использования функциональных возможностей
  55. Параметризуйте своё приложение, используя внешнюю конфигурацию
  56. Анализируйте последовательность выполняемых действий с целью повышения параллельности
  57. Общее состояние - неверное состояние
  58. Случайные отказы часто вызваны осложнениями, возникающими в связи с параллельностью
  59. Пользуйтесь акторами, чтобы достичь параллельности без общего состояния
  60. Пользуйтесь классными досками для координации потока выполнения
  61. Прислушивайтесь к своим внутренним чувствам
  62. Не программируйте по совпадению
  63. Оценивайте порядок производительности своих алгоритмов
  64. Проверяйте свои оценки
  65. Выполняйте рефакторинг кода как можно раньше и чаще
  66. Тестирование предназначено не для выявления программных ошибок
  67. Тест - первый пользователь вашего кода
  68. Стройте комплексно, а не сверху вниз или снизу вверх
  69. Проектируйте с учетом тестирования
  70. Тестируйте свои программы сами, иначе их будут тестировать пользователи
  71. Пользуйтесь тестами на основе свойств для проверки правильности ваших предположений
  72. Не усложняйте код и минимизируйте поверхность атак
  73. Устанавливайте обновления системы безопасности незамедлительно
  74. Именуйте правильно; а по мере надобности переименовывайте
  75. Никто точно не знает, чего он хочет
  76. Программисты помогают людям понять, чего они хотят на самом деле
  77. Требования изучаются с помощью обратной связи
  78. Работайте с пользователем, чтобы мыслить как пользователь
  79. Бизнес-правила - это метаданные
  80. Пользуйтесь словарем терминов проекта
  81. Вместо того, чтобы мыслить нешаблонно, находите шаблон
  82. Не вникайте в исходный код в одиночку
  83. Гибкость - это не существительное, а порядок выполнения действий
  84. Поддерживайте небольшие, устойчивые команды
  85. Планируйте и воплощайте в жизнь пополнение багажа знаний
  86. Организовывайте полнофункциональные команды
  87. Делайте то, что пригодно, а не то, что модно
  88. Выпускайте программное обеспечение, когда оно требуется пользователям
  89. Пользуйтесь контролем версий, чтобы проводить сборки, тесты и выпуски
  90. Текстируйте как можно раньше, чаще и автоматически
  91. Программирование нельзя считать завершенным до тех пор, пока не пройдут все тесты
  92. Пользуйтесь саботажем для проверки своих тестов
  93. Проверяйте покрытие тестами состояний, а не исходного кода
  94. Обнаруживайте ошибки единожды
  95. Не пользуйтесь ручными процедурами
  96. Доставляйте пользователям не просто код, а удовольствие
  97. Подписывайте свою работу
  98. Прежде всего, не нанесите вред
  99. Не потакайте всякой шушере
  100. Это ваша жизнь. Делитесь ею, празднуйте и стройте её. И ЖЕЛАЕМ ПОЛУЧИТЬ ОТ ЭТОГО УДОВОЛЬСТВИЕ!

Некоторые советы кажутся очевидными, некоторые - интригующе-непонятными, но будучи вырванными из контекста они мало что дают для понимания без прочтения самой книги. Прочтите - не пожалеете!

Работа с NSIS скриптами в Venis_IX

Для редактирования и компиляции NSIS скриптов есть замечательная программа Venis_IX.

Для шаблонной генерации скрипта установщика в программе присутствует Venis Install Wizard:

В основном окне можно создать / открыть скрипт (расширение .nsi), отредактировать его и откомпилировать. Результат компиляции отображается в консольном окне Compile Results. В случае успеха будет показан конечный размер установщика (Total size:) в байтах, как показано на изображении ниже:

В случае ошибки в консоли будет указана причина и строка на которой ошибка случилась.

К сожалению в программе нет привычного разработчикам дебаггера, поэтому отладка скрипта, как правило, осуществляется при его выполнении через всплывающие окна MessageBox или вывод в консоль с помощью DetailPrint.

Также в программе есть великолепная и подробная справка по языку, командам и макросам NSIS (NSIS User Manual):

 

Введение в синтаксис NSIS скриптов

Типичный скрипт NSIS состоит из:

  • Глобальных переменных (Var nameServer)

  • Стэка

  • Регистров ($0-$9,$R0-$R9)

  • Встроенных функций (StrCmp, IntCmp, IfErrors, Goto …)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    StrCmp $0 'some value' 0 +3
    MessageBox MB_OK '$$0 is some value'
    Goto done
    StrCmp $0 'some other value' 0 +3
    MessageBox MB_OK '$$0 is some other value'
    Goto done
    # else
    MessageBox MB_OK '$$0 is "$0"'
    done:
  • удобной библиотеки LogicLib, превращающей логические операции встроенных функций в некое подобие высокоуровневой библиотеки

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ${If} $0 == 'some value'
    MessageBox MB_OK '$$0 is some value'
    ${ElseIf} $0 == 'some other value'
    MessageBox MB_OK '$$0 is some other value'
    ${Else}
    MessageBox MB_OK '$$0 is "$0"'
    ${EndIf}

    ${Switch}, ${If}, ${While}, ${For} etc.
  • Комментариев ; # /**/

  • Переноса на следующую строку \

  • Объявления и использования переменных

    1
    2
    Var BLA ;Declare the variable
    StrCpy $BLA "123" ;Now you can use the variable $BLA
  • Подключения внешних скриптов

    1
    !include LogicLib.nsh
  • Определения констант

    1
    !define APPNAME "Installer“
  • Ветвления

    1
    2
    3
    4
    5
    6
    7
    8
    ${If} $Dialog == error
    Abort
    ${EndIf}

    ${IfNot} ${Silent}
    ${AndIf} ${SectionIsSelected} ${SectionAgent}
    ...code...
    ${EndIf}
  • Функций

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Function bla
    Push $R0
    Push $R1
    ...code...
    Pop $R1
    Pop $R0
    FunctionEnd

    Call bla ;call the function. Arguments pass by stack

    Function un.bla
    ...code...
    FunctionEnd
  • Макросов

    1
    2
    3
    4
    5
    6
    7
    8
    !macro LogDetailPrintMessageBox message
    !insertmacro GetTimeStampString $R9
    nsislog::log "$EXEDIR\${LOGFILE}" "$R9: ${message}"
    ${IfNot} ${Silent}
    DetailPrint "${message}"
    MessageBox MB_OK "${message}"
    ${EndIf}
    !macroend
  • Окон сообщений MessageBox

    1
    2
    3
    4
    5
    6
    7
    8
    MessageBox MB_OK "simple message box"
    MessageBox MB_YESNO "is it true?" IDYES true IDNO false
    true:
    DetailPrint "it's true!"
    Goto next
    false:
    DetailPrint "it's false"
    next:
  • Отладочных сообщений DetailPrint

    1
    DetailPrint "this message will show on the installation window"
  • Секций Sections

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    Section "-hidden section"
    SectionEnd

    Section # hidden section
    SectionEnd

    Section "!bold section"
    SectionEnd

    Section /o "optional"
    SectionEnd

    Section "install something" SEC_IDX
    SectionEnd

    InstType "full"
    InstType "minimal"

    Section "a section"
    SectionIn 1 2
    SectionEnd

    Section "another section"
    SectionIn 1
    SectionEnd

    Section "Uninstall"
    Delete $INSTDIR\Uninst.exe ; delete self (see explanation below why this works)
    Delete $INSTDIR\myApp.exe
    RMDir $INSTDIR
    DeleteRegKey HKLM SOFTWARE\myApp
    SectionEnd
  • Плагинов Plug-in DLLs

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    SimpleSC::ExistsService "$paramServiceName"
    Pop $0 ; returns an errorcode if the service doesn?t exists (<>0)/service exists (0)
    ${If} $0 == 0
    Call smartRemover
    Abort
    ${EndIf}

    ClearErrors
    nsJSON::Serialize /format /file "$INSTDIR\${appName}\appsettings.json" ;save to file
    ${If} ${Errors}
    Call smartRemover
    Abort
    ${EndIf}
  • Шаблонов современного пользовательского интерфейса Modern User Interface

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Installer pages
    MUI_PAGE_WELCOME
    MUI_PAGE_LICENSE textfile
    MUI_PAGE_COMPONENTS
    MUI_PAGE_DIRECTORY
    MUI_PAGE_STARTMENU pageid variable
    MUI_PAGE_INSTFILES
    MUI_PAGE_FINISH

    Uninstaller pages
    MUI_UNPAGE_WELCOME
    MUI_UNPAGE_CONFIRM
    MUI_UNPAGE_LICENSE textfile
    MUI_UNPAGE_COMPONENTS
    MUI_UNPAGE_DIRECTORY
    MUI_UNPAGE_INSTFILES
    MUI_UNPAGE_FINISH

О NSIS

Если вам однажды потребуется изготовить установщик для Windows, то обратите внимание на NSIS (Nullsoft Scriptable Install System). NSIS - это профессиональная система с открытым исходным кодом, компактная и гибкая. У неё есть свои минусы (низкоуровневый синтаксис языка скриптов NSIS, включающий работу с регистрами, макросы и т.п.), но плюсы, как правило, их перевешивают.

К основным плюсам можно отнести:

  • сверх компактный размер (оверхэд самого NSIS всего 34 KB)
  • совместимость с основными версиями ОС: Windows 95, Windows 98, Windows ME, Windows NT, Windows 2000, Windows XP, Windows Server 2003, Windows Vista, Windows Sever 2008, Windows 7, Windows Server 2008R2, Windows 8, Windows Server 2012, Windows 8.1, Windows Server 2012R2, Windows Server 2016 и Windows 10
  • большое количество плагинов, позволяющих реализовать почти все задумки
  • безоплатность
  • стабильность
  • многоязычность (в одном установщике до 60-ти языков)
  • создание пользовательских диалоговых окон
  • создание собственных плагинов (C, C++, Delphi)

Плагины включают в себя взаимодействие с БД, реестром, папками и файлами, переменными окружения, Windows службами, IIS, командной строкой, Powershell, Win32 API вызовы, перезагрузку системы, интернет соединения, создание ярлыков, деинсталляторов и многое многое другое.

Взаимодействие с NSIS основано на скриптах (на собственном языке программирования, базовый синтаксис которого описан в посте), которые собираются в Windows установщик. NSIS до сих пор поддерживается (последний релиз NSIS 3.06.1 от 31 июля 2020), имеет большое сообщество и массу туториалов.

Структура скрипта:

  • Атрибуты установщика (Константы: WINDIR, SYSDIR, INSTDIR, PROGRAMFILES … Атрибуты: BrandingText, Caption, Icon …)
  • Страницы (лицензия, компоненты, выбор директории, подтверждение деинсталляции …)
  • Секции (установка, деинсталляция …)
  • Функции (.onInit, un.onInit, .onGUIInit, .onInstSuccess, .onInstFailed, .onMouseOverSection …)

Подробнее с синтаксисом NSIS можно ознакомиться тут: Введение в синтаксис NSIS скриптов

Пример простого NSIS скрипта:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
!include "MUI2.nsh"
!include LogicLib.nsh

Name "SectionTest"
OutFile "SectionTest.exe"
ShowInstDetails show

!insertmacro MUI_PAGE_COMPONENTS
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_LANGUAGE "English"

Section "One" SecOne
SectionEnd

Section "Two" SecTwo
SectionEnd

Section "-OneTwo"
${If} ${SectionIsSelected} ${SecOne}
DetailPrint "Section one"
${If} ${SectionIsSelected} ${SecTwo}
DetailPrint "Section two is selected"
${EndIf}
${EndIf}

${If} ${SectionIsSelected} ${SecTwo}
DetailPrint "Section two"
${EndIf}
SectionEnd

Для работы со скриптами рекомендую пользоваться программой Venis IX. Работа с программой описана тут: Работа с NSIS скриптами в Venis_IX.

В конце хотел бы привести несколько типичных изображений работы установщика NSIS с которым каждый пользователь Windows хоть раз, да сталкивался: