PVS-Studio: تحليل طلبات السحب في Azure DevOps باستخدام وكلاء مستضافين ذاتيًا





يكون تحليل الكود الثابت أكثر فاعلية عند إجراء تغييرات على مشروع ، نظرًا لأن إصلاح الأخطاء يكون دائمًا أكثر صعوبة في المستقبل من منع حدوثها في المراحل المبكرة. نواصل توسيع خيارات استخدام PVS-Studio في أنظمة التطوير المستمر وإظهار كيفية إعداد تحليل طلبات السحب باستخدام وكلاء مستضافين ذاتيًا في Microsoft Azure DevOps ، باستخدام مثال لعبة Minetest.



باختصار حول ما نتعامل معه



Minetest عبارة عن محرك ألعاب متعدد المنصات مفتوح المصدر يحتوي على حوالي 200000 سطر من كود C و C ++ و Lua. يسمح لك بإنشاء أوضاع لعب مختلفة في مساحة فوكسل. يدعم تعدد اللاعبين ، والكثير من تعديلات المجتمع. يتم استضافة مستودع المشروع هنا: https://github.com/minetest/minetest .



تم استخدام الأدوات التالية لإعداد بحث منتظم عن الأخطاء:



PVS-Studio هو محلل أكواد ثابت في C و C ++ و C # و Java للعثور على الأخطاء وعيوب الأمان.



Azure DevOps هو نظام أساسي قائم على السحابة يوفر القدرة على تطوير التطبيقات وتشغيلها وتخزين البيانات على الخوادم البعيدة.



يمكنك استخدام الأجهزة الظاهرية لنظامي التشغيل Windows و Linux لتنفيذ مهام التطوير في Azure. ومع ذلك ، فإن تشغيل الوكلاء على الأجهزة المحلية له عدة مزايا مهمة:



  • يمكن أن يكون لدى المضيف المحلي موارد أكثر من Azure VM ؛
  • لا "يختفي" الوكيل بعد انتهاء مهمته ؛
  • القدرة على تخصيص البيئة بشكل مباشر ، والمزيد من التحكم المرن في عمليات البناء ؛
  • تخزين الملفات الوسيطة محليًا له تأثير إيجابي على سرعة البناء ؛
  • يمكنك إكمال أكثر من 30 مهمة شهريًا مجانًا.


الاستعداد لاستخدام وكيل مستضاف ذاتيًا



تم وصف عملية البدء في Azure بالتفصيل في المقالة " PVS-Studio Goes to the Clouds: Azure DevOps " ، لذا سأنتقل مباشرة إلى إنشاء وكيل مستضاف ذاتيًا.



لكي يكون للوكلاء الحق في الاتصال بمجمعات المشاريع ، فإنهم يحتاجون إلى رمز وصول خاص. يمكنك الحصول عليه من صفحة "رموز الوصول الشخصية" ، في قائمة "إعدادات المستخدم".



image2.png


بعد النقر على "رمز مميز جديد" ، تحتاج إلى تحديد اسم وتحديد قراءة مجموعات الوكيل وإدارتها (قد تحتاج إلى توسيع القائمة الكاملة من خلال "إظهار كافة النطاقات").



image3.png


تحتاج إلى نسخ الرمز المميز ، حيث لن يعرضه Azure بعد الآن ، وسيتعين عليك إنشاء رمز جديد.



image4.png


سيتم استخدام حاوية Docker القائمة على Windows Server Core كعامل. المضيف هو كمبيوتر العمل الخاص بي على Windows 10 x64 مع Hyper-V.



أولاً ، تحتاج إلى توسيع مقدار مساحة القرص المتوفرة لحاويات Docker.



في نظام التشغيل Windows ، تحتاج إلى تعديل الملف "C: \ ProgramData \ Docker \ config \ daemon.json" على النحو التالي:



{
  "registry-mirrors": [],
  "insecure-registries": [],
  "debug": true,
  "experimental": false,
  "data-root": "d:\\docker",
  "storage-opts": [ "size=40G" ]
}


لإنشاء صورة Docker للوكلاء الذين لديهم نظام إنشاء وكل ما تحتاجه ، في دليل "D: \ docker-agent" ، أضف ملف Docker بالمحتوى التالي:



# escape=`

FROM mcr.microsoft.com/dotnet/framework/runtime

SHELL ["cmd", "/S", "/C"]

ADD https://aka.ms/vs/16/release/vs_buildtools.exe C:\vs_buildtools.exe
RUN C:\vs_buildtools.exe --quiet --wait --norestart --nocache `
  --installPath C:\BuildTools `
  --add Microsoft.VisualStudio.Workload.VCTools `
  --includeRecommended

RUN powershell.exe -Command `
  Set-ExecutionPolicy Bypass -Scope Process -Force; `
  [System.Net.ServicePointManager]::SecurityProtocol =
    [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; `
  iex ((New-Object System.Net.WebClient)
    .DownloadString('https://chocolatey.org/install.ps1')); `
  choco feature enable -n=useRememberedArgumentsForUpgrades;
  
RUN powershell.exe -Command `
  choco install -y cmake --installargs '"ADD_CMAKE_TO_PATH=System"'; `
  choco install -y git --params '"/GitOnlyOnPath /NoShellIntegration"'

RUN powershell.exe -Command `
  git clone https://github.com/microsoft/vcpkg.git; `
  .\vcpkg\bootstrap-vcpkg -disableMetrics; `
  $env:Path += '";C:\vcpkg"'; `
  [Environment]::SetEnvironmentVariable(
    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine); `
  [Environment]::SetEnvironmentVariable(
    '"VCPKG_DEFAULT_TRIPLET"', '"x64-windows"',
  [System.EnvironmentVariableTarget]::Machine)

RUN powershell.exe -Command `
  choco install -y pvs-studio; `
  $env:Path += '";C:\Program Files (x86)\PVS-Studio"'; `
  [Environment]::SetEnvironmentVariable(
    '"Path"', $env:Path, [System.EnvironmentVariableTarget]::Machine)

RUN powershell.exe -Command `
  $latest_agent =
    Invoke-RestMethod -Uri "https://api.github.com/repos/Microsoft/
                          azure-pipelines-agent/releases/latest"; `
  $latest_agent_version =
    $latest_agent.name.Substring(1, $latest_agent.tag_name.Length-1); `
  $latest_agent_url =
    '"https://vstsagentpackage.azureedge.net/agent/"' + $latest_agent_version +
  '"/vsts-agent-win-x64-"' + $latest_agent_version + '".zip"'; `
  Invoke-WebRequest -Uri $latest_agent_url -Method Get -OutFile ./agent.zip; `
  Expand-Archive -Path ./agent.zip -DestinationPath ./agent

USER ContainerAdministrator
RUN reg add hklm\system\currentcontrolset\services\cexecsvc
        /v ProcessShutdownTimeoutSeconds /t REG_DWORD /d 60  
RUN reg add hklm\system\currentcontrolset\control
        /v WaitToKillServiceTimeout /t REG_SZ /d 60000 /f

ADD .\entrypoint.ps1 C:\entrypoint.ps1
SHELL ["powershell", "-Command",
       "$ErrorActionPreference = 'Stop';
     $ProgressPreference = 'SilentlyContinue';"]
ENTRYPOINT .\entrypoint.ps1


ستكون النتيجة نظام بناء يعتمد على MSBuild لـ C ++ ، مع Chocolatey لتثبيت PVS-Studio و CMake و Git. للإدارة المريحة للمكتبات التي يعتمد عليها المشروع ، تم إنشاء Vcpkg. وكذلك يتم تنزيل أحدث إصدار من Azure Pipelines Agent.



لتهيئة الوكيل من ملف ENTRYPOINT Docker ، يتم استدعاء البرنامج النصي PowerShell 'entrypoint.ps1' ، والذي تحتاج إلى إضافة عنوان URL الخاص بـ "المؤسسة" للمشروع ، والرمز المميز لتجمع الوكيل ، ومعلمات ترخيص PVS-Studio:



$organization_url = "https://dev.azure.com/< Microsoft Azure>"
$agents_token = "<token >"

$pvs_studio_user = "<  PVS-Studio>"
$pvs_studio_key = "< PVS-Studio>"

try
{
  C:\BuildTools\VC\Auxiliary\Build\vcvars64.bat

  PVS-Studio_Cmd credentials -u $pvs_studio_user -n $pvs_studio_key
  
  .\agent\config.cmd --unattended `
    --url $organization_url `
    --auth PAT `
    --token $agents_token `
    --replace;
  .\agent\run.cmd
} 
finally
{
  # Agent graceful shutdown
  # https://github.com/moby/moby/issues/25982
  
  .\agent\config.cmd remove --unattended `
    --auth PAT `
    --token $agents_token
}


أوامر لبناء الصورة وبدء الوكيل:



docker build -t azure-agent -m 4GB .
docker run -id --name my-agent -m 4GB --cpu-count 4 azure-agent


image5.png


الوكيل قيد التشغيل وجاهز لأداء المهام.



image6.png


تشغيل التحليل على وكيل ذاتي الاستضافة



لتحليل العلاقات العامة ، يتم إنشاء خط أنابيب جديد بالبرنامج النصي التالي:



image7.png


trigger: none

pr:
  branches:
    include:
    - '*'

pool: Default

steps:
- script: git diff --name-only
    origin/%SYSTEM_PULLREQUEST_TARGETBRANCH% >
    diff-files.txt
  displayName: 'Get committed files'

- script: |
    cd C:\vcpkg
    git pull --rebase origin
    CMD /C ".\bootstrap-vcpkg -disableMetrics"
    vcpkg install ^
    irrlicht zlib curl[winssl] openal-soft libvorbis ^
    libogg sqlite3 freetype luajit
    vcpkg upgrade --no-dry-run
  displayName: 'Manage dependencies (Vcpkg)'

- task: CMake@1
  inputs:
    cmakeArgs: -A x64
      -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake
      -DCMAKE_BUILD_TYPE=Release -DENABLE_GETTEXT=0 -DENABLE_CURSES=0 ..
  displayName: 'Run CMake'

- task: MSBuild@1
  inputs:
    solution: '**/*.sln'
    msbuildArchitecture: 'x64'
    platform: 'x64'
    configuration: 'Release'
    maximumCpuCount: true
  displayName: 'Build'

- script: |
    IF EXIST .\PVSTestResults RMDIR /Q/S .\PVSTestResults
    md .\PVSTestResults
    PVS-Studio_Cmd ^
    -t .\build\minetest.sln ^
    -S minetest ^
    -o .\PVSTestResults\minetest.plog ^
    -c Release ^
    -p x64 ^
    -f diff-files.txt ^
    -D C:\caches
    PlogConverter ^
    -t FullHtml ^
    -o .\PVSTestResults\ ^
    -a GA:1,2,3;64:1,2,3;OP:1,2,3 ^
    .\PVSTestResults\minetest.plog
    IF NOT EXIST "$(Build.ArtifactStagingDirectory)" ^
    MKDIR "$(Build.ArtifactStagingDirectory)"
    powershell -Command ^
    "Compress-Archive -Force ^
    '.\PVSTestResults\fullhtml' ^
    '$(Build.ArtifactStagingDirectory)\fullhtml.zip'"
  displayName: 'PVS-Studio analyze'
  continueOnError: true

- task: PublishBuildArtifacts@1
  inputs:
    PathtoPublish: '$(Build.ArtifactStagingDirectory)'
    ArtifactName: 'psv-studio-analisys'
    publishLocation: 'Container'
  displayName: 'Publish analysis report'


سيتم تشغيل هذا البرنامج النصي عند استلام العلاقات العامة وسيتم تنفيذه على الوكلاء المعينين إلى التجمع الافتراضي. ما عليك سوى منحه إذنًا للعمل مع هذا المجمع.



image8.png




image9.png


يحفظ البرنامج النصي قائمة الملفات المتغيرة التي تم الحصول عليها باستخدام git diff. ثم يتم تحديث التبعيات ، ويتم إنشاء حل المشروع من خلال CMake ، ويتم بناؤه.



إذا كان البناء ناجحًا ، فسيبدأ تحليل الملفات التي تم تغييرها (علامة '-f diff-files.txt') ، متجاهلاً المشاريع الإضافية التي أنشأها CMake (حدد المشروع المطلوب فقط مع علامة "-S minetest"). لتسريع البحث عن الروابط بين ملفات الرأس وملفات المصدر C ++ ، يتم إنشاء ذاكرة تخزين مؤقت خاصة ، والتي سيتم تخزينها في دليل منفصل (علامة '-DC: \ caches').



وبالتالي ، يمكننا الآن تلقي تقارير حول تحليل التغييرات في المشروع.



image10.png




image11.png


كما قيل في بداية المقال ، فإن المكافأة السارة لاستخدام الوكلاء المستضافين ذاتيًا هي تسريع ملحوظ في تنفيذ المهام بسبب التخزين المحلي للملفات الوسيطة.



image13.png


تم العثور على بعض الأخطاء في Minetest



الكتابة فوق النتيجة



V519 يتم تعيين قيم لمتغير "color_name" مرتين على التوالي. ربما هذا خطأ. تحقق من الأسطر: 621 ، 627. string.cpp 627



static bool parseNamedColorString(const std::string &value,
                                  video::SColor &color)
{
  std::string color_name;
  std::string alpha_string;

  size_t alpha_pos = value.find('#');
  if (alpha_pos != std::string::npos) {
    color_name = value.substr(0, alpha_pos);
    alpha_string = value.substr(alpha_pos + 1);
  } else {
    color_name = value;
  }

  color_name = lowercase(value); // <=

  std::map<const std::string, unsigned>::const_iterator it;
  it = named_colors.colors.find(color_name);
  if (it == named_colors.colors.end())
    return false;
  ....
}


يجب أن تقوم هذه الوظيفة بتحليل اسم لون مع معلمة شفافية (على سبيل المثال ، Green # 77 ) وإرجاع رمزه. اعتمادًا على نتيجة التحقق من الشرط ، يتم تمرير نتيجة تقسيم الخط أو نسخة من وسيطة الوظيفة إلى متغير color_name . ومع ذلك ، فلن يتم تحويل السلسلة الناتجة نفسها إلى أحرف صغيرة ، ولكن الوسيطة الأصلية. نتيجة لذلك ، لا يمكن العثور عليها في قاموس الألوان إذا كانت معلمة الشفافية موجودة. يمكننا إصلاح هذا الخط مثل هذا:



color_name = lowercase(color_name);


يتحقق شرط



غير ضروري من V547 أن التعبير "الأقرب_emergefull_d == -1" دائمًا صحيح. زبونيفاس. cpp 363



void RemoteClient::GetNextBlocks (....)
{
  ....
  s32 nearest_emergefull_d = -1;
  ....
  s16 d;
  for (d = d_start; d <= d_max; d++) {
    ....
      if (block == NULL || surely_not_found_on_disk || block_is_invalid) {
        if (emerge->enqueueBlockEmerge(peer_id, p, generate)) {
          if (nearest_emerged_d == -1)
            nearest_emerged_d = d;
        } else {
          if (nearest_emergefull_d == -1) // <=
            nearest_emergefull_d = d;
          goto queue_full_break;
        }
  ....
  }
  ....
queue_full_break:
  if (nearest_emerged_d != -1) { // <=
    new_nearest_unsent_d = nearest_emerged_d;
  } else ....
}


لا يتغير متغير الأقرب_emergefull_d أثناء عملية الحلقة ، ولا يؤثر فحصه على تنفيذ الخوارزمية. إما أن هذا ناتج عن لصق نسخ غير دقيق ، أو أنهم نسوا إجراء بعض العمليات الحسابية به.



V560 جزء من التعبير الشرطي خطأ دائمًا: y> max_spawn_y. 262.cpp



int MapgenV7::getSpawnLevelAtPoint(v2s16 p)
{
  ....
  while (iters > 0 && y <= max_spawn_y) {               // <=
    if (!getMountainTerrainAtPoint(p.X, y + 1, p.Y)) {
      if (y <= water_level || y > max_spawn_y)          // <=
        return MAX_MAP_GENERATION_LIMIT; // Unsuitable spawn point

      // y + 1 due to biome 'dust'
      return y + 1;
    }
  ....
}


يتم فحص قيمة المتغير " y " قبل التكرار التالي للحلقة. ستعود المقارنة اللاحقة والعكسية دائمًا إلى خطأ ولا تؤثر بشكل عام على نتيجة اختبار الحالة.



تحقق فقدان المؤشر



V595 من مؤشر "m_client" تم استخدامه من قبل تم التحقق من IT WAS مقابل nullptr. تحقق من الخطوط: 183 ، 187. game.cpp 183



void gotText(const StringMap &fields)
{
  ....
  if (m_formname == "MT_DEATH_SCREEN") {
    assert(m_client != 0);
    m_client->sendRespawn();
    return;
  }

  if (m_client && m_client->modsLoaded())
    m_client->getScript()->on_formspec_input(m_formname, fields);
}


قبل الوصول إلى مؤشر m_client ، يتم فحصه إذا لم يكن فارغًا باستخدام ماكرو التأكيد . لكن هذا ينطبق فقط على بناء التصحيح. يتم استبدال هذا الاحتياط ، عند البناء في الإصدار ، بدمية ، وهناك خطر إلغاء الإشارة إلى مؤشر فارغ.



بت أم لا قليلا؟



V616 يتم استخدام الثابت "(FT_RENDER_MODE_NORMAL)" المسمى بالقيمة 0 في عملية البت. 360 مشروع صناعة الحديد



typedef enum  FT_Render_Mode_
{
  FT_RENDER_MODE_NORMAL = 0,
  FT_RENDER_MODE_LIGHT,
  FT_RENDER_MODE_MONO,
  FT_RENDER_MODE_LCD,
  FT_RENDER_MODE_LCD_V,

  FT_RENDER_MODE_MAX
} FT_Render_Mode;

#define FT_LOAD_TARGET_( x )   ( (FT_Int32)( (x) & 15 ) << 16 )
#define FT_LOAD_TARGET_NORMAL  FT_LOAD_TARGET_( FT_RENDER_MODE_NORMAL )

void update_load_flags()
{
  // Set up our loading flags.
  load_flags = FT_LOAD_DEFAULT | FT_LOAD_RENDER;
  if (!useHinting()) load_flags |= FT_LOAD_NO_HINTING;
  if (!useAutoHinting()) load_flags |= FT_LOAD_NO_AUTOHINT;
  if (useMonochrome()) load_flags |= 
    FT_LOAD_MONOCHROME | FT_LOAD_TARGET_MONO | FT_RENDER_MODE_MONO;
  else load_flags |= FT_LOAD_TARGET_NORMAL; // <=
}


يتم توسيع الماكرو FT_LOAD_TARGET_NORMAL إلى الصفر ، و "أو" لن يقوم بتعيين أي إشارات في load_flags ، ويمكن إزالة فرع else .



تقريب القسمة الصحيحة



V636 تم تحويل التعبير "rect.getHeight () / 16" ضمنيًا من النوع "int" إلى النوع "float". ضع في اعتبارك استخدام نوع صريح من النوع المصبوب لتجنب فقد جزء كسري. مثال: double A = (double) (X) / Y ؛. hud.cpp 771



void drawItemStack(....)
{
  float barheight = rect.getHeight() / 16;
  float barpad_x = rect.getWidth() / 16;
  float barpad_y = rect.getHeight() / 16;

  core::rect<s32> progressrect(
    rect.UpperLeftCorner.X + barpad_x,
    rect.LowerRightCorner.Y - barpad_y - barheight,
    rect.LowerRightCorner.X - barpad_x,
    rect.LowerRightCorner.Y - barpad_y);
}


ترجع الأحرف المستقيمة قيمًا صحيحة. تتم كتابة نتيجة قسمة الأعداد الصحيحة إلى متغير النقطة العائمة ، ويتم فقد الجزء الكسري. يبدو أن هناك أنواع بيانات غير متطابقة في هذه الحسابات.



عبارات



تفريع التسلسل المشبوه V646 ضع في الاعتبار فحص منطق التطبيق. من المحتمل أن تكون الكلمة الرئيسية "else" مفقودة. تريجن. cpp 413



treegen::error make_ltree(...., TreeDef tree_definition)
{
  ....
  std::stack <core::matrix4> stack_orientation;
  ....
    if ((stack_orientation.empty() &&
      tree_definition.trunk_type == "double") ||
      (!stack_orientation.empty() &&
      tree_definition.trunk_type == "double" &&
      !tree_definition.thin_branches)) {
      ....
    } else if ((stack_orientation.empty() &&
      tree_definition.trunk_type == "crossed") ||
      (!stack_orientation.empty() &&
      tree_definition.trunk_type == "crossed" &&
      !tree_definition.thin_branches)) {
      ....
    } if (!stack_orientation.empty()) {                  // <=
  ....
  }
  ....
}


فيما يلي تسلسلات else-if في خوارزمية توليد الشجرة. في الوسط، وبعد ذلك إذا كتلة على نفس الخط مع إغلاق قوسين من سابقة آخر . ربما تعمل الشفرة بشكل صحيح: قبل هذا إذا -a ، يتم إنشاء كتل الجذع ، ثم الأوراق ؛ أو ربما فاتهم الآخر . بالتأكيد لا يمكن قول هذا إلا من قبل المطور.



فحص تخصيص الذاكرة غير الصحيح



V668 لا معنى لاختبار مؤشر "السحب" مقابل القيمة الفارغة ، حيث تم تخصيص الذاكرة باستخدام عامل التشغيل "الجديد". سيتم إنشاء الاستثناء في حالة حدوث خطأ في تخصيص الذاكرة. اللعبة. cpp 1367



bool Game::createClient(....)
{
  if (m_cache_enable_clouds) {
    clouds = new Clouds(smgr, -1, time(0));
    if (!clouds) {
      *error_message = "Memory allocation error (clouds)";
      errorstream << *error_message << std::endl;
      return false;
    }
  }
}


في حالة جديدة فشل لإنشاء كائن، و الأمراض المنقولة جنسيا :: bad_alloc سيتم طرح استثناء ، ويجب أن يتم التعامل معها من قبل محاولة اللحاق كتلة. والتحقق من هذا النموذج غير مجدي.



قراءة مصفوفة خارج



الحدود V781 يتم فحص قيمة الفهرس "i" بعد استخدامها. ربما هناك خطأ في منطق البرنامج. 572



bool equalsn(const string<T,TAlloc>& other, u32 n) const
{
  u32 i;
  for(i=0; array[i] && other[i] && i < n; ++i) // <=
    if (array[i] != other[i])
      return false;

  // if one (or both) of the strings was smaller then they
  // are only equal if they have the same length
  return (i == n) || (used == other.used);
}


يتم الوصول إلى عناصر المصفوفة قبل فحص الفهرس ، مما قد يؤدي إلى حدوث خطأ. قد يكون من المفيد إعادة كتابة الحلقة على النحو التالي:



for (i=0; i < n; ++i) // <=
  if (!array[i] || !other[i] || array[i] != other[i])
    return false;


أخطاء أخرى تتعلق



هذه المقالة بتحليل طلبات السحب في Azure DevOps ولا تهدف إلى تقديم نظرة عامة مفصلة عن الأخطاء في مشروع Minetest. فيما يلي بعض مقتطفات التعليمات البرمجية التي وجدتها ممتعة. نقترح ألا يتبع مؤلفو المشروع هذه المقالة لإصلاح الأخطاء وإجراء تحليل أكثر شمولاً للتحذيرات التي سيصدرها PVS-Studio.



خاتمة



بفضل التكوين المرن في وضع سطر الأوامر ، يمكن تضمين تحليل PVS-Studio في مجموعة متنوعة من سيناريوهات CI / CD. والاستخدام الصحيح للموارد المتاحة يؤتي ثماره في زيادة الإنتاجية.



تجدر الإشارة إلى أن وضع فحص طلب السحب متاح فقط في إصدار Enterprise من المحلل. للحصول على ترخيص Enterprise التجريبي ، يرجى الإشارة إليه في التعليقات عند طلب ترخيص على صفحة التنزيل . يمكنك معرفة المزيد حول الاختلاف بين التراخيص في صفحة طلب PVS-Studio .





إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Alexey Govorov. PVS-Studio: تحليل طلبات السحب في Azure DevOps باستخدام وكلاء مستضافين ذاتيًا .



All Articles