@@ -3,7 +3,6 @@ package com.bugsnag.android.mazerunner
33import android.app.Activity
44import android.content.Context
55import android.content.Intent
6- import android.content.SharedPreferences
76import android.os.Build
87import android.os.Bundle
98import android.os.Handler
@@ -18,21 +17,23 @@ import java.net.URL
1817import kotlin.concurrent.thread
1918
2019private const val MAZE_RUNNER_COMMAND_TIMEOUT_MS = 5000
20+ private const val POLLING_INTERVAL_MS = 1000L
21+ private const val MIN_ERROR_MESSAGE_WIDTH = 1
2122private const val LEGACY_MAZE_ADDRESS = " bs-local.com:9339"
2223
2324class MainActivity : Activity (), CommandExecutor {
2425
2526 private companion object {
2627 var hasClearedCommandUUIDForProcess = false
28+ var activeCommandRunnerThread: Thread ? = null
2729 }
2830
31+ private val activityInstanceId = Integer .toHexString(System .identityHashCode(this ))
2932 private val mainHandler = Handler (Looper .getMainLooper())
3033 private val commandHandler = MazeRunnerCommandHandler (this )
31- private var commandRunnerThread: Thread ? = null
3234
3335 private val apiKeyKey = " BUGSNAG_API_KEY"
3436 private val commandUUIDKey = " MAZE_COMMAND_UUID"
35- lateinit var prefs: SharedPreferences
3637
3738 var scenario: Scenario ? = null
3839 var isActivityRecreate = false
@@ -41,13 +42,14 @@ class MainActivity : Activity(), CommandExecutor {
4142 override fun onCreate (savedInstanceState : Bundle ? ) {
4243 super .onCreate(savedInstanceState)
4344 this .isActivityRecreate = savedInstanceState != null
44- log(" MainActivity.onCreate called" )
45+ log(" MainActivity.onCreate called: activity= $activityInstanceId " )
4546 requestWindowFeature(Window .FEATURE_NO_TITLE )
4647 setContentView(R .layout.activity_main)
47- prefs = getPreferences(Context .MODE_PRIVATE )
48+
49+ val prefs = getSharedPreferences(" mazerunner" , Context .MODE_PRIVATE )
4850
4951 if (! hasClearedCommandUUIDForProcess) {
50- // clearStoredCommandUUID( )
52+ CiLog .info( " First onCreate for this process, last command UUID: ' ${getStoredCommandUUID()} ' " )
5153 hasClearedCommandUUIDForProcess = true
5254 }
5355
@@ -81,17 +83,24 @@ class MainActivity : Activity(), CommandExecutor {
8183 super .onResume()
8284 log(" MainActivity.onResume called" )
8385
84- // Don't start the command runner again if the activity is being recreated,
85- // as it results in two threads executing commands concurrently and causing flakes.
86- if (! this .isActivityRecreate) {
87- startCommandRunner()
88- }
86+ startCommandRunner()
8987 log(" MainActivity.onResume complete" )
9088 }
9189
90+ override fun onDestroy () {
91+ CiLog .info(" MainActivity.onDestroy called: activity=$activityInstanceId " )
92+ stopCommandRunner()
93+ super .onDestroy()
94+ }
95+
9296 private fun setMazeRunnerAddress () {
93- mazeAddress = MazeRunnerAddressReader .readFromConfig(applicationContext, timeout = false )
94- if (! mazeAddress.isNullOrBlank()) {
97+ var address = MazeRunnerAddressReader .readFromConfig(applicationContext, timeout = false )
98+ if (address == " local:9339" ) {
99+ address = " bs-local.com:9339"
100+ }
101+
102+ if (! address.isNullOrBlank()) {
103+ mazeAddress = address
95104 CiLog .info(" Maze Runner address set from config file: $mazeAddress " )
96105 return
97106 }
@@ -108,64 +117,113 @@ class MainActivity : Activity(), CommandExecutor {
108117 return
109118 }
110119
111- val refreshedMazeAddress = MazeRunnerAddressReader .readFromConfig(applicationContext, timeout = false )
120+ var refreshedMazeAddress = MazeRunnerAddressReader .readFromConfig(applicationContext, timeout = false )
121+ if (refreshedMazeAddress == " local:9339" ) {
122+ refreshedMazeAddress = " bs-local.com:9339"
123+ }
124+
112125 if (! refreshedMazeAddress.isNullOrBlank()) {
113126 mazeAddress = refreshedMazeAddress
114127 CiLog .info(" Maze Runner address refreshed from config file: $mazeAddress " )
115128 }
116129 }
117130
118131 override fun setStoredCommandUUID (commandUUID : String ) {
119- with (prefs.edit()) {
120- putString(commandUUIDKey, commandUUID)
121- commit()
122- }
123- CiLog .info(" lastCommandUUID set to: $commandUUID " )
132+ getSharedPreferences(" mazerunner" , Context .MODE_PRIVATE ).edit().putString(commandUUIDKey, commandUUID).commit()
133+ CiLog .info(" lastCommandUUID set to: $commandUUID (activity=$activityInstanceId )" )
124134 }
125135
126136 override fun clearStoredCommandUUID () {
127- with (prefs.edit()) {
128- remove(commandUUIDKey)
129- commit()
130- }
131- CiLog .info(" lastCommandUUID set to empty" )
137+ getSharedPreferences(" mazerunner" , Context .MODE_PRIVATE ).edit().remove(commandUUIDKey).commit()
138+ CiLog .info(" lastCommandUUID cleared (activity=$activityInstanceId )" )
132139 }
133140
134- private fun getStoredCommandUUID (): String? {
135- return prefs .getString(commandUUIDKey, " " ).orEmpty()
141+ private fun getStoredCommandUUID (): String {
142+ return getSharedPreferences( " mazerunner " , Context . MODE_PRIVATE ) .getString(commandUUIDKey, " " ).orEmpty()
136143 }
137144
138145 // Starts a thread to poll for Maze Runner actions to perform
139146 @Synchronized
140147 private fun startCommandRunner () {
141- if (commandRunnerThread ?.isAlive == true ) {
142- CiLog .info(" Maze Runner command runner already active" )
148+ if (activeCommandRunnerThread ?.isAlive == true ) {
149+ CiLog .info(" Maze Runner command runner already active (current activity= $activityInstanceId ) " )
143150 return
144151 }
145152
146- val runner = thread(start = false ) {
153+ CiLog .info(
154+ " Starting command runner: " +
155+ " activity=$activityInstanceId , " +
156+ " thread=${Thread .currentThread().name} , " +
157+ " uuid='${getStoredCommandUUID()} '"
158+ )
159+
160+ val runner = thread(
161+ start = false ,
162+ name = " maze-command-$activityInstanceId "
163+ ) {
147164 try {
165+ CiLog .info(
166+ " Command runner thread started: " +
167+ " activity=$activityInstanceId , " +
168+ " thread=${Thread .currentThread().name} "
169+ )
170+
148171 if (mazeAddress == null ) setMazeRunnerAddress()
149172 runCommandRunnerLoop()
150173 } finally {
151- synchronized(this @MainActivity) {
152- if (commandRunnerThread == = Thread .currentThread()) {
153- commandRunnerThread = null
174+ CiLog .info(
175+ " Command runner thread finished: " +
176+ " activity=$activityInstanceId , " +
177+ " thread=${Thread .currentThread().name} "
178+ )
179+
180+ synchronized(MainActivity ::class .java) {
181+ if (activeCommandRunnerThread == = Thread .currentThread()) {
182+ activeCommandRunnerThread = null
154183 }
155184 }
156185 }
157186 }
158187
159- commandRunnerThread = runner
188+ activeCommandRunnerThread = runner
160189 runner.start()
161190 }
162191
192+ private fun stopCommandRunner () {
193+ val runner = synchronized(MainActivity ::class .java) {
194+ val r = activeCommandRunnerThread
195+ activeCommandRunnerThread = null
196+ r
197+ }
198+
199+ if (runner?.isAlive == true ) {
200+ CiLog .info(
201+ " Stopping command runner: " +
202+ " activity=$activityInstanceId , " +
203+ " thread=${runner.name} "
204+ )
205+ runner.interrupt()
206+ }
207+ }
208+
163209 private fun runCommandRunnerLoop () {
164210 var polling = true
165- while (polling) {
166- Thread .sleep(1000 )
167- polling = fetchAndHandleNextCommand()
211+
212+ while (polling && ! Thread .currentThread().isInterrupted) {
213+ try {
214+ Thread .sleep(POLLING_INTERVAL_MS )
215+ polling = fetchAndHandleNextCommand()
216+ } catch (interrupted: InterruptedException ) {
217+ CiLog .info(
218+ " Command runner interrupted: " +
219+ " activity=$activityInstanceId "
220+ )
221+ Thread .currentThread().interrupt()
222+ polling = false
223+ }
168224 }
225+
226+ CiLog .info(" Maze Runner command runner stopped" )
169227 }
170228
171229 private fun fetchAndHandleNextCommand (): Boolean {
@@ -209,28 +267,45 @@ class MainActivity : Activity(), CommandExecutor {
209267 }
210268
211269 private fun readCommand (): String {
212- val commandUrl = " http://$mazeAddress /command?after=${getStoredCommandUUID().orEmpty()} "
213- CiLog .info(" Requesting Maze Runner command from: $commandUrl " )
270+ val storedUUID = getStoredCommandUUID()
271+ val commandUrl = " http://$mazeAddress /command?after=$storedUUID "
272+
273+ CiLog .info(
274+ " Requesting Maze Runner command: " +
275+ " activity=$activityInstanceId , " +
276+ " thread=${Thread .currentThread().name} , " +
277+ " uuid='$storedUUID ', " +
278+ " url=$commandUrl "
279+ )
280+
214281 val urlConnection = URL (commandUrl).openConnection() as HttpURLConnection
215282 urlConnection.connectTimeout = MAZE_RUNNER_COMMAND_TIMEOUT_MS
216283 urlConnection.readTimeout = MAZE_RUNNER_COMMAND_TIMEOUT_MS
217284 try {
218- return urlConnection.inputStream.use { it.reader().readText() }
219- } catch (ioe: IOException ) {
220- CiLog .error(" Read of Maze Runner command failed" , ioe)
221- try {
222- val errorMessage = urlConnection.errorStream.use { it.reader().readText() }
223- CiLog .error(
224- " Failed to GET $commandUrl (HTTP ${urlConnection.responseCode} " +
225- " ${urlConnection.responseMessage} ):\n " +
226- " ${" -" .repeat(errorMessage.width)} \n " +
227- " $errorMessage \n " +
228- " -" .repeat(errorMessage.width)
229- )
230- } catch (e: Exception ) {
231- log(" Failed to retrieve error message from connection" , e)
285+ val responseCode = urlConnection.responseCode
286+ if (responseCode == HttpURLConnection .HTTP_OK ) {
287+ return urlConnection.inputStream.use { it.reader().readText() }
288+ }
289+
290+ if (responseCode == HttpURLConnection .HTTP_BAD_REQUEST ) {
291+ val rejectedUuid = getStoredCommandUUID()
292+ CiLog .warn(" Maze Runner returned 400 Bad Request for command UUID: $rejectedUuid " )
293+
294+ clearStoredCommandUUID()
295+ CiLog .info(" Command UUID after clearing: '${getStoredCommandUUID()} '" )
232296 }
233297
298+ val errorMessage = urlConnection.errorStream?.use { it.reader().readText() }.orEmpty()
299+ CiLog .error(
300+ " Failed to GET $commandUrl (HTTP $responseCode " +
301+ " ${urlConnection.responseMessage} ):\n " +
302+ " ${" -" .repeat(errorMessage.width.coerceAtLeast(MIN_ERROR_MESSAGE_WIDTH ))} \n " +
303+ " $errorMessage \n " +
304+ " -" .repeat(errorMessage.width.coerceAtLeast(MIN_ERROR_MESSAGE_WIDTH ))
305+ )
306+ throw IOException (" Failed to GET $commandUrl (HTTP $responseCode )" )
307+ } catch (ioe: IOException ) {
308+ CiLog .error(" Read of Maze Runner command failed" , ioe)
234309 throw ioe
235310 }
236311 }
@@ -273,6 +348,7 @@ class MainActivity : Activity(), CommandExecutor {
273348 // Clear persistent data (used to stop scenarios bleeding into each other)
274349 override fun clearPersistentData () {
275350 CiLog .info(" Clearing persistent data" )
351+ scenario = null
276352 PersistentData (applicationContext).clear()
277353 }
278354
@@ -291,6 +367,7 @@ class MainActivity : Activity(), CommandExecutor {
291367 else -> " a35a2a72bd230ac0aa0f52715bbdc6aa"
292368 }
293369
370+ val prefs = getSharedPreferences(" mazerunner" , Context .MODE_PRIVATE )
294371 if (manualMode) {
295372 log(" Running in manual mode with API key: $apiKey " )
296373 prefs.setStoredApiKey(apiKeyKey, apiKey)
0 commit comments