GrossGeo.SDK.Stub
2.2.6
dotnet add package GrossGeo.SDK.Stub --version 2.2.6
NuGet\Install-Package GrossGeo.SDK.Stub -Version 2.2.6
<PackageReference Include="GrossGeo.SDK.Stub" Version="2.2.6" />
<PackageVersion Include="GrossGeo.SDK.Stub" Version="2.2.6" />
<PackageReference Include="GrossGeo.SDK.Stub" />
paket add GrossGeo.SDK.Stub --version 2.2.6
#r "nuget: GrossGeo.SDK.Stub, 2.2.6"
#:package GrossGeo.SDK.Stub@2.2.6
#addin nuget:?package=GrossGeo.SDK.Stub&version=2.2.6
#tool nuget:?package=GrossGeo.SDK.Stub&version=2.2.6
GrossGeo SDK Stub
Легковесный SDK (~25 KB) для лицензирования AutoCAD-плагинов через платформу GrossGeo. Поддерживает планы, features, лимиты, concurrent-сессии и офлайн-режим.
⚠️ Уведомление об устаревшей секции (2026-09-21, DOC-111, вслед за DOC-033 от 2026-05-08): раздел «Манифест продукта (
product-manifest.json)» ниже описывает legacy-формат до BC-PR2/BC-PR3 (2026-05-06). Текущая модель — два отдельных файла:
plans-manifest.json— импорт планов и фич на существующий продукт через Developer Panel.release-manifest.json— метаданные релиза, лежит внутри bundle и читается сервером при upload.
plans-manifest.jsonразрабатывается в репозитории студии и загружается через Developer Panel;release-manifest.jsonформируется и кладётся внутрь.bundleпри подготовке релиза. Оба формата — с примерами по каждому тарифному сценарию — смотрите вplans-manifest.jsonиrelease-manifest.jsonвнутри любого из samples (например,samples/TestProduct.Free/для простого случая,samples/TestProduct.Subscription/— для планов с триалом). Справочник методов и свойств ниже (API Reference) в рамках этой же правки (DOC-111) сверен с кодом и дополнен — устаревшим не является.
Требования
| Компонент | Подробности |
|---|---|
| GrossGeo User Panel | Установлен и запущен. Скачать |
| AutoCAD 2019–2024 | .NET Framework 4.8 |
| AutoCAD 2025–2026 | .NET 8.0 |
SDK общается с User Panel через Named Pipe IPC. User Panel должен быть запущен — без него SDK работает в офлайн-режиме из кэша (до 7 дней).
Установка
<PackageReference Include="GrossGeo.SDK.Stub" Version="2.2.6" />
Версия закрепляется точно, а не диапазоном.
2.*разрешается в любую версию ветки 2 — в том числе в 2.1.0, где обфускация ломала разбор типов и продукт видел лицензию какFree/Invalid. Снятие версии с витрины этого не лечит: unlist не удаляет пакет, и уже объявленный диапазон продолжает его тянуть.
Все необходимые типы (PlanTier, BillingModel, LicenseMode, LicenseCheckStatus) включены в пакет.
Поддерживаемые TFM:
net48— AutoCAD 2019–2024net8.0-windows— AutoCAD 2025–2026net10.0-windows— AutoCAD 2027
Для AutoCAD 2027 требуется сборка под
net10.0-windows;net8не поддерживается. Замер 23.09.2026 показал, что сборка подnet8физически ЗАГРУЖАЕТСЯ в AutoCAD 2027 — вывод «значит поддерживается» был неверен: по данным Autodesk сборка подnet8не совместима с AutoCAD 2027 и требует пересборки подnet10.0-windows. Дело не в хосте .NET 10 как таковом — на AutoCAD 2026.1.2 и 2025 U1.4, тоже на хосте .NET 10,net8-сборки грузятся штатно. Обещание из более раннего текста снято этим же выпуском SDK.
Сам пакет SDK собран под net48 и net8.0-windows; из проекта под net10.0-windows он
подключается штатно — отдельной сборки под .NET 10 не требуется.
Быстрый старт
1. Получите Product Key
Зарегистрируйтесь на grossgeo.ru/developer, создайте продукт и получите ключ формата GG-XXXX-XXXX-XXXX-XXXX.
2. Инициализация
using GrossGeo.SDK;
using GrossGeo.Contracts.Licensing;
public class MyPlugin : IExtensionApplication
{
private const string ProductKey = "GG-XXXX-XXXX-XXXX-XXXX";
/// <summary>
/// Доступ к лицензии через computed property.
/// ProductLicenseAccessor — лёгкий stateless-объект,
/// читает данные из внутреннего кэша SDK при каждом обращении.
/// </summary>
private static ProductLicenseAccessor License => GrossGeoLicense.ForProduct(ProductKey);
public void Initialize()
{
Task.Run(async () =>
{
var result = await GrossGeoLicense.Initialize(new LicenseOptions
{
ProductKey = ProductKey,
PluginVersion = "1.0.0"
});
if (result.IsValid)
{
// result.PlanTier — уровень плана (Free, Pro, ProPlus)
// result.BillingModel — модель оплаты (Subscription, Perpetual)
// result.ExpiresAt — дата истечения лицензии; у подписки в окне продления — конец окна, а не
// конец оплаченного периода (см. «Срок подписки в окне продления»). Право определяет result.IsValid.
}
});
}
public void Terminate()
{
GrossGeoLicense.Shutdown(ProductKey);
}
}
Почему computed property?
ProductLicenseAccessor— stateless-объект (~24 байта), который при каждом обращении читает актуальные данные лицензии из внутреннего кэша SDK. Нет необходимости сохранять его в поле — это устраняет проблемы с таймингом асинхронной инициализации в AutoCAD.
3. Защита команды
[CommandMethod("MYCOMMAND")]
public void MyCommand()
{
// Инициализация идёт в фоне (иначе загрузка AutoCAD встанет), и команду можно
// запустить раньше, чем она закончится. Дождитесь — иначе получите отказ,
// неотличимый от «лицензии нет», и покажете «купите» вместо «подождите».
var ready = GrossGeoLicense.WaitUntilReady(TimeSpan.FromSeconds(10));
if (ready.Status == LicenseCheckStatus.Unknown)
{
ShowMessage(ready.Message); // «спросить не удалось», а не «прав нет»
return;
}
License.Protect(
action: () => DoWork(),
onBlocked: reason => ShowMessage(reason?.Status == LicenseCheckStatus.NetworkError
? $"Проверить лицензию не удалось: {reason.Message}"
: "Требуется лицензия")
);
}
Дождаться инициализации — обязательная половина приёма.
Initializeв фоне — верно; спрашивать о правах, не дождавшись, — нет.WaitUntilReadyдля обычных команд,await WhenReadyAsync(...)дляasync-обработчиков. Оба по истечении срока отдаютStatus = UnknownиErrorCode = NOT_CHECKED— явное «не спрашивали», а не отказ.Забыли вызвать
Initializeвовсе (не просто не дождались, а не позвали). БезInitializeProtect,CheckиWaitUntilReadyотвечаютStatus = Unknown,ErrorCode = NOT_CHECKED— «не спрашивали», а не отказ. Явная проверка безInitialize—CheckAsyncилиRefreshAsync, статические и у аксессора — с 2.2.0 отвечаетStatus = NetworkError,ErrorCode = NOT_INITIALIZED, а неBlocked: SDK не смог спросить, а не «прав нет» (отдельного значенияUnavailableвLicenseCheckStatusнет — это тот же статус, что и у сетевого отказа, потому что предмет тот же: «не смогли проверить»). Этот ответ запоминается, и следующийProtectпередаст его вonBlocked. В обоих случаяхIsValid = falseиonBlockedвызывается — решение о вызове коллбэка смотрит наIsValid, не наStatus. Если ваш обработчик различает причины (как в примере выше), этот случай попадёт в веткуUnknownилиNetworkErrorнаравне с настоящими «не смогли проверить».
Защита кода
Три способа проверки лицензии
// 1. С fallback, знающим ПРИЧИНУ отказа
License.Protect(
action: () => DoWork(),
onBlocked: reason =>
{
// «Спросить не удалось» — не «прав нет». Панель не запущена, канал молчит, бюджет
// исчерпан: право не отозвано, звать покупать нельзя.
if (reason?.Status == LicenseCheckStatus.NetworkError)
{
ShowMessage($"Проверить лицензию не удалось: {reason.Message}");
return;
}
ShowUpgradeDialog();
}
);
// 1a. Прежняя форма — без причины. Она осталась и работает, но обработчик в ней
// НЕ МОЖЕТ отличить неподнятую панель от отсутствия лицензии и потому неизбежно
// говорит одно и то же в обоих случаях.
License.Protect(
action: () => DoWork(),
onBlocked: () => ShowUpgradeDialog()
);
// 2. С исключением LicenseException
License.ProtectOrThrow(() => DoWork());
// 3. Ручная проверка
if (License.IsValid)
{
DoWork();
}
Проверка уровня плана
// Без IsValid не сравнивайте тариф по порядку: при отсутствии вердикта PlanTier = Unknown (255),
// а 255 >= Pro истинно
if (License.IsValid && License.PlanTier != PlanTier.Unknown && License.PlanTier >= PlanTier.Pro)
{
EnableAdvancedTools();
}
if (License.BillingModel == BillingModel.Perpetual)
{
ShowPerpetualBadge();
}
Feature Guards
Фичи позволяют гибко управлять функционалом плагина через Developer Portal.
// Проверка наличия фичи
if (License.HasFeature("advanced-export"))
{
ShowExportMenu();
}
// Guard с fallback
License.RequireFeature("batch-processing",
action: () => ProcessBatch(),
onMissing: () => ShowUpgradeDialog()
);
// Guard с исключением FeatureNotAvailableException
License.RequireFeatureOrThrow("batch-processing", () => ProcessBatch());
Feature Limits
Ограничивайте количественные параметры по планам:
// Проверка лимита
var limit = License.GetFeatureLimit("batch-export", "maxPerCall");
if (limit.HasValue && objects.Count > limit.Value)
{
ShowMessage($"Лимит: {limit.Value} объектов. Выбрано: {objects.Count}");
ShowUpgradePrompt();
return;
}
// Проверка одной строкой (true = не превышен)
if (!License.CheckLimit("batch-export", "maxPerCall", objects.Count))
{
ShowUpgradePrompt("Превышен лимит объектов");
return;
}
// С исключением LimitExceededException — только статический вход (у ProductLicenseAccessor такого
// метода нет); бросает и при недействительной лицензии (Limit = 0)
GrossGeoLicense.RequireLimitOrThrow("batch-export", "maxPerCall", objects.Count);
Feature Key Material
Начиная с SDK 2.2.0 (SDK, серверная и панельная части выпущены — панели с
1.0.2637.18001, 18.09.2026). Для фичей, чья ценность в самих данных продукта (таблицы, шаблоны, коэффициенты), платформа может отдавать не просто «да/нет», а секрет, которым вы шифруете эти данные при сборке и расшифровываете на машине пользователя. Секрет заводится в Developer Portal и привязан к паре(featureCode, kid)—kidпридумываете сами, для ротации без поломки уже выпущенных релизов.
// Синхронно, из уже полученного вердикта
if (License.TryGetFeatureKey("advanced-templates", "2026-09", out byte[]? material))
{
var decrypted = DecryptTemplates(EncryptedTemplatesBytes, material);
// material — копия при каждом вызове, можно безопасно обнулить после использования
}
else
{
switch (License.GetFeatureKeyAvailability("advanced-templates", "2026-09"))
{
case FeatureKeyAvailability.PanelUnavailable:
ShowMessage("Откройте GrossGeo User Panel"); break;
case FeatureKeyAvailability.NotEntitled:
ShowUpgradePrompt(); break;
case FeatureKeyAvailability.OfflineExpired:
ShowMessage("Восстановите подключение к User Panel"); break;
// KidNotIssued — обновите продукт; Denied — текст из LastResult; Unknown — ещё проверяется
}
}
// Асинхронно — дождаться готовности продукта перед первым обращением
var lookup = await License.GetFeatureKeyAsync(
"advanced-templates", "2026-09", TimeSpan.FromSeconds(10));
if (lookup.Found)
DecryptTemplates(EncryptedTemplatesBytes, lookup.Material);
PanelUnavailable≠ «лицензии нет». SDK может подтверждать лицензию из собственного офлайн-кэша несколько дней, но материал в этом кэше никогда не хранится (панель — единственный его источник). Без хотя бы недавнего живого ответа панелиGetFeatureKeyAvailabilityвернётPanelUnavailable, даже когдаLicense.IsValid == true. Различайте эти два сообщения в UI.
Usage Tracking (v3)
Для лимитов MaxPerDay / MaxPerMonth необходим серверный подсчёт использования.
// Проверка дневного лимита перед выполнением
var dailyUsage = await License.GetCurrentUsageAsync("export-batch", "maxPerDay");
var dailyLimit = License.GetFeatureLimit("export-batch", "maxPerDay");
if (dailyLimit.HasValue && dailyUsage + objects.Count > dailyLimit.Value)
{
ShowMessage($"Дневной лимит: {dailyLimit.Value}, использовано: {dailyUsage}");
ShowUpgradePrompt();
return;
}
// Выполняем операцию
ExportObjects(objects);
// Инкремент после успешного выполнения
var result = await License.IncrementUsageAsync("export-batch", "maxPerDay", objects.Count);
if (!result.IsSuccess)
{
// Обработка ошибки (лимит превышен или проблема сети)
}
UsageResult
| Свойство | Тип | Описание |
|---|---|---|
IsSuccess |
bool |
Успешна ли операция |
CurrentUsage |
int |
Текущее значение usage |
Limit |
int? |
Значение лимита (null = безлимитно при IsSuccess или LimitExceeded; при ошибке не заполнено) |
Remaining |
int? |
Оставшийся quota |
ErrorCode |
string? |
Код ошибки |
ErrorMessage |
string? |
Сообщение об ошибке |
Concurrent Sessions
Для лицензий с плавающими слотами (LicenseMode.Concurrent):
// Получение сессии при запуске
var session = await GrossGeoLicense.AcquireSessionAsync(
clientInfo: Environment.MachineName);
if (!session.IsSuccess)
{
// IsRetrying = true — отказ временный: SDK повторит сам и поднимет SessionAcquired
ShowMessage($"Сессию не получили: {session.ErrorMessage}");
return;
}
// Heartbeat отправляется автоматически
// Подписка на потерю сессии. Событие приходит с ФОНОВОГО потока (таймер heartbeat):
// API AutoCAD (Editor, окна) — только через главный поток: Control (WinForms) или Dispatcher (WPF),
// запомненный синхронно в Initialize (см. RunOnMainThread в Samples/TestProduct.Concurrent и
// в руководстве разработчика, раздел 5, шаг 3).
// Исключение обработчика SDK пишет в журнал и AutoCAD не роняет (с 2.2.3, LGC-1350).
GrossGeoLicense.SessionExpired += (s, e) =>
RunOnMainThread(() => ShowMessage($"Сессия потеряна: {e.Message}"));
// Освобождение при завершении. Shutdown сам освобождает место Concurrent-сессии и ждёт панель
// не дольше 2 с; ReleaseSessionAsync().Wait() здесь не нужен — он держал выход AutoCAD до 20 с,
// а на .NET Framework при зависшей панели без конца (LGC-1351; с 2.2.5 обмен ограничен и там).
public void Terminate()
{
GrossGeoLicense.Shutdown();
}
Офлайн-режим (Graceful Degradation)
SDK автоматически кэширует результат лицензии (DPAPI). При недоступности User Panel:
| Ситуация | Поведение |
|---|---|
| User Panel доступен | Полная проверка → результат кэшируется |
| User Panel недоступен, кэш не истёк | Работа из подписанного офлайн-кэша, IsOfflineMode = true. Срок подписывает платформа для этой лицензии: у Concurrent офлайна нет, у подписки на пользователя — до 7 суток, у остальных — до 30, и не дальше срока самой лицензии. IsInGracePeriod при этом остаётся false |
| User Panel недоступен, кэш истёк | У продукта нет бесплатного тарифа — IsValid = false, CACHE_EXPIRED. Есть — до подписанного срока мягкой посадки IsValid = true с бесплатным тарифом, IsOfflineMode = true, ExpiresAt и DaysRemaining равны null |
if (License.IsOfflineMode)
{
// Оставшиеся дни офлайна SDK кладёт в Message
ShowWarning(License.LastResult?.Message);
}
Проверка обновлений
var update = await GrossGeoLicense.CheckForUpdatesAsync();
if (update.HasUpdate)
{
ShowMessage($"Доступна версия {update.AvailableVersion}!\n{update.Changelog}");
}
update.Completeness (UpdateCheckCompleteness, с 2.2.0) говорит, насколько полон был опрос установленных продуктов, а не только был ли он. Отсутствие обновления при Completeness != Complete и != NothingInstalled (например, Partial или AllFailed) — это «опрос не закончен», а не «обновлений нет»; показывать такое как «всё актуально» нельзя.
Deep-link: старт Trial из плагина
Для кнопки «Попробовать бесплатно» — открывает User Panel на карточке продукта последнего Initialize в процессе (по его LicenseOptions.ProductKey; при нескольких GrossGeo-продуктах в одном AutoCAD это не обязательно вызывающий продукт — варианта через ForProduct у навигации нет) и просит показать подтверждение старта trial. Лицензию не активирует сама — только открывает панель; сам trial стартует в панели, и только с подтверждением пользователя и если сервер это разрешает (CanStartTrial=true). Если User Panel не запущен — SDK пытается его запустить, так же как при проверке лицензии. Вызовы сессий, учёта использования и проверки обновлений панель не запускают.
[CommandMethod("MY_TRIAL_BUTTON")]
public async void OnTrialButtonClick()
{
await GrossGeoLicense.RequestTrialAsync();
}
Для открытия карточки продукта без trial (например по кнопке «Подробнее») — OpenProductPageAsync:
await GrossGeoLicense.OpenProductPageAsync(); // просто открыть карточку продукта
await GrossGeoLicense.OpenProductPageAsync("purchase"); // сразу на блоке покупки
Версия User Panel. Открытие карточки без диалога и секции
purchase/reviewsработают с User Panel новее 1.0.2643 (LGC-1425). На более старых панеляхOpenProductPageAsync()показывает диалог старта пробного периода, а секции игнорируются — ответ панели SDK не читает, ошибка продукту не возвращается.
Эквивалент через grossgeo:// URI-протокол (например для кнопок вне AutoCAD-контекста) описан в руководстве по deep-link'ам — оно выдаётся вместе с доступом в Developer Portal.
LicenseOptions
new LicenseOptions
{
ProductKey = "GG-XXXX-XXXX-XXXX-XXXX", // Обязательно
PluginVersion = "1.0.0", // Передаётся при проверке лицензии (диагностика) и проверке обновлений
IpcTimeoutSeconds = 10, // Таймаут IPC (по умолчанию 10)
GracePeriodDays = 7, // Срок офлайна для ответов без подписанного срока (по умолчанию 7)
CheckForUpdatesOnInit = true, // Проверка обновлений при старте
CacheDirectory = null, // Путь к кэшу (null = по умолчанию)
Logger = null // ILicenseLogger для диагностики
}
API Reference
Enums (GrossGeo.Contracts)
PlanTier
| Значение | Описание |
|---|---|
Free (0) |
Бесплатный план |
Pro (1) |
Основной коммерческий план |
ProPlus (2) |
Расширенный план |
Maintenance (10) |
MaintenanceYearlyPrice) |
Enterprise (99) |
Индивидуальный контракт (зарезервирован) |
Unknown (255) |
Ответа нет (не спрашивали, панель не ответила, отказ) — это не тариф. Сравнение порядка (>=, >) с Unknown истинно — всегда проверяйте IsValid |
BillingModel
| Значение | Описание |
|---|---|
Free (0) |
Бесплатно |
Subscription (1) |
Подписка |
Perpetual (2) |
Бессрочная лицензия |
Contract (3) |
|
Unknown (255) |
Ответа нет — это не модель оплаты |
LicenseMode
| Значение | Описание |
|---|---|
User (0) |
Привязка к пользователю (seat) |
Machine (1) |
Привязка к машине (fingerprint) |
Concurrent (2) |
Плавающие лицензии (пул сессий) |
Unknown (255) |
Ответа нет — это не режим лицензирования |
FeatureKeyAvailability
Начиная с 2.2.0 (см. Feature Key Material).
| Значение | Описание | Что сказать пользователю |
|---|---|---|
Unknown (0) |
Проверка лицензии ещё не завершилась | «лицензия проверяется» |
Available (1) |
Материал получен, срок не истёк | — |
NotEntitled (2) |
Вердикт получен, но возможности нет в плане | «не входит в ваш план» |
KidNotIssued (3) |
Возможность есть, но этот kid платформа не выдавала |
«обновите продукт» |
PanelUnavailable (4) |
User Panel не отвечает, вердикт из офлайн-запаса SDK | «откройте User Panel» |
OfflineExpired (5) |
Материал был, но истёк срок офлайн-работы без связи с панелью | «восстановите подключение» |
Denied (6) |
Лицензия окончательно отклонена | текст из LastResult/LicenseResult |
GrossGeoLicense — свойства
| Свойство | Тип | Описание |
|---|---|---|
IsInitialized |
bool |
SDK инициализирован |
HasActiveSession |
bool |
Есть concurrent-сессия |
SessionToken |
string? |
Токен активной concurrent-сессии |
SessionExpiresAt |
DateTime? |
Срок действия сессии |
ProductLicenseAccessor — свойства
Получается через GrossGeoLicense.ForProduct(productKey). Рекомендуется использовать как computed property:
private static ProductLicenseAccessor License => GrossGeoLicense.ForProduct(ProductKey);
| Свойство | Тип | Описание |
|---|---|---|
IsValid |
bool |
Лицензия валидна |
PlanTier |
PlanTier |
Уровень плана |
BillingModel |
BillingModel |
Модель оплаты |
LicenseMode |
LicenseMode |
Режим лицензирования |
ExpiresAt |
DateTime? |
Дата истечения; в окне продления подписки — конец окна, а не конец оплаченного периода (см. «Срок подписки в окне продления»); право определяет IsValid |
DaysRemaining |
int? |
Дней до ExpiresAt (окончания срока лицензии) — считается на момент проверки, в том числе без сети (с 2.2.5) |
IsInGracePeriod |
bool |
В SDK 2.2.x не выставляется (всегда false); ответ из кэша определяйте по IsOfflineMode |
IsOfflineMode |
bool |
Работа из кэша |
Features |
IReadOnlyList<string> |
Список доступных features |
FeatureLimits |
IReadOnlyDictionary<string, int>? |
Лимиты фичей |
HasActiveSession |
bool |
Есть concurrent-сессия |
SessionToken |
string? |
Токен активной concurrent-сессии этого продукта |
SessionExpiresAt |
DateTime? |
Срок действия сессии |
LastResult |
LicenseResult? |
Последний результат проверки |
DaysRemaining— дни до окончания срока лицензии, а не признак права. ПриIsValid = falseчисло может быть положительным (например, место не назначено или машина не привязана). Действует ли лицензия, решает толькоIsValid.
Срок подписки в окне продления
Подписку продлевает автосписание, и проходит оно не в ту же секунду, когда кончается оплаченный период. Чтобы
пользователь не терял право на время продления, у действующей подписки, не отменённой на конец периода (у подписки
на пул — для продуктов его текущего состава), сервер примерно за последний час периода переносит срок лицензии на
24 часа после конца периода — это окно продления. В это время ExpiresAt и DaysRemaining считают до конца окна, а не
до конца оплаченного периода. Не переносится срок, который администратор сократил раньше конца периода или продлил
дальше конца окна; срок, поставленный администратором внутри окна, сервер переставляет как обычный.
Когда продление решено:
- оплата продления прошла — вперёд, до конца нового периода;
- оплата не прошла и началась льгота по оплате — вперёд, до конца льготы;
- подписку отменили на конец периода — назад, к концу оплаченного периода;
- продлить нельзя (продукт снят с продажи, продлевать нечего) — подписка закрывается, и право кончается сразу, не дожидаясь конца окна.
Если к концу окна продление всё ещё не решено (например, платёж ждёт подтверждения 3-D Secure), право по этой подписке
заканчивается вместе с окном и может вернуться позже — когда платёж пройдёт или, если его отклонят, на время льготы
по оплате. Конец окна не окончателен: перепроверяйте IsValid.
Если у учётной записи на продукт несколько действующих лицензий со сроком (кроме базовой бесплатной — в том числе
пробная и выданная администратором), ExpiresAt и DaysRemaining показывают самый ранний из их сроков — даже если
право даёт другая из них.
Поэтому право решайте по IsValid: не сравнивайте ExpiresAt с текущим временем сами и не показывайте его как
«оплачено до».
GrossGeoLicense — методы
| Метод | Описание |
|---|---|
Initialize(LicenseOptions) |
Async-инициализация |
Initialize(string productKey) |
Упрощённая инициализация |
InitializeSync(LicenseOptions) |
Синхронная инициализация: ждёт первый вердикт не дольше 5 с, иначе NOT_CHECKED и позже LicenseRefreshed |
WaitUntilReady(TimeSpan timeout) |
Синхронно дождаться результата первой проверки (не дольше timeout) |
WhenReadyAsync(TimeSpan timeout, CancellationToken) |
Асинхронно дождаться результата первой проверки |
ForProduct(string productKey) |
Получить ProductLicenseAccessor для продукта |
Shutdown() |
Завершение работы (все продукты) |
Shutdown(string productKey) |
Завершение работы (конкретный продукт) |
AcquireSessionAsync(string?, CancellationToken) |
Получить concurrent-сессию |
ReleaseSessionAsync(CancellationToken) |
Освободить сессию — не сообщает, подтвердил ли шлюз освобождение |
ReleaseSessionWithResultAsync(CancellationToken) |
2.2.0: то же, но с признаком результата (SessionReleaseResult.IsSuccess/ErrorCode) |
SendSessionHeartbeatAsync() |
Отправить heartbeat |
SessionExpired (event) |
Событие потери concurrent-сессии |
CheckForUpdatesAsync(CancellationToken) |
Проверка обновлений |
ClearLocalCache() |
Очистить кэш |
InvalidateCacheAsync(string? productKey, CancellationToken) |
Сбросить кэш конкретного продукта (или активного, если productKey не задан) |
CheckAndUpdateCacheVersion(long serverCacheVersion) |
Сверить версию кэша с сервером; true, если кэш очищен из-за расхождения версий |
LicenseRefreshed (event) |
Событие обновления данных лицензии |
IncrementUsageAsync(string, string, int, CancellationToken) |
v3: Инкремент usage (featureCode, limitCode, count) для текущего продукта |
GetCurrentUsageAsync(string, string, CancellationToken) |
v3: Текущий usage (featureCode, limitCode) для текущего продукта; при любом отказе возвращает 0 — неотличимо от «израсходовано 0» |
RequestTrialAsync(CancellationToken) |
Открыть панель на карточке продукта и запросить старт trial (с подтверждением; лицензию не активирует сама) |
OpenProductPageAsync(string?, CancellationToken) |
Открыть панель на карточке продукта, опционально на секции (purchase, reviews) |
Check() |
Быстрая проверка (из памяти), для текущего продукта |
HasFeatureAsync(string, CancellationToken) |
Асинхронная проверка фичи, для текущего продукта |
ProductLicenseAccessor — методы
| Метод | Описание |
|---|---|
WaitUntilReady(TimeSpan timeout) |
Синхронно дождаться результата первой проверки этого продукта |
WhenReadyAsync(TimeSpan timeout, CancellationToken) |
Асинхронно дождаться результата первой проверки этого продукта |
Check() |
Быстрая проверка (из памяти) |
CheckAsync(CancellationToken) |
Полная проверка (запрос к User Panel); не чаще 10 раз в минуту на процесс, сверх этого отдаётся прошлый результат без запроса |
RefreshAsync(CancellationToken) |
Принудительное обновление |
Protect(Action, Action?) |
Защита блока с fallback |
ProtectOrThrow(Action) |
Защита с исключением |
HasFeature(string) |
Проверка фичи |
RequireFeature(string, Action, Action?) |
Фича с fallback |
RequireFeatureOrThrow(string, Action) |
Фича с исключением |
GetFeatureLimit(string, string) |
Получить лимит |
CheckLimit(string, string, int) |
Проверить лимит |
RequireLimit(string, string, int, Action, Action?) |
Лимит с fallback |
IncrementUsageAsync(string, string, int, CancellationToken) |
v3: Инкремент usage для MaxPerDay/MaxPerMonth |
GetCurrentUsageAsync(string, string, CancellationToken) |
v3: Текущий usage для MaxPerDay/MaxPerMonth |
TryGetFeatureKey(string featureCode, string kid, out byte[]? material) |
2.2.0: Ключевой материал возможности, синхронно из памяти |
GetFeatureKeyAvailability(string featureCode, string kid) |
2.2.0: Причина отсутствия материала, см. FeatureKeyAvailability |
GetFeatureKeyAsync(string featureCode, string kid, TimeSpan timeout, CancellationToken) |
2.2.0: Дожидается готовности продукта, затем отвечает как TryGetFeatureKey |
AcquireSessionAsync(string?, CancellationToken) |
Получить concurrent-сессию ЭТОГО продукта |
ReleaseSessionAsync(CancellationToken) |
Освободить сессию ЭТОГО продукта |
ReleaseSessionWithResultAsync(CancellationToken) |
2.2.0: Освободить Concurrent-сессию ЭТОГО продукта, с признаком результата |
SendSessionHeartbeatAsync(CancellationToken) |
Отправить heartbeat для сессии ЭТОГО продукта |
Guards (вспомогательные классы)
// LicenseGuard — обёртка над лицензией
LicenseGuard.Protect(() => DoWork(), () => ShowFallback());
LicenseGuard.OrThrow(() => DoWork());
// FeatureGuard — обёртка над фичами
FeatureGuard.Has("export");
FeatureGuard.Require("export", () => DoExport(), () => ShowUpgrade());
FeatureGuard.OrThrow("export", () => DoExport());
Исключения
| Исключение | Ключевые свойства | Когда выбрасывается |
|---|---|---|
LicenseException |
Status |
ProtectOrThrow при невалидной лицензии |
FeatureNotAvailableException |
FeatureCode |
FeatureGuard.OrThrow при отсутствии фичи |
LimitExceededException |
FeatureCode, LimitCode, Limit, ActualValue |
RequireLimitOrThrow при превышении лимита |
LicenseResult
| Свойство | Тип | Описание |
|---|---|---|
IsValid |
bool |
Лицензия валидна |
Status |
LicenseCheckStatus |
Статус проверки |
PlanTier |
PlanTier |
Уровень плана |
BillingModel |
BillingModel |
Модель оплаты |
LicenseMode |
LicenseMode |
Режим лицензирования |
LicenseId |
Guid? |
ID лицензии |
ExpiresAt |
DateTime? |
Дата истечения; в окне продления подписки — конец окна, а не конец оплаченного периода (см. «Срок подписки в окне продления»); право определяет IsValid |
Features |
IReadOnlyList<string> |
Доступные features |
FeatureLimits |
IReadOnlyDictionary<string, int>? |
Лимиты фичей |
Message |
string |
Сообщение для пользователя |
IsInGracePeriod |
bool |
В SDK 2.2.x не выставляется (всегда false); ответ из кэша определяйте по IsOfflineMode |
IsOfflineMode |
bool |
Офлайн-режим |
Манифест продукта (product-manifest.json)
При создании продукта на платформе GrossGeo разработчик описывает его через product-manifest.json. Этот файл определяет метаданные продукта, тарифные планы, фичи, лимиты и релизы.
Примечание:
PackageContents.xmlдля AutoCAD Autoloader генерируется платформой автоматически при установке — создавать его вручную не нужно.
Минимальный манифест (бесплатный продукт)
{
"product": {
"name": "My Plugin",
"slug": "my-plugin",
"shortDescription": "Краткое описание плагина",
"fullDescription": "Полное описание плагина для страницы в каталоге",
"licensingMode": "GrossGeo",
"trialDays": 0,
"tags": ["autocad", "tools"]
},
"plans": [
{
"code": "free",
"name": "Free",
"displayName": "Бесплатный",
"tier": "Free",
"billingModel": "Free",
"licenseMode": "Machine",
"monthlyPrice": 0,
"yearlyPrice": 0,
"oneTimePrice": null,
"currency": "RUB",
"maxSeats": 1,
"maxConcurrentSessions": null,
"trialDays": 0,
"trialBindingMode": null,
"isActive": true
}
],
"features": [
{
"code": "basic",
"name": "Базовый функционал",
"description": "Основные инструменты",
"isDefault": true
}
],
"planFeatures": {
"free": ["basic"]
},
"featureLimits": {},
"releases": [
{
"version": "1.0.0",
"channel": "Stable",
"changelog": "Первый релиз",
"distributionType": "Bundle",
"postInstallAction": "RequireRestart",
"netloadDllPath": "Contents/MyPlugin.dll",
"minAutoCADVersion": "R25.0",
"maxAutoCADVersion": "R25.1",
"targetPlatforms": ["AutoCAD", "Civil3D"],
"supportedOS": ["Win64"],
"loadOnStartup": true,
"fixtureFile": "fixtures/MyPlugin.v1.0.0.bundle.zip"
}
]
}
Структура манифеста
product — метаданные продукта
| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
name |
string |
✅ | Название продукта |
slug |
string |
✅ | URL-идентификатор (латиница, дефисы) |
shortDescription |
string |
✅ | Краткое описание (1-2 предложения) |
fullDescription |
string |
✅ | Полное описание для каталога |
licensingMode |
string |
✅ | Режим лицензирования (см. ниже) |
trialDays |
int |
✅ | Длительность trial-периода (0 = без trial) |
tags |
string[] |
— | Теги для поиска в каталоге |
externalPurchaseUrl |
string? |
— | URL покупки (для ExternalOnly) |
externalDownloadUrl |
string? |
— | URL скачивания (для ExternalOnly) |
externalLicenseInstructions |
string? |
— | Инструкция активации (для ExternalOnly) |
licensingMode:
| Значение | Описание |
|---|---|
GrossGeo |
Лицензирование через платформу (SDK + планы + фичи) |
ExternalOnly |
Каталог и аналитика через GrossGeo, лицензирование — на стороне разработчика |
plans[] — тарифные планы
| Поле | Тип | Описание |
|---|---|---|
code |
string |
Уникальный код плана (латиница, дефисы) |
name |
string |
Системное имя |
displayName |
string |
Отображаемое имя |
tier |
string |
Уровень: Free, Pro, ProPlus, Enterprise |
billingModel |
string |
Модель: Free, Subscription, Perpetual |
billingPeriod |
string? |
Deprecated в v3 — период на Order/Subscription, не на плане |
licenseMode |
string |
Режим: User, Machine, Concurrent |
monthlyPrice |
decimal? |
Цена за месяц |
yearlyPrice |
decimal? |
Цена за год |
oneTimePrice |
decimal? |
Разовая цена (Perpetual) |
currency |
string |
Валюта (RUB, USD) |
maxSeats |
int |
Количество рабочих мест |
maxConcurrentSessions |
int? |
Макс. одновременных сессий (для Concurrent) |
trialDays |
int |
Trial для этого плана (0 = без trial) |
trialBindingMode |
string? |
Привязка trial: Account, AccountAndMachine |
requiresPlanCode |
string? |
Deprecated в v3 — Maintenance через maintenanceYearlyPrice |
maintenanceYearlyPrice |
decimal? |
v3: Годовая цена Maintenance (только для Perpetual) |
description |
string? |
v3: Короткое описание плана |
highlights |
string[]? |
v3: Маркетинговые буллеты для pricing table |
isActive |
bool |
Активен ли план |
features[] — фичи продукта
| Поле | Тип | Описание |
|---|---|---|
code |
string |
Уникальный код фичи |
name |
string |
Название |
description |
string |
Описание |
isDefault |
bool |
Включена в план по умолчанию |
planFeatures — привязка фичей к планам
{
"free": ["basic-tools", "simple-export"],
"pro": ["basic-tools", "simple-export", "advanced-tools", "batch"]
}
Ключ — code плана, значение — массив code фичей.
featureLimits — количественные ограничения
Ключ формата {planCode}.{featureCode}, значение — массив лимитов или null (без ограничений):
{
"free.simple-export": [
{
"limitCode": "maxObjects",
"limitType": "MaxPerCall",
"limitValue": 5
},
{
"limitCode": "maxFileSize",
"limitType": "MaxSize",
"limitValue": 10485760
}
],
"pro.simple-export": null
}
Типы лимитов (limitType):
| Тип | Описание | Пример |
|---|---|---|
MaxPerCall |
Максимум за одну операцию | 5 объектов в экспорте |
MaxPerSession |
Максимум за сессию | 50 операций |
MaxPerDay |
Максимум за день (сброс в 00:00 UTC) | 100 экспортов/день |
MaxPerMonth |
Максимум за месяц (сброс 1-го числа) | 500 экспортов/мес |
MaxSize |
Максимальный размер (байты) | 10 MB |
releases[] — релизы продукта
| Поле | Тип | Описание |
|---|---|---|
version |
string |
Версия (SemVer) |
channel |
string |
Канал: Stable, Beta, Alpha |
changelog |
string |
Описание изменений |
distributionType |
string |
Тип: Bundle (Autoloader) или Installer (EXE) |
postInstallAction |
string |
Действие: RequireRestart, None |
netloadDllPath |
string? |
Путь к DLL внутри bundle (для Bundle) |
minAutoCADVersion |
string |
Мин. серия AutoCAD. Обязана быть достижима вашей нагрузкой: сборка под net8 не грузится в AutoCAD 2024 и старше — там .NET Framework. Для net8-продукта нижняя граница R25.0 (2025); R24.3 (2024) и ниже честны только при наличии сборки под net48 |
maxAutoCADVersion |
string? |
Макс. серия AutoCAD — потолок вашего продукта; для net8 — R25.1 (2026), для net10 — R26.0 (2027); серии вне полосы рантайма (net8 с R26.x) сервер отвергает, см. врезку ниже |
targetPlatforms |
string[] |
Платформы: AutoCAD, Civil3D, Map |
supportedOS |
string[] |
ОС: Win64 |
Про
maxAutoCADVersion: это ПОТОЛОК вашего продукта, и объявляете его вы. Значение доезжает до AutoCAD какSeriesMaxв манифесте бандла, и AutoCAD отсекает по нему строго: замер 03.09.2026 на одной машине — один бандл, различалась одна строка, сR25.0продукта в AutoCAD 2026 нет, сR25.1есть. ПоставитеR25.0— продукт не загрузится на 2026, и узнаете вы об этом не от нас.Путей до
SeriesMaxдва, и потолком ваше значение остаётся на обоих. Если вы указалиnetloadDllPathилиtargetRuntimes, панель пишет ваше значение вSeriesMaxдословно. Если не указали — она сама разбирает нагрузку по таргетам (net48→R23.0–R24.3,net8→R25.0–R25.9,net10→R26.0–R26.9) и пересекает свой диапазон с вашим, беря меньшее. Дословная запись — путь большинства: она срабатывает всякий раз, когдаnetloadDllPathзадан; объявленное вtargetRuntimesпанель при этом тоже обрезает по полосе рантайма.Верное значение зависит от рантайма вашей сборки. Для
net8—R25.1, это AutoCAD 2026: цель сборки для 2025–2026, дальше неёnet8не идёт. Для AutoCAD 2027 требуется сборка подnet10.0-windows;net8не поддерживается — она физически ЗАГРУЖАЕТСЯ в процесс 2027, но по данным Autodesk не совместима с AutoCAD 2027 — нужна пересборка подnet10.0-windows(несовместимость с самим AutoCAD 2027, не с .NET 10 как таковым: на AutoCAD 2026.1.2 и 2025 U1.4, тоже на хосте .NET 10, net8-сборки грузятся штатно). Ранняя редакция этого текста читала «загружается» как «поддерживается» и была неверна. Дляnet10—R26.0, это AutoCAD 2027. Какая серия окажется у следующей версии AutoCAD, мы не выводим формулой — пару «серия → год» назначает Autodesk (см.AutoCadSeries), поэтому и вы не угадывайте: когда она станет известна, значение обновится в этой документации.Пример релиза продукта, собранного под AutoCAD 2027 (
net10.0-windows):{ "version": "2.0.0", "distributionType": "Bundle", "netloadDllPath": "Contents/MyPlugin.dll", "minAutoCADVersion": "R26.0", "maxAutoCADVersion": "R26.0", "targetPlatforms": ["AutoCAD", "Civil3D"], "supportedOS": ["Win64"] }
| loadOnStartup | bool | Загружать при старте AutoCAD |
| fixtureFile | string | Путь к .bundle.zip файлу |
Примеры сценариев
Подписка с trial
{
"product": {
"name": "GeoExport Pro",
"slug": "geoexport-pro",
"licensingMode": "GrossGeo",
"trialDays": 14,
...
},
"plans": [
{
"code": "pro",
"tier": "Pro",
"billingModel": "Subscription",
"licenseMode": "User",
"monthlyPrice": 990,
"yearlyPrice": 9900,
"trialDays": 14,
"trialBindingMode": "Account",
"description": "Полный набор инструментов",
"highlights": ["Безлимитный экспорт", "Приоритетная поддержка"],
...
}
]
}
Perpetual + Maintenance
{
"plans": [
{
"code": "pro",
"tier": "Pro",
"billingModel": "Perpetual",
"licenseMode": "Machine",
"oneTimePrice": 9990,
"maintenanceYearlyPrice": 2990,
"description": "Бессрочная лицензия",
"highlights": ["Покупка навсегда", "Обновления с Maintenance"]
}
]
}
Concurrent (плавающие лицензии)
{
"plans": [
{
"code": "team",
"tier": "ProPlus",
"billingModel": "Subscription",
"licenseMode": "Concurrent",
"maxSeats": 10,
"maxConcurrentSessions": 10,
...
}
]
}
Freemium (Free + Pro с лимитами)
{
"plans": [
{ "code": "free", "tier": "Free", "billingModel": "Free", ... },
{ "code": "pro", "tier": "Pro", "billingModel": "Subscription", ... }
],
"planFeatures": {
"free": ["basic-tools", "export"],
"pro": ["basic-tools", "export", "advanced", "batch"]
},
"featureLimits": {
"free.export": [{ "limitCode": "maxObjects", "limitType": "MaxPerCall", "limitValue": 5 }],
"pro.export": null
}
}
Внешнее лицензирование (ExternalOnly)
{
"product": {
"name": "My External Plugin",
"licensingMode": "ExternalOnly",
"externalPurchaseUrl": "https://example.com/buy",
"externalDownloadUrl": "https://example.com/download",
"externalLicenseInstructions": "Получите ключ на сайте и введите в настройках плагина",
...
},
"plans": [],
"features": [],
"planFeatures": {},
"featureLimits": {}
}
Полные примеры
Все примеры product-manifest.json доступны в каталоге samples/:
| Пример | Сценарий |
|---|---|
TestProduct.Free |
Бесплатный продукт |
TestProduct.Licensed |
Perpetual + Maintenance |
TestProduct.Subscription |
Подписка с trial |
TestProduct.Freemium |
Free + Pro с Feature Limits |
TestProduct.Concurrent |
Плавающие лицензии (Concurrent) |
TestProduct.Analytics |
Внешнее лицензирование (ExternalOnly) |
TestProduct.Installer |
Installer-дистрибуция (MSI/EXE) |
Примеры плагинов
| Пример | Описание |
|---|---|
TestProduct.Free |
Бесплатный плагин без лицензии |
TestProduct.Licensed |
Базовое лицензирование |
TestProduct.Subscription |
Подписочная модель |
TestProduct.Freemium |
Free-план + платные фичи |
TestProduct.Concurrent |
Плавающие лицензии |
TestProduct.Analytics |
Телеметрия и аналитика |
TestProduct.Installer |
Installer-дистрибуция (MSI/EXE) |
Поддержка
- Документация: Grossgeo-Platform-SDK
- Developer Portal: grossgeo.ru/developer
- Вопросы: GitHub Issues
Лицензия
Proprietary. © 2025 GrossGeo. All rights reserved.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0-windows7.0 is compatible. net9.0-windows was computed. net10.0-windows was computed. |
| .NET Framework | net48 is compatible. net481 was computed. |
-
.NETFramework 4.8
- No dependencies.
-
net8.0-windows7.0
- System.Management (>= 8.0.0)
- System.Security.Cryptography.ProtectedData (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
v2.2.6 (recommended: SDK messages follow the AutoCAD interface language - English for any language but Russian, so do not compare Message, branch on Status and ErrorCode, and call Initialize from the AutoCAD main thread; PRODUCT_NOT_FOUND is now a final refusal - no endless background re-checks and no offline cache, a product deleted without being blocked loses its cache at once, a developer checking a product before publication gets the refusal at once; a machine-fingerprint failure now leaves its reason in the SDK journal): see CHANGELOG.md in the package.
v2.2.5 (REQUIRED — fixes the verification-key cache that the SDK wrote on AutoCAD 2019–2024 and then could not use, closes a licence served from the offline cache with the machine clock more than 2 hours behind the latest time the SDK had seen for that product on that runtime, removes the start-up delay that SDK 2.2.4 introduced on AutoCAD 2025 and later (the first licence check waited about 15 seconds and answered MACHINE_NOT_IDENTIFIED), and keeps the offline licence cache in the folder of the product that owns it; on AutoCAD 2025 and later the platform loads its SDK copy from the User Panel bundle normally first, before the products, and your product normally runs that copy, so a fix reaches participants with the platform release that carries the same SDK version): (1) The SDK now checks the machine clock before serving its own offline cache and before accepting a verdict the User Panel marks as served from its stale cache. If the clock is more than 2 hours behind the latest time the SDK has seen for the product on that runtime (.NET Framework and .NET 8 keep separate records), or has stepped back more than 2 hours in total, a licence check returns CACHE_CLOCK_MISMATCH instead of a licence from the cache. The refusal is not final and the offline cache is kept. Setting the clock right does not lift it by itself; the next server answer the SDK accepts does (an answer is not accepted while the clock is more than 5 minutes behind the server's). The message names both actions: check the date and time, or connect to the internet. The code exists since 2.1.14 but now arrives in more cases — if you branch on LicenseResult.ErrorCode, make sure it is handled. With a User Panel release that judges the clock itself, IncrementUsage and session results can also return CACHE_CLOCK_MISMATCH. (LGC-178.) (2) An older signed server refusal no longer erases a newer offline cache of the running build target. A refusal that revokes the right (LICENSE_NOT_FOUND, LICENSE_EXPIRED, LICENSE_NOT_ACTIVE, PRODUCT_BLOCKED, USER_NOT_ASSIGNED) and carries a signed envelope is judged by the SDK itself when it has a verification key: it erases the offline cache (of both build targets) only if the signature verifies, the envelope is a refusal for this product with one of these codes, and it was signed no earlier than the running target's cached answer — whether or not the panel marked it as stale; if the running target has no readable cached answer, it erases without comparing. An envelope that is incomplete or does not verify never erases. A refusal without an envelope, or with one while the SDK has no keys at all, follows the 2.2.4 rule: it erases unless the panel marked it as served from its stale cache. The logic is the same on .NET Framework and .NET 8. (LGC-1150.) (3) DaysRemaining no longer freezes offline at the moment of signing: the SDK computes it at every check from the signed expiry, rounding up like the server (previously a cache three days offline still reported 30 when 27 remained). DaysRemaining is the number of days until the licence term ends, not an entitlement flag: with IsValid = false it can be positive (for example, the seat is not assigned or the machine is not bound). Whether the licence is in effect is decided only by IsValid. (LGC-1402.) (4) .NET Framework: the SDK wrote its public-key cache in a PEM form it did not accept on load, so after a live answer on AutoCAD 2019–2024 the next start on any runtime found no usable key in the cache, and a start that fell back to the offline licence cache before any successful User Panel answer could report that the panel had not passed the verification key yet. The key cache is now written and read in one canonical PEM form; SDK 2.2.5 also accepts a key cache written by earlier versions. (LGC-1405.) (5) .NET Framework, Concurrent seats with several products in one AutoCAD: a heartbeat that a hung or slow panel accepted but did not confirm now holds the shared heartbeat timer for at most 10 seconds — as on .NET 8 — instead of up to 2 × IpcTimeoutSeconds per such session. Known limitation: with five or more simultaneous Concurrent products in one AutoCAD, when the panel is slow enough that each heartbeat exchange takes its full 10-second budget without failing, a seat can still be lost without an event. (LGC-1360.) (6) AutoCAD 2025 and later: the first licence check in a process no longer waits out the 15-second fingerprint window. In 2.2.4 the first check of every start that reached a licence request waited the whole 15 seconds and returned the temporary MACHINE_NOT_IDENTIFIED; the licence became valid about 20 seconds later, on the automatic re-ask. The cause was a stall inside the fingerprint collection: the SDK's own assemblies (System.Management, System.Security.Cryptography.ProtectedData, GrossGeo.Contracts) were first loaded on a pool thread, and on AutoCAD 2025 a thread other than the main one cannot load an assembly from outside the process's own list while the main thread is still inside a plug-in's Initialize waiting for that very work. The SDK now loads these assemblies on the calling thread when it is an STA thread, such as AutoCAD's main thread, and always looks up the cache-invalidation type there, before the background collection starts. Measured in an AutoCAD 2025 console: 2.2.4 answered in about 16 seconds with MACHINE_NOT_IDENTIFIED on 5 of 5 cold starts, this version in 1.1-1.5 seconds with none; on AutoCAD 2022 (.NET Framework) the first check took under half a second on 5 of 5 cold starts, with 2.2.4 and with this version's code alike (the change does not touch that runtime). The message of the temporary MACHINE_NOT_IDENTIFIED refusal is softer now (the licence check is still running and will repeat by itself); the code now reaches only a product built against 2.2.4 or newer, an older build gets NETWORK_ERROR (in 2.2.4 the code was not limited by the build version). (LGC-1424.) (7) The offline licence cache lives in the folder of the product that owns it. Until now the SDK created one cache per process from the CacheDirectory of the FIRST product initialised and silently ignored the CacheDirectory of every other product in the same process; on AutoCAD 2025 and later a product's cache could end up in another product's folder and, in a session where a different product initialised first, was looked for somewhere else, so an offline start did not find its signed cache. Now each product writes to its own CacheDirectory. The first time a product's cache is read in a process (and again after another product's folder becomes known), the SDK also reads the folders the process knows (other products' and the default one); if one holds a newer signed answer than the product's own folder, or the own folder has none, it moves that answer to the product's own folder and removes the other copies once the moved one reads back. A refusal that revokes the right erases the product's cache in every folder the process knows and stops the SDK from reading copies elsewhere for that product for the rest of the process. Limits: a copy written by 2.2.1-2.2.3 (a shared hash.cache file) in another product's folder is not moved; a copy in a folder the process did not know at the time of the erase survives until a session that does know it. (LGC-1423.) (8) OpenProductPageAsync() opens the product card: the product page no longer starts the trial dialog, and the purchase and reviews sections with a product key work. This needs a User Panel newer than 1.0.2643; on an older panel the trial dialog still appears and the sections are ignored, with no error returned to your product. If such a panel is running but not answering, the trial dialog does not appear either: the older panel reads product/KEY only with a product GUID. (LGC-1425.) (9) Machine fingerprint: the SDK now computes it the way the User Panel does on computers whose firmware reports a blank or padded board serial number. Where the firmware returns a motherboard serial number made of spaces only, a manufacturer placeholder (To be filled by O.E.M.) with spaces around it, or an answer made of spaces ahead of the real serial number, SDK 2.2.4 and earlier took the first non-empty answer as is (rejecting only the exact string To be filled by O.E.M.), so the SDK and the User Panel got different fingerprints on such computers. What changes there: licences in Machine mode that were refused with MACHINE_NOT_BOUND although the computer is registered and bound in the User Panel start to pass, and the offline licence cache of every product that gets the new reading becomes invalid once, on those computers only; it is restored at the first contact with the User Panel, and until then the product cannot start offline on such a computer. Nothing changes on other computers. Under AutoCAD 2019-2024 only a product rebuilt on 2.2.5 gets the new reading; under AutoCAD 2025 and later, once the platform ships SDK 2.2.5. (10) InitializeSync waits at most 5 seconds for the first licence verdict (all targets), regardless of IpcTimeoutSeconds. If no verdict arrives in time - for example, the User Panel does not answer - it returns a transient still-checking result (ErrorCode = NOT_CHECKED, Status = Unknown). The check then continues in the background; when its outcome differs from what was returned, LicenseRefreshed is raised (if the background check fails with an error, the event is not raised). Subscribe to LicenseRefreshed before calling InitializeSync. A product built against an SDK older than 2.1.14 receives the familiar transient NETWORK_ERROR instead of NOT_CHECKED. (11) Initialize no longer holds the calling thread on .NET Framework (AutoCAD 2019-2024). Before, the whole exchange with a User Panel that accepted the connection but did not answer ran on the caller's thread; AutoCAD's start-up stood for about 40-55 seconds, even for a product that did not wait for the returned task. Now only a short preparation runs on the calling thread. (12) The update check at Initialize (CheckForUpdatesOnInit) runs in the background after the licence verdict, instead of before Initialize returns. (13) On .NET Framework a dropped connection to the User Panel is now retried, as intended; before, the retry never ran. (14) Note for your logger: on .NET Framework the SDK now calls your ILicenseLogger from a background thread, including during Initialize. Do not block the main thread on SDK calls (.Wait(), .Result) while your logger marshals synchronously to the main thread (Invoke): that deadlock has no timeout. Wait with await, or marshal log calls asynchronously (BeginInvoke). On AutoCAD 2019-2024 these changes reach a product once it is rebuilt against 2.2.5; on AutoCAD 2025 and later, once the platform ships SDK 2.2.5. (15) Platform: submitting a release whose .NET 8+ main assembly cannot be read is refused (400 SDK_STUB_VERSION_UNREADABLE). On AutoCAD 2025 and later the platform loads its own copy of the SDK first, and the server reads the GrossGeo.SDK.Stub version each release needs from the assemblies of its .NET 8+ folders (assemblies whose obfuscator duplicates metadata streams or puts pointer tables into the compressed table stream - tricks AutoCAD still loads - are read the way the runtime reads them). The submission is refused only when no reference to GrossGeo.SDK.Stub could be read and, in a .NET 8+ folder that contains GrossGeo.SDK.Stub.dll, the main assembly of that folder (the dllPath of each declared runtime; if the release declares no runtimes, its netloadDllPath; if that is empty too, the one the server finds from PackageContents.xml or the folder layout) cannot be parsed at all - the file is damaged or is not a valid PE image, or its metadata is hidden by an obfuscator. The message names those files and the release keeps its status. Check the assembly, rebuild it without metadata obfuscation (leave GrossGeo.SDK.Stub.dll out of it), upload the corrected archive under the same file name (or delete the old release file first) and submit again. The refusal starts working when the first platform release that ships an SDK newer than 2.2.4 is published; before that the submission goes through. It does not apply to a folder without GrossGeo.SDK.Stub.dll, to unreadable secondary assemblies, to .NET Framework folders or folders of undetermined runtime, to installers (.msi), or to files skipped because of size or count. (16) Documentation: for a subscription that is due to renew (active, not cancelled at period end), in about the last hour of the paid period the server moves the licence expiry to 24 hours after the period end — the renewal window. ExpiresAt and DaysRemaining then count to the window end, not to the paid-until date. The expiry moves again when the renewal is decided: forward when the renewal is paid or a payment grace starts, back to the period end if the subscription is cancelled at period end. Decide the right by IsValid; do not compare ExpiresAt with the current time yourself or show it as the paid-until date. (LGC-1394.) (17) Documentation: the developer guide (section 17) now lists every ErrorCode the SDK can return - what it means, whether the SDK reads it as final, temporary or sign-in needed, what happens to the offline cache (erased, withheld, served), which result type carries it and from which SDK version. Rule for studios: branch on Status and ErrorCode, never on the text of Message; read a code your SDK version does not know as a temporary failure - the SDK does the same. (18) Documentation: do not wait for background work in a plug-in's Initialize (developer guide, section 6): on AutoCAD 2025 and later a thread other than the main one cannot load an assembly from outside the process's own list while the main thread runs your Initialize, so waiting there for a background task that touches such an assembly stalls until its own limit; your ILicenseLogger is called from a background thread, including during the synchronous initialisation. No breaking changes; no existing public signature changes, and one public constant is added (IpcErrorCodes.ClockUntrusted).
v2.2.4 (recommended if your product uses Concurrent seats, or if it targets a machine whose WMI subsystem can be slow to start, e.g. shortly after boot or inside a VM): (1) A Concurrent seat request left unfulfilled after a temporary AcquireSession refusal (panel busy, connection lost) used to stay that way until your product called Acquire again on its own — typically not until the next restart. AcquireSessionAsync/AcquireSessionForProductAsync now retry a non-authoritative refusal in the background with the same rhythm LicenseRefreshed already uses (5/15/30/60 s, then every 5 minutes) until the seat is acquired, the SDK shuts down, or the request is released. A refusal by substance (SESSION_LIMIT_REACHED, LICENSE_NOT_ACTIVE and similar) still returns immediately, unchanged. New: SessionResult.IsRetrying — true while a background retry is in progress; GrossGeoLicense.SessionAcquired event — fires once when a retry succeeds, so a product that showed "connecting…" on IsRetrying=true can clear it. (LGC-1353×LGC-1354.) (2) A machine whose WMI subsystem had not warmed up yet (cold boot, some VMs) could get its licence check answered using placeholder hardware IDs (UNKNOWN_CPU/ UNKNOWN_MB/UNKNOWN_VOL) instead of waiting — which could quietly change the machine fingerprint, and the offline-cache key derived from it, between runs of the same physical machine. WMI reads are now bounded by a 15-second window started at Initialize, before the first licence request is even sent; a machine that answers inside the window gets its real fingerprint. A machine that still has not answered when the window closes now gets a new transient result instead — ErrorCode=MACHINE_NOT_IDENTIFIED — picked up by the same retry rhythm as (1), so the check is simply asked again once WMI responds. If you branch on LicenseResult.ErrorCode, add this case; it is new, not a renaming of anything you already handle. (LGC-1356.) (3) The SDK now checks the loaded copy of GrossGeo.Contracts against the revision this SDK version was built for, at Initialize, and logs one diagnostic line — no exception, no behavior change — when an older copy from another GrossGeo product in the same AutoCAD process won a strong-name collision. Useful for diagnosing a TypeLoadException traced to a co-loaded product; see the developer guide. (LGC-1358.) (4) Gateway error code FEATURE_NOT_AVAILABLE covered three different situations under one code: not in your plan, no limit data cached right now, and a limit the studio never configured. The last two now have their own codes, FEATURE_LIMITS_UNAVAILABLE and FEATURE_LIMIT_NOT_CONFIGURED, recognized starting with this SDK version; an SDK built against an earlier version keeps receiving FEATURE_NOT_AVAILABLE for both, unchanged. No product code path in this SDK sends the two request types these codes answer today (GrossGeoLicense.GetFeatureLimit reads the local cache, not IPC) — forward groundwork, not something you will observe with the SDK as shipped. (LGC-1376.) (4b) On AutoCAD 2025+ (the platform preload from LGC-1358) every product in the process runs the code of the newest loaded SDK.Stub, no matter which version each was individually built against — so far, that meant a visible-semantics change shipped in one release (v2.2.3's ProtectOrThrow reporting "not checked yet" instead of "licence invalid" when no verdict exists yet — see v2.2.3 above) reached even a product that had never been rebuilt against 2.2.3, because the loaded SDK.Stub had no way to tell it apart from one that had. Starting with this version, the SDK determines what a product was built against from that product's own assembly reference to GrossGeo.SDK.Stub, not from the loaded copy's version — every public entry point that can initialize a product (Initialize, InitializeSync, and their overloads) captures its own caller independently, so a product not yet rebuilt against 2.2.3 keeps getting the exact pre-2.2.3 ProtectOrThrow reason, even running under a newer preloaded SDK.Stub. No product code changes are required; rebuilding against 2.2.3 or later is what switches ProtectOrThrow to the newer, more truthful reason. This mechanism is the general fix for the class of problem LGC-1355 exposed — later semantic changes will use the same gate. (LGC-1385.) No breaking changes: items (1) to (4b) above are additive (a new SessionResult property, a new event, a new ErrorCode value, a new diagnostic log line, two new wire-level codes an unmodified SDK never receives, and a version-awareness mechanism that only preserves old behavior for old builds); nothing existing is removed or renamed, and a product built against 2.2.3 keeps compiling and behaving as before without a rebuild — rebuilding is what gets you the retry and the wait window. (5) Offline cache is now kept per runtime (net48 and net8 each have their own file). Previously a product running in AutoCAD 2022 (.NET Framework) and in AutoCAD 2025 (.NET 8) on the same machine overwrote each other's offline cache and read the other's as missing, so offline for more than a day the user was refused while the signed offline window was still open. On first use the previous shared cache file is picked up once if it is in this runtime's format; the old file itself is left untouched. Resetting the licence (refresh, invalidate, clear) removes the cache for all runtimes. (LGC-1380.) (5b) An authoritative refusal about the product right itself — LICENSE_NOT_FOUND, LICENSE_EXPIRED, LICENSE_NOT_ACTIVE, PRODUCT_BLOCKED, USER_NOT_ASSIGNED — that the User Panel delivers live in response to a licence check now erases the SDK's offline licence cache for that product. Previously a revoked or blocked licence kept working from that cache — when the panel was not running, answered without network, or the check threw — until the signed offline window ended (up to 7 days for a per-user subscription, up to 30 for per-machine and perpetual; never past the signed expiry date), then on the free-tier fallback where the product offers one. The LicenseResult of the refusal itself is unchanged (same ErrorCode, Status, IsValid). Later checks no longer fall back to the erased cache: if the panel is not running and the SDK cannot start it, Initialize and later checks return Status = NetworkError with ErrorCode USER_PANEL_NOT_RUNNING; if the check throws, NetworkError with NETWORK_ERROR; if the panel runs without network, you get what the panel itself holds — its stored answer flagged IsFromStaleCache, or NetworkError with the panel's transient code. The cache is kept on temporary refusals (panel busy, timeout, no network); on refusals that are not about the right (FEATURE_NOT_AVAILABLE, SESSION_EXPIRED, INVALID_PRODUCT_KEY, FINGERPRINT_REQUIRED and, for now, MACHINE_NOT_BOUND / MACHINE_LIMIT_EXCEEDED); on refusals to a usage counter or a Concurrent seat request; and on any answer the panel serves from its own records (IsFromStaleCache = true) — including a refusal it received from the server earlier and replays offline. A refusal flagged IsFromStaleCache means the cache was kept, and a later check without the panel may return the old cached verdict. With User Panel 1.0.2633.8001 to 1.0.2638.19001, machine and fingerprint refusals arrive as LICENSE_NOT_ACTIVE and therefore do erase it. User Panel 1.0.2632.3001 and older sends every refusal the server returns inside a licence answer — revoked, suspended or blocked licence, user not assigned, machine, fingerprint — without a code; the SDK treats them as temporary and keeps (and serves) the cache, so with those panels only LICENSE_NOT_FOUND, LICENSE_EXPIRED and, when the panel has no record of its own, PRODUCT_BLOCKED erase it. No code change is required. (LGC-1150.) (6) When all SDK gateway slots of a running User Panel were busy, the SDK treated the panel as not running and started a second copy, which popped the panel window up over the user's work. The SDK now checks the panel's single-instance mutex: a running panel that does not answer — busy, still starting, or shutting down — is not launched again, and the check returns a new transient result — ErrorCode=USER_PANEL_BUSY — picked up by the same retry mechanism as (1). OpenProductPageAsync and RequestTrialAsync, which are explicit user actions, still bring the running panel's window up and now also deliver the navigation to the product page. (LGC-1381.) Items (5) to (6) change what the SDK does at run time without changing any public signature; each says so in its own text. (19) Documentation: compare the plan tier only together with IsValid. With no verdict PlanTier, BillingModel and LicenseMode are Unknown (255), so an order comparison such as PlanTier >= PlanTier.Pro is true; the README and migration guide examples now check IsValid and Unknown first. Review your own tier comparisons. IsInGracePeriod is not set by SDK 2.2.x (always false): an answer served from the offline cache is IsOfflineMode = true, the remaining offline days are in Message.
Earlier versions: v2.2.3 (REQUIRED — closes an AutoCAD crash and an AutoCAD 2019–2024 hang caused by the SDK, and a Concurrent seat being lost without any event) and older — see CHANGELOG.md in the public repository, https://github.com/2805028/Grossgeo-Platform-SDK/blob/HEAD/CHANGELOG.md