1- require " uri"
2- require " digest"
3- require " connect-proxy"
41require " placeos-models"
52require " placeos-resource"
63require " ./module_manager"
4+ require " ./driver_manager/**"
75
86module PlaceOS::Core
9- class DriverStore
10- BINARY_PATH = ENV [" PLACEOS_DRIVER_BINARIES" ]?.presence || Path [" ./bin/drivers" ].expand.to_s
11-
12- protected getter binary_path : String
13-
14- def initialize (@binary_path : String = BINARY_PATH )
15- Dir .mkdir_p binary_path
16- end
17-
18- def compiled ?(file_name : String , commit : String , branch : String , uri : String ) : Bool
19- Log .debug { {message: " Checking whether driver is compiled or not?" , driver: file_name, commit: commit, branch: branch, repo: uri} }
20- path = Path [binary_path, executable_name(file_name, commit)]
21- return true if File .exists?(path)
22- resp = BuildApi .compiled?(file_name, commit, branch, uri)
23- return false unless resp.success?
24- ret = fetch_binary(LinkData .from_json(resp.body)) rescue nil
25- ! ret.nil?
26- end
27-
28- def compile (file_name : String , url : String , commit : String , branch : String , force : Bool , username : String ? = nil , password : String ? = nil , fetch : Bool = true ) : Result
29- Log .info { {message: " Requesting build service to compile driver" , driver_file: file_name, branch: branch, repository: url} }
30- begin
31- resp = BuildApi .compile(file_name, url, commit, branch, force, username, password)
32- unless fetch
33- return Result .new(success: true )
34- end
35- resp = resp.not_nil!
36- unless resp.success?
37- Log .error { {message: resp.body, status_code: resp.status_code, driver: file_name, commit: commit, branch: branch, force: force} }
38- return Result .new(output: resp.body, name: file_name)
39- end
40- link = LinkData .from_json(resp.body)
41- begin
42- driver = fetch_binary(link)
43- rescue ex
44- return Result .new(output: ex.message.not_nil!, name: file_name)
45- end
46- Result .new(success: true , name: driver, path: binary_path)
47- rescue ex
48- msg = ex.message || " compiled returned no exception message"
49- Log .error(exception: ex) { {message: msg, driver: file_name, commit: commit, branch: branch, force: force} }
50- Result .new(output: msg, name: file_name)
51- end
52- end
53-
54- def metadata (file_name : String , commit : String , branch : String , uri : String )
55- resp = BuildApi .metadata(file_name, commit, branch, uri)
56- return Result .new(success: true , output: resp.body.as(String )) if resp.success?
57- Result .new(output: " Metadata not found. Server returned #{ resp.status_code } " )
58- rescue ex
59- Result .new(output: ex.message.not_nil!, name: file_name)
60- end
61-
62- def defaults (file_name : String , commit : String , branch : String , uri : String )
63- resp = BuildApi .defaults(file_name, commit, branch, uri)
64- return Result .new(success: true , output: resp.body.as(String )) if resp.success?
65- Result .new(output: " Driver defaults not found. Server returned #{ resp.status_code } " )
66- rescue ex
67- Result .new(output: ex.message.not_nil!, name: file_name)
68- end
69-
70- def built ?(file_name : String , commit : String , branch : String , uri : String ) : String ?
71- return nil unless compiled?(file_name, commit, branch, uri)
72- driver_binary_path(file_name, commit).to_s
73- end
74-
75- def driver_binary_path (file_name : String , commit : String )
76- Path [binary_path, executable_name(file_name, commit)]
77- end
78-
79- def path (driver_file : String ) : Path
80- Path [binary_path, driver_file]
81- end
82-
83- def compiled_drivers : Array (String )
84- Dir .children(binary_path)
85- end
86-
87- def executable_name (driver_source, commit)
88- driver_source = driver_source.rchop(" .cr" ).gsub(/\/ |\. / , " _" )
89- commit = commit[..6] if commit.size > 6
90- {driver_source, commit, Core ::ARCH }.join(" _" ).downcase
91- end
92-
93- def reload_driver (driver_id : String )
94- if driver = Model ::Driver .find?(driver_id)
95- repo = driver.repository!
96-
97- if compiled?(driver.file_name, driver.commit, repo.branch, repo.uri)
98- manager = ModuleManager .instance
99- stale_path = manager.reload_modules(driver)
100- if path = stale_path
101- File .delete(path) rescue nil if File .exists?(path)
102- end
103- else
104- return {status: 404 , message: " Driver not compiled or not available on S3" }
105- end
106- else
107- return {status: 404 , message: " Driver with id #{ driver_id } not found " }
108- end
109- {status: 200 , message: " OK" }
110- end
111-
112- private def fetch_binary (link : LinkData ) : String
113- url = URI .parse(link.url)
114- driver_file = Path [url.path].basename
115- filename = Path [binary_path, driver_file]
116- resp = if Core .production? || url.scheme == " https"
117- ConnectProxy ::HTTPClient .get(url.to_s)
118- else
119- uri = URI .new(path: url.path, query: url.query)
120- ConnectProxy ::HTTPClient .new(url.host.not_nil!, 9000 ).get(uri.to_s)
121- end
122- if resp.success?
123- unless link.size == resp.headers.fetch(" Content-Length" , " 0" ).to_i
124- Log .error { {message: " Expected content length #{ link.size } , but received #{ resp.headers.fetch(" Content-Length" , " 0" ) } " , driver_file: driver_file} }
125- raise Error .new(" Response size doesn't match with build service returned result" )
126- end
127-
128- body_io = IO ::Digest .new(resp.body_io? || IO ::Memory .new(resp.body), Digest ::MD5 .new)
129- File .open(filename, " wb+" ) do |f |
130- IO .copy(body_io, f)
131- f.chmod(0o755 )
132- end
133- filename.to_s
134- else
135- raise Error .new(" Unable to fetch driver. Error : #{ resp.body } " )
136- end
137- end
138-
139- private record LinkData , size : Int64 , md5 : String , modified : Time , url : String , link_expiry : Time do
140- include JSON ::Serializable
141- @[JSON ::Field (converter: Time ::EpochConverter )]
142- getter modified : Time
143- @[JSON ::Field (converter: Time ::EpochConverter )]
144- getter link_expiry : Time
145- end
146- end
147-
148- module BuildApi
149- BUILD_API_BASE = " /api/build/v1"
150-
151- def self.metadata (file_name : String , commit : String , branch : String , uri : String )
152- host = URI .parse(Core .build_host)
153- file_name = URI .encode_www_form(file_name)
154- ConnectProxy ::HTTPClient .new(host) do |client |
155- path = " #{ BUILD_API_BASE } /metadata/#{ file_name } "
156- params = URI ::Params .encode({" url" => uri, " branch" => branch, " commit" => commit})
157- uri = " #{ path } ?#{ params } "
158- rep = client.get(uri)
159- Log .debug { {message: " Getting driver metadata. Server respose: #{ rep.status_code } " , file_name: file_name, commit: commit, branch: branch} }
160- rep
161- end
162- end
163-
164- def self.defaults (file_name : String , commit : String , branch : String , uri : String )
165- host = URI .parse(Core .build_host)
166- file_name = URI .encode_www_form(file_name)
167- ConnectProxy ::HTTPClient .new(host) do |client |
168- path = " #{ BUILD_API_BASE } /defaults/#{ file_name } "
169- params = URI ::Params .encode({" url" => uri, " branch" => branch, " commit" => commit})
170- uri = " #{ path } ?#{ params } "
171- rep = client.get(uri)
172- Log .debug { {message: " Getting driver defaults. Server respose: #{ rep.status_code } " , file_name: file_name, commit: commit, branch: branch} }
173- rep
174- end
175- end
176-
177- def self.compiled ?(file_name : String , commit : String , branch : String , uri : String )
178- host = URI .parse(Core .build_host)
179- file_name = URI .encode_www_form(file_name)
180- ConnectProxy ::HTTPClient .new(host) do |client |
181- path = " #{ BUILD_API_BASE } /#{ Core ::ARCH } /compiled/#{ file_name } "
182- params = URI ::Params .encode({" url" => uri, " branch" => branch, " commit" => commit})
183- uri = " #{ path } ?#{ params } "
184- rep = client.get(uri)
185- Log .debug { {message: " Checking if driver is compiled?. Server respose: #{ rep.status_code } " , file_name: file_name, commit: commit, branch: branch, server_rep: rep.body} }
186- rep
187- end
188- end
189-
190- def self.compile (file_name : String , url : String , commit : String , branch : String , force : Bool , username : String ? = nil , password : String ? = nil , fetch : Bool = true )
191- host = URI .parse(Core .build_host)
192- file_name = URI .encode_www_form(file_name)
193- headers = HTTP ::Headers .new
194- headers[" X-Git-Username" ] = username.not_nil! unless username.nil?
195- headers[" X-Git-Password" ] = password.not_nil! unless password.nil?
196-
197- resp = ConnectProxy ::HTTPClient .new(host) do |client |
198- path = " #{ BUILD_API_BASE } /#{ Core ::ARCH } /#{ file_name } "
199- params = URI ::Params .encode({" url" => url, " branch" => branch, " commit" => commit, " force" => force.to_s})
200- uri = " #{ path } ?#{ params } "
201- rep = client.post(uri, headers: headers)
202- Log .debug { {message: " Build URL host : #{ client.host } , URI: #{ uri } . Server response: #{ rep.status_code } " , server_resp: rep.body} }
203- rep
204- end
205-
206- raise " Build API returned #{ resp.status_code } while 202 was expected. Returned error: #{ resp.body } " unless resp.status_code == 202
207- link = resp.headers[" Content-Location" ] rescue raise " Build API returned invalid response, missing Content-Location header"
208-
209- task = JSON .parse(resp.body).as_h
210- loop do
211- resp = ConnectProxy ::HTTPClient .new(host) do |client |
212- rep = client.get(link)
213- Log .debug { {message: " Invoked request: URI: #{ link } . Server response: #{ rep.status_code } " , server_resp: rep.body} }
214- rep
215- end
216-
217- raise " Returned invalid response code: #{ resp.status_code } , #{ link } , resp: #{ resp.body } " unless resp.success? || resp.status_code == 303
218- task = JSON .parse(resp.body).as_h
219- break if task[" state" ].in?(" cancelled" , " error" , " done" )
220- sleep 5 .seconds
221- end
222- if resp.success? && task[" state" ].in?(" cancelled" , " error" )
223- raise task[" message" ].to_s
224- end
225- raise " Build API end-point #{ link } returned invalid response code #{ resp.status_code } , expected 303" unless resp.status_code == 303
226- raise " Build API end-point #{ link } returned invalid state #{ task[" state" ] } , expected 'done'" unless task[" state" ] == " done"
227- hdr = resp.headers[" Location" ] rescue raise " Build API returned compilation done, but missing Location URL"
228- if fetch
229- ConnectProxy ::HTTPClient .new(host) do |client |
230- client.get(hdr)
231- end
232- end
233- end
234- end
235-
236- record Result , success : Bool = false , output : String = " " , name : String = " " , path : String = " "
237-
2387 class DriverResource < Resource (Model ::Driver )
2398 private getter? startup : Bool = true
2409 private getter module_manager : ModuleManager
@@ -244,7 +13,7 @@ module PlaceOS::Core
24413 def initialize (
24514 @startup : Bool = true ,
24615 @binary_dir : String = " #{ Dir .current} /bin/drivers" ,
247- @module_manager : ModuleManager = ModuleManager .instance
16+ @module_manager : ModuleManager = ModuleManager .instance,
24817 )
24918 @store = DriverStore .new
25019 buffer_size = System .cpu_count.to_i
@@ -265,6 +34,7 @@ module PlaceOS::Core
26534 driver.update_fields(compilation_output: nil ) unless driver.compilation_output.nil?
26635 Resource ::Result ::Success
26736 in .deleted?
37+ DriverResource .remove_driver(driver, store)
26838 Result ::Skipped
26939 end
27040 rescue exception
@@ -275,7 +45,7 @@ module PlaceOS::Core
27545 driver : Model ::Driver ,
27646 store : DriverStore ,
27747 startup : Bool = false ,
278- module_manager : ModuleManager = ModuleManager .instance
48+ module_manager : ModuleManager = ModuleManager .instance,
27949 ) : Core ::Result
28050 driver_id = driver.id.as(String )
28151 repository = driver.repository!
@@ -361,10 +131,28 @@ module PlaceOS::Core
361131 Log .info { {message: " updated commit on driver" , id: driver.id, name: driver.name, commit: commit} }
362132 end
363133
134+ def self.remove_driver (driver : Model ::Driver , store : DriverStore )
135+ path = store.driver_binary_path(driver.file_name, driver.commit)
136+ Log .info { {message: " removing driver binary as it got removed from drivers" , driver_id: driver.id.as(String ), path: path.to_s} }
137+ remove_stale_driver(path, driver.id.as(String ))
138+ end
139+
140+ def start_driver_jobs
141+ DriverIntegrity .start_integrity_checker
142+ DriverCleanup .start_cleanup
143+ end
144+
364145 def start
365146 super
366147 @startup = false
148+ start_driver_jobs
367149 self
368150 end
151+
152+ def stop
153+ super
154+ DriverIntegrity .stop_integrity_checker
155+ DriverCleanup .stop_cleanup
156+ end
369157 end
370158end
0 commit comments