diff --git a/carp/install.sh b/carp/install.sh new file mode 100644 index 0000000..e69de29 diff --git a/src/Carp/CarpApplication.class.st b/src/Carp/CarpApplication.class.st index e9dcf95..a48de25 100644 --- a/src/Carp/CarpApplication.class.st +++ b/src/Carp/CarpApplication.class.st @@ -10,7 +10,7 @@ Class { { #category : #'start-stop' } CarpApplication class >> start [ - ^ self startWith: LanguageLinkSettings jsDefaultSettings. + ^ self startWith: LanguageLinkSettings carpDefaultSettings. ] { #category : #accessing } @@ -18,10 +18,17 @@ CarpApplication >> baseApplication [ ^ CarpApplication ] +{ #category : #accessing } +CarpApplication >> debuggerClientFor: anException [ + "Answer the debugger client to be used by the Gt Post Mortem debugger" + + ^ CarpPostMortemDebugger new exception: anException +] + { #category : #accessing } CarpApplication >> initializeHandlers [ loggingHandler := LanguageLinkLoggingHandler application: self. communicationHandler := LanguageLinkCommunicationHandler application: self. processHandler := LanguageLinkServerHandler application: self. - "executionHandler := CarpExecutionHandler application: self" + executionHandler := CarpExecutionHandler application: self ] diff --git a/src/Carp/CarpCommandFactory.class.st b/src/Carp/CarpCommandFactory.class.st new file mode 100644 index 0000000..cf97cb1 --- /dev/null +++ b/src/Carp/CarpCommandFactory.class.st @@ -0,0 +1,5 @@ +Class { + #name : #CarpCommandFactory, + #superclass : #LanguageLinkCommandFactory, + #category : #'Carp-Execution' +} diff --git a/src/Carp/CarpDeserializer.class.st b/src/Carp/CarpDeserializer.class.st new file mode 100644 index 0000000..4cbf7c2 --- /dev/null +++ b/src/Carp/CarpDeserializer.class.st @@ -0,0 +1,22 @@ +Class { + #name : #CarpDeserializer, + #superclass : #LanguageLinkDeserializer, + #category : #'Carp-Serialization' +} + +{ #category : #accessing } +CarpDeserializer >> buildProxyFor: rawObject [ + | proxy | + proxy := CarpProxyObject + carpType: (rawObject at: #carptype) + var: (rawObject at: #carpvar) asJSGI + application: self application. + self executionHandler registerObject: proxy. + ^ proxy +] + +{ #category : #accessing } +CarpDeserializer >> deserialize: anObject [ + ^ self new + deserialize: anObject +] diff --git a/src/Carp/CarpExecutionHandler.class.st b/src/Carp/CarpExecutionHandler.class.st new file mode 100644 index 0000000..4bb8f79 --- /dev/null +++ b/src/Carp/CarpExecutionHandler.class.st @@ -0,0 +1,20 @@ +Class { + #name : #CarpExecutionHandler, + #superclass : #LanguageLinkExecutionHandler, + #category : #'Carp-LanguageLink' +} + +{ #category : #accessing } +CarpExecutionHandler >> initializeHandler [ + + commandQueue := LanguageLinkCommandRegistry executionHandler: self. + mapperFactory := LanguageLinkMapperFactory forExecutionHandler: self. + promiseRegistry := LanguageLinkPromiseRegistry new. + weakRegistry := self settings platform weakRegistry. + objectRegistry := LanguageLinkObjectRegistry new. + + self communicationHandler + addHandler: [ :msg | self notifyHandler: msg ] forMessageClass: LanguageLinkUpdatePromiseMessage; + addHandler: [ :msg | self notifyErrorHandler: msg ] forMessageClass: LanguageLinkErrorMessage; + addHandler: [ :msg | self notifyCallbackHandler: msg ] forMessageClass: LanguageLinkCallbackMessage +] diff --git a/src/Carp/CarpLinkBinding.class.st b/src/Carp/CarpLinkBinding.class.st new file mode 100644 index 0000000..68ddd8e --- /dev/null +++ b/src/Carp/CarpLinkBinding.class.st @@ -0,0 +1,5 @@ +Class { + #name : #CarpLinkBinding, + #superclass : #LanguageLinkBinding, + #category : #'Carp-Execution' +} diff --git a/src/Carp/CarpPlatform.class.st b/src/Carp/CarpPlatform.class.st new file mode 100644 index 0000000..0e366a4 --- /dev/null +++ b/src/Carp/CarpPlatform.class.st @@ -0,0 +1,117 @@ +Class { + #name : #CarpPlatform, + #superclass : #LanguageLinkPlatform, + #classVars : [ + 'RuntimeSourceDirectory' + ], + #category : #'Carp-Processes' +} + +{ #category : #hooks } +CarpPlatform class >> weakRegistryClass [ + ^ LanguageLinkPharoWeakRegistry +] + +{ #category : #accessing } +CarpPlatform >> copyApplicationTo: appFolder application: application [ + "Copy the Carp runtime environment to the specified folder" + | srcDir cpCommand proc error | + + srcDir := self runtimeSourceDirectoryFor: application. + srcDir resolve = appFolder resolve ifTrue: [ ^ self ]. + + "Copy the runtime directory" + cpCommand := String streamContents: [ :stream | + stream + << 'cp -a "'; + << srcDir fullName; + << '" "'; + << appFolder fullName; + << '"' ]. + proc := GtSubprocessWithInMemoryOutput new + shellCommand: cpCommand; + runAndWait. + proc isSuccess ifFalse: + [ error := LanguageLinkProcessError new + messageText: 'Unable to install Carp runtime'; + application: application; + process: proc. + error signal ]. + +] + +{ #category : #accessing } +CarpPlatform >> ensureApplicationDirectory: application [ + "If the runtimeFolder doesn't exist, attempt to symlink to the respository directory" + | appFolder | + + application settings workingDirectory exists ifFalse: + [ appFolder := application settings workingDirectory. + appFolder exists ifTrue: [ ^ self ]. + self copyApplicationTo: appFolder application: application ]. + self setExecutableFor: application. + +] + +{ #category : #accessing } +CarpPlatform >> ensureEnvironmentForApp: anApplication [ + + self ensureApplicationDirectory: anApplication. +] + +{ #category : #accessing } +CarpPlatform >> folderForApplication [ + "Answer the directory where JSLink runs from" + + ^ FileLocator imageDirectory / 'carp' +] + +{ #category : #accessing } +CarpPlatform >> newRandomName [ + ^ 'carp' , UUID new asString36 +] + +{ #category : #accessing } +CarpPlatform >> runtimeSourceDirectoryFor: aCarpApplication [ + "Answer the source directory containing the runtime files. + This is the first of: + 1. RuntimeSourceDirectory (if specified) + 2. The git repository copy. + 3. An existing copy in the image directory" + | fileReference | + + (RuntimeSourceDirectory isNotNil and: [ RuntimeSourceDirectory exists ]) ifTrue: + [ ^ RuntimeSourceDirectory ]. + IceRepository registry + detect: [ :each | each includesPackageNamed: aCarpApplication class package name ] + ifFound: [ :repository | + fileReference := repository location / 'carp'. + fileReference exists ifTrue: [ ^ fileReference ] ] + ifNone: [ ]. + fileReference := self folderForApplication. + fileReference exists ifTrue: [ ^ fileReference ]. + self error: 'Unable to locate Carp runtime source'. +] + +{ #category : #accessing } +CarpPlatform >> setExecutableFor: application [ + "Copy the Carp runtime environment to the specified folder" + | chmodCommand proc error appFolderString | + + appFolderString := application settings workingDirectory resolve fullName copyReplaceAll: '"' with: '\"'. + chmodCommand := String streamContents: [ :stream | + stream + << 'chmod +x "'; + << appFolderString; + << '"/*.sh' ]. + proc := GtSubprocessWithInMemoryOutput new + shellCommand: chmodCommand; + runAndWait. + proc isSuccess ifFalse: + [ error := LanguageLinkProcessError new + messageText: 'Unable to set Carp runtime executable bits'; + application: application; + process: proc. + error signal ]. + +] diff --git a/src/Carp/CarpPostMortemDebugger.class.st b/src/Carp/CarpPostMortemDebugger.class.st new file mode 100644 index 0000000..8fc9caf --- /dev/null +++ b/src/Carp/CarpPostMortemDebugger.class.st @@ -0,0 +1,83 @@ +Class { + #name : #CarpPostMortemDebugger, + #superclass : #Object, + #instVars : [ + 'exception', + 'stackFrames', + 'frameRegex' + ], + #category : #'Carp-Debugger' +} + +{ #category : #accessing } +CarpPostMortemDebugger >> exception [ + ^ exception +] + +{ #category : #accessing } +CarpPostMortemDebugger >> exception: anException [ + exception := anException +] + +{ #category : #accessing } +CarpPostMortemDebugger >> initialize [ + + super initialize. + frameRegex := '\s+at.+\(([^:]+)\:(\d+)\:(\d+)\)' asRegexIgnoringCase. +] + +{ #category : #accessing } +CarpPostMortemDebugger >> sourceStyler [ + "Answer the styler used by the source code editor for this exception" + + ^ CarpParser gtStyler +] + +{ #category : #accessing } +CarpPostMortemDebugger >> stackFrameFromLine: aString ordinal: ordinal [ + "Answer a frame if the supplied string contains a valid file and line number, or nil" + + | file line column | + + ^ (frameRegex search: aString) ifTrue: + [ file := frameRegex subexpression: 2. + line := frameRegex subexpression: 3. + column := frameRegex subexpression: 4. + CarpPostMortemStackFrame new + ordinal: ordinal; + displayString: aString; + exception: exception; + file: file asFileReference; + line: line asNumber; + column: column asNumber ] + ifFalse: + [ nil ] + +] + +{ #category : #accessing } +CarpPostMortemDebugger >> stackFrames [ + "Answer a ordered collection of stack frames. + This is called many times by the debugger, so cache" + | ordinal | + + ^ stackFrames ifNil: + [ ordinal := 1. + stackFrames := OrderedCollection new. + exception trace lines do: [ :line | + (self stackFrameFromLine: line ordinal: ordinal) ifNotNil: [ :frame | + stackFrames add: frame. + ordinal := ordinal + 1 ] ]. + stackFrames ]. + +] + +{ #category : #accessing } +CarpPostMortemDebugger >> stderr [ + ^ exception application stderr +] + +{ #category : #accessing } +CarpPostMortemDebugger >> stdout [ + ^ exception application stdout +] diff --git a/src/Carp/CarpPostMortemStackFrame.class.st b/src/Carp/CarpPostMortemStackFrame.class.st new file mode 100644 index 0000000..eb71e7f --- /dev/null +++ b/src/Carp/CarpPostMortemStackFrame.class.st @@ -0,0 +1,5 @@ +Class { + #name : #CarpPostMortemStackFrame, + #superclass : #GtJavaScriptPostMortemStackFrame, + #category : #'Carp-Debugger' +} diff --git a/src/Carp/CarpProcess.class.st b/src/Carp/CarpProcess.class.st new file mode 100644 index 0000000..fb1ca26 --- /dev/null +++ b/src/Carp/CarpProcess.class.st @@ -0,0 +1,142 @@ +Class { + #name : #CarpProcess, + #superclass : #LanguageLinkAbstractProcess, + #instVars : [ + 'process', + 'environmentVariables' + ], + #classVars : [ + 'CarpPath' + ], + #category : #'Carp-Processes' +} + +{ #category : #accessing } +CarpProcess class >> resolveCarpPath [ + | proc | + + proc := GtSubprocessWithInMemoryOutput new + command: 'which'; + arguments: { 'carp' }. + CarpPlatform subProcessEnvironmentDictionary keysAndValuesDo: [ :key :value | + proc environmentAt: key put: value ]. + proc runAndWait. + (#(0 1) includes: proc exitCode) ifFalse: + [ self error: 'Unable to request carp location' ]. + ^ proc stdout trim asFileReference + +] + +{ #category : #accessing } +CarpProcess class >> resolveNodejsPath [ +] + +{ #category : #accessing } +CarpProcess class >> serverPath [ + ^ CarpPath + ifNil: [ CarpPath := self resolveCarpPath ] + ifNotNil: [ CarpPath ] +] + +{ #category : #accessing } +CarpProcess >> exitCode [ + + ^ process + ifNil: [ nil ] + ifNotNil: [ process exitCode ] +] + +{ #category : #accessing } +CarpProcess >> hasProcess [ + "Answer a boolean indicating whether the receiver has a process object" + + ^ process isNotNil +] + +{ #category : #accessing } +CarpProcess >> initialize [ + super initialize. + environmentVariables := Dictionary new. + self setDefaultEnvironmentVariables +] + +{ #category : #accessing } +CarpProcess >> isRunning [ + ^ process + ifNil: [ false ] + ifNotNil: [ process isRunning ] +] + +{ #category : #accessing } +CarpProcess >> newProcess| newProcess [ | + newProcess := GtSubprocessWithInMemoryOutput new + command: self serverPath fullName; + arguments: self processArguments; + workingDirectory: self workingDirectory resolve fullName; + terminateOnShutdown; + yourself. + environmentVariables associationsDo: [ :assoc | + newProcess environmentAt: assoc key put: assoc value ]. + ^ newProcess +] + +{ #category : #accessing } +CarpProcess >> processArguments [ + | args | + + args := OrderedCollection new. + self settings serverDebugMode ifTrue: + [ args add: '--inspect' ]. + args + add: (self workingDirectory / 'src/languagelink.carp') resolve fullName; + add: self settings serverSocketAddress port asString; + add: self settings clientSocketAddress port asString. + ^ args +] + +{ #category : #accessing } +CarpProcess >> serverPath [ + | fileReference | + + fileReference := self settings serverExecutable. + fileReference ifNil: [ fileReference := self class serverPath ]. + ^ fileReference. +] + +{ #category : #accessing } +CarpProcess >> setDefaultEnvironmentVariables [ + + environmentVariables := CarpPlatform subProcessEnvironmentDictionary. +] + +{ #category : #accessing } +CarpProcess >> start [ + process := self newProcess. + process run. + self settings serverDebugMode ifTrue: + [ self startServerDebugger ]. +] + +{ #category : #accessing } +CarpProcess >> stderr [ + "Answer the process stderr contents" + + ^ process stderr +] + +{ #category : #accessing } +CarpProcess >> stdout [ + "Answer the process stdout contents" + + ^ process stdout +] + +{ #category : #accessing } +CarpProcess >> stop [ + process ifNil: [ ^ self ]. + [ process queryExitStatus ifNil: [ process terminate ] ] + on: Error + do: [ "Do nothing.":e | ]. + process closeAndCleanStreams. + process := nil +] diff --git a/src/Carp/CarpProxyObject.class.st b/src/Carp/CarpProxyObject.class.st new file mode 100644 index 0000000..990513e --- /dev/null +++ b/src/Carp/CarpProxyObject.class.st @@ -0,0 +1,34 @@ +Class { + #name : #CarpProxyObject, + #superclass : #Object, + #instVars : [ + 'carpVariable', + 'carpType', + 'application' + ], + #category : #'Carp-Serialization' +} + +{ #category : #accessing } +CarpProxyObject class >> carpType: aType var: aVar application: application [ + ^ self new + carpVariable: aVar; + carpType: aType; + application: application; + yourself +] + +{ #category : #accessing } +CarpProxyObject >> application: anObject [ + application := anObject +] + +{ #category : #accessing } +CarpProxyObject >> carpType: aType [ + carpType := aType +] + +{ #category : #accessing } +CarpProxyObject >> carpVariable: aVar [ + carpVariable := aVar +] diff --git a/src/Carp/GtCarpCoderModel.class.st b/src/Carp/GtCarpCoderModel.class.st index 2c8ce6d..8850c27 100644 --- a/src/Carp/GtCarpCoderModel.class.st +++ b/src/Carp/GtCarpCoderModel.class.st @@ -28,12 +28,12 @@ GtCarpCoderModel >> bindAndExecute: sourceString [ | carpSource trimmedSource ast varNames lastStatement application commandFactory | trimmedSource := SmaCCString on: sourceString trimRight. - ast := JSParser parse: trimmedSource. + ast := CarpParser parse: trimmedSource. "The variables to be returned are names that are in pharoBindings" varNames := pharoBindings bindingNames asSet. "Assign the final statement to snippetResult" - lastStatement := ast items last. + lastStatement := ast expressions last. trimmedSource insert: '(defdynamic snippetResult ' at: lastStatement startPosition. @@ -108,6 +108,19 @@ GtCarpCoderModel >> pharoBindings: anObject [ pharoBindings := anObject ] +{ #category : #accessing } +GtCarpCoderModel >> primitiveEvaluate: aSourceString inContext: aGtSourceCoderEvaluationContext onFailDo: anEvaluationFailBlock [ + | result | + + result := self bindAndExecute: aSourceString. + result associationsDo: [ :binding | + (pharoBindings bindingOf: binding key asSymbol) value: binding value ]. + + ^ result + at: 'snippetResult' + ifAbsent: anEvaluationFailBlock +] + { #category : #accessing } GtCarpCoderModel >> sourceFrom: trimmedSourceString returnedVarNames: varNames [ "Answer the modified source to return the declared variables" diff --git a/src/Carp/LanguageLinkSettings.extension.st b/src/Carp/LanguageLinkSettings.extension.st index a700a07..13b8f09 100644 --- a/src/Carp/LanguageLinkSettings.extension.st +++ b/src/Carp/LanguageLinkSettings.extension.st @@ -9,12 +9,12 @@ LanguageLinkSettings class >> carpDefaultSettings [ serverSocketAddress: (LanguageLinkSocketAddress ipOrName: 'localhost' port: (9900 + 99 atRandom)); messageBrokerStrategy: LanguageLinkHttpMessageBroker; - serverProcessClass: JSLinkPharoNodejsProcess; - platform: JSLinkPharoPlatform new; - commandFactoryClass: JSLinkCommandFactory; + serverProcessClass: CarpProcess; + platform: CarpPlatform new; + commandFactoryClass: CarpCommandFactory; commandClass: LanguageLinkCommand; serializerClass: LanguageLinkSerializer; - deserializerClass: JSLinkDeserializer; + deserializerClass: CarpDeserializer; parserClass: CarpParser; yourself ] diff --git a/src/Carp/LeCarpApplicationStrategy.class.st b/src/Carp/LeCarpApplicationStrategy.class.st index 4511da9..894747a 100644 --- a/src/Carp/LeCarpApplicationStrategy.class.st +++ b/src/Carp/LeCarpApplicationStrategy.class.st @@ -14,9 +14,9 @@ LeCarpApplicationStrategy class >> strategyName [ LeCarpApplicationStrategy >> applicationServer [ content database isDatabase ifFalse: [ ^ nil ]. - JSLinkApplication uniqueInstance ifNil: - [ JSLinkApplication uniqueInstance: (self newJavaScriptApplicationFor: content database) ]. - ^ JSLinkApplication uniqueInstance + CarpApplication uniqueInstance ifNil: + [ CarpApplication uniqueInstance: (self newCarpApplicationFor: content database) ]. + ^ CarpApplication uniqueInstance ] { #category : #accessing } @@ -24,16 +24,16 @@ LeCarpApplicationStrategy >> applicationSettings [ "Answer the settings that will be used by the server. This musn't actually start the server as that should be deferred until a snippet is evaluated for the first time." - ^ JSLinkApplication isRunning ifTrue: - [ JSLinkApplication uniqueInstance settings ] + ^ CarpApplication isRunning ifTrue: + [ CarpApplication uniqueInstance settings ] ifFalse: - [ self updatedSettings: JSLinkApplication defaultSettings ] + [ self updatedSettings: CarpApplication defaultSettings ] ] { #category : #accessing } -LeCarpApplicationStrategy >> newJavaScriptApplicationFor: aLeDatabase [ +LeCarpApplicationStrategy >> newCarpApplicationFor: aLeDatabase [ - ^ JSLinkApplication new initializeWith: + ^ CarpApplication new initializeWith: (self updatedSettings: LanguageLinkSettings carpDefaultSettings). ]