С чего начиналось

В 98-м году я начинал программистом на MUMPS в среде MSM (Micronetics Standard MUMPS). Спустя годы решил проверить: что стало с этим сейчас, в 26-м?

MSM и Caché — закат

MSM как продукт не развивается уже около 20 лет. Хотя, где-то слышал, что в некоторых местах (например, в больницах Бразилии), до сих пор работает в проде.

Caché, прямой потомок MSM/ISM, тоже уходит на покой: финальные релизы поддержки Caché и Ensemble запланированы на первый квартал 2027 года. InterSystems перенесла разработку в IRIS — современную платформу с embedded Python, векторным поиском, FHIR-поддержкой (IRIS for Health) и контейнеризацией. Однако Epic — одна из крупнейших систем электронных медкарт в США — исторически построен на Caché/MUMPS, так что технология по факту жива и обслуживает половину американских больниц, просто невидимо.

Open-source: YottaDB

Раз IRIS проприетарен, любопытнее оказался open-source лагерь:

  • YottaDB — форк GT.M, AGPL-лицензия, активно развивается отдельной компанией. Главная архитектурная фишка — daemonless: движок базы работает прямо в адресном пространстве процесса приложения, без отдельного серверного демона, что убирает единую точку отказа. Для конкурентного доступа — Optimistic Concurrency Control вместо блокировок.
  • GT.M — оригинал от Fidelity/FIS, тоже открыт, но развитие сосредоточено в YottaDB.
  • FreeM — GPL, для энтузиастов, ближе к “чистому” ANSI MUMPS.

Вокруг YottaDB есть неплохая экосистема: Octo даёт SQL-доступ к тем же globals, Xider позволяет Redis-приложениям использовать YottaDB как бэкенд, есть C API и биндинги под Go и другие языки. Ставится через Docker в пару команд — внутри классический M-терминал, синтаксис почти не изменился с девяностых.

Раскопки в архиве: аптечная синхронизация по модему

Пока разбирался с YottaDB, нашёл свои древние программки 2000 года — самодельный протокол передачи данных между двумя MUMPS-системами через модем. По сути — кастомный аналог Kermit/XMODEM, только заточенный под передачу глобалов целиком, а не файлов.

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

 9:16 PM  7-SEP-00

COMGLB
COMGLB ; ОТМЕТКА ГЛОБАЛИ ДЛЯ ПЕРЕДАЧИ;;[ 07/09/00 20:48 ]
 New (NS,GLB,LN,LNZ,NEWD)
 Set J=$J,I=1,NEWD=0,G=$P(GLB,"^",2) Kill ^UTILITY(J)
 ;
 Set ^UTILITY(J,"TRANSINF")=""
 ;
 Set SLNZ=$O(@GLB@(NS,LN,LNZ)) Goto SLN:SLNZ="" 
 Set ^UTILITY(J,G,I)=3
 Set ^UTILITY(J,G,I,1,1)=1,^UTILITY(J,G,I,1,2)=NS
 Set ^UTILITY(J,G,I,2,1)=1,^UTILITY(J,G,I,2,2)=LN
 Set ^UTILITY(J,G,I,3,1)=2,^UTILITY(J,G,I,3,2)=SLNZ
 Set ^UTILITY(J,G,I,3,3)=$O(@GLB@(NS,LN,""),-1),I=I+1,NEWD=1
 ;
SLN Set SLN=$O(@GLB@(NS,LN)) Goto SEND:SLN="" 
 Set ^UTILITY(J,G,I)=2
 Set ^UTILITY(J,G,I,1,1)=1,^UTILITY(J,G,I,1,2)=NS
 Set ^UTILITY(J,G,I,2,1)=2,^UTILITY(J,G,I,2,2)=SLN
 Set ^UTILITY(J,G,I,2,3)=$O(@GLB@(NS,""),-1),NEWD=1
SEND Quit

COMINI
COMINI ; ИНИЦИАЛИЗАЦИЯ ПАРАМЕТРОВ СИСТЕМЫ ДЛЯ СВЯЗИ;;[ 07/09/00 20:53 ]
 Set INI=0                                         ;ФЛАГ ИНИЦИАЛИЗАЦИИ
 Set ^COM=NS                                       ;НОМЕР СИСТЕМЫ
 Set %IO=$G(^COM(NS,"%IO"),4)                      ;ПО УМОЛЧАНИЮ ПОРТ СВЯЗИ=4
 Set AT=$G(^COM(NS,"AT"),"ATDP620085") Quit:AT=""  ;СТРОКА СВЯЗИ (ATDP620085)
 Set KIP=$G(^COM(NS,"KIP"),"MGR:XXX") Quit:KIP=""  ;КИП ЦЕНТР-ГО КОМПЬЮТЕРА
 Set PSW=$G(^COM(NS,"PSW"),"COM")                  ;ПАРОЛЬ ДЛЯ ИДЕНТИФИКАЦИИ
 Set GLB=$G(^COM(NS,"GLB"),"^COMTMP") Quit:GLB=""  ;ГЛОБАЛЬ ДЛЯ ПЕРЕДАЧИ
 Set INI=1
 Quit

COMINIC
COMINIC ; ИНИЦИАЛИЗАЦИЯ ПАРАМЕТРОВ СИСТЕМЫ ДЛЯ СВЯЗИ;;[ 07/09/00 20:34 ]
        ; В ЦЕНТРАЛЬНОЙ СИСТЕМЕ
 Set INI=0                    ; ФЛАГ ИНИЦИАЛИЗАЦИИ
 Set COM="^[""MGR,XXX""]COM"  ; ГЛОБАЛЬ В MGR
 Set PSW=$G(@COM@(NS,"PSW"),"COM") ;ПАРОЛЬ ДЛЯ ИДЕНТИФИКАЦИИ
 Set GLB=$G(@COM@(NS,"GLB"),"^COMTMP") Quit:COMTMP=""  ;ГЛОБАЛЬ ДЛЯ ПЕРЕДАЧИ
 Set AFTER=$G(@COM@(NS,"AFTER"),"") ;ПРОГРАММА ДЛЯ ЗАПУСКА ПОСЛЕ СЕАНСА В ЦЕНТРЕ
 Set INI=1
 Quit

COMRCV
COMRCV ; ПРИЁМ;;[ 07/09/00 20:48 ]
 Set %IO=0,RES=$G(RES,1)
RGL ;ПРИЕМ ГЛОБАЛИ
 Set %COUNT=0,%ER=0 Use %IO:(0::::#802001:64)
 Set (%ER,%LAST,%SEQ,%TRFLG)=0,%HEX="0123456789ABCDEF"
 Set %REC=0,%DATA="#MSMTRANSGO#" Do SD Goto:%ER EXIT
 Set (%NEW,%REC)=1
RGL1 Do RD Goto EXIT:%ER
 If $E(%DATA)'="*" Set %GNAM=%DATA Do RD Goto EXIT:%ER Set @%GNAM=%DATA Goto RGL1
 If %DATA'="**" Set %NEW=1,%COUNT=0 Goto RGL1
 Goto EXIT
 ;
JIZMB ; ВОССТАНОВЛЕНИЕ ПО ЖУРНАЛУ ^JIZM
 Set RES=$G(RES,1)
 Set %COUNT=0,%ER=0 Use %IO:(0::::#802001:64)
 Set (%ER,%LAST,%SEQ,%TRFLG)=0,%HEX="0123456789ABCDEF"
 Set %REC=0,%DATA="#JIZMB#" Do SD Goto:%ER EXIT Set %REC=1
 Set NS=$G(^TRANSINF("NS")) Quit:NS=""
 Do ^COMINIC Quit:INI=0  ; ИНИЦИАЛИЗАЦИЯ ПАРАМЕТРОВ В ЦЕНТРЕ
 If PSW'=PSW0 Quit
BRUN Goto INFO:AFTER=""
 Set DN=$G(@GLB@(NS,0,"LAST","DATA"),$O(@GLB@(NS,0)))
 Set DNNZ=$G(@GLB@(NS,0,"LAST","NZ"),$O(@GLB@(NS,DN,"")))
 Set DK=$O(@GLB@(NS,""),-1),DKNZ=$O(@GLB@(NS,DK,""),-1)
 Do @AFTER
 ;
INFO ;;ПОДТВЕРЖДЕНИЕ
 Set ^TRANSINF="Ok",@GLB@(NS,0,"LAST","DATA")=DK,@GLB@(NS,0,"LAST","NZ")=DKNZ
 Set RES=$G(RES,1)
 Set %COUNT=0,%ER=0 Use %IO:(0::::#802001:64)
 Set (%ER,%LAST,%SEQ,%TRFLG)=0,%HEX="0123456789ABCDEF"
 Set %REC=1 
 Goto EXIT
 ;
SD ;SEND DATA ENTRY
 U %IO:0 G SD1:%DATA?.E1C.E S %HXD=0 F %I=1:94:$L(%DATA) S %FRAG=$E(%DATA,%I,%I+93),%EOD=0 S:%I+94>$L(%DATA) %EOD=1 D SD2 Q:%ER
 I %DATA="" S %FRAG="" D SD2
 Q
SD1 S %HXD=1 F %I=1:47:$L(%DATA) S %FRAG=$E(%DATA,%I,%I+46),%EOD=0 S:%I+47>$L(%DATA) %EOD=1 D CHEX,SD2 Q:%ER
 I %DATA="" S %FRAG="" D SD2
 Q
SD2 S %RT=0,%L=$L(%FRAG),%SEQ=%SEQ+1,%X=%SEQ#16+(16*%EOD)+(32*%HXD),%CODE=$C(%X+32)_$C(%L+32)_%FRAG
 S %CHK=$ZCRC($E(%CODE,1,%L+2),1)
SD3 W %CODE,%CHK,*13
 R %ANS:7 E  S %RT=%RT+1,%ERR(%RT)="1 "_%ANS G SD3:%RT<21,ER1
 I %ANS=(%SEQ#16_"R"_%CHK) S %LAST=%ANS W %SEQ#16,"S",%CHK,*13 Q
 I %ANS=%LAST S %RT=%RT+1,%ERR(%RT)="8 "_%ANS G:%RT>20 ER8 W $P(%LAST,"R",1),"S",$P(%LAST,"R",2),*13 G SD3
 S %RT=%RT+1,%ERR(%RT)="2 "_%ANS G SD3:%RT<21,ER2
 ;
RD ;RECEIVE DATA ENTRY
 S %DATA="",%RT=0 U %IO:0
RD1 R %CODE:30 E  G ER3
 S %X=$A(%CODE,1)-32,%L=$A(%CODE,2)-32,%FRAG=$E(%CODE,3,%L+2),%CHK=$E(%CODE,%L+3,999)
 I $L(%CODE)<2 S %CHK=""
 S %SQ=%X#16,%EOD=%X\16#2,%HXD=%X\32
 S %Y=$ZCRC($E(%CODE,1,%L+2),1)
 I %Y'=%CHK S %RT=%RT+1,%ERR(%RT)="4 "_%CODE G:%RT>20 ER4 W -1,*13 G RD1
 G RD1:%SQ=%SEQ,ER5:%SQ-%SEQ>1
 S %SEQ=%SQ S %ANS=%SEQ_"R"_%CHK S:%SEQ=15 %SEQ=-1
 D CHEX:%HXD S %DATA=%DATA_%FRAG
RD2 W %ANS,*13
RD3 R %X:4 E  S %RT=%RT+1,%ERR(%RT)=6 G RD2:%RT<21,ER6
 I %ANS=($P(%X,"S",1)_"R"_$P(%X,"S",2,3)) G RD1:'%EOD Q
 S %RT=%RT+1,%ERR(%RT)="7 "_%X G RD3:%RT<21,ER7
 ;
CHEX S %X=%FRAG,%FRAG="" G:%REC RD4
 F %J=1:1:$L(%X) S %Y=$A(%X,%J),%FRAG=%FRAG_$E(%HEX,%Y\16+1)_$E(%HEX,%Y#16+1)
 Q
RD4 F %J=1:2:$L(%X) S %FRAG=%FRAG_$C($F(%HEX,$E(%X,%J))-2*16+$F(%HEX,$E(%X,%J+1))-2)
 Q
LINKUP S %QF=0 Use:'RES 0:(::::1) D EOF
LINKUP1 S (%ER,%LAST,%SEQ,%TRFLG)=0,%HEX="0123456789ABCDEF"
 Use:'RES 0 W:'RES !,"Соединение с удаленной системой..." Use %IO:0 W *13 D CLR W %GO,*13 H 1
 D RD I '%ER,%DATA="#MSMTRANSGO#" Use:'RES 0 W:'RES !!,"Запуск пеpедачи данных глобали" Use %IO Q
 S %QF=%QF+1 I %QF<2 Use:'RES 0 W:'RES !!,"С удаленной системы не получено подтвеpждение, повтоp..." G LINKUP1
 If 'RES Use 0 W !,"СОЕДИНЕНИЕ НЕ УДАЛОСЬ" ;S %QUE=4 D ^%TRANS5
 Q
CLR F %I=0:0 R %X:1 Q:'$T
 Q
EON ; ECHO ON
 Use %IO:(:::::#840001) Q
EOF ; ECHO OFF + LINEFEED
 Use %IO:(::::#842001) Q
EXIT ;
 I '%REC,'%ER Use 0 W !,"Sending acknowledgement to remote system." S %DATA="**" D SD
 D EON Use:'RES 0:(:::::1)
 Q
ER1 S %ER=1,%X="No reply from remote system." G ERR
ER2 S %ER=2,%X="Bad checksum received; expected = "_(%SEQ#16)_"R"_%CHK_", received= "_$S(%ANS="":"NULL",1:%ANS)_"." G ERR
ER3 S %ER=3,%X="No new data received in 30 seconds." G ERR
ER4 S %ER=4,%X="Checksum received with new data= "_$S(%CHK="":"NULL",1:%CHK)_", calculated= "_$S(%Y="":"NULL",1:%Y)_"." G ERR
ER5 S %ER=5,%X="Missing record." G ERR
ER6 S %ER=6,%X="No checksum acknowledgement received." G ERR
ER7 S %ER=7,%X="Incorrect checksum received, expected= "_$P(%ANS,"R",1)_"S"_$P(%ANS,"R",2)_", received= "_$S(%X="":"NULL",1:%X)_"." G ERR
ER8 S %ER=8,%X="Checksum received from previous record." G ERR
ERR If 'RES Use 0 W !!,"Transmission Terminated",$S(%ER=3!(%ER=5):".",1:" after 20 retries."),!,%X
 Q

COMSEND
COMSEND ; ПЕРЕДАЧА В УДАЛЕННУЮ СИСТЕМУ;;[ 07/09/00 20:49 ]
 New (NS,SENDERR,RES)
 Set SENDERR=1 ;ФЛАГ ОШИБКИ
 Set RES=$G(RES,0) ;ПРИЗНАК РЕЗИДЕНТНОСТИ ЗАПУСКА
 Set NS=1 ;!!! ОТЛАДКА -ЕРМЕК
 Quit:'$D(NS)  ;НОМЕРА ЖУРНАЛОВ ДЛЯ ПЕРЕДАЧИ
 Set $ZT="ERROR^COMSEND" 
 If 'RES Use 0::0 Else  W "Окно занято... Запустите с параметром RES=1" Hang 2 Quit
 If 'RES Use 0 Write #,!,"Передача данных ...(Для остановки нажмите CTRL/C)",!
S Set %MSM=2,%PC=1 ; ОТКРЫТИЕ ПОРТА
 Do ^COMINI Quit:INI=0  ; ИНИЦИАЛИЗАЦИЯ ПАРАМЕТРОВ
 ;
 Open %IO::0 Else  W "Линия занята..." Hang 2 Quit
 Do EOF ; СВЯЗЫВАНИЕ
NUM Use %IO Write "+++ATH0",*13 Do CLR Hang 2
 Use %IO Write AT,*13 Do CLR Hang 2 Set K=""
 For  Use %IO Read S:1 Use:'RES 0 Read:'RES K:0 Write:'RES S Set S=$S(S["CONNECT":1,S["NO CARRIER":2,S["BUSY":3,$A(K)>0:4,1:0) Quit:S
 If S'=1 Goto EXIT
 Hang 2 Use %IO Write *13 Do CLR
EU Use %IO:(0::::#801001:64) Use:'RES 0:(0::::64:#800001) Break 1 ; ВХОД В КИП
 Use %IO Write KIP,*13 
 ;
 ;ОПРЕДЕЛЕНИЕ ПОСЛЕДНЕЙ ДАТЫ И НОМЕРА ЗАПИСИ, 
 ;ПЕРЕДАННЫХ ПО ДАННОЙ ЛИНИИ, ПО НОМЕРАМ ЖУРНАЛОВ
 Set OK=0
 Goto NOGLB:'$D(@GLB@(NS))
 Set ^TRANSINF("NS")=NS ;ПЕРЕДАЁТСЯ НОМЕР СИСТЕМЫ
 If $D(@GLB@(NS,0,"LAST","DATA"))+$D(@GLB@(NS,0,"LAST","NZ"))#10=2 Set LN=@GLB@(NS,0,"LAST","DATA"),LNZ=@GLB@(NS,0,"LAST","NZ")
 Else  Set LN=$O(@GLB@(NS,0)),LNZ=""
 Goto NOGLB:+LN<1  ; ВЫХОД ЕСЛИ НЕТ ЖУРНАЛЬНЫХ ЗАПИСЕЙ
 If 'RES Use 0 Write !,"Глобаль: ",GLB,"из системы № ",NS," Последняя дата передачи: ",$ZD(LN),!
 Set %ER=0 
 Do ^COMGLB ;ВЫДЕЛЕНИЕ @GLB = D INT^%GSEL
 ;
SEND Goto NOGLB:'NEWD 
 Do SG Goto:%ER EXIT
SENDED Set:OK=1 SENDERR=0
 Goto EXIT
NOGLB Set OK=OK+1 Goto SENDED
 ;
SG Set %GO="S %IO=0,%MSM=2,%PC=1 D ^COMRCV",%JOB=$J,%REC=0
 Do LINKUP Quit:%ER  Set %GSINT=0
 Quit:'$D(^UTILITY($J))  Set %GN=""
 Kill ^COMTRN
 Set %NEW=1 Use %IO Do 1^COMTRN
 If %ER Do COMTRN Do EXIT Goto COMSEND
 If '%ER,'RES Use 0 Write !,"Передача ",GLB," завершена."
 Set %DATA="**" Do SD
 Set %GO="Set PSW0="_PSW_" Do JIZMB^COMTRN",%REC=0 Do LINKUP Quit:%ER
 Set (DATA,@GLB@(NS,0,"LAST","DATA"))=$O(@GLB@(NS,""),-1)
 Set @GLB@(NS,0,"LAST","NZ")=$O(@GLB@(NS,DATA,""),-1)
 Set OK=OK+1
 Quit
 ;
COMTRN ; АНАЛИЗ ПОСЛЕДНЕГО ПЕРЕДАННОГО УЗЛА 
 Set COMTRN=$P($G(^COMTRN(NS)),")") Quit
 Set DATA=$P(COMTRN,",",2)
 Set NZ=$P(COMTRN,",",3)
 If DATA'="",NZ'="" Set @GLB@(NS,0,"LAST","DATA")=DATA,@GLB@(NS,0,"LAST","NZ")=NZ
 Quit
 ;
SD ;SEND DATA ENTRY
 U %IO:0 G SD1:%DATA?.E1C.E S %HXD=0 F %I=1:94:$L(%DATA) S %FRAG=$E(%DATA,%I,%I+93),%EOD=0 S:%I+94>$L(%DATA) %EOD=1 D SD2 Q:%ER
 I %DATA="" S %FRAG="" D SD2
 Q
SD1 S %HXD=1 F %I=1:47:$L(%DATA) S %FRAG=$E(%DATA,%I,%I+46),%EOD=0 S:%I+47>$L(%DATA) %EOD=1 D CHEX,SD2 Q:%ER
 I %DATA="" S %FRAG="" D SD2
 Q
SD2 S %RT=0,%L=$L(%FRAG),%SEQ=%SEQ+1,%X=%SEQ#16+(16*%EOD)+(32*%HXD),%CODE=$C(%X+32)_$C(%L+32)_%FRAG
 S %CHK=$ZCRC($E(%CODE,1,%L+2),1)
SD3 W %CODE,%CHK,*13
 R %ANS:7 E  S %RT=%RT+1,%ERR(%RT)="1 "_%ANS G SD3:%RT<21,ER1
 I %ANS=(%SEQ#16_"R"_%CHK) S %LAST=%ANS W %SEQ#16,"S",%CHK,*13 Q
 I %ANS=%LAST S %RT=%RT+1,%ERR(%RT)="8 "_%ANS G:%RT>20 ER8 W $P(%LAST,"R",1),"S",$P(%LAST,"R",2),*13 G SD3
 S %RT=%RT+1,%ERR(%RT)="2 "_%ANS G SD3:%RT<21,ER2
 ;
RD ;RECEIVE DATA ENTRY
 S %DATA="",%RT=0 U %IO:0
RD1 R %CODE:30 E  G ER3
 S %X=$A(%CODE,1)-32,%L=$A(%CODE,2)-32,%FRAG=$E(%CODE,3,%L+2),%CHK=$E(%CODE,%L+3,999)
 I $L(%CODE)<2 S %CHK=""
 S %SQ=%X#16,%EOD=%X\16#2,%HXD=%X\32
 S %Y=$ZCRC($E(%CODE,1,%L+2),1)
 I %Y'=%CHK S %RT=%RT+1,%ERR(%RT)="4 "_%CODE G:%RT>20 ER4 W -1,*13 G RD1
 G RD1:%SQ=%SEQ,ER5:%SQ-%SEQ>1
 S %SEQ=%SQ S %ANS=%SEQ_"R"_%CHK S:%SEQ=15 %SEQ=-1
 D CHEX:%HXD S %DATA=%DATA_%FRAG
RD2 W %ANS,*13
RD3 R %X:4 E  S %RT=%RT+1,%ERR(%RT)=6 G RD2:%RT<21,ER6
 I %ANS=($P(%X,"S",1)_"R"_$P(%X,"S",2,3)) G RD1:'%EOD Q
 S %RT=%RT+1,%ERR(%RT)="7 "_%X G RD3:%RT<21,ER7
 ;
CHEX S %X=%FRAG,%FRAG="" G:%REC RD4
 F %J=1:1:$L(%X) S %Y=$A(%X,%J),%FRAG=%FRAG_$E(%HEX,%Y\16+1)_$E(%HEX,%Y#16+1)
 Q
RD4 F %J=1:2:$L(%X) S %FRAG=%FRAG_$C($F(%HEX,$E(%X,%J))-2*16+$F(%HEX,$E(%X,%J+1))-2)
 Q
LINKUP Set %QF=0 Use:'RES 0:(::::1) Do EOF
LINKUP1 Set (%ER,%LAST,%SEQ,%TRFLG)=0,%HEX="0123456789ABCDEF"
 If 'RES Use 0 Write !,"Соединение с удаленной системой..." 
 Use %IO:0 Write *13 Do CLR Write %GO,*13 Hang 1
 Do RD 
 If '%ER,%DATA="#MSMTRANSGO#" Use:'RES 0 Write:'RES !!,"Запуск пеpедачи данных глобали" Use %IO Quit
 If '%ER,%DATA="#JIZMB#" Use:'RES 0 Write:'RES !!,"Запуск восстановления данных по журналу" Use %IO Quit
 Set %QF=%QF+1 If %QF<2 Use:'RES 0 Write:'RES !!,"С удаленной системы не получено подтвеpждение, повтоp..." Goto LINKUP1
 If 'RES,'RES Use 0 Write !,"СОЕДИНЕНИЕ НЕ УДАЛОСЬ" ;Set %QUE=4 Do ^%TRANS5
 Quit
CLR F %I=0:0 R %X:1 Q:'$T
 Q
EON ; ECHO ON
 U %IO:(:::::#840001) Q
EOF ; ECHO OFF + LINEFEED
 U %IO:(::::#842001) Q
EXIT ;
 Do EON 
 Use %IO:(:::::#801001) Write *3,*13,"H ",*13 Close %IO
 If 'RES Use 0:(:::::#800001) Write !!,"Соединение завершено",!
 View 0:$J:$ZB($V(0,$J,2),#400,7):2
 Quit
ERROR ;
 If $F($ZE,"<INRPT>") Use:'RES 0 Write:'RES !,"...Прервано" G EXIT ;V 0:$J:$ZB($V(0,$J,2),#400,7):2
 If $F($ZE,"<DSCON>") Use:'RES 0 Write:'RES !,"...Отсоединение" G EXIT ;V 0:$J:$ZB($V(0,$J,2),#400,7):2
 Q
ER1 S %ER=1,%X="Нет ответа от удаленной системмы." G ERR
ER2 S %ER=2,%X="Ошибка в контрольной сумме; ожидалось= "_(%SEQ#16)_"R"_%CHK_", получено= "_$S(%ANS="":"NULL",1:%ANS)_"." G ERR
ER3 S %ER=3,%X="Не получено новых данных в течении 30 секунд." G ERR
ER4 S %ER=4,%X="Полученная контрольная сумма содержит новые данные= "_$S(%CHK="":"NULL",1:%CHK)_", подсчитано= "_$S(%Y="":"NULL",1:%Y)_"." G ERR
ER5 S %ER=5,%X="Потеряна запись." G ERR
ER6 S %ER=6,%X="Нет подтверждения о приёме." G ERR
ER7 S %ER=7,%X="Ошибка в контрольной сумме, ожидалось= "_$P(%ANS,"R",1)_"S"_$P(%ANS,"R",2)_", получено= "_$S(%X="":"NULL",1:%X)_"." G ERR
ER8 S %ER=8,%X="Контрольная сумма получена от предыдущей записи." G ERR
ERR If 'RES Use 0 Write !!,"Передача прервана ",$S(%ER=3!(%ER=5):".",1:" после 20 повторных попыток."),!,%X
 Q

COMTRN
%TRANS4 ;CDS;BFH;[ 07/09/00 20:37 ]
 ;Copyright Micronetics Design Corp. @1984
0 K %GN Q:'$D(^UTILITY(%JOB))  S %GN="" Q
1 S %GN=$O(^UTILITY(%JOB,%GN)) G:%GN="" DONE S %COUNT=0 S %GI1=$O(^UTILITY(%JOB,%GN,""))
 I $D(^UTILITY(%JOB,%GN))=1 S (%GV,%GT)="^"_%GN D:$D(@%GT)#2 PROC G:%ER DONE G 5
 I $D(^UTILITY(%JOB,%GN))=1 S %SN=1,(%P,%RG,%S(1,1))=0,%S(1,2)="" G 3
2 S %SN=^UTILITY(%JOB,%GN,%GI1),(%P,%RG)=0 S:%SN<1 %P=1,%SN=-%SN
 I %SN=0 S (%GV,%GT)="^"_%GN D:$D(@%GT)#2 PROC G:%ER DONE G 4
 F %GI2=1:1:%SN S %S(%GI2,1)=^UTILITY(%JOB,%GN,%GI1,%GI2,1) S %S(%GI2,2)=$S(^(1):^(2),1:"") S:^(1)=2 %S(%GI2,3)=^(2),%S(%GI2,4)=^(3)
3 S %T="^"_%GN_"(",%GT=%T_",",%GI2=1 D PR1
4 S %GI1=$O(^UTILITY(%JOB,%GN,%GI1)) D BREAK G:%ER DONE G 2:%GI1'="",1
5 S %GT=%GT_"("""")"
 F %GI1=0:0 S (%GT,%GV)=$ZO(@%GT) Q:%GT=""  D PROC G:%ER DONE
 G:%ER DONE D BREAK G:%ER DONE G 1
PR1 S %S(%GI2,5)=$L(%T),%S(%GI2,6)=$L(%GT)-1
 S %T=%T_"%S("_%GI2_",2)",%GV=%T_")",%T=%T_","
 G PR2:%S(%GI2,1)=1,SET0:%S(%GI2,1)=0
SET2 S:%S(%GI2,2)="" %S(%GI2,2)=%S(%GI2,3) S %RG=1
 I %S(%GI2,3)?.N&(%S(%GI2,4)?.N) S %NXT2="I %S(%GI2,2)'>%S(%GI2,4)",%NX2=1
 E  S %NXT2="I %S(%GI2,2)']%S(%GI2,4)",%NX2=0
 G PR2
SET0 I %S(%GI2,2)="" S %GV1=$E(%T,1,%S(%GI2,5))_""""")",%S(%GI2,2)=$O(@%GV1) Q:%S(%GI2,2)=""  S %RG=1
PR2 S %DGV="""" F %GI3=1:1 S %DGV=%DGV_$P(%S(%GI2,2),"""",%GI3)_"""" Q:$P(%S(%GI2,2),"""",%GI3+1)=""  S %DGV=%DGV_""""
 S:$E(%S(%GI2,2),$L(%S(%GI2,2)))="""" %DGV=%DGV_""""""
 S %GT=$E(%GT,1,%S(%GI2,6))_$S(%GI2>1:",",1:"")_%DGV_")",%DGV=$D(@%GV)
 I %GI2'<%SN,(%DGV#2) D PROC G:%ER DONE G PR3
 I %GI2=%SN,'%RG,'%DGV D UNDEF Q
 I %GI2=%SN,'%RG,%P,'(%DGV#2) D NOVAL Q
PR3 I '(%GI2=%SN&%P),(%DGV'<10) S %GI2=%GI2+1 S:%GI2>%SN %S(%GI2,1)=0,%S(%GI2,2)="" D PR1
 Q:(%P&'%RG)  G PR4:%S(%GI2,1)=1,NXT0:%S(%GI2,1)=0
NXT2 S %S(%GI2,2)=$O(@%GV)
 I %S(%GI2,2)'="",'(%NX2&(%S(%GI2,2)'?.N)) X %NXT2 G:$T PR2
 S %S(%GI2,2)=%S(%GI2,3) G PR4
NXT0 S %S(%GI2,2)=$O(@%GV) I %S(%GI2,2)'="" G PR2
PR4 Q:%GI2=1  S %T=$E(%T,1,%S(%GI2,5)-1),%GV=%T_")",%T=%T_"," S %GI2=%GI2-1 Q
PROC ; PROCESS ONE GLOBAL RECORD
 I %NEW S %NEW=0,%COUNT=0 ;I $D(%IO) U 0 W !,%GT U %IO
 S %DATA=%GT D SD Q:%ER  S:'%TRFLG %TRFLG=1 S %DATA=@%GV D SD Q:%ER  S %COUNT=%COUNT+1 ;I $D(%IO),'(%COUNT#50) U 0 F %I=$X:-1:1 W $C(8,32,8) I %I=1 W %GT U %IO
 Set ^COMTRN(NS)=%GV ; ЗАПОМИНАЕМ ПОСЛЕДНИЙ УСПЕШНО ПЕРЕДАННЫЙ УЗЕЛ
 Q
UNDEF S %NEW=1 Q  ;:'$D(%IO)  ;U 0 W !,*7,%GT," is undefined." U %IO Q
NOVAL S %NEW=1 Q  ;:'$D(%IO)  ;U 0 W !,*7,%GT," does not have a value." U %IO Q
BREAK S %DATA="*" D SD Q:%ER  S %NEW=1 ;I $D(%IO) U 0 W ?17,"   completed" U %IO
 Q  ;END OF GLOBAL
SD ;SEND DATA ENTRY
 G SD1:%DATA?.E1C.E S %HXD=0 F %I=1:94:$L(%DATA) S %FRAG=$E(%DATA,%I,%I+93),%EOD=0 S:%I+94>$L(%DATA) %EOD=1 D SD2 Q:%ER
 I %DATA="" S %FRAG="" D SD2
 Q
SD1 S %HXD=1 F %I=1:47:$L(%DATA) S %FRAG=$E(%DATA,%I,%I+46),%EOD=0 S:%I+47>$L(%DATA) %EOD=1 D CHEX,SD2 Q:%ER
 I %DATA="" S %FRAG="" D SD2
 Q
SD2 S %RT=0,%L=$L(%FRAG),%SEQ=%SEQ+1,%X=%SEQ#16+(16*%EOD)+(32*%HXD),%CODE=$C(%X+32)_$C(%L+32)_%FRAG
 S %X=$E(%CODE,1,%L+2) I %MSM>1!($D(%IO)) S %CHK=$ZCRC(%X,1)
 E  D ZCRC^MSMTRAN2 S %CHK=%Y
SD3 W %CODE,%CHK,*13
 R %ANS:7 E  S %RT=%RT+1,%ERR(%RT)="1 "_%ANS W:'%TRFLG $C(1,13) G SD3:%RT<21 S %ER=1 G ERR
 S %ANS=$TR(%ANS,$C(0),"")
 I %ANS=(%SEQ#16_"R"_%CHK) S %LAST=%ANS W %SEQ#16,"S",%CHK,*13 Q
 I %ANS=%LAST S %RT=%RT+1,%ERR(%RT)="8 "_%ANS H:%PC 3 S:%RT>20 %ER=8 G:%RT>20 ERR W $P(%LAST,"R",1),"S",$P(%LAST,"R",2),*13 G SD3
 S %RT=%RT+1,%ERR(%RT)="2 "_%ANS H:%PC 3 W:%ANS="1R942" "1S942",*13 G SD3:%RT<21 S %ER=2 D:%MSM>0!(%MSM=-2) ^%ET Q
CHEX S %X=%FRAG,%FRAG=""
 F %J=1:1:$L(%X) S %Y=$A(%X,%J),%FRAG=%FRAG_$E(%HEX,%Y\16+1)_$E(%HEX,%Y#16+1)
 Q
 ;
DONE Q  ;K %S,%SN,%GV,%GT,%GV1,%GI1,%GI2,%GI3,%P,%RG,%T,%DGV,%NXT2,%NX2 Q
ERR ;Transmission Terminated.
 ;I $D(%IO) G ERR^COMTRN
 D:%MSM>0!(%MSM<0&(%MSM>-5)) ^%ET Q
SUM S %CHK=0 F %I=1:1 S %T=$T(+%I) Q:%T=""  F %J=1:1:$L(%T) S %CHK=%CHK+$A(%T,%J)
 W "*"_"S"_"U"_"M"_"*"_%CHK,*13 K %CHK,%I,%J,%T Q

Из интересного в самом коде:

  • COMGLB — маркировщик изменений: проходит по узлам глобаля и записывает в ^UTILITY, что именно нужно передать с прошлой синхронизации.
  • COMINI/COMINIC — читают параметры связи (AT — строка дозвона модема вида ATDP620085, KIP — логин на удалённой машине, PSW — пароль) из ^COM(NS,...).
  • SD/RD (Send Data / Receive Data) — ручная реализация транспортного протокола поверх COM-порта: данные режутся на фрагменты по 94 символа (47 в hex-режиме), на каждый фрагмент считается контрольная сумма через $ZCRC, отправляется, ожидается ACK с той же суммой, при неудаче — до 20 ретраев. Есть даже защита от повторно доставленных ACK-подтверждений.
  • COMSEND — верхнеуровневый сценарий: открывает порт, шлёт модему +++ATH0, набирает номер, ждёт CONNECT/NO CARRIER/BUSY, логинится и удалённо запускает ^COMRCV через переменную %GO, которая отправляется как команда на выполнение прямо в терминал удалённой машины — самодельный RPC через ввод в командную строку.
  • COMTRN (он же %TRANS4) — вшитая утилита из поставки MSM с копирайтом Micronetics Design Corp. 1984 года, для рекурсивного обхода дерева глобаля произвольной вложенности.

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

Итог

MUMPS не умер — он просто ушёл в тень крупных проприетарных систем (Epic на Caché/IRIS) и в open-source нишу (YottaDB, GT.M, FreeM). А код из 2000 года — отличное напоминание, что инженерная смекалка не зависит от толщины канала связи.