Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
Travis CI is in ferspraat webtsjinst foar it bouwen en testen fan software dy't GitHub brûkt as boarnekoade-hosting. Neist de boppesteande operaasjescenario's kinne jo jo eigen tafoegje troch de wiidweidige konfiguraasjeopsjes. Yn dit artikel sille wy Travis CI konfigurearje om te wurkjen mei PVS-Studio mei it PPSSPP-koadefoarbyld.

Ynlieding

Travis C.I. is in webtsjinst foar it bouwen en testen fan software. It wurdt normaal brûkt yn kombinaasje mei trochgeande yntegraasjepraktiken.

PPSSPP - PSP-spielkonsole-emulator. It programma is yn steat om de lansearring fan alle spultsjes te emulearjen fan skiifôfbyldings bedoeld foar Sony PSP. It programma waard útbrocht op 1 novimber 2012. PPSSPP is lisinsje ûnder GPL v2. Eltsenien kin meitsje ferbetterings oan projekt boarne koade.

PVS-Studio - in statyske koade-analyzer foar it sykjen nei flaters en potensjele kwetsberens yn programmakoade. Yn dit artikel sille wy foar in feroaring PVS-Studio net lokaal op 'e masine fan' e ûntwikkelders starte, mar yn 'e wolk, en sykje nei flaters yn PPSSPP.

It ynstellen fan Travis CI

Wy sille in repository nedich hawwe op GitHub, wêr't it projekt dat wy nedich is leit, lykas ek in kaai foar PVS-Studio (jo kinne krije proef kaai of fergees foar Open Source-projekten).

Litte wy nei de side gean Travis C.I.. Nei autorisaasje mei jo GitHub-akkount sille wy in list mei repositories sjen:

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
Foar de test forkearde ik PPSSPP.

Wy aktivearje it repository dat wy wolle sammelje:

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
Op it stuit kin Travis CI ús projekt net bouwe, om't d'r gjin ynstruksjes binne foar it bouwen. Dus it is tiid foar konfiguraasje.

Tidens de analyze, guon fariabelen sille wêze nuttich foar ús, bygelyks, de kaai foar PVS-Studio, dat soe wêze net winske te spesifisearje yn de konfiguraasje triem. Dat litte wy omjouwingsfariabelen tafoegje mei de bouynstellingen yn Travis CI:

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
Wy sille nedich wêze:

  • PVS_USERNAME - brûkersnamme
  • PVS_KEY - kaai
  • MAIL_USER - e-post dy't brûkt wurdt om it rapport te ferstjoeren
  • MAIL_PASSWORD - e-postwachtwurd

De lêste twa binne opsjoneel. Dizze sille brûkt wurde om resultaten per post te ferstjoeren. As jo ​​it rapport op in oare manier ferspriede wolle, hoege jo dy net oan te jaan.

Dat, wy hawwe de omjouwingsfariabelen tafoege dy't wy nedich binne:

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
Litte wy no in bestân oanmeitsje .travis.yml en pleats it yn 'e woartel fan it projekt. PPSSPP hie al in konfiguraasjetriem foar Travis CI, lykwols, it wie te grut en folslein net geskikt foar it foarbyld, dus wy moasten it sterk ferienfâldigje en allinich de basiseleminten litte.

Litte wy earst de taal oanjaan, de ferzje fan Ubuntu Linux dy't wy wolle brûke yn 'e firtuele masine, en de nedige pakketten foar it bouwen:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

Alle pakketten dy't neamd binne binne eksklusyf nedich foar PPSSPP.

No jouwe wy de gearstalling matrix oan:

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PPSSPP_BUILD_TYPE=Linux PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"
      env: PPSSPP_BUILD_TYPE=Linux

In bytsje mear oer de seksje matrix. Yn Travis CI binne d'r twa manieren om bouwopsjes te meitsjen: de earste is om in list fan gearstallers, typen fan bestjoeringssysteem, omjouwingsfariabelen, ensfh., wêrnei't in matriks fan alle mooglike kombinaasjes wurdt oanmakke; de twadde is in eksplisite oantsjutting fan 'e matrix. Fansels kinne jo dizze twa oanpakken kombinearje en in unyk gefal tafoegje, of krekt oarsom, it útslute mei de seksje útslute. Jo kinne mear oer dit lêze yn Travis CI dokumintaasje.

Alles wat oerbliuwt is projektspesifike montage-ynstruksjes te leverjen:

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Travis CI lit jo jo eigen kommando's tafoegje foar ferskate stadia fan it libben fan in firtuele masine. Ôfdieling before_install útfierd foardat jo pakketten ynstallearje. Dan ynstallearje, dy't folget de ynstallaasje fan pakketten út 'e list addons.aptdy't wy hjirboppe oanjûn hawwe. De gearkomste sels fynt plak yn skrift. As alles goed gie, dan fine wy ​​ússels yn after_success (it is yn dizze seksje dat wy statyske analyze sille útfiere). Dit binne net alle stappen dy't kinne wurde wizige, as jo mear nedich binne, dan moatte jo deryn sjen Travis CI dokumintaasje.

Foar it gemak fan it lêzen waarden de kommando's yn in apart skript pleatst .travis.sh, dat wurdt pleatst by de projekt root.

Sa hawwe wy de folgjende triem .travis.yml:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Foardat jo de pakketten ynstallearje, sille wy de submodules bywurkje. Dit is nedich om PPSSPP te bouwen. Litte wy de earste funksje tafoegje oan .travis.sh (note de útwreiding):

travis_before_install() {
  git submodule update --init --recursive
}

No komme wy direkt by it ynstellen fan de automatyske lansearring fan PVS-Studio yn Travis CI. Earst moatte wy it PVS-Studio-pakket op it systeem ynstallearje:

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}

Oan it begjin fan 'e funksje travis_install wy ynstallearje de gearstallers dy't wy nedich hawwe mei help fan omjouwingsfariabelen. Dan as de fariabele $PVS_ANALYSE winkels wearde Ja (wy hawwe it oanjûn yn 'e seksje sawat tidens build matrix konfiguraasje), ynstallearje wy it pakket pvs-studio. Dêrnjonken wurde ek pakketten oanjûn libio-socket-ssl-perl и libnet-ssleay-perl, lykwols, se binne nedich foar mailing resultaten, dus se binne net nedich as jo hawwe keazen in oare metoade foar it leverjen fan jo rapport.

function download_extract downloadt en útpakt it opjûne argyf:

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

It is tiid om it projekt byinoar te bringen. Dit bart yn 'e seksje skrift:

travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}

Yn feite is dit in ferienfâldige orizjinele konfiguraasje, útsein dizze rigels:

if [ "$PVS_ANALYZE" = "Yes" ]; then
  CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
fi

Yn dizze seksje fan koade wy ynstelle foar cmke flagge foar it eksportearjen fan kompilaasjekommando's. Dit is nedich foar in statyske koade analyzer. Jo kinne hjir mear oer lêze yn it artikel "Hoe kinne jo PVS-Studio útfiere op Linux en macOS".

As de gearkomste suksesfol wie, dan komme wy oan after_success, wêr't wy statyske analyze útfiere:

travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}

Litte wy de folgjende rigels in tichterby besjen:

pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                -o PVS-Studio-${CC}.log 
                                --disableLicenseExpirationCheck
plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html

De earste rigel genereart in lisinsjebestân fan 'e brûkersnamme en kaai dy't wy oan it begjin spesifisearre hawwe by it ynstellen fan de Travis CI-omjouwingsfariabelen.

De twadde rigel begjint de analyze direkt. Flagge -j stelt it oantal triedden foar analyze, flagge -l jout lisinsje, flagge -o definiearret de triem foar it útfieren fan logs, en de flagge -disableLicenseExpirationCheck fereaske foar proefferzjes, sûnt standert pvs-studio-analyzer sil de brûker warskôgje dat de lisinsje op it punt is te ferrinnen. Om foar te kommen dat dit bart, kinne jo dizze flagge opjaan.

It lochbestân befettet rau útfier dy't net lêzen wurde kin sûnder konverzje, dus jo moatte earst it bestân lêsber meitsje. Litte wy de logs trochjaan plog-konverter, en de útfier is in html-bestân.

Yn dit foarbyld haw ik besletten om rapporten per post te stjoeren mei it kommando Stjoer email.

As gefolch hawwe wy it folgjende bestân krigen .travis.sh:

#/bin/bash

travis_before_install() {
  git submodule update --init --recursive
}

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}
travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}
travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}
set -e
set -x

$1;

No is it tiid om de wizigingen nei it git-repository te triuwen, wêrnei't Travis CI de build automatysk sil útfiere. Klikje op "ppsspp" om nei de bourapporten te gean:

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
Wy sille in oersjoch sjen fan 'e hjoeddeistige bou:

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator
As de bou mei súkses foltôge is, krije wy in e-post mei de resultaten fan 'e statyske analyse. Fansels is mailing net de ienige manier om in rapport te ûntfangen. Jo kinne elke ymplemintaasjemetoade kieze. Mar it is wichtich om te ûnthâlden dat nei it bouwen is foltôge, it sil net mooglik wêze om tagong te krijen ta de firtuele masine-bestannen.

Flater gearfetting

Wy hawwe it dreechste diel mei súkses foltôge. Litte wy no soargje dat al ús ynspanningen it wurdich binne. Litte wy ris nei wat nijsgjirrige punten sjen út it statyske analyserapport dat per post by my kaam (it wie net foar neat dat ik it oanjûn hie).

Gefaarlike optimalisaasje

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  memset( &ctx, 0, sizeof( sha1_context ) );
}

PVS-Studio warskôging: V597 De kompilator koe de 'memset'-funksjeoprop wiskje, dy't brûkt wurdt om 'som'-buffer te spoelen. De funksje RtlSecureZeroMemory() moat brûkt wurde om de priveegegevens te wiskjen. sha1.cpp 325

Dit stik koade leit yn 'e befeilige hashingmodule, it befettet lykwols in serieuze befeiligingsfout (CWE-14). Litte wy nei de gearstallingslist sjen dy't wurdt generearre by it kompilearjen fan de Debug-ferzje:

; Line 355
  mov r8d, 20
  xor edx, edx
  lea rcx, QWORD PTR sum$[rsp]
  call memset
; Line 356

Alles is yn oarder en de funksje memeset wurdt útfierd, dêrmei oerskriuwe wichtige gegevens yn RAM, lykwols net bliid krekt noch. Litte wy sjen nei de gearstallingslist fan 'e Release-ferzje mei optimisaasje:

; 354  :
; 355  :  memset( sum, 0, sizeof( sum ) );
; 356  :}

Lykas kin wurde sjoen út de list, negearre de gearstaller de oprop memeset. Dit komt troch it feit dat yn 'e funksje sha1 nei de oprop memeset gjin ferwizing mear nei struktuer ctx. Dêrom, de gearstaller sjocht gjin punt yn fergrieme prosessor tiid oerskriuwen ûnthâld dat wurdt net brûkt yn 'e takomst. Jo kinne dit reparearje mei de funksje RtlSecureZeroMemory of ferlykber oan har.

Korrekt:

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  RtlSecureZeroMemory(&ctx, sizeof( sha1_context ) );
} 

Unnedich ferliking

static u32 sceAudioOutputPannedBlocking
             (u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    if (leftvol >= 0) {
      chans[chan].leftVolume = leftvol;
    }
    if (rightvol >= 0) {
      chans[chan].rightVolume = rightvol;
    }
    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

PVS-Studio warskôging: V547 Ekspresje 'leftvol>= 0' is altyd wier. sceAudio.cpp 120

Jou omtinken oan 'e oare tûke foar de earste if. De koade sil allinich útfierd wurde as alle betingsten leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rjochts < 0 sil blike te wêzen falsk. Dêrom krije wy de folgjende útspraken, dy't wier wêze sille foar de oare branch: leftvol <= 0xFFFF, rightvol <= 0xFFFF, leftvol >= 0 и rjochtsvol >= 0. Let op de lêste twa útspraken. Hat it sin om te kontrolearjen wat in needsaaklike betingst is foar de útfiering fan dit stik koade?

Sa kinne wy ​​dizze betingsten útspraken feilich fuortsmite:

static u32 sceAudioOutputPannedBlocking
(u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    chans[chan].leftVolume = leftvol;
    chans[chan].rightVolume = rightvol;

    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

In oar senario. D'r is wat soarte flater ferburgen efter dizze oerstallige betingsten. Miskien hawwe se net kontrolearre wat der nedich wie.

Ctrl+C Ctrl+V slacht werom

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfData) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

V501 D'r binne identike sub-ekspresjes '!Memory::IsValidAddress(psmfData)' links en rjochts fan '||' operator. scePsmf.cpp 703

Jou omtinken oan de kontrôle binnen if. Fynsto it net nuver dat wy kontrolearje oft it adres jildich is? psmfData, twa kear safolle? Dit liket my dus nuver... Eins is dit fansels in typflater, en it idee wie om beide ynfierparameters te kontrolearjen.

Korrekte opsje:

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfStruct) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

Ferjitten fariabele

extern void ud_translate_att(
  int size = 0;
  ....
  if (size == 8) {
    ud_asmprintf(u, "b");
  } else if (size == 16) {
    ud_asmprintf(u, "w");
  } else if (size == 64) {
    ud_asmprintf(u, "q");
  }
  ....
}

PVS-Studio warskôging: V547 Ekspresje 'grutte == 8' is altyd falsk. syn-att.c 195

Dizze flater leit yn 'e map ext, Dus net echt relevant foar it projekt, mar de brek waard fûn foardat ik it opmurken, dus ik besleat it te ferlitten. Dit artikel is ommers net oer it besjen fan flaters, mar oer yntegraasje mei Travis CI, en gjin konfiguraasje fan 'e analysator waard útfierd.

Variable grutte wurdt inisjalisearre troch in konstante, lykwols, it wurdt net brûkt hielendal yn de koade, rjocht omleech nei de operator if, dy't fansels jout falsk wylst wy de betingsten kontrolearje, om't, lykas wy ús ûnthâlde, grutte gelyk oan nul. De folgjende kontrôles hawwe ek gjin sin.

Blykber fergeat de skriuwer fan it koadefragmint de fariabele te oerskriuwen grutte dêrfoar.

Ophâlde

Dit is wêr't wy wierskynlik einigje mei de flaters. It doel fan dit artikel is om it wurk fan PVS-Studio tegearre mei Travis CI te demonstrearjen, en net it projekt sa yngeand mooglik te analysearjen. As jo ​​​​gruttere en moaier flaters wolle, kinne jo se altyd bewûnderje hjir :).

konklúzje

It brûken fan webtsjinsten om projekten te bouwen tegearre mei de praktyk fan inkrementele analyze kinne jo in protte problemen fine direkt nei it fusearjen fan koade. Ien build kin lykwols net genôch wêze, dus it opsetten fan testen tegearre mei statyske analyse sil de kwaliteit fan 'e koade signifikant ferbetterje.

Nuttige keppelings

Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan in PSP-spielkonsole-emulator

As jo ​​​​dit artikel wolle diele mei in Ingelsktalig publyk, brûk dan de oersettingskeppeling: Maxim Zvyagintsev. Hoe kinne jo PVS-Studio yn Travis CI ynstelle mei it foarbyld fan PSP-spielkonsole-emulator.

Boarne: www.habr.com

Add a comment