xolo-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +177 -0
- data/README.md +7 -0
- data/bin/xoloserver +106 -0
- data/data/com.pixar.xoloserver.plist +29 -0
- data/data/uninstall-pkgs-by-id.zsh +103 -0
- data/lib/xolo/server/app.rb +133 -0
- data/lib/xolo/server/command_line.rb +216 -0
- data/lib/xolo/server/configuration.rb +739 -0
- data/lib/xolo/server/constants.rb +70 -0
- data/lib/xolo/server/helpers/auth.rb +257 -0
- data/lib/xolo/server/helpers/client_data.rb +415 -0
- data/lib/xolo/server/helpers/file_transfers.rb +265 -0
- data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
- data/lib/xolo/server/helpers/log.rb +97 -0
- data/lib/xolo/server/helpers/maintenance.rb +401 -0
- data/lib/xolo/server/helpers/notification.rb +145 -0
- data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
- data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
- data/lib/xolo/server/helpers/title_editor.rb +92 -0
- data/lib/xolo/server/helpers/titles.rb +145 -0
- data/lib/xolo/server/helpers/versions.rb +160 -0
- data/lib/xolo/server/log.rb +286 -0
- data/lib/xolo/server/mixins/changelog.rb +315 -0
- data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
- data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
- data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
- data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
- data/lib/xolo/server/object_locks.rb +102 -0
- data/lib/xolo/server/routes/auth.rb +89 -0
- data/lib/xolo/server/routes/jamf_pro.rb +89 -0
- data/lib/xolo/server/routes/maint.rb +174 -0
- data/lib/xolo/server/routes/title_editor.rb +71 -0
- data/lib/xolo/server/routes/titles.rb +285 -0
- data/lib/xolo/server/routes/uploads.rb +93 -0
- data/lib/xolo/server/routes/versions.rb +261 -0
- data/lib/xolo/server/routes.rb +168 -0
- data/lib/xolo/server/title.rb +1143 -0
- data/lib/xolo/server/version.rb +902 -0
- data/lib/xolo/server.rb +205 -0
- data/lib/xolo-server.rb +8 -0
- metadata +243 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Helpers
|
|
16
|
+
|
|
17
|
+
# This is mixed in to Xolo::Server::App (as a helper, available in route processing)
|
|
18
|
+
# and in Xolo::Server::Title and Xolo::Server::Version.
|
|
19
|
+
#
|
|
20
|
+
# This holds methods and constants for sending alerts and emails.
|
|
21
|
+
#
|
|
22
|
+
module Notification
|
|
23
|
+
|
|
24
|
+
# Constants
|
|
25
|
+
#####################
|
|
26
|
+
#####################
|
|
27
|
+
|
|
28
|
+
DFT_EMAIL_FROM = 'xolo-server-do-not-reply'
|
|
29
|
+
|
|
30
|
+
ALERT_TOOL_EMAIL_PREFIX = 'email:'
|
|
31
|
+
|
|
32
|
+
# Module Methods
|
|
33
|
+
#######################
|
|
34
|
+
#######################
|
|
35
|
+
|
|
36
|
+
# when this module is included
|
|
37
|
+
def self.included(includer)
|
|
38
|
+
Xolo.verbose_include includer, self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Instance Methods
|
|
42
|
+
#######################
|
|
43
|
+
######################
|
|
44
|
+
|
|
45
|
+
# Send a message thru the alert_tool, if one is defined in the config.
|
|
46
|
+
#
|
|
47
|
+
# Messages are prepended with "#{level} ALERT: "
|
|
48
|
+
# This should be called by passing alert: true to one of the
|
|
49
|
+
# logging wrapper methods
|
|
50
|
+
#
|
|
51
|
+
# @param msg [String] the message to send
|
|
52
|
+
# @param level [Symbol] the log level of the message
|
|
53
|
+
#
|
|
54
|
+
# @return [void]
|
|
55
|
+
###############################
|
|
56
|
+
def send_alert(msg, level)
|
|
57
|
+
return unless Xolo::Server.config.alert_tool
|
|
58
|
+
return if send_email_alert(msg, level)
|
|
59
|
+
|
|
60
|
+
alerter = nil # just in case we need the ensure clause below.
|
|
61
|
+
alerter = IO.popen(Xolo::Server.config.alert_tool, 'w')
|
|
62
|
+
alerter.puts "#{level} ALERT: #{msg}"
|
|
63
|
+
|
|
64
|
+
# this catches the quitting of the alerter before expected
|
|
65
|
+
rescue Errno::EPIPE
|
|
66
|
+
true
|
|
67
|
+
ensure
|
|
68
|
+
# this flushes the pipe and makes the msg go
|
|
69
|
+
alerter&.close
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Send an alert via email
|
|
73
|
+
# @param msg [String] the message to send
|
|
74
|
+
# @param level [Symbol] the log level of the message
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean] true if the email was sent, false otherwise
|
|
77
|
+
################################
|
|
78
|
+
def send_email_alert(msg, level)
|
|
79
|
+
return false unless Xolo::Server.config.smtp_server
|
|
80
|
+
return false unless Xolo::Server.config.alert_tool.start_with? ALERT_TOOL_EMAIL_PREFIX
|
|
81
|
+
|
|
82
|
+
send_email(
|
|
83
|
+
to: Xolo::Server.config.alert_tool.delete_prefix(ALERT_TOOL_EMAIL_PREFIX).strip,
|
|
84
|
+
subject: "#{level} ALERT from Xolo Server",
|
|
85
|
+
msg: msg
|
|
86
|
+
)
|
|
87
|
+
true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Send an email, if the smtp_server is defined in the config.
|
|
91
|
+
#
|
|
92
|
+
# @param to [String] the email address to send to
|
|
93
|
+
# @param subject [String] the subject of the email
|
|
94
|
+
# @param msg [String] the body of the email
|
|
95
|
+
# @param html [Boolean] should the email be sent as HTML?
|
|
96
|
+
#
|
|
97
|
+
# @return [void]
|
|
98
|
+
###############################
|
|
99
|
+
def send_email(to:, subject:, msg:, html: false)
|
|
100
|
+
return unless Xolo::Server.config.smtp_server
|
|
101
|
+
|
|
102
|
+
headers = [
|
|
103
|
+
"From: #{server_name} <#{email_from}>",
|
|
104
|
+
"Date: #{Time.now.rfc2822}",
|
|
105
|
+
"To: #{to} <#{to}>",
|
|
106
|
+
"Subject: #{subject}"
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
if html
|
|
110
|
+
headers << 'MIME-Version: 1.0'
|
|
111
|
+
headers << 'Content-type: text/html'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
msg = "#{headers.join "\n"}\n\n#{msg}"
|
|
115
|
+
|
|
116
|
+
Net::SMTP.start(Xolo::Server.config.smtp_server) do |smtp|
|
|
117
|
+
smtp.send_message msg, email_from, to
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @return [String] the from address for emails
|
|
122
|
+
###############################
|
|
123
|
+
def email_from
|
|
124
|
+
@email_from ||= Xolo::Server.config.email_from || "#{DFT_EMAIL_FROM}@#{server_fqdn}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# @return [String] the human-readable name of the server for sending emails
|
|
128
|
+
###############################
|
|
129
|
+
def server_name
|
|
130
|
+
@server_name ||= "Xolo Server on #{server_fqdn}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# @return [String] the server's fully qualified domain name
|
|
134
|
+
###############################
|
|
135
|
+
def server_fqdn
|
|
136
|
+
@server_fqdn ||= Addrinfo.getaddrinfo(Socket.gethostname, nil).first.getnameinfo.first
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
end # Log
|
|
140
|
+
|
|
141
|
+
end # Helpers
|
|
142
|
+
|
|
143
|
+
end # Server
|
|
144
|
+
|
|
145
|
+
end # module Xolo
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Helpers
|
|
16
|
+
|
|
17
|
+
# constants and methods for signing packages
|
|
18
|
+
module PkgSigning
|
|
19
|
+
|
|
20
|
+
# Module methods
|
|
21
|
+
#
|
|
22
|
+
# These are available as module methods but not as 'helper'
|
|
23
|
+
# methods in sinatra routes & views.
|
|
24
|
+
#
|
|
25
|
+
##############################
|
|
26
|
+
##############################
|
|
27
|
+
|
|
28
|
+
# when this module is included
|
|
29
|
+
##############################
|
|
30
|
+
def self.included(includer)
|
|
31
|
+
Xolo.verbose_include includer, self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# when this module is extended
|
|
35
|
+
def self.extended(extender)
|
|
36
|
+
Xolo.verbose_extend extender, self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Instance methods
|
|
40
|
+
#
|
|
41
|
+
# These are available directly in sinatra routes and views
|
|
42
|
+
#
|
|
43
|
+
##############################
|
|
44
|
+
##############################
|
|
45
|
+
|
|
46
|
+
# do we need to sign a pkg?
|
|
47
|
+
# TODO: sign zipped bundle installers? prob not, they shouldn't be used anymore
|
|
48
|
+
# (I'm looking at YOU Adobe)
|
|
49
|
+
# @param pkg [Pathname] Path to a .pkg to see if it's signed.
|
|
50
|
+
# @return [Boolean] should we sign it?
|
|
51
|
+
def need_to_sign?(pkg)
|
|
52
|
+
log_debug "Checking need to sign uploaded pkg '#{pkg}'"
|
|
53
|
+
unless Xolo::Server.config.sign_pkgs
|
|
54
|
+
log_debug "No need to sign '#{pkg.basename}': xolo server is not configured to sign pkgs."
|
|
55
|
+
return false
|
|
56
|
+
end
|
|
57
|
+
if pkg.extname == Xolo::DOT_ZIP
|
|
58
|
+
log_debug "No need to sign '#{pkg.basename}': It is a compressed .pkg bundle. TODO: maybe support signing these?"
|
|
59
|
+
return false
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
!pkg_signed?(pkg)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @param pkg [Pathname] Path to a .pkg to see if it's already signed.
|
|
66
|
+
# @return [Boolean] is then pkg at the given pathname signed?
|
|
67
|
+
#########################
|
|
68
|
+
def pkg_signed?(pkg)
|
|
69
|
+
`/usr/sbin/pkgutil --check-signature #{Shellwords.escape pkg.to_s}`
|
|
70
|
+
already_signed = $CHILD_STATUS.success?
|
|
71
|
+
if already_signed
|
|
72
|
+
log_debug "No need to sign '#{pkg.basename}': It is already signed."
|
|
73
|
+
else
|
|
74
|
+
log_debug "About to sign '#{pkg.basename}'"
|
|
75
|
+
end
|
|
76
|
+
already_signed
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Sign a package
|
|
80
|
+
#
|
|
81
|
+
# @param unsigned_pkg [Pathname] the unsigned pkg to sign
|
|
82
|
+
# @param signed_pkg [Pathname] the destination file to write the signed version of the pkg.
|
|
83
|
+
#
|
|
84
|
+
# @return [void]
|
|
85
|
+
#######################################################
|
|
86
|
+
def sign_uploaded_pkg(unsigned_pkg, signed_pkg)
|
|
87
|
+
unlock_signing_keychain
|
|
88
|
+
|
|
89
|
+
sh_unsigned = Shellwords.escape unsigned_pkg.to_s
|
|
90
|
+
sh_signed = Shellwords.escape signed_pkg.to_s
|
|
91
|
+
sh_kch = Shellwords.escape Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
|
|
92
|
+
sh_ident = Shellwords.escape Xolo::Server.config.pkg_signing_identity
|
|
93
|
+
|
|
94
|
+
cmd = "/usr/bin/productsign --sign #{sh_ident} --keychain #{sh_kch} #{sh_unsigned} #{sh_signed}"
|
|
95
|
+
log_debug "Signing #{signed_pkg.basename} using this command: #{cmd}"
|
|
96
|
+
|
|
97
|
+
stdouterr, exit_status = Open3.capture2e(cmd)
|
|
98
|
+
return if exit_status.success?
|
|
99
|
+
|
|
100
|
+
msg = "Failed to sign uploaded pkg: #{stdouterr}"
|
|
101
|
+
log_error msg
|
|
102
|
+
halt 400, { status: 400, error: msg }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# unlock the pkg signing keychain
|
|
106
|
+
# TODO: Be DRY with the keychain stuff in Xolo::Admin::Credentials
|
|
107
|
+
#############################
|
|
108
|
+
def unlock_signing_keychain
|
|
109
|
+
log_debug 'Unlocking the signing keychain'
|
|
110
|
+
|
|
111
|
+
pw = Xolo::Server.config.pkg_signing_keychain_pw
|
|
112
|
+
# first escape backslashes
|
|
113
|
+
pw = pw.to_s.gsub '\\', '\\\\\\'
|
|
114
|
+
# then single quotes
|
|
115
|
+
pw.gsub! "'", "\\\\'"
|
|
116
|
+
# then warp in sgl quotes
|
|
117
|
+
pw = "'#{pw}'"
|
|
118
|
+
|
|
119
|
+
outerrs = Xolo::BLANK
|
|
120
|
+
exit_status = nil
|
|
121
|
+
|
|
122
|
+
Open3.popen2e('/usr/bin/security -i') do |stdin, stdout_err, wait_thr|
|
|
123
|
+
stdin.puts "unlock-keychain -p #{pw} '#{Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN}'"
|
|
124
|
+
stdin.close
|
|
125
|
+
outerrs = stdout_err.read
|
|
126
|
+
exit_status = wait_thr.value # Process::Status object returned.
|
|
127
|
+
end # Open3.popen2e
|
|
128
|
+
return if exit_status.success?
|
|
129
|
+
|
|
130
|
+
msg = "Error unlocking signing keychain: #{outerrs}"
|
|
131
|
+
log_error msg
|
|
132
|
+
halt 400, { status: 400, error: msg }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
end # JamfPro
|
|
136
|
+
|
|
137
|
+
end # Helpers
|
|
138
|
+
|
|
139
|
+
end # Server
|
|
140
|
+
|
|
141
|
+
end # module Xolo
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Helpers
|
|
16
|
+
|
|
17
|
+
# This is used both as a 'helper' in the Sinatra server,
|
|
18
|
+
# and an included mixin for the Xolo::Server::Title and
|
|
19
|
+
# Xolo::Server::Version classes
|
|
20
|
+
# to provide common methods for long-running routes that deliver
|
|
21
|
+
# realtime progress updates via http streaming.
|
|
22
|
+
module ProgressStreaming
|
|
23
|
+
|
|
24
|
+
# Constants
|
|
25
|
+
#######################
|
|
26
|
+
#######################
|
|
27
|
+
|
|
28
|
+
PROGRESS_THREAD_NAME_PREFIX = 'xolo-progress-stream-'
|
|
29
|
+
|
|
30
|
+
# Module Methods
|
|
31
|
+
#######################
|
|
32
|
+
#######################
|
|
33
|
+
|
|
34
|
+
# when this module is included
|
|
35
|
+
def self.included(includer)
|
|
36
|
+
Xolo.verbose_include includer, self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Instance Methods
|
|
40
|
+
#######################
|
|
41
|
+
#######################
|
|
42
|
+
|
|
43
|
+
# Call this from long-running routes.
|
|
44
|
+
#
|
|
45
|
+
# It runs a block in a thread with streaming
|
|
46
|
+
#
|
|
47
|
+
# The block should call #progress as needed to write
|
|
48
|
+
# to the progress file, and optionally the log
|
|
49
|
+
#
|
|
50
|
+
# Always sends back a JSON response body with
|
|
51
|
+
# {
|
|
52
|
+
# status: :running,
|
|
53
|
+
# progress_stream_url_path: progress_stream_url_path
|
|
54
|
+
# }
|
|
55
|
+
# Any errors should be written to the stream file,
|
|
56
|
+
# as will unhandled exceptions.
|
|
57
|
+
#
|
|
58
|
+
# @yield The block to run in the thread with streaming
|
|
59
|
+
##########################
|
|
60
|
+
def with_streaming
|
|
61
|
+
raise 'No block given to run in streaming thread' unless block_given?
|
|
62
|
+
|
|
63
|
+
@streaming_now = true
|
|
64
|
+
|
|
65
|
+
# always call this first in a
|
|
66
|
+
# long-running route that will use progress streaming
|
|
67
|
+
setup_progress_streaming
|
|
68
|
+
|
|
69
|
+
log_debug 'Starting with_streaming block in thread'
|
|
70
|
+
|
|
71
|
+
@streaming_thread = Thread.new do
|
|
72
|
+
log_debug 'Thread with_streaming is starting and yielding to block'
|
|
73
|
+
yield
|
|
74
|
+
log_debug 'Thread with_streaming is finished'
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
progress "ERROR: #{e.class}: #{e}", log: :error
|
|
77
|
+
e.backtrace.each { |l| log_debug "..#{l}" }
|
|
78
|
+
ensure
|
|
79
|
+
stop_progress_streaming
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
@streaming_thread.name = "#{PROGRESS_THREAD_NAME_PREFIX}#{session[:xolo_id]}"
|
|
83
|
+
|
|
84
|
+
resp_body = {
|
|
85
|
+
status: :running,
|
|
86
|
+
progress_stream_url_path: progress_stream_url_path
|
|
87
|
+
}
|
|
88
|
+
body resp_body
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Setup for streaming:
|
|
92
|
+
# create the tmp file so that any threads will see it
|
|
93
|
+
# and do any other pre-streaming stuff
|
|
94
|
+
#
|
|
95
|
+
# @param xadm_command [String] the xadm command that is causing this stream.
|
|
96
|
+
# may be used for finding a returning the progress file after the fact.
|
|
97
|
+
#
|
|
98
|
+
##########################
|
|
99
|
+
def setup_progress_streaming
|
|
100
|
+
log_debug "Setting up for progress streaming. progress_stream_file is: #{progress_stream_file}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# End che current progress stream
|
|
104
|
+
###############################
|
|
105
|
+
def stop_progress_streaming
|
|
106
|
+
log_debug "Stopping progress streaming to file: #{progress_stream_file}"
|
|
107
|
+
progress Xolo::Server::PROGRESS_COMPLETE
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# The file to which we write progess messages
|
|
111
|
+
# for long-running processes, which might in turn
|
|
112
|
+
# be streamed to xadm now or in the future
|
|
113
|
+
#
|
|
114
|
+
#############################
|
|
115
|
+
def progress_stream_file
|
|
116
|
+
return @progress_stream_file if @progress_stream_file
|
|
117
|
+
|
|
118
|
+
tempf = Tempfile.create "#{PROGRESS_THREAD_NAME_PREFIX}#{session[:xolo_id]}-"
|
|
119
|
+
tempf.close # we'll write to it later
|
|
120
|
+
log_debug "Created progress_stream_file: #{tempf.path}"
|
|
121
|
+
@progress_stream_file = Pathname.new(tempf.path)
|
|
122
|
+
@progress_stream_file.pix_touch
|
|
123
|
+
@progress_stream_file
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# The file to which we write progess messages
|
|
127
|
+
# for long-running processes, which might in turn
|
|
128
|
+
# be streamed to xadm.
|
|
129
|
+
#############################
|
|
130
|
+
def progress_stream_url_path
|
|
131
|
+
"/streamed_progress/?stream_file=#{CGI.escape progress_stream_file.to_s}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Append a message to the progress stream file,
|
|
135
|
+
# optionally sending it also to the log
|
|
136
|
+
#
|
|
137
|
+
# @param message [String] the message to append
|
|
138
|
+
# @param log [Symbol] the level at which to log the message
|
|
139
|
+
# one of :debug, :info, :warn, :error, :fatal, or :unknown.
|
|
140
|
+
# Default is nil, which doesn't log the message at all.
|
|
141
|
+
#
|
|
142
|
+
# @return [void]
|
|
143
|
+
###################
|
|
144
|
+
def progress(msg, log: :nil)
|
|
145
|
+
# log_debug "Progress method called from #{caller_locations.first}"
|
|
146
|
+
|
|
147
|
+
progress_stream_file.pix_append "#{msg.chomp}\n"
|
|
148
|
+
|
|
149
|
+
unless log
|
|
150
|
+
# log_debug 'Processed unlogged progress message'
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
case log
|
|
155
|
+
when :debug
|
|
156
|
+
log_debug msg
|
|
157
|
+
when :info
|
|
158
|
+
log_info msg
|
|
159
|
+
when :warn
|
|
160
|
+
log_warn msg
|
|
161
|
+
when :error
|
|
162
|
+
log_error msg
|
|
163
|
+
when :fatal
|
|
164
|
+
log_fatal msg
|
|
165
|
+
when :unknown
|
|
166
|
+
log_unknown msg
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Stream lines from the given file to the given stream
|
|
171
|
+
#
|
|
172
|
+
# @param stream_file: [Pathname] the file to stream from
|
|
173
|
+
# @param stream: [Sinatra::Helpers::Stream] the stream to send to
|
|
174
|
+
#
|
|
175
|
+
# @return [void]
|
|
176
|
+
#############################
|
|
177
|
+
def stream_progress(stream_file:, stream:)
|
|
178
|
+
log_debug "About to tail: usr/bin/tail -f -c +1 #{Shellwords.escape stream_file.to_s}"
|
|
179
|
+
|
|
180
|
+
stdin, stdouterr, wait_thr = Open3.popen2e("/usr/bin/tail -f -c +1 #{Shellwords.escape stream_file.to_s}")
|
|
181
|
+
stdin.close
|
|
182
|
+
|
|
183
|
+
while line = stdouterr.gets
|
|
184
|
+
break if line.chomp == Xolo::Server::PROGRESS_COMPLETE
|
|
185
|
+
|
|
186
|
+
stream << line
|
|
187
|
+
end
|
|
188
|
+
stdouterr.close
|
|
189
|
+
wait_thr.exit
|
|
190
|
+
# TODO: deal with wait_thr.value.exitstatus > 0 ?
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# TEMP TESTING - Thisis happening in a thread and
|
|
194
|
+
# should send updates via #progress
|
|
195
|
+
#
|
|
196
|
+
######################
|
|
197
|
+
def a_long_thing_with_streamed_feedback
|
|
198
|
+
log_debug 'Starting a_long_thing_with_streamed_feedback'
|
|
199
|
+
|
|
200
|
+
progress 'Doing a quick thing...'
|
|
201
|
+
sleep 3
|
|
202
|
+
progress 'Quick thing done.'
|
|
203
|
+
|
|
204
|
+
progress "Doing a slow thing at #{Time.now} ..."
|
|
205
|
+
log_debug 'Starting thread in a_long_thing_with_streamed_feedback'
|
|
206
|
+
|
|
207
|
+
# even tho we are in a thread, if we want to
|
|
208
|
+
# send updates while some long sub-task is running
|
|
209
|
+
# we do it in another thread like this
|
|
210
|
+
long_thr = Thread.new { sleep 30 }
|
|
211
|
+
sleep 3
|
|
212
|
+
|
|
213
|
+
while long_thr.alive?
|
|
214
|
+
log_debug 'Thread still alive...'
|
|
215
|
+
progress "Slow thing still happening at #{Time.now} ..."
|
|
216
|
+
sleep 3
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
log_debug 'Thread in a_long_thing_with_streamed_feedback is done'
|
|
220
|
+
|
|
221
|
+
progress 'Slow thing done.'
|
|
222
|
+
|
|
223
|
+
progress "Doing a medium thing at #{Time.now} ..."
|
|
224
|
+
log_debug 'Starting another thread in a_long_thing_with_streamed_feedback - sends output from the thread'
|
|
225
|
+
|
|
226
|
+
# Doing this in a thead is just for academics...
|
|
227
|
+
# as is doing anything in a thread and immediately doing thr.join
|
|
228
|
+
med_thr = Thread.new do
|
|
229
|
+
3.times do |x|
|
|
230
|
+
progress "the medium thing has done #{x + 1} things", log: :debug
|
|
231
|
+
sleep 5
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
progress 'Now waiting for medium thing to finish...'
|
|
236
|
+
med_thr.join
|
|
237
|
+
|
|
238
|
+
progress 'Medium thing is done.'
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
########## TMP
|
|
242
|
+
def jsonify_stream_msg(msg)
|
|
243
|
+
@out_to_stream << { msg: msg }.to_json
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
end # Streaming
|
|
247
|
+
|
|
248
|
+
end # Helpers
|
|
249
|
+
|
|
250
|
+
end # Server
|
|
251
|
+
|
|
252
|
+
end # module Xolo
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Copyright 2025 Pixar
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the terms set forth in the LICENSE.txt file available at
|
|
4
|
+
# at the root of this project.
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
|
|
8
|
+
# frozen_string_literal: true
|
|
9
|
+
|
|
10
|
+
# main module
|
|
11
|
+
module Xolo
|
|
12
|
+
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Helpers
|
|
16
|
+
|
|
17
|
+
# constants and methods for accessing the Title Editor server
|
|
18
|
+
#
|
|
19
|
+
# This is both uses as a 'helper' in the Sinatra server,
|
|
20
|
+
# and an included mixin for the Xolo::Server::Title and
|
|
21
|
+
# Xolo::Server::Version classes.
|
|
22
|
+
#
|
|
23
|
+
# This means methods here are available in instances of
|
|
24
|
+
# those classes, and in all routes, views, and helpers in
|
|
25
|
+
# Sinatra.
|
|
26
|
+
#
|
|
27
|
+
# NOTE: The names of various attributes of Title Editor SoftwareTitles
|
|
28
|
+
# and Xolo Titles are not always in sync.
|
|
29
|
+
# For example:
|
|
30
|
+
# - the :display_name of the Xolo Title is the :name of a SoftwareTitle
|
|
31
|
+
# - the :title of a Xolo Title id the :id of a SoftwareTitle
|
|
32
|
+
# - the numeric :softwareTitleId of the SoftwareTitle doesn't exist in a Xolo Title
|
|
33
|
+
#
|
|
34
|
+
# See Windoo::SoftwareTitle::JSON_ATTRIBUTES for more details about them.
|
|
35
|
+
# The Xolo server code will deal with all the translations.
|
|
36
|
+
#
|
|
37
|
+
module TitleEditor
|
|
38
|
+
|
|
39
|
+
# Module methods
|
|
40
|
+
#
|
|
41
|
+
# These are available as module methods but not as 'helper'
|
|
42
|
+
# methods in sinatra routes & views.
|
|
43
|
+
#
|
|
44
|
+
##############################
|
|
45
|
+
##############################
|
|
46
|
+
|
|
47
|
+
# when this module is included
|
|
48
|
+
##############################
|
|
49
|
+
def self.included(includer)
|
|
50
|
+
Xolo.verbose_include includer, self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Instance methods
|
|
54
|
+
#
|
|
55
|
+
# These are available directly in sinatra routes and views
|
|
56
|
+
#
|
|
57
|
+
##############################
|
|
58
|
+
##############################
|
|
59
|
+
|
|
60
|
+
# A connection to the Title Editor via Windoo
|
|
61
|
+
# We don't use the Windoo default connection but
|
|
62
|
+
# use this method to create standalone ones as needed
|
|
63
|
+
# and ensure they are disconnected, (or will timeout)
|
|
64
|
+
# when we are done.
|
|
65
|
+
#
|
|
66
|
+
# @return [Windoo::Connection] A connection object
|
|
67
|
+
##############################
|
|
68
|
+
def ted_cnx
|
|
69
|
+
return @ted_cnx if @ted_cnx
|
|
70
|
+
|
|
71
|
+
@ted_cnx = Windoo::Connection.new(
|
|
72
|
+
name: "title-editor-cnx-#{Time.now.strftime('%F-%T')}",
|
|
73
|
+
host: Xolo::Server.config.ted_hostname,
|
|
74
|
+
user: Xolo::Server.config.ted_api_user,
|
|
75
|
+
pw: Xolo::Server.config.ted_api_pw,
|
|
76
|
+
open_timeout: Xolo::Server.config.ted_open_timeout,
|
|
77
|
+
timeout: Xolo::Server.config.ted_timeout,
|
|
78
|
+
keep_alive: false
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
log_debug "Title Editor: Connected at #{@ted_cnx.base_url}, user '#{Xolo::Server.config.ted_api_user}'. KeepAlive: #{@ted_cnx.keep_alive?}, Expires: #{@ted_cnx.token.expires}. cnxID: #{@ted_cnx.object_id}"
|
|
82
|
+
|
|
83
|
+
@ted_cnx
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end # TitleEditor
|
|
87
|
+
|
|
88
|
+
end # Helpers
|
|
89
|
+
|
|
90
|
+
end # Server
|
|
91
|
+
|
|
92
|
+
end # module Xolo
|