Pull to refresh
640.2
Альфа-Банк
Лучший мобильный банк по версии Markswebb

О разработке интерактивных приложений под ОС IBM i (aka AS/400)

Reading time27 min
Views8.3K
Здравствуйте, уважаемые читатели. Меня зовут Владимир Лебедев, я работаю в Альфа-Банке и занимаюсь тем, что пытаюсь максимально упростить жизнь разработчиков АБС Equation, занимающихся разработкой приложений под операционную систему IBM i.

Сразу скажу, что литературный слог – точно не мой конек. Я больше про исследования и скрупулезное изучение технических возможностей, про поиск инженерных решений. Однако у нас в сообществе разработчиков IBMi в банке принято, что по итогам года выбираются законченные реализации, которыми мы делимся с читателями и за пределами Альфа-Банка. В этом году в их число попала и моя работа.

Также скажу, что пытливый читатель не найдет в статье сногсшибательных прорывов и идей, которые кардинально меняют мир вокруг. Скорее, работу можно рискнуть сравнить с процессом доказательства теоремы Ферма. Известно, что Пьер Ферма еще в 1637 году сформулировал свою великую теорему. Почти четыре столетия ученые пытались расколоть этот орешек. Но удалось это сделать только в 1994 году Эндрю Уайлсу, а в 2016 году этот гениальный норвежец получил за дело своей жизни Абелевскую премию. Доказательство теоремы Ферма не несет за собой исключительной практической ценности или стремления к славе и успеху, но в процессе решения задачи были найдены интересные идеи, выросли целые поколения ученых.

Мой скромный (но, похвалю себя =), честный и кропотливый) труд — он не про решения во фронтовом программном обеспечении Альфа-Банка. Хотя, надо признать, что мои коллеги здесь крайне преуспели, и банк занимает свое заслуженное место в рейтингах. Мой труд про исследования, которые являются неотъемлемой историей любого профессионального сообщества, стремящегося к саморазвитию на всех уровнях.

Итак, начнем. ОС IBM i и ее предшественницы — aka AS/400 — известны тем, что все ее интерактивные приложения до сих пор работают через так называемый зеленый экран (green screen или GS). Выглядит это примерно так:


Пример Green Screen-а

Если вы разработчик под IBMi, то, несомненно, согласитесь, что разработка такого рода приложений вызывает неудобства (а, если прямо сказать, то большую головную боль и внутреннее раздражение). Даже для продвинутых разработчиков (особенно для них) привыкших к разработке современных пользовательских интерфейсов на базе веб-технологий.

Надо признать, что при всем неудобстве интерактивные приложения с консольным GS до сих пор очень нужны многим зрелым, многолетним предприятиям. И Альфа-Банк не исключение. Такую «зеленую» картинку можно наблюдать на мониторе большинства сотрудников бэк-офиса. Green Screen для IBM i — это как SSH и SHELL для Unix-а. К слову, SSH и SHELL есть в IBM i. Но здесь вы попадаете в особый runtime, именуемый PASE, со своими правилами игры.

Важно отметить, что платформа IBMi поддерживает 2 базовых программных модели – уже устаревшую OPM (Original Program Model) и активно развиваемую на данный момент ILE (Integrated Language Environment). Если кратко, то под программной моделью здесь понимается совокупность решений операционной системы по квотированию ресурсов, возможностям кросс-языковых реализаций и т.д. Но при всех преимуществах новой ILE-концепции эффективность рекомендуемых IBM-ом способов разработки Green Screens оставляет желать лучшего.

Так как же разрабатываются интерактивные приложения IBM i?


Я уже упомянул, что разработка интерактивных приложений под IBM i с использованием традиционных подходов — занятие не самое увлекательное. А уж детальный рассказ про эту разработку вызовет зевоту или бурное негодование среди видавших виды. Однако, я не могу опустить это описание, так как это важно для общего понимания проблематики. Насколько подробно следить за мыслью — решайте сами.

Объекты, типы, подтипы


Стоит обратить внимание, что IBMi является одной из самых ранних объектных операционных систем. В отличие от традиционных Unix/Windows, здесь нет файлов в обычном понимании. Система оперирует специализированными объектами разных по назначению типов – файлы (БД и интерфейсы I/O), программы (исполняемый код), профайлы (учетные записи) и т.д. Внутри этих объектов прячутся как сами данные (согласно назначению), так и все необходимые метаданные, позволяющие правильно интерпретировать объект и правильно им воспользоваться.

Некоторые типы объектов имеют подтипы. Например, объект типа *FILE, подтипа PF – т.н. физический файл — инкапсулирует внутри себя все необходимое для работы с ним, как с таблицей БД. А тип *FILE c подтипом LF (логический файл) реализует индекс таблицы.
Обратим внимание на важный для описываемой истории подтип объекта *FILE — дисплейный файл (DSPF). Его предназначение как раз в описании пользовательского интерфейса (терминальный green screen). Интерактивное взаимодействие программы и пользователя выполняется именно через этот объект.

Форматы записи


Основное содержимое дисплейного файла — форматы записи. Каждый такой формат записи определяет шаблон внешнего вида пользовательского экрана или его части. Т.е. типизирует поля (например, только для вывода или ввода/вывода, текст или число), фиксирует их размерность и расположение на экране.

Поле — это наименьшая единица данных, которая распознается и обрабатывается системой управления данными. Форматы записи в дисплейных файлах определяются с помощью спецификации описания данных (DDS).

Описание файла описывает данные на трех разных уровнях:

  1. Уровень поля
  2. Уровень записи
  3. Уровень файла

Описание уровня поля позволяет дать системе подробные характеристики поля, такие как расположение на экране, допустимый тип данных, представление поля при операциях ввода и вывода, тип поля, возможность редактирования, выравнивание данных в поле и прочее.

Запись — это упорядоченный набор из одного или нескольких полей. Описание уровня записи позволяет вам сказать системе, как выглядит конкретная запись или ее формат записи. Запись является минимальной единицей обмена данными между системой и прикладной программой, независимо от наличия полей.

Описание уровня файла — это описание, которое применяется к файлу в целом. Для файла дисплея вы можете указать используемые форматы записи, дисплейную станцию, набор графических символов.

Использование дисплейных файлов


Формирование исходного файла дисплея осуществляется с помощью кодирования DDS.


Пример дисплейного файла в редакторе SEU

Да, печально, но дисплейный файл имеет только позиционную форму записи. Других форм на 2020-й год нет. Однако, давайте все же пройдем не самый увлекательный путь, которым ходили и в прошлом веке, чтобы двигаться дальше и искать пути решения.

Пример примером, давайте рассмотрим задачку.

Создадим дисплейный файл для некоторой программы WINDEMOR:

  1. программа принимает на вход два параметра
  2. если второй аргумент равен ‘A’, выводит окно с сообщением «Внештатная ситуация №1»
  3. если второй аргумент равен ‘B’, то выводит окно с сообщением «Внештатная ситуация №2»
  4. при любом другом аргументе выводит «Ошибка вызова».
  5. Окно можно закрыть клавишей F3. Нажатие любых других клавиш приводят к сообщению «Функциональная клавиша недопустима».
  6. В верхней рамке, окно должно содержать базовую информацию о модуле среды АБС Equation:
    <ИМЯ ПРОГРАММЫ> <ВЕРСИЯ> <ТЕКУЩАЯ БИЗНЕС-ДАТА>


Пример окна с выводом сообщения

Текст программы
  A*----------------------------------------------------------------
     A*              System Name - PUB400
     A*
     A*                File Name - WINDEMOD
     A*         File Description - Демонстрация работы с окном
     A*                File Type - Display
     A*L                   Level - 100
     A*
     A*            Creation Date - 2020/02/25
     A*      Last Amendment Date - 2020/02/25
     A*----------------------------------------------------------------
     A*   Change Control
     A*   ~~~~~~~~~~~~~~
     A*   Date            Initials      Amendment
     A*   ~~~~            ~~~~~~~~      ~~~~~~~~~
     A*   2020/02/25      USRXXX        Создано
     A*
     A*----------------------------------------------------------------
     A*   Purpose
     A*   ~~~~~~
     A*   Демонстрация работы с экраном
     A*----------------------------------------------------------------
     A*   Indicators
     A*   ~~~~~~~~~~
     A*   Number    Description
     A*   ~~~~~~    ~~~~~~~~~~~
     A*   01-04     Attribute control indicators (message to display)
     A*   10-13     External account number indicators
     A*      71     Modify data tag
     A*      73     Errors to display
     A*      74     Warnings to display
     A*      76     Protect branch
     A*      83     Load message subfile
     A*      90     Keyboard key pressed
     A*      91     Rollup
     A*      92     Rolldown
     A*      93     Help key pressed
     A*      94     Home key pressed
     A*      95     Clear key pressed
     A*----------------------------------------------------------------
     A                                      DSPSIZ(24 80 *DS3)
     A                                      REF(*LIBL/EQFREF)
     A                                      INDARA
     A                                      PRINT
     A                                      VLDCMDKEY(90)
     A                                      HOME(94)
     A                                      CLEAR(95)
     A                                      ALTHELP
     A                                      HELP
     A                                      CF02
     A                                      CF03
     A                                      CF04
     A                                      CF05
     A                                      CF06
     A                                      CF09
     A                                      CF10
     A                                      CF11
     A                                      CF12
     A                                      CF13
     A                                      CF14
     A                                      CF15
     A                                      CF16
     A                                      CF17
     A                                      CF18
     A                                      CF19
     A                                      CF20
     A                                      CF21
     A                                      CF22
     A                                      CF23
     A                                      CF24
     A*----------------------------------------------------------------
     A          R HEADER
     A                                      WINDOW(7 10 7 59)
     A                                      BLINK
     A                                      LOCK
     A            KAPPG7         7A  O  1  1COLOR(BLU)
     A            KAPLVL    R        O  1  9REFFLD(FPL)
     A                                      COLOR(BLU)
     A            ZLTITL    R        O  1 13REFFLD(ONM)
     A                                      DSPATR(HI)
     A            ZUDATE    R        O  1 49REFFLD(DAZ)
     A*
     A*----------------------------------------------------------------
     A          R WINDEMODA
     A                                      WINDOW(HEADER)
     A                                      CHANGE(84)
     A                                      RTNCSRLOC(&ZHRFMT &ZHFLD)
     A                                      BLINK
     A                                      INVITE
     A                                      OVERLAY
     A                                      PUTOVR
     A                                      RMVWDW
     A*
     A            ZHFLD         10A  H
     A            ZHRFMT        10A  H
     A  01N02                           3 15'Внештатная ситуация №1'
     A                                      DSPATR(HI)
     A N01N02                           3 17'Внештатная ситуация №2'
     A                                      DSPATR(HI)
     A N01 02                           3 24'Ошибка вызова'
     A                                      DSPATR(HI)
     A                                  6  1'F3=Выход'
     A                                      COLOR(BLU)
     A*
     A*----------------------------------------------------------------
     A          R OPTION
     A                                      CHANGE(84)
     A                                      BLINK
     A                                      LOCK
     A                                      OVERLAY
     A                                      PUTOVR
     A N83                                  ERASE(MSGSFL)
     A                                      WINDOW(HEADER)
     A            ZLCMD         58A  O  6  1COLOR(BLU)
     A                                      OVRDTA
     A*
     A*----------------------------------------------------------------
     A          R MSGSFL                    SFL SFLMSGRCD(06)
     A            FLDKEY                    SFLMSGKEY
     A            FLDPGM                    SFLPGMQ
     A*
     A*
     A          R MSGCTL                    SFLCTL(MSGSFL)
     A                                      WINDOW(HEADER)
     A                                      BLINK
     A                                      LOCK
     A                                      OVERLAY
     A                                      PUTOVR
     A                                      SFLDSP
     A                                      SFLINZ
     A  83                                  SFLEND
     A                                      SFLSIZ(0020)
     A                                      SFLPAG(0001)
     A            FLDPGM                    SFLPGMQ(10)
     A*
     A*----------------------------------------------------------------
     A          R CLEAN
     A                                      ASSUME
     A                                 24  2' '



Функции-расширения


В операционной системе существуют функции-расширения стандартного API для С/С++ или встроенные в языки инструкции, которые позволяют открывать дисплейные файлы и взаимодействовать с ними.

Важно отметить, что строки «R HEADER», «R WINDEMODA», «R OPTION» и другие определяют то, что называется записью дисплейного файла, или по-другому Record-форматом. Этот формат определяет структуру буфера, через который будет происходить обмен данными между эмулятором терминала пользователя и программой, которая работает с «дисплейником».

Если программе необходимо интерактивное взаимодействие с терминалом пользователя, то она должна записать в Record-формат, что хочет вывести в поля экрана (если программа ничего не хочет выводить на экран, ей все равно нужно записать пустые бланковые поля). После этого необходимо произвести чтение Record-формата, при этом управление передается операционной системе, которая выводит пользователю статическое содержимое (например, фраза «Внештатная ситуация №1» в позицию 3/15»).

Под спойлером можете посмотреть код программы на языке IBM RPG, который демонстрирует работу с данным окном и реализует логику приложения.

Текст программы
  /TITLE Демонстрация работы с окном
      *---------------------------------------------------------------------
      *              System Name - PUB400
      *
      *             Program Name - WINDEMOR
      *            Program Title - Демонстрация работы с окном
      *
      *L                   Level - 100
      *
      *            Creation Date - 25FEB20
      *      Last Amendment Date - 25FEB20
      *---------------------------------------------------------------------
      *P Purpose
      *P ~~~~~~~
      *P Демонстрация работы с окном
      *P
      *---------------------------------------------------------------------
      *H Change Control
      *H ~~~~~~~~~~~~~~
      *H Project     Programmer    Date         Level
      *H ~~~~~~~     ~~~~~~~~~~    ~~~~         ~~~~~
      *H WINDEMO     USERXXX       25FEB20       100
      *H  Создано
      *---------------------------------------------------------------------
      /COPY EQCPYLESRC,HSPEC
     H DFTNAME(WINDEMOR)                                                        WINDEMO
      *
      *---------------------------------------------------------------------
      *  FILES
      *---------------------------------------------------------------------
     FWINDEMOD  CF   E             WORKSTN INFSR(*PSSR) USROPN
      *
      *---------------------------------------------------------------------
      *  DATA STRUCTURES
      *---------------------------------------------------------------------
      *
     D @EM             S             37    DIM(20)
     D #Check1         S              1A
      *---------------------------------------------------------------------
      *  PRE-INITIALISED DATA STRUCTURES
      *---------------------------------------------------------------------
      *
      *  KAPROG data structure
     D KAPROG          DS
     D  MISYS                  1      6    INZ('MISYS ')
     D  KAPLVL                 8     10    INZ('100')
     D  KAPPGM                12     17
     D  KAPPG7                12     18
      *
      /COPY EQCPYLESRC,DSJOBE_A
      /COPY EQCPYLESRC,DSSYSE
      *
      /COPY EQCPYLESRC,DSEPMS
      /COPY EQCPYLESRC,DPGMDS                    Data structure for progra
      /COPY EQCPYLESRC,DMSGDS_A                  DS for dump message *
      *---------------------------------------------------------------------
      *  NAMED CONSTANTS
      *---------------------------------------------------------------------
      /COPY EQCPYLESRC,STDCNST                   Standard program constant
      *---------------------------------------------------------------------
      /COPY EQCPYLESRC,STDFLDS
      /COPY EQCPYLESRC,CMRINZS
      /COPY EQCPYLESRC,CMRMAIN
      /COPY EQCPYLESRC,CMRABRT
      /COPY EQCPYLESRC,CMRPSSR
     ?*---------------------------------------------------------------------
     ?*  C SPEC START
     ?*---------------------------------------------------------------------
     C     CSpecStart    TAG
      /COPY EQCPYLESRC,MAIN
      *
      *---------------------------------------------------------------------
      *  PARAMETER AND KEY LISTS
      *---------------------------------------------------------------------
      *
      *  Parameter lists
     C     *ENTRY        PLIST
     C                   PARM                    @Exit             1
     C                   PARM                    @Chk1             1            № сообщ. для вывода
      *=====================================================================
      *  MAINLINE
      *=====================================================================
     C                   MOVE      #NO           @Exit
     C                   If        %PARMS = 2
     C                   Eval      #Check1 = @Chk1
     C                   Else
     C                   Eval      #Check1 = *Blanks
     C                   EndIf
      *
      * Если #Check1 = 'A' => первое сообщение, #Check2 = 'B' => второе сообщение
      * иначе - сообщение об ошибке вызова
      *
     C                   If        #Check1 <> 'A' And #Check1 <> 'B'
     C* если неверный параметр, устанавливаем флаг завершения
     C                   MOVE      #YES          @Exit
     C                   EndIf
     C*
     C                   MOVE      #NO           #EOP              1
      *
     C                   IF        not %OPEN(WINDEMOD)
     C                   OPEN      WINDEMOD
     C                   ENDIF
     C                   Z-ADD     *ZERO         S3
     C                   EXSR      Z000
      *
     C                   DOW       #EOP = #NO
     C                   EXSR      B000
     C                   ENDDO
      *
      *  Return to caller
     C                   EXSR      C000
      *
     C                   RETURN
      *=====================================================================
      *  SUBROUTINES
      *=====================================================================
      *---------------------------------------------------------------------
     C     *INZSR        BEGSR
      /COPY EQCPYLESRC,INZSR
      *---------------------------------------------------------------------
      *
      *S Program initialization
      *
      *---------------------------------------------------------------------
      *
     C     *Dtaara       Define    DAJOBCTLE     DSJOBE
     C     *Dtaara       Define    DASYSCTL      DSSYSE
     C                   IN        *DTAARA
      *
     C                   MOVEL(P)  @PGMID        @PGM
     C                   MOVEL(P)  @PGMID        FLDPGM
     C                   MOVEL(P)  @PGMID        KAPPG7
      *
     C                   MOVE      $ZDATE        ZUDATE
      *
     C                   ENDSR
      /EJECT
      *---------------------------------------------------------------------
     C     B000          BEGSR
      *---------------------------------------------------------------------
      *
     C                   READ      WINDEMODA
      *
     C                   Z-ADD     *ZERO         S3
      *
     C                   MOVE      *BLANKS       #CMDKY            2
     C                   MOVE      '0'           #TAGNO            1
      *
     C     *IN90         CASEQ     *ON           B100
     C                   END
     C     #TAGNO        CABEQ     '2'           B000XT
      *
      *
B001 C                   IF        #EOP = #NO
     C                   EXSR      Z000
E001 C                   ENDIF
      *
     C     B000XT        ENDSR
      /EJECT
      *---------------------------------------------------------------------
     C     B100          BEGSR
      *---------------------------------------------------------------------
      *
B001 C                   IF        *INKC = *ON
     C                   MOVEL(P)  '03'          #CMDKY
     C                   MOVE      #YES          #EOP
     C                   MOVE      '2'           #TAGNO
E001 C                   ENDIF
      *
      *
     C                   MOVEL(P)  'KSM0807'     DSEPMS
     C                   EXSR      USA10R
      *
     C                   ENDSR
      /EJECT
      *---------------------------------------------------------------------
     C     Z000          BEGSR
      *---------------------------------------------------------------------
      *
      *  Write header format
     C                   WRITE     HEADER
      *
     C                   Z-ADD     *ZERO         S3
      *
     C                   MOVE      #YES          @CLRQ
     C                   CALLB     'UTM14C'
     C                   PARM                    @PGM             10
     C                   PARM                    @EM
     C                   PARM                    S3                2 0
     C                   PARM                    @CLRQ             1
      *
      *  Write detail format
S001 C                   SELECT
W001 C                   WHEN      #Check1 = 'A'
     C                   MOVE      *ON           *IN01
     C                   MOVE      *OFF          *IN02
W001 C                   WHEN      #Check1 = 'B'
     C                   MOVE      *OFF          *IN01
     C                   MOVE      *OFF          *IN02
B001 C                   OTHER
     C                   MOVE      *OFF          *IN01
     C                   MOVE      *ON           *IN02
E001 C                   ENDSL
     C                   WRITE     WINDEMODA
      *
     C                   ENDSR
      /EJECT
      *---------------------------------------------------------------------
     C     C000          BEGSR
      *---------------------------------------------------------------------
      *
B001 C                   IF        %OPEN(WINDEMOD)
     C                   CLOSE     WINDEMOD
E001 C                   ENDIF
      *
     C                   RETURN
      *
     C                   ENDSR
      /EJECT
      *---------------------------------------------------------------------
     C     USA10R        BEGSR
      *---------------------------------------------------------------------
      *
     C                   MOVEL(P)  @ERM          @MSGID
     C                   MOVEL(P)  @PMALL        @MSGDA
      *
     C                   CALLB     'UTM02C'
     C                   PARM                    @MSGID            7
     C                   PARM                    @MSGDA           30
     C                   PARM                    @MSGDT          132
     C                   PARM                    @MSGSV            2 0
      *
B001 C                   IF        S3 < 20
     C                   ADD       1             S3
     C                   EVAL      @EM(S3) = DSEPMS
E001 C                   ENDIF
      *
     C                   MOVE      *BLANKS       DSEPMS
      *
     C                   ENDSR
     ?*
      /COPY EQCPYLESRC,PSSR_B
      *
      *---------------------------------------------------------------------
      * COMPILE TIME ARRAY DATA
      *---------------------------------------------------------------------
      *


Итоги по DDS и недостатки


DDS представляет широкие возможности по работе с экраном дисплея и взаимодействия с прикладной программой, реализуя возможности протокола обмена данными терминала 5250 для IBM i. Несмотря на то, что кодирование файлов дисплея является фактически стандартом для взаимодействия с экраном, этот способ, на мой взгляд, имеет и ряд недостатков:

  • Статическое определение атрибутов экрана (полей и записей) и, как следствие, невозможность динамического переопределения представления экрана.
  • DDS является позиционным языком, то есть атрибуты и ключевые слова вводятся в определенных позициях (как на перфокарте).
  • Сложность визуального восприятия и понимания исходных файлов.
  • DDS довольно сложен и непривычен для изучения, содержит большое количество ключевых слов.
  • DDS непривычен для большинства программистов, не знакомых с таким стилем кодирования.
  • Сложность создания нетривиальных экранных конструкций и реализации манипуляций с экраном, не предусмотренных DDS.

Итак, мы с вами прошлись по головной боли. Посмотрели каких «милых позиционных монстров» разработчикам приходится, скрепя сердце, создавать для реализации даже простейших задач, при этом понимая, что к созданным изваяниям придется еще не раз возвращаться и вносить изменения по новым требованиям к задаче. Настало время осознать всю тяжесть темного прошлого и посмотреть в светлое будущее.

Альтернатива стандарту


Есть ли альтернатива DDS? Безусловно, есть. Внимание привлек интерфейс (API) менеджера динамического экрана (Dynamic Screen Manager — DSM), реализующий протокол потока данных терминала 5250 для IBM i.

Давайте поподробнее и подотошнее познакомимся с ним. Далее разберемся, какие же выгоды можно извлечь по сравнению с DDS. Читать именно этот пункт подробно или нет — зависит от того, собираетесь ли вы использовать результаты или захотите погрузиться в реализацию.

DSM API


DSM API представляет собой набор экранных интерфейсов ввода/вывода, которые обеспечивают возможности динамического создания и управления экранами для языков программирования ILE. Поскольку DSM-интерфейсы являются связанными, они доступны только для ILE-программ.

DSM API обеспечивает альтернативу существующему способу определения представления экрана вне программы посредством кодирования в DDS или UIM. Вместо этого программисты могут использовать в своих программах последовательность обращений к DSM для динамического определения и управления представлением экрана. В отличие от статических методов, интерфейс DSM обеспечивает приложениям, нуждающимся в более динамичном управлении экраном, необходимую гибкость.

Поддержка, предоставляемая DSM, варьируется от низкоуровневых интерфейсов для непосредственных манипуляций с экраном до работы с окнами.

DSM API состоит из следующих функциональных групп:

  1. Низкоуровневые сервисы (Low-level services), обеспечивают непосредственный интерфейс к командам потока данных 5250 для выполнения базовых операций экранного ввода/вывода и состоят из следующих функциональных групп:
    • Screen Manipulation and Query APIs, для осуществления запросов и манипулирования состоянием экрана.
    • Buffer Manipulation and Query APIs, для создания, опроса и манипулирования буферами ввода и команд, использующимися для взаимодействия с экраном.
    • Screen Input APIs, для чтения данных полей и другой информации с экрана.
    • Screen Output APIs, для определения полей и записи данных на экран.
  2. Window services, оконные сервисы, используемые для создания, удаления, перемещения и изменения размеров окон.
  3. Session services, сервисы сессии, обеспечивающие общий интерфейс прокрутки, который может использоваться для создания, опроса и манипулирования сессиями, для выполнения операций ввода и вывода с сессиями.

Атрибуты


Казалось бы, простейшая и для понимания, и для применения вещь. Однако здесь тоже кроются подводные камни. Многое зависит не только от правильной интерпретации имеющейся документации, но и от накопленного опыта хождения по схожим граблям.

В общем случае вывод информации на экран не отличается от других подобных устройств. Информация выводится блоками из одного или более символов цветом предшествующего ей атрибута. Атрибут определяет цвет всех выводимых за ним символов, пока не встретится следующий атрибут, и так далее. При отсутствии атрибутов применяется атрибут зеленого цвета. Каждый атрибут занимает одну позицию и на экране выглядит как пустая позиция (пробел).

Например:



X — символ
R — атрибут красного цвета
G — атрибут зеленого цвета
B — атрибут синего цвета

На экране это может выглядеть так:



Ввод информации осуществляется с помощью полей ввода, создаваемых и обслуживаемых терминалом. Количество полей ввода ограничено, допускается одновременное существование не более 256 полей. Характеристиками полей (цветом лидирующего атрибута, длиной, типом и способом ввода данных, выравниванием и т.д.) можно управлять. Имеется набор клавиш перемещения внутри полей и между полями, а также набор клавиш завершения редактирования. Поля могут быть простыми и композитными, т.е. состоящими из нескольких простых полей и ведущими себя как одно целое. Поля предваряются лидирующим атрибутом, определяющим цвет поля, и завершаются атрибутом конца поля (всегда “зеленый на черном”).

Так выглядят на экране поля ввода:



Окна


Если вы разработчик и собираетесь «залезть» в DSM, то при работе с DSM важно понимать структуру и порядок формирования окон, а также порядок вывода и отображения информации на экране. Невнимание к этому аспекту может привести к удивительным или даже весьма комичным ошибкам, которые легко устраняются, когда педантично все разложишь по полочкам.
Окном является прямоугольная область экрана, не выходящая за его пределы. Положение и размеры окна определяются координатами левого верхнего угла окна и количеством строк и столбцов окна. Окно может иметь или не иметь рамку. В наличии могут быть не все элементы, составляющие окно.

Окно может не иметь рамки, но если она есть, то возможно определение атрибутов рамки. Атрибуты рамки одинаковы и определяют ее цвет. Лидирующий атрибут окна определяет цвет символов, выводимых в окно. Правый атрибут завершения – это не жестко определенный атрибут, а признак необходимости расчета завершающего атрибута и место для его вывода на экран.


Пример структуры окна


Окно с атрибутами

Так выглядит окно, созданное программой с учетом представленной структуры. Зеленое окно с синей рамкой на белом экране. Для наглядности выбраны инверсные цвета. Хочу напомнить, что символы имеют цвет, определяемый предваряющим их атрибутом. Цвет символов сохраняется, пока не встретится следующий атрибут. На этой картинке правый атрибут завершения окна во всех строках имеет значение “белый инверсный”. Таким образом восстанавливается цвет экрана.

Наличие атрибутов обеспечивает правильное закрашивание рамки и информации в окне. Правильные цвета всех окон на экране обеспечивает программа управления окнами (терминал).
В наличии могут быть не все элементы, составляющие окно. Каждый элемент, относящийся к окну занимает одну позицию экрана, как лидирующий или завершающий атрибут, или две позиции экрана, как рамка или атрибуты рамки. В случае наличия всех элементов окна его ширина не будет превышать ширины экрана минус 6 позиций, а высота – высоты экрана минус 2 позиции.


Пример структуры окна без рамки и ее атрибутов


Окно без атрибутов

Терминал 5250 и DSM


Терминал 5250 имеет блочно-ориентированную организацию потока данных, а атрибуты на экране располагаются отдельно от символов информации. К тому же терминал сам исполняет функцию менеджера окон, отслеживает их взаимное расположение, перемещение и обеспечивает правильное отображение элементов окон в соответствии с их иерархией.

DSM в явном виде поддерживает все необходимые операции с экраном, хотя и довольно специфичен и непривычен в применении, что объясняется особенностями потока данных 5250. Первоначально была предпринята попытка максимально использовать возможности DSM в плане, касающемся оконных конструкций. Но это оказалось не лучшим вариантом, поскольку система управления окнами, восстановления содержимого окон и экрана была не самой удобной и не обеспечивала необходимых требований к визуальному восприятию, иногда передаче подлежал излишне большой объем информации, что в конце концов сказывалось на удобстве использования и быстроте программы. В то же время попытка реализации экранных окон своими силами показала неплохие результаты в плане быстродействия и удобства работы. В этом направлении и было принято решение двигаться далее.

Безусловно, базовые вопросы взаимодействия с терминалом 5250 никуда не делись и лежат в основе нашей разработки. Основными особенностями работы при этом является блочно-ориентированное взаимодействие с терминалом и наличие высоких функциональных возможностей последнего. Можно сказать, что терминал выполняет роль полноценного менеджера экрана, а DSM – обертки над протоколом обмена данными, скрывающей детали его реализации

ASDU для 5250. Решение или приговор?


Так получилось, что к моменту исследования у меня был достаточно богатый положительный опыт использования библиотеки CURSES. Этот опыт был для проекта Enterprise-уровня. Я взял на себя смелость посмотреть, насколько прежние мысли ложатся на требования, возможности и ограничения, предъявляемые со стороны DSM.

Кроме того, библиотека CURSES в UNIX-системах фактически является стандартом текстового ввода/вывода, и было принято решение попытаться использовать имеющийся опыт для написания функционала ввода/вывода для терминала 5250.

Снова погрузимся в теорию и терминологию. А куда без нее?

Элементы


Элементы, составляющие окно в нашей системе, не отличаются от аналогичных в терминале. При этом возможно создание следующих экранных конструкций:

  • WIN – Прямоугольная область экрана, не превышающая его размеров, в которую возможен вывод данных, и с которой могут быть связаны поля ввода терминала. Фактически окно отражает область памяти, соответствующую его координатам на экране.
  • SUBWIN – Прямоугольная область экрана, не выходящая за пределы окна (WIN) и разделяющая с ним одну и ту же область памяти. Использование SUBWIN бывает очень удобно при осуществлении сегментации окна (WIN).
  • PAD – Область памяти, не имеющая привязки к окну или экрану, и способная отличаться от них по размерам в любую сторону. Синхронизация PAD и экрана может осуществляться отдельной командой. В остальном поведении PAD аналогичен окну WIN.
  • SUBPAD – Структура, которая разделяет одну и ту же область памяти с PAD и не может превышать его по размерам. Аналогична SUBWIN, но для PAD.

Структуры


И если окна (WIN, SUBWIN) проецируются в содержащую их структуру целиком, то проекция PADа зависит от соотношения его размеров и внешней структуры. В случае, например, когда PAD больше экрана, на экран проецируется только часть PADа.



Основные конструкции


На базе структур созданы предопределенные конструкции:

  • STDSCR – Имитирует всю область экрана, лежащую на самом нижнем уровне иерархии окон.
  • NEWSCR – Виртуальный экран, содержащий в себе содержимое всех окон и STDSCR, для которых была выполнена операция обновления. Порядок обновления окон при выполнении этой операции будет определять содержимое экрана NEWSCR.
  • CURSCR – Виртуальный экран, предположительно соответствующий физическому экрану терминала после выполнения операции обновления.
  • STDSCR доступен для всех оконных операций, в то время, как использование NEWSCR и CURSCR носит, скорее, служебный характер.

Операции с окнами


Определены следующие операции с окнами:

  • создание, удаление, изменение размеров, перемещение, обновление.
  • копирование областей окон.
  • определение областей прокрутки, прокрутка содержимого окна.
  • вывод данных в окно и получение данных из окна.
  • удаление и вставка строк в окно.
  • получение информации о курсоре и управление положением курсора в окне.
  • получение характеристик окна и управление ими.



Особенности ввода данных


Ввод данных обеспечивается терминалом. Это связано с тем, что эта функциональность реализована терминалом, и подавляющая часть клавиш недоступна для иного использования, кроме как для ввода информации в поля ввода терминала. Информация, введенная в поля ввода, становится доступной только по завершении редактирования. Таким образом идеология вывода информации в значительной степени базируется на идеях библиотеки CURSES, в то время как ввод обеспечивается исключительно терминалом. Эта ситуация порождает некоторые особенности синхронизации данных между прикладной программой и терминалом, к тому же переносит акцент в организации управления экраном и вывода данных в прикладную задачу.



Поля ввода на экране терминала создаются с помощью API DSM и инициализируются значениями с виртуального экрана.

Данные, введенные в поля ввода с клавиатуры, не отображаются ни в одном окне, а, следовательно, и в виртуальном экране программы. Поэтому необходимо прочитать введенную информацию, учесть ее изменения в программе, модифицировать переменные и отобразить ее в виртуальном экране программы, чтобы его содержимое совпало с экраном терминала.

Фигуры с выделенным красным цветом контуром отображают синхронизированные состояния терминала и виртуального экрана.



Создание FRAME


Для учета иерархии окон и более удобного управления ими была создана структура FRAME.

В своем составе FRAME имеет окна WIN и SUBWIN и, возможно, PAD. Окна разделяют одну и ту же область памяти, но SUBWIN меньше на величину рамки и сопутствующих элементов. Фактически это изолированная область вывода. Фреймы же поддерживают иерархию окон и позволяют обновлять их в нужном порядке, что обеспечивает целостность результирующего экрана. Кроме того, осуществляется надзор за атрибутами всей выводимой на экран информации с учетом иерархии окон. Каждый фрейм характеризуется наличием или отсутствием рамки, ее формой, строки заголовка и сноски, атрибутами (цветами элементов). В дополнение к операциям с окнами, фрейм позволяет работать с иерархией окон, перемещать окна в ней, делать их невидимыми и наоборот.

Вывод информации в окна осуществляется с некоторой оптимизацией, с отметкой измененных символов, чтобы в дальнейшем уменьшить объем передаваемых данных.

Вывод информации в эти оконные конструкции не приводит к изменению состояния экрана до вызова команды синхронизации содержимого окна и виртуального экрана и команды обновления физического экрана терминала. Фактически работа строится на предположении, что виртуальный и физический экраны соответствуют друг другу, и на мерах по обеспечению этого.

Поля ввода, управляемые терминалом, несколько выбиваются из этого порядка. Во-первых, поля ввода способны принимать данные не только с клавиатуры, но и с экрана. Это позволяет инициализировать их нужными значениями и изменять их содержимое при необходимости. Во-вторых, необходимо обеспечить синхронизацию полей ввода с соответствующими оконными структурами. Иначе возникает достаточно труднонаходимая неоднозначность. В связи с тем, что об окнах ASDU терминалу ничего не известно, а поля ввода создаются по определенным правилам и с заданными ограничениями (в частности, слева направо и сверху вниз, без возможности наложения), вопрос создания, удаления и изменения полей решается динамически при соответствующих изменениях окон. Так, при создании нескольких окон с полями ввода поля удаляются из нижележащего окна и создаются в окне, лежащем выше по стеку. Фактически в каждый момент времени существуют только те поля, которые относятся к последнему окну в иерархии. Поля, относящиеся к одному окну, объединяются в FIELD_SET, что позволяет в том числе и управлять ими одновременно и однообразно.

Процесс вывода информации в окна и на виртуальные экраны подвергается оптимизации на каждом этапе. Поэтому, несмотря на возможно значительный объем выводимых данных, в итоге между прикладной программой и терминалом пересылаются довольно небольшие объемы, а фактически – только изменения экрана. Это подтвердила и трассировка обмена программы и терминала. К тому же, везде, где только можно, используются операции буферизации потока команд DSM. В частности, практически все команды от одного обновления экрана до другого пересылаются в одном буфере. Наблюдения показали, что это значительно увеличивает скорость обмена и реакции терминала.



Появились новые возможности


Функции управления окнами и их содержимым довольно просты и интуитивно понятны. По своему смыслу и синтаксису они близки к соответствующим функциям библиотеки CURSES в UNIX, за исключением функций ввода данных. Вот некоторые из функций работы с экраном:

  1. Создание окна определенного размера с заданными координатами: _Win_T(…)
  2. Получение координат курсора в окне: getcury, getcurx
  3. Получение координат и размеров окна: getbegy, getbegx, getmaxy, getmaxx
  4. Обновление окна: wrefresh, wnoutrefresh, prefresh, pnoutrefresh, doupdate
  5. Очистка окна или его части: wclrtobot, wclrtoeol, werase, wclear
  6. Прокрутка окна, удаление и вставка строк: wsetscrreg, wscrl, winsdelln
  7. Перемещение курсора: wmove
  8. Получение символов и строк из окна: winch, mvwinch, winnstr, mvwinnstr
  9. Вставка и удаление символов и строк: wdelch, mvwdelch, winsch, mvwinsch, winsnstr, mvwinsnstr
  10. Вывод символов и строк в окно: waddch, wechochar, mvwaddch, waddnstr, mvwaddnstr, wprintw, mvwprintw
  11. Копирование окон: copywin, overlay, overwrite
  12. Рисование бордюра, вертикальной и горизонтальной линии: wborder, wvline, mvwvline, whline, mvwhline
  13. Перемещение и изменение размеров окон: mvwin, wresize

Разработанных базовых функций вполне достаточно для осуществления ввода информации с терминала и вывода информации на экран. Для удобства работы на их основе были реализованы более сложные конструкции, такие как окна вывода сообщений, разнообразные меню, формы ввода данных, табличный просмотр результатов выполнения SQL-запросов, подключена ли мышь.

Теперь посмотрим, что же можно сделать с практической точки зрения. Будет также интересно сравнить «как было» и «как стало». Надеюсь, что следующий раздел будет интересен не только пытливому и цепкому взгляду опытного и погруженного в тему человеку.

Примеры использования ASDU


Теория закончилась. Давайте посмотрим на примеры реализации. Посмотрим, стало ли веселее и на правильном ли мы пути. Приведу несколько примеров использования библиотеки работы с экраном.

Вот так выглядит программа, выводящая на стандартный экран строки разного цвета, и результат ее работы:



Вместо «перфокарт»


Под спойлером приведена программа, написанная на С++ с использованием ASDU, функционально аналогичная приведенной выше на языке RPG с использованием DDS.



Программа
int main(int argc, char **argv)
 {
    char *msg = "Ошибка вызова";
    // Описание рамки окна
    _Box_T box = { TRUE,  '.', '.', '.', ':', ':', '.', ':', ':', ':', '.', '+' };

    if(argc > 1) {
        if(*argv[1] == 'A') {
            msg = "Внештатная ситуация №1";
        } else if(*argv[1] == 'B') {
            msg = "Внештатная ситуация №2";
        }
    }
    _Screen_T       screen('3');                // Инициализация экрана. Режим 24x80
    // Вывод сообщения на стандартный экран
    STDSCR->mvwcenter(0, "%cПример программы, использующей%cASDU", QSN_SA_WHT, QSN_SA_RED);

    _FrameDesc_T    desc(&box);                 // Создание описания фрейма по умолчанию

    desc.set_leading_attr(QSN_SA_WHT);          // Установить лидирующий атрибут окна
    desc.set_cur_border_attr(QSN_SA_BLU);       // Установить цвет рамки текущего окна

    _Frame_T        frame(6, 9, 9, 63, &desc);  // Создать окно
    _Win_T         *subwin = frame.sub();       // Получить указатель на область вывода

    frame.set_color(0, QSN_SA_BLU);             // Установить цвет строки 0
    subwin->mvwprintw(0, 0, "WINDEMO 100");     // Вывести наименование программы

    subwin->mvwprintw(0, subwin->getmaxx()-12, "%c18 ЯНВ 2019", QSN_SA_NORM);

    subwin->mvwcenter(2, "%s", msg);            // Вывести сообщение по центру строки 2

    frame.set_color(subwin->getmaxy()-2, QSN_SA_BLU);       // Установить цвет строки
    subwin->mvwprintw(subwin->getmaxy()-2, 0, "F3=Выход");  // Вывести сообщение

    for(;;) {
        subwin->wmove(0, 0);        // Установить курсор в координаты (0, 0) области вывода окна
        frame.refresh();            // Обновить экран при необходимости

        switch(subwin->wgetch()) {  // Получить код нажатой клавиши

            case QSN_F3:            // F3 - выход
                break;

            case QSN_ENTER:         // ENTER - стереть сообщение об ошибке в последней строке
                subwin->wmove(subwin->getmaxy()-1, 0);
                subwin->wclrtoeol();
                continue;

            default:                // Вывести сообщение об ошибке в последней строке
                subwin->mvwprintw(subwin->getmaxy()-1, 0, "Функциональная клавиша недопустима.");
                continue;
        }
        break;
    }
    // Завершение программы
    return 0;
 }


Если сравнить со статическим позиционным монстром, листинг которого я приводил в начале статьи, то здесь уже что-то управляемое и «живое». Конечно же, это все требует дальнейшей обертки. Важно отметить, что обертка здесь уже имеет право на существование, чего в статике не сделаешь. Таким образом, ASDU-собрат неплохо начинает себя показывать.

Массив строк


Реализацию этой и следующей задачи для статики можно записать в «unreal». Наверное, если пойти на принцип, то можно исхитриться, однако, стандарт явно не для таких задач.
Просмотр массива строк с возможностью поиска и выделения цветом найденных фрагментов.



Область отображения и вертикальной прокрутки может быть меньше размеров окна, что позволяет выводить дополнительную информацию. На слайде это текст TITLE и FOOTER. Возможна горизонтальная прокрутка, если ширина строк превышает ширину окна.

Массив строк с прокруткой


Просмотр массива строк с выделением текущей строки, вертикальной, горизонтальной прокруткой и возможностью организации меню.



SQL-процессор?


Если предыдущие два примера простейшие и вряд ли из-за них, как говорится, стоило огород городить, то теперь мы с вами заденем конкретику, которой не было в стандартном подходе в принципе. С ASDU в «зеленом экране», например, появляется возможность вывода результата произвольного SQL-запроса на экран. А если можем вывести и подсветить отдельные строки, то можно прикрутить и редактирование, удаление, маркировку, удобную фильтрацию и прочее.

Напомню, ранее это было принципиально невозможно. Сейчас же на базе ASDU логично сделать SQL-процессор, которого нет по умолчанию. Мои коллеги сделали такой SQL-процессор и, возможно, это будет темой для одной из статей.

Давайте посмотрим на экраны и описания функциональности в том виде, как у меня получилось передать словами и скриншотами.

Произвольный SQL-запрос


Реализация просмотра результатов выполнения произвольного SQL-запроса:

  1. Окно просмотра содержит горизонтальные секции, каждая из которых имеет свой источник данных. На скриншоте показано 5 секций. Секция заголовков, горизонтальных разделителей, данных, еще одна секция разделителей и секция сносок.
  2. Для каждой секции указывается количество строк экрана для отображения одной записи и количество отображаемых записей. Одна секция может быть расширяемой, то есть занимать все доступное от остальных секций пространство окна.
  3. Источники данных должны удовлетворять определенным требованиям. Должна быть обеспечена возможность получения количества полей, их размеров и содержимого, позиционирования на нужную запись.
  4. Все секции обеспечивают одинаковую и независимую друг от друга функциональность, то есть. фиксацию столбцов, скроллинг, фильтрацию, поиск, перемещение по записям и прочее. Команды управления секциями можно подавать в произвольном порядке.
  5. Все секции равноправны и могут иметь разную ширину столбцов и разное количество полей отображения, а также разное количество строк. Фактически каждая секция – это самостоятельная реализация просмотра, связанная с определенным источником данных.
  6. Данные в ячейках могут быть преобразованы при необходимости. Есть возможность использования callback-функций для любых полей, что позволяет применять нестандартные механизмы фильтрации данных и преобразовывать содержимое полей для отображения.
  7. Есть возможность выделять определенным цветом строки и столбцы, а также отдельные ячейки в зависимости от произвольных условий.
  8. Есть возможность фильтрации записей и их поиска с помощью указания метасимволов или по точному совпадению. Информация в ячейках может выравниваться в различных направлениях по горизонтали и вертикали.
  9. Рамка и символы-разделители между столбцами могут отсутствовать.
  10. Ширина всех отображаемых полей может превышать горизонтальные размеры экрана. В этом случае будет использована горизонтальная прокрутка. Левые поля таблицы просмотра могут быть зафиксированы и, таким образом, исключены из горизонтальной прокрутки.

Просмотр результатов выполнения SQL-запроса с возможностью редактирования некоторых полей текущей строки


Каждая запись секции данных занимает 3 строки экрана. Для включения режима редактирования необходимо указание уникальных ключевых полей.

Редактироваться могут поля текущей строки которые не подвержены горизонтальной прокрутке и полностью помещаются в ячейке. Измененные значения полей сохраняются в памяти до момента их использования. Данные в ячейках могут быть приведены к виду, необходимому для редактирования, с помощью callback-функций и провалидированы. Допустимо и обратное преобразование – от редактирования к просмотру. Для данных, занимающих более одной строки, создаются композитные поля.

Есть возможность фильтрации записей и их поиска с помощью указания метасимволов или по точному совпадению. При этом фильтрация осуществляется по первоначальной информации, а поиск – по вновь введенной.



Возможно и редактирование информации в нескольких секциях одновременно.



Просмотр SQL-запроса с возможностью редактирования некоторых полей


Этот режим аналогичен предыдущему, за исключением того, что возможно редактирование выбранных полей всех строк экрана. Возможно, в некоторых простых ситуациях такое табличное редактирование полей может быть уместно.



Да-да, эти примеры работают. Повторюсь, что они не придут на смену современным фреймворкам, например, по созданию web-приложений. Более того, кто-то из читателей моего труда по-прежнему останется при мнении о «потерянном зря времени» и предложит простые понятные (и, возможно, не совсем верные =)) пути решения подобного рода задач. Буду рад и благодарен прочесть аргументированные конструктивные комментарии.

При этом стоит отметить, что эти примеры активно и успешно заменяют ранее принятые в банке подходы к реализации приложений на «зеленом экране», войдя в число наших стандартов и в состав программного обеспечения (наряду с тем, про что писал мой коллега в прошлом году aka AS/400), получившего Роспатент. Также мне приятно осознавать, что результаты муторных исследований и кропотливого труда используются не только в Альфа-Банке.

Идем дальше. Впереди осталось положенное подведение итогов, литература и прочие стандартные атрибуты повествования.

Планы


В планах более полное использование возможностей потока данных 5250:

  1. Попытаться вернуться к использованию windows services и session services.
  2. Расширенные атрибуты (Write Extended Attribute Order). Возможность использовать одну позицию экрана для вывода символа и атрибута.
  3. Всплывающие меню (pop-up menus)
  4. Графические элементы оформления
  5. Поля выбора (selection fields)
  6. Поля прокрутки (scrollbar fields)
  7. Возможности выравнивания и форматирования полей ввода

Выводы


У прочитавшего статью все еще может возникнуть справедливый вопрос «И что?». Очень хочется понимать практический эффект от такого рода исследований.

Во-первых, возможность использования приведенных здесь наработок нашла живой отклик в сердцах бизнес-пользователей. Конечно, не будем преувеличивать и ожидать, что пользователи смогли оценить именно инженерную составляющую. Они оценили удобство работы, которое было предложено взамен стандартных возможностей «от производителя», а также скорость вносимых изменений. А Time-2-Market это наше, если не все, то многое.

Во-вторых, мы уже не первый год видим пользу от системного подхода к работе. В этом контексте это про работу коллектива разработчиков. Коллектив занимается не только кодированием, но и вкладывается в подробное фокусное изучение смежных областей. Это дает свои плоды иногда даже там, где изначально не ожидали. Однако, не зря говорят, что «везет тому, кто везет». Например, проделанная работа предоставила расширенные возможности по использованию роботов в автотестировании приложений, а это уже не только инженерная история, но и прямая и заметная экономия на затратах на тестирование приложений. Но это уже тема отдельной статьи.

И, наконец, повторюсь, что очень важно в современном мире: получилось не просто решить исследовательско-инженерную задачу, но и получить довольных бизнес-пользователей. А это значит, что продолжение следует.

Спасибо, что дочитали до конца.

Всем интересных задач и успехов в их разрешении.

P.S. В декабре 2019 года состоялась очередная Конференция разработчиков IBMi, где в том числе был сделан доклад на тему этой статьи.

Конференция разработчиков IBMi – это первая и единственная в России площадка для общения разработчиков на одноименной платформе. Здесь можно расширить круг своего профессионального общения, принести на всеобщее внимание свой кейс и пообщаться с коллегами. В этом году Конференция состоится 2-4 декабря. Читайте наши статьи, оставляйте комментарии.

Используемая документация


  1. Dynamic screen manager APIs (sc41-5606-00)
  2. IBM 5250 Information Display System Functions Reference Manual (sa21-9247-00)
  3. 5494 Remote Control Unit Functions Reference (sc30-3533-00)
  4. Application Display Programming (sc41-5715-00)
  5. DDS Reference (sc41-5712-00)
  6. Programming DDS for Display files
  7. Programming DDS Concepts
  8. IBM System i (aka AS/400) — Как мы делали автотесты приложений зеленого экрана
Tags:
Hubs:
Total votes 26: ↑26 and ↓0+26
Comments31

Articles

Information

Website
digital.alfabank.ru
Registered
Founded
1990
Employees
over 10,000 employees
Location
Россия