С чего начиналось
В 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 года — отличное напоминание, что инженерная смекалка не зависит от толщины канала связи.